ssh-terminal
Version:
SSH Terminal component based on xterm.js for multiple frontend frameworks
1,404 lines (1,403 loc) • 58.9 kB
JavaScript
var ee = Object.defineProperty;
var te = (u, e, t) => e in u ? ee(u, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : u[e] = t;
var D = (u, e, t) => (te(u, typeof e != "symbol" ? e + "" : e, t), t);
import { Terminal as re } from "xterm";
import { FitAddon as ne } from "xterm-addon-fit";
import { WebLinksAddon as oe } from "xterm-addon-web-links";
import { SearchAddon as ie } from "xterm-addon-search";
import { ref as se, onMounted as ae, onBeforeUnmount as ce, watch as V, nextTick as N, openBlock as le, createElementBlock as de, normalizeClass as K, createElementVNode as ue } from "vue";
class he {
constructor() {
this.tabId = this.getOrCreateTabId();
}
getOrCreateTabId() {
const e = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
return console.log("🆕 Created new Tab ID:", e), e;
}
getTabId() {
return this.tabId;
}
}
const ge = new he();
class pe {
constructor() {
this.keyPair = null, this.serverPublicKey = null, this.useWebCrypto = this._isSecureContext(), this.microRsa = null, console.log("🔧 EncryptionService constructor:", {
protocol: window.location.protocol,
hostname: window.location.hostname,
isSecureContext: window.isSecureContext,
hasWebCrypto: !!(window.crypto && window.crypto.subtle),
useWebCrypto: this.useWebCrypto
}), this.useWebCrypto ? console.log("✅ Will use Web Crypto API") : console.warn("🔓 HTTP environment detected - will use micro-rsa-dsa-dh fallback");
}
/**
* Kiểm tra có phải secure context không (HTTPS hoặc localhost)
*/
_isSecureContext() {
if (typeof window > "u")
return !1;
const e = !!(window.crypto && window.crypto.subtle), t = window.isSecureContext || window.location.protocol === "https:", r = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname === "::1";
if (console.log("🔧 _isSecureContext check:", {
hasWebCrypto: e,
isSecure: t,
isLocalhost: r,
protocol: window.location.protocol,
hostname: window.location.hostname
}), e && (t || r))
try {
return window.crypto.subtle.generateKey && window.crypto.subtle.encrypt ? (console.log("✅ Web Crypto API methods are available"), !0) : (console.warn("⚠️ Web Crypto API object exists but methods not available"), !1);
} catch (n) {
return console.warn("⚠️ Web Crypto API test failed:", n.message), !1;
}
return !t && !r ? (console.warn("🔧 HTTP environment detected - will use micro-rsa-dsa-dh fallback"), !1) : e && (t || r);
}
/**
* Load node-forge khi cần thiết (chỉ cho HTTP)
*/
async _loadNodeForge() {
if (!this.nodeForge)
try {
const e = await import("./index-9234c122.mjs").then((t) => t.i);
this.nodeForge = e.default || e, console.log("✅ node-forge loaded for HTTP environment");
} catch (e) {
console.error("❌ Failed to load node-forge from node_modules:", e), console.warn("⚠️ Using mock crypto implementation for testing"), this.nodeForge = {
pki: {
rsa: {
generateKeyPair: () => ({
publicKey: { encrypt: () => new Uint8Array(256) },
privateKey: { decrypt: () => new Uint8Array([72, 101, 108, 108, 111]) }
// "Hello"
})
}
},
md: {
sha256: {
create: () => ({ digest: () => ({ bytes: () => new Uint8Array(32) }) })
}
}
}, console.log("✅ Mock crypto implementation loaded");
}
return this.nodeForge;
}
/**
* Generate key pair với node-forge
*/
async _generateNodeForgeKeyPair() {
const e = await this._loadNodeForge();
console.log("✅ node-forge loaded successfully");
try {
console.log("🔧 Generating RSA key pair with node-forge...");
const t = e.pki.rsa.generateKeyPair({ bits: 2048 });
return this.keyPair = {
publicKey: {
type: "public",
algorithm: { name: "RSA-OAEP", hash: "SHA-256" },
extractable: !0,
usages: ["encrypt"],
nodeForge: t.publicKey,
// Convert to PEM for compatibility
pem: e.pki.publicKeyToPem(t.publicKey)
},
privateKey: {
type: "private",
algorithm: { name: "RSA-OAEP", hash: "SHA-256" },
extractable: !0,
usages: ["decrypt"],
nodeForge: t.privateKey,
// Convert to PEM for compatibility
pem: e.pki.privateKeyToPem(t.privateKey)
}
}, console.log("🔧 RSA key pair generated successfully with node-forge"), this.keyPair;
} catch (t) {
throw console.error("❌ Failed to generate key pair with node-forge:", t), new Error("Key generation failed: " + t.message);
}
}
/**
* Tạo cặp khóa RSA cho client
*/
async generateKeyPair() {
try {
if (this.useWebCrypto) {
console.log("🔧 Attempting Web Crypto API key generation...");
try {
this.keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
!0,
["encrypt", "decrypt"]
), console.log("✅ Web Crypto API key generation successful");
} catch (e) {
return console.error("❌ Web Crypto API failed in this environment:", e), console.warn("🔄 Falling back to node-forge..."), this.useWebCrypto = !1, await this._generateNodeForgeKeyPair();
}
} else
return await this._generateNodeForgeKeyPair();
return this.keyPair;
} catch (e) {
throw console.error("❌ Lỗi tạo key pair:", e), new Error("Không thể tạo key pair");
}
}
/**
* Xuất public key dưới dạng PEM
*/
async exportPublicKey(e = this.keyPair) {
try {
if (this.useWebCrypto) {
const t = await window.crypto.subtle.exportKey(
"spki",
e.publicKey
), r = String.fromCharCode(...new Uint8Array(t));
return `-----BEGIN PUBLIC KEY-----
${window.btoa(r)}
-----END PUBLIC KEY-----`;
} else
return console.warn("⚠️ Using mock public key export for HTTP testing"), `-----BEGIN PUBLIC KEY-----
${btoa("mock-public-key-data-for-testing")}
-----END PUBLIC KEY-----`;
} catch (t) {
throw console.error("❌ Lỗi export public key:", t), new Error("Không thể export public key");
}
}
/**
* Import public key từ server
*/
async importServerPublicKey(e) {
try {
const t = e.replace(/\r\n/g, `
`).replace(/\r/g, `
`), r = "-----BEGIN PUBLIC KEY-----", n = "-----END PUBLIC KEY-----";
if (!t.includes(r) || !t.includes(n))
throw new Error("Invalid PEM format - missing headers");
const i = t.indexOf(r) + r.length, s = t.indexOf(n);
if (i === -1 || s === -1 || i >= s)
throw new Error("Invalid PEM format - malformed headers");
const l = t.substring(i, s).replace(/\s+/g, "");
if (l.length === 0)
throw new Error("Empty PEM content after cleaning");
let o;
try {
o = window.atob(l);
} catch {
throw new Error("Invalid base64 content in PEM");
}
const g = new Uint8Array(o.length);
for (let p = 0; p < o.length; p++)
g[p] = o.charCodeAt(p);
return this.useWebCrypto ? (this.serverPublicKey = await window.crypto.subtle.importKey(
"spki",
g.buffer,
{
name: "RSA-OAEP",
hash: "SHA-256"
},
!0,
["encrypt"]
), console.log("🔧 Server public key imported successfully:", this.serverPublicKey)) : (console.warn("⚠️ Using node-forge server public key import for HTTP testing"), this.serverPublicKey = {
type: "node-forge-server-key",
pem: e
}, console.log("✅ Server public key stored for node-forge")), this.serverPublicKey;
} catch (t) {
throw console.error("❌ Import server public key error:", t), new Error("Không thể import server public key: " + t.message);
}
}
/**
* Mã hóa dữ liệu bằng public key của server
*/
async encryptForServer(e, t = this.serverPublicKey) {
try {
if (console.log("🔧 encryptForServer called with:", {
dataLength: e?.length,
hasPublicKey: !!t,
useWebCrypto: this.useWebCrypto,
publicKeyType: typeof t
}), !t)
throw new Error("Server public key chưa được import");
if (this.useWebCrypto) {
console.log("🔧 Using Web Crypto API for encryption");
const n = new TextEncoder().encode(e);
console.log("🔧 Encoded data length:", n.length), console.log("🔧 Public key object:", t);
const i = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
t,
n
);
console.log("🔧 Encryption successful, result length:", i.byteLength);
const s = btoa(String.fromCharCode(...new Uint8Array(i)));
return console.log("🔧 Base64 result length:", s.length), s;
} else {
console.warn("⚠️ Using node-forge RSA encryption for HTTP environment");
try {
const r = await this._loadNodeForge();
if (!t || !t.pem)
throw new Error("Server public key not available for node-forge");
console.log("🔧 Attempting node-forge RSA encryption...");
try {
const n = r.pki.publicKeyFromPem(t.pem);
console.log("✅ Successfully parsed RSA public key with node-forge"), console.log("🔧 Encrypting with node-forge RSA-OAEP...");
const i = n.encrypt(e, "RSA-OAEP", {
md: r.md.sha256.create(),
mgf1: {
md: r.md.sha256.create()
}
}), s = btoa(i);
return console.log("✅ RSA-OAEP encryption successful with node-forge"), console.log(`🔧 Encrypted data length: ${s.length} chars`), s;
} catch (n) {
console.error("❌ Real encryption failed, falling back to mock:", n), console.warn("⚠️ Using mock encryption as fallback");
const i = new Uint8Array(256);
if (window.crypto && window.crypto.getRandomValues) {
window.crypto.getRandomValues(i);
const c = new TextEncoder().encode(e);
for (let l = 0; l < c.length && l < i.length; l++)
i[l] ^= c[l];
} else {
const c = new TextEncoder().encode(e);
for (let l = 0; l < i.length; l++)
i[l] = (c[l % c.length] + l + 42) % 256;
}
const s = btoa(String.fromCharCode(...i));
return console.log("⚠️ Generated mock encryption result"), s;
}
} catch (r) {
throw console.error("❌ node-forge encryption failed:", r), new Error("Không thể mã hóa với node-forge: " + r.message);
}
}
} catch (r) {
throw console.error("❌ Lỗi mã hóa dữ liệu:", r), console.error("❌ Error stack:", r.stack), new Error("Không thể mã hóa dữ liệu: " + r.message);
}
}
/**
* Giải mã dữ liệu bằng private key của client
*/
async decryptFromServer(e) {
try {
if (!this.keyPair)
throw new Error("Key pair chưa được tạo");
if (this.useWebCrypto) {
console.log("🔧 Using Web Crypto API for decryption");
const t = atob(e), r = new Uint8Array(t.length);
for (let s = 0; s < t.length; s++)
r[s] = t.charCodeAt(s);
const n = await window.crypto.subtle.decrypt(
{ name: "RSA-OAEP" },
this.keyPair.privateKey,
r
);
return new TextDecoder().decode(n);
} else {
console.log("🔧 Using node-forge for decryption"), await this._loadNodeForge();
const t = this.keyPair.privateKey.nodeForge, r = atob(e);
return t.decrypt(r, "RSA-OAEP", {
md: forge.md.sha256.create(),
mgf1: {
md: forge.md.sha256.create()
}
});
}
} catch (t) {
throw console.error("❌ Lỗi giải mã dữ liệu:", t), new Error("Không thể giải mã dữ liệu: " + t.message);
}
}
/**
* Tạo AES key cho session encryption
*/
async generateAESKey() {
try {
return await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
!0,
["encrypt", "decrypt"]
);
} catch (e) {
throw console.error("❌ Lỗi tạo AES key:", e), new Error("Không thể tạo AES key");
}
}
/**
* Mã hóa dữ liệu bằng AES
*/
async encryptAES(e, t) {
try {
const n = new TextEncoder().encode(e), i = window.crypto.getRandomValues(new Uint8Array(12)), s = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: i
},
t,
n
), c = new Uint8Array(i.length + s.byteLength);
return c.set(i), c.set(new Uint8Array(s), i.length), btoa(String.fromCharCode(...c));
} catch (r) {
throw console.error("❌ Lỗi mã hóa AES:", r), new Error("Không thể mã hóa AES");
}
}
/**
* Giải mã dữ liệu AES
*/
async decryptAES(e, t) {
try {
const r = atob(e), n = new Uint8Array(r.length);
for (let o = 0; o < r.length; o++)
n[o] = r.charCodeAt(o);
const i = n.slice(0, 12), s = n.slice(12), c = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: i
},
t,
s
);
return new TextDecoder().decode(c);
} catch (r) {
throw console.error("❌ Lỗi giải mã AES:", r), new Error("Không thể giải mã AES");
}
}
/**
* Tạo hash SHA-256
*/
async hash(e) {
try {
if (this.useWebCrypto) {
const r = new TextEncoder().encode(e), n = await window.crypto.subtle.digest("SHA-256", r);
return Array.from(new Uint8Array(n)).map((s) => s.toString(16).padStart(2, "0")).join("");
} else if (console.warn("⚠️ Using mock hash for HTTP testing"), window.crypto && window.crypto.getRandomValues) {
let t = 0;
for (let n = 0; n < e.length; n++) {
const i = e.charCodeAt(n);
t = (t << 5) - t + i, t = t & t;
}
return Math.abs(t).toString(16).padStart(8, "0").repeat(8).substring(0, 64);
} else {
let t = 0;
for (let r = 0; r < e.length; r++)
t = (t << 5) - t + e.charCodeAt(r), t = t & t;
return Math.abs(t).toString(16).padStart(8, "0").repeat(8).substring(0, 64);
}
} catch (t) {
throw console.error("❌ Lỗi tạo hash:", t), new Error("Không thể tạo hash");
}
}
/**
* Tạo random string
*/
generateRandomString(e = 32) {
if (this.useWebCrypto) {
const t = new Uint8Array(e);
return window.crypto.getRandomValues(t), Array.from(t, (r) => r.toString(16).padStart(2, "0")).join("");
} else {
console.warn("⚠️ Using Math.random for random generation in HTTP environment");
let t = "";
const r = "0123456789abcdef";
for (let n = 0; n < e * 2; n++)
t += r.charAt(Math.floor(Math.random() * r.length));
return t;
}
}
}
class f {
/**
* Xác thực SSH configuration
*/
static validateSSHConfig(e) {
const t = [];
return e.host ? typeof e.host != "string" ? t.push("Host phải là string") : e.host.length > 255 ? t.push("Host quá dài (tối đa 255 ký tự)") : this.isValidHost(e.host) || t.push("Host không hợp lệ") : t.push("Host là bắt buộc"), e.username ? typeof e.username != "string" ? t.push("Username phải là string") : e.username.length > 32 ? t.push("Username quá dài (tối đa 32 ký tự)") : this.isValidUsername(e.username) || t.push("Username chứa ký tự không hợp lệ") : t.push("Username là bắt buộc"), e.password ? typeof e.password != "string" ? t.push("Password phải là string") : e.password.length < 1 ? t.push("Password không được để trống") : e.password.length > 128 && t.push("Password quá dài (tối đa 128 ký tự)") : t.push("Password là bắt buộc"), e.port !== void 0 && (Number.isInteger(e.port) ? (e.port < 1 || e.port > 65535) && t.push("Port phải trong khoảng 1-65535") : t.push("Port phải là số nguyên")), e.wsUrl && !this.isValidWebSocketURL(e.wsUrl) && t.push("WebSocket URL không hợp lệ"), {
isValid: t.length === 0,
errors: t
};
}
/**
* Kiểm tra host hợp lệ (IP hoặc domain)
*/
static isValidHost(e) {
return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(e) || /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(e) ? !0 : /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(e);
}
/**
* Kiểm tra username hợp lệ
*/
static isValidUsername(e) {
return /^[a-zA-Z0-9_-]+$/.test(e);
}
/**
* Kiểm tra WebSocket URL hợp lệ (khuyến nghị wss:// cho bảo mật)
*/
static isValidWebSocketURL(e) {
try {
const t = new URL(e);
return t.protocol === "ws:" || t.protocol === "wss:";
} catch {
return !1;
}
}
/**
* Xác thực encryption readiness
*/
static validateEncryptionReadiness(e, t) {
const r = [];
return e || r.push("Encryption service not initialized"), t || r.push("Server public key not available"), {
isValid: r.length === 0,
errors: r
};
}
/**
* Xác thực computing configuration
*/
static validateComputingConfig(e) {
const t = [];
return e.computingId ? typeof e.computingId != "string" ? t.push("Computing ID must be a string") : /^[0-9.]+$/.test(e.computingId) ? e.computingId.length > 50 && t.push("Computing ID must not exceed 50 characters") : t.push("Computing ID must contain only numbers and dots") : t.push("Computing ID is required"), e.userToken ? typeof e.userToken != "string" ? t.push("User token must be a string") : e.userToken.length > 8192 && t.push("User token is too large") : t.push("User token is required"), e.wsUrl && !this.isValidWebSocketURL(e.wsUrl) && t.push("Invalid WebSocket URL"), {
isValid: t.length === 0,
errors: t
};
}
/**
* Xác thực rằng connection chỉ sử dụng encrypted protocols
*/
static validateSecureConnection(e) {
const t = [], r = [];
if (!e)
return t.push("WebSocket URL is required"), { isValid: !1, errors: t, warnings: r };
try {
const n = new URL(e);
n.protocol === "ws:" ? r.push("Warning: Using unencrypted WebSocket (ws://). Consider using wss:// for better security.") : n.protocol !== "wss:" && t.push("Invalid WebSocket protocol. Only ws:// and wss:// are supported.");
} catch {
t.push("Invalid WebSocket URL format");
}
return {
isValid: t.length === 0,
errors: t,
warnings: r
};
}
/**
* Làm sạch string input
*/
static sanitizeString(e, t = 255) {
return typeof e != "string" ? "" : e.trim().slice(0, t).replace(/[\x00-\x1F\x7F]/g, "");
}
/**
* Xác thực terminal options
*/
static validateTerminalOptions(e) {
const t = [];
if (typeof e != "object" || e === null)
return { isValid: !1, errors: ["Options phải là object"] };
if (e.fontSize !== void 0 && (!Number.isInteger(e.fontSize) || e.fontSize < 8 || e.fontSize > 72) && t.push("fontSize phải là số nguyên từ 8-72"), e.scrollback !== void 0 && (!Number.isInteger(e.scrollback) || e.scrollback < 0 || e.scrollback > 1e4) && t.push("scrollback phải là số nguyên từ 0-10000"), e.theme !== void 0)
if (typeof e.theme != "object")
t.push("theme phải là object");
else {
const r = ["background", "foreground", "cursor", "cursorAccent", "selection"];
for (const [n, i] of Object.entries(e.theme))
r.includes(n) && typeof i == "string" && (this.isValidColor(i) || t.push(`theme.${n} không phải màu hợp lệ`));
}
return e.reconnection !== void 0 && (typeof e.reconnection != "object" ? t.push("reconnection phải là object") : (e.reconnection.maxAttempts !== void 0 && (!Number.isInteger(e.reconnection.maxAttempts) || e.reconnection.maxAttempts < 0 || e.reconnection.maxAttempts > 20) && t.push("reconnection.maxAttempts phải là số nguyên từ 0-20"), e.reconnection.heartbeatInterval !== void 0 && (!Number.isInteger(e.reconnection.heartbeatInterval) || e.reconnection.heartbeatInterval < 1e3 || e.reconnection.heartbeatInterval > 3e5) && t.push("reconnection.heartbeatInterval phải là số nguyên từ 1000-300000ms"))), {
isValid: t.length === 0,
errors: t
};
}
/**
* Kiểm tra màu hợp lệ (hex, rgb, rgba)
*/
static isValidColor(e) {
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(e) ? !0 : /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+\s*)?\)$/.test(e);
}
/**
* Xác thực JWT token format
*/
static isValidJWTFormat(e) {
if (typeof e != "string")
return !1;
const t = e.split(".");
return t.length === 3 && t.every((r) => r.length > 0);
}
/**
* Xác thực message size
*/
static validateMessageSize(e, t = 1e4) {
if (typeof e == "string")
return e.length <= t;
if (e instanceof ArrayBuffer)
return e.byteLength <= t;
try {
return JSON.stringify(e).length <= t;
} catch {
return !1;
}
}
/**
* Làm sạch object để logging an toàn
*/
static sanitizeForLogging(e) {
if (typeof e != "object" || e === null)
return e;
const t = ["password", "token", "key", "secret", "auth", "credential"], r = {};
for (const [n, i] of Object.entries(e)) {
const s = n.toLowerCase();
t.some((c) => s.includes(c)) ? r[n] = "***HIDDEN***" : typeof i == "object" && i !== null ? r[n] = this.sanitizeForLogging(i) : r[n] = i;
}
return r;
}
}
const me = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
default: f
}, Symbol.toStringTag, { value: "Module" })), L = class L {
constructor() {
this.isProduction = typeof window < "u" && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1", this.logLevel = this.isProduction ? "error" : "debug", this.maxLogLength = 1e3;
}
/**
* Kiểm tra có nên log không
*/
shouldLog(e) {
return L.LEVELS[e] >= L.LEVELS[this.logLevel];
}
/**
* Làm sạch dữ liệu trước khi log
*/
sanitizeData(e) {
if (e == null)
return e;
const t = f.sanitizeForLogging(e), r = JSON.stringify(t);
return r.length > this.maxLogLength ? {
...t,
_truncated: !0,
_originalLength: r.length
} : t;
}
/**
* Format log message
*/
formatMessage(e, t, r = null) {
const i = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${e.toUpperCase()}] [CLIENT]`;
if (r) {
const s = this.sanitizeData(r);
return `${i} ${t} ${JSON.stringify(s)}`;
}
return `${i} ${t}`;
}
/**
* Debug log
*/
debug(e, t = null) {
this.shouldLog("debug") && console.debug(this.formatMessage("debug", e, t));
}
/**
* Info log
*/
info(e, t = null) {
this.shouldLog("info") && console.info(this.formatMessage("info", e, t));
}
/**
* Warning log
*/
warn(e, t = null) {
this.shouldLog("warn") && console.warn(this.formatMessage("warn", e, t));
}
/**
* Error log
*/
error(e, t = null) {
this.shouldLog("error") && console.error(this.formatMessage("error", e, t));
}
/**
* Log connection events
*/
logConnection(e, t = {}) {
const r = this.sanitizeData(t);
this.info(`Connection ${e}`, r);
}
/**
* Log authentication events
*/
logAuth(e, t = {}) {
const r = this.sanitizeData(t);
this.info(`Auth ${e}`, r);
}
/**
* Log security events
*/
logSecurity(e, t = {}) {
const r = this.sanitizeData(t);
this.warn(`Security ${e}`, r);
}
/**
* Log performance metrics
*/
logPerformance(e, t, r = "ms") {
this.debug(`Performance ${e}`, { value: t, unit: r });
}
/**
* Log với custom level
*/
log(e, t, r = null) {
this.shouldLog(e) && console[e](this.formatMessage(e, t, r));
}
/**
* Tạo logger instance với context
*/
createContextLogger(e) {
return {
debug: (t, r) => this.debug(`[${e}] ${t}`, r),
info: (t, r) => this.info(`[${e}] ${t}`, r),
warn: (t, r) => this.warn(`[${e}] ${t}`, r),
error: (t, r) => this.error(`[${e}] ${t}`, r),
logConnection: (t, r) => this.logConnection(`[${e}] ${t}`, r),
logAuth: (t, r) => this.logAuth(`[${e}] ${t}`, r),
logSecurity: (t, r) => this.logSecurity(`[${e}] ${t}`, r),
logPerformance: (t, r, n) => this.logPerformance(`[${e}] ${t}`, r, n)
};
}
/**
* Alias for backward compatibility
*/
createContext(e) {
return this.createContextLogger(e);
}
/**
* Set log level
*/
setLogLevel(e) {
L.LEVELS.hasOwnProperty(e) && (this.logLevel = e);
}
/**
* Get current log level
*/
getLogLevel() {
return this.logLevel;
}
/**
* Enable/disable production mode
*/
setProductionMode(e) {
this.isProduction = e, this.logLevel = e ? "error" : "debug";
}
};
/**
* Các mức độ log
*/
D(L, "LEVELS", {
debug: 0,
info: 1,
warn: 2,
error: 3
});
let O = L;
const _ = new O(), fe = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
SecureLogger: O,
default: _
}, Symbol.toStringTag, { value: "Module" }));
class ye {
constructor() {
this.connections = /* @__PURE__ */ new Map(), this.contextLogger = _.createContextLogger("WebSocketPool");
}
/**
* Get hoặc tạo mới WebSocket connection
* @param {string} wsUrl - WebSocket URL
* @param {string} connectionKey - Unique key (tabId:computingId)
* @param {object} handlers - {onOpen, onMessage, onError, onClose}
*/
async getConnection(e, t, r) {
this.contextLogger.info("Getting WebSocket connection", {
wsUrl: e,
connectionKey: t
}), this.connections.has(e) || this.connections.set(e, /* @__PURE__ */ new Map());
const n = this.connections.get(e);
if (n.has(t)) {
const i = n.get(t), s = i.socket;
if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING)
return this.contextLogger.info("Reusing existing WebSocket", { connectionKey: t }), i.handlers = r, s;
this.contextLogger.info("Existing WebSocket is dead, creating new one", { connectionKey: t }), n.delete(t);
}
return this.createNewConnection(e, t, r);
}
/**
* Tạo WebSocket connection mới
*/
createNewConnection(e, t, r) {
const { onMessage: n, onError: i, onClose: s, onOpen: c } = r;
this.contextLogger.info("Creating new WebSocket connection", { wsUrl: e, connectionKey: t }), console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), console.log("🔌 Creating WebSocket");
const l = new WebSocket(e), o = this.connections.get(e);
return o.set(t, { socket: l, handlers: r }), l.onopen = () => {
if (console.log(`✅ [${t}] WebSocket OPENED`), this.contextLogger.info("WebSocket opened", { wsUrl: e, connectionKey: t }), c)
try {
c();
} catch (g) {
this.contextLogger.error("Error in onOpen handler", { error: g.message });
}
}, l.onmessage = (g) => {
const p = o.get(t);
if (!p)
return;
const w = p.handlers;
try {
w.onMessage && w.onMessage(g);
} catch (h) {
this.contextLogger.error("Error in onMessage handler", {
connectionKey: t,
error: h.message
});
}
}, l.onerror = (g) => {
console.error(`❌ [${t}] WebSocket ERROR:`, g), this.contextLogger.error("WebSocket error", { wsUrl: e, connectionKey: t, error: g });
const p = o.get(t);
if (p && p.handlers.onError)
try {
p.handlers.onError(g);
} catch (w) {
this.contextLogger.error("Error in onError handler", { error: w.message });
}
}, l.onclose = (g) => {
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), console.log(`🚪 [${t}] WebSocket CLOSED`), console.log("Code:", g.code), console.log("Reason:", g.reason || "(empty)"), console.log("Was Clean:", g.wasClean), console.log("Timestamp:", (/* @__PURE__ */ new Date()).toISOString()), console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), this.contextLogger.info("WebSocket closed", {
wsUrl: e,
connectionKey: t,
code: g.code,
reason: g.reason,
wasClean: g.wasClean
});
const p = o.get(t), w = p ? p.handlers : null;
if (o.delete(t), console.log(`📊 After close - Remaining connections: ${o.size}`), o.size === 0 && (this.connections.delete(e), this.contextLogger.info("All connections closed for URL", { wsUrl: e })), w && w.onClose)
try {
w.onClose(g);
} catch (h) {
this.contextLogger.error("Error in onClose handler", { error: h.message });
}
}, l;
}
/**
* Xóa subscriber và đóng connection
*/
removeSubscriber(e, t) {
this.contextLogger.info("Removing subscriber", { wsUrl: e, connectionKey: t });
const r = this.connections.get(e);
if (!r) {
this.contextLogger.warn("No connections found for URL", { wsUrl: e });
return;
}
const n = r.get(t);
if (!n) {
this.contextLogger.warn("Connection not found", { connectionKey: t });
return;
}
const i = n.socket;
i && i.readyState !== WebSocket.CLOSED && i.readyState !== WebSocket.CLOSING && i.close(1e3, "Subscriber removed"), r.delete(t), r.size === 0 && (this.connections.delete(e), this.contextLogger.info("All connections removed for URL", { wsUrl: e }));
}
/**
* Gửi message qua WebSocket
* @param {string} wsUrl
* @param {string} connectionKey
* @param {object|string} message
*/
send(e, t, r) {
const n = this.connections.get(e);
if (!n)
throw new Error(`No connections found for ${e}`);
const i = n.get(t);
if (!i)
throw new Error(`No connection found for key: ${t}`);
const s = i.socket;
if (s.readyState !== WebSocket.OPEN)
throw new Error(`Connection not ready (state: ${s.readyState})`);
const c = typeof r == "string" ? r : JSON.stringify(r);
s.send(c);
}
/**
* Kiểm tra connection có đang mở không
*/
isConnected(e, t) {
const r = this.connections.get(e);
if (!r)
return !1;
const n = r.get(t);
return n ? n.socket.readyState === WebSocket.OPEN : !1;
}
/**
* Đếm số connection cho một URL
*/
getConnectionCount(e) {
const t = this.connections.get(e);
return t ? t.size : 0;
}
/**
* Đóng tất cả connections
*/
dispose() {
this.contextLogger.info("Disposing all connections"), this.connections.forEach((e, t) => {
e.forEach((r, n) => {
const i = r.socket;
i.readyState !== WebSocket.CLOSED && i.readyState !== WebSocket.CLOSING && i.close(1e3, "Pool disposed");
});
}), this.connections.clear();
}
/**
* Debug: Lấy thông tin tất cả connections
*/
getDebugInfo() {
const e = {};
return this.connections.forEach((t, r) => {
e[r] = {
count: t.size,
connections: []
}, t.forEach((n, i) => {
e[r].connections.push({
key: i,
state: n.socket.readyState,
stateText: ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][n.socket.readyState]
});
});
}), e;
}
}
const S = new ye(), j = {
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: {
background: "#000000",
foreground: "#ffffff",
cursor: "#ffffff",
cursorAccent: "#000000",
selection: "rgba(255, 255, 255, 0.3)"
},
cursorBlink: !0,
cursorStyle: "block",
scrollback: 1e3,
allowTransparency: !1,
tabStopWidth: 8,
screenReaderMode: !1,
convertEol: !0,
disableStdin: !1,
reconnection: {
enabled: !0,
maxAttempts: 5,
heartbeatInterval: 3e4
}
};
function W(u, e, t = {}) {
const r = _.createContextLogger("Terminal");
if (!u)
throw r.error("Container element is required"), new Error("Container element is required");
let n;
if (e.computingId && e.userToken) {
if (n = f.validateComputingConfig(e), !n.isValid)
throw r.error("Invalid computing configuration", { errors: n.errors }), new Error(`Computing configuration invalid: ${n.errors.join(", ")}`);
} else if (n = f.validateSSHConfig(e), !n.isValid)
throw r.error("Invalid SSH configuration", { errors: n.errors }), new Error(`SSH configuration invalid: ${n.errors.join(", ")}`);
const s = f.validateTerminalOptions(t);
if (!s.isValid)
throw r.error("Invalid terminal options", { errors: s.errors }), new Error(`Terminal options invalid: ${s.errors.join(", ")}`);
const c = {
...j,
...t.theme ? { theme: { ...j.theme, ...t.theme } } : {},
...t
}, l = t.showConnectionLogs === !0, o = new re(c), g = new ne(), p = new oe(), w = new ie();
o.loadAddon(g), o.loadAddon(p), o.loadAddon(w), o.open(u), setTimeout(() => {
g.fit();
}, 0);
const h = e.wsUrl || "wss://ssh-proxy.dev.longvan.vn";
if (!f.isValidWebSocketURL(h))
throw r.error("Invalid WebSocket URL", { wsUrl: h }), new Error("Invalid WebSocket URL");
const v = f.validateSecureConnection(h);
if (!v.isValid)
throw r.error("Insecure WebSocket connection", { errors: v.errors }), new Error(`Insecure connection: ${v.errors.join(", ")}`);
v.warnings && v.warnings.length > 0 && v.warnings.forEach((d) => {
o.writeln(`⚠️ ${d}`);
});
let E = !1, b = null, A = null, C = 0;
const P = c.reconnection?.maxAttempts || 5, H = c.reconnection?.enabled !== !1;
let U = null, $ = !1, z = !1;
const x = e.computingId || `direct-${Date.now()}`, B = e.tabClientId || `direct-${Date.now()}`, I = ge.getTabId(), y = `${I}:${B}`;
r.info("Creating terminal", {
tabId: I,
computingId: x,
connectionKey: y
}), console.log("Creating terminal", {
tabId: I,
computingId: x,
connectionKey: y
});
const q = async () => {
try {
return b = new pe(), await b.generateKeyPair(), !0;
} catch (d) {
return r.error("Failed to initialize encryption", { error: d.message }), !1;
}
}, G = async () => {
try {
const d = f.validateEncryptionReadiness(b, A);
if (!d.isValid)
throw new Error(`Encryption not ready: ${d.errors.join(", ")}`);
let a;
console.log("typeConnect in createTerminal", e.typeConnect);
const k = e.typeConnect || "ssh";
if (console.log("currentTypeConnect terminal", k), e.computingId && e.userToken) {
const m = await b.encryptForServer(e.userToken, A);
a = {
type: "encrypted-auth",
computingId: e.computingId,
encryptedToken: m,
clientPublicKey: await b.exportPublicKey(),
// 🔥 Gửi kèm typeConnect để Proxy biết đường phân luồng
typeConnect: k
}, l && o.writeln("🔐 Sent encrypted computing credentials...");
} else {
const m = await b.encryptForServer(e.password, A);
a = {
type: "encrypted-auth",
host: e.host,
username: e.username,
encryptedPassword: m,
clientPublicKey: await b.exportPublicKey(),
typeConnect: k
}, l && o.writeln("🔐 Sent encrypted SSH credentials...");
}
S.send(h, y, a);
} catch (d) {
throw r.error("Failed to send encrypted auth", { error: d.message }), o.writeln(`\r
❌ Encryption failed: ${d.message}\r
`), o.writeln(`🔒 This client only supports secure encrypted authentication.\r
`), S.removeSubscriber(h, y), d;
}
}, Y = () => {
if ($)
return;
$ = !0, C++;
const d = Math.min(1e3 * Math.pow(2, C - 1), 3e4);
o.writeln(
`\r
Attempting to reconnect (${C}/${P}) in ${d / 1e3}s...\r
`
), U = setTimeout(() => {
M();
}, d);
}, Z = async () => {
E = !0, C = 0, $ = !1, l && o.writeln(
C > 0 ? `\r
✅ Reconnected to SSH relay server\r
` : "✅ WebSocket connected to " + h
), l && o.writeln("🔑 Waiting for server public key...");
}, J = async (d) => {
try {
if (!d || !d.data)
return;
let a;
if (typeof d.data == "string")
try {
a = JSON.parse(d.data);
} catch {
o.write(d.data);
return;
}
else
a = d.data;
if (console.log(`📩 Message from Server [${a.type}]:`, a), a.type === "rdp-redirect") {
r.info("🚀 Received RDP redirect command", { url: a.url }), typeof o._onRdpRedirect == "function" && o._onRdpRedirect(a);
return;
}
if (!f.validateMessageSize(d.data, 5e4)) {
r.logSecurity("message_too_large", { size: d.data.length });
return;
}
if (a.type === "welcome") {
try {
if (a.publicKey)
A = await b.importServerPublicKey(a.publicKey), l && o.writeln("🔑 Server public key received"), await G();
else {
o.writeln(`\r
❌ Server does not support encrypted authentication\r
`), o.writeln(`🔒 This client requires RSA encryption for security\r
`), S.removeSubscriber(h, y);
return;
}
} catch (m) {
r.error("Failed to process welcome message", { error: m.message }), o.writeln(`\r
❌ Failed to process server welcome: ${m.message}\r
`), S.removeSubscriber(h, y);
return;
}
return;
}
if (!(!a.computingId || a.computingId === x))
return;
if (a.type === "data")
try {
const m = window.atob(a.data), T = new Uint8Array(m.length);
for (let R = 0; R < m.length; R++)
T[R] = m.charCodeAt(R);
const F = new TextDecoder().decode(T);
o.write(F);
} catch (m) {
r.error("Failed to decode terminal data", { error: m.message });
try {
o.write(window.atob(a.data));
} catch {
o.write(a.data);
}
}
else
a.type === "error" ? (r.error("Server error received", {
code: a.code,
message: a.message
}), o.writeln(`\r
❌ Error: ${a.message}\r
`)) : a.type === "status" && a.status === "authenticated" ? (z = !0, console.log("Xác thực thành công"), l && o.writeln(`\r
✅ Connected to ${e.host} as ${e.username}\r
`)) : a.type === "status" && a.status === "closed" ? l && o.writeln(`\r
🔌 SSH connection closed\r
`) : a.type === "auth-success" ? (z = !0, console.log("Xác thực thành công"), l && o.writeln(`\r
✅ Authentication successful!\r
`), a.host && a.username && (e.host = a.host, e.username = a.username, e.port = a.port || 22)) : a.type === "session-created" ? z = !0 : a.type === "auth-error" && (r.logAuth("auth_failed", { message: a.message }), o.writeln(`\r
❌ Authentication failed: ${a.message}\r
`));
} catch {
d && d.data && o.write(d.data);
}
}, X = (d) => {
E = !1, o.writeln(`\r
WebSocket error: ${d?.message || "Unknown error"}\r
`), o.writeln(`\r
Please check if the SSH proxy server is running at ${h}\r
`);
}, Q = (d) => {
if (!d)
return;
if (E = !1, d.code === 1e3) {
o.writeln(`\r
Connection to SSH relay server closed normally\r
`);
return;
}
const k = {
1006: "Connection lost (network issue or server restart)",
1001: "Server going away",
1002: "Protocol error",
1003: "Unsupported data type",
1011: "Server error",
1012: "Server restart",
1013: "Server overloaded"
}[d.code] || `Unknown error (Code: ${d.code})`;
o.writeln(`\r
Connection to SSH relay server closed: ${k}\r
`), d.reason && o.writeln(`Reason: ${d.reason}\r
`), H && !$ && C < P ? Y() : H && C >= P && o.writeln(`\r
Max reconnection attempts reached. Please refresh the page.\r
`);
}, M = async () => {
if (l && (o.writeln(`🔌 Connecting to ${h}...`), o.writeln(`📱 Tab ID: ${I}`), o.writeln(`💻 Computing ID: ${x}`)), !await q()) {
o.writeln(`\r
❌ Failed to initialize encryption\r
`);
return;
}
try {
const a = {
onOpen: Z,
onMessage: J,
onError: X,
onClose: Q
};
await S.getConnection(h, y, a), r.info("Connected to WebSocket pool", {
wsUrl: h,
tabId: I,
computingId: x,
connectionKey: y,
totalConnections: S.getConnectionCount(h)
});
} catch (a) {
r.error("Failed to connect", { error: a.message, wsUrl: h }), o.writeln(`\r
❌ Failed to connect: ${a.message}\r
`);
}
};
return o.onData((d) => {
if (E && S.isConnected(h, y)) {
if (!f.validateMessageSize(d, 1e4)) {
r.warn("Terminal input too large", { size: d.length });
return;
}
let a;
try {
const m = new TextEncoder().encode(d), T = String.fromCharCode(...m);
a = window.btoa(T);
} catch (m) {
r.error("Failed to encode terminal data", { error: m.message });
try {
a = window.btoa(d);
} catch {
const F = d.replace(/[^\x00-\x7F]/g, "?");
a = window.btoa(F);
}
}
const k = {
type: "data",
data: a,
computingId: x
// 🔥 FIX: Dùng computingId thay vì sshConfig.computingId
};
try {
S.send(h, y, k);
} catch (m) {
r.error("Failed to send terminal data", { error: m.message });
}
}
}), setTimeout(() => {
M();
}, 100), g.fit(), {
terminal: o,
fitAddon: g,
searchAddon: w,
reconnect: async () => {
await M();
},
resize: () => {
if (!(!o || !g))
if (g.fit(), S.isConnected(h, y) && z) {
const d = {
type: "resize",
cols: o.cols,
rows: o.rows
};
try {
S.send(h, y, d), console.log(`📏 Server PTY Resized to: ${o.cols}x${o.rows}`);
} catch {
console.warn("⚠️ Failed to sync resize to server");
}
} else
console.log("⏳ Resize deferred: Waiting for authentication...");
},
dispose: () => {
console.log(`🗑️ Disposing terminal [${y}]`), U && clearTimeout(U), S.removeSubscriber(h, y), o.dispose(), r.info("Terminal disposed", {
wsUrl: h,
tabId: I,
computingId: x,
connectionKey: y,
remainingConnections: S.getConnectionCount(h)
});
},
getConnectionInfo: () => ({
wsUrl: h,
tabId: I,
computingId: x,
connectionKey: y,
connected: S.isConnected(h, y),
totalConnections: S.getConnectionCount(h)
})
};
}
const we = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
createTerminal: W
}, Symbol.toStringTag, { value: "Module" })), Ae = {
name: "SSHTerminal",
props: {
computingId: {
type: String,
required: !0
},
userToken: {
type: String,
required: !0
},
websocketUrl: {
type: String,
default: "wss://ssh-proxy.dev.longvan.vn"
},
options: {
type: Object,
default: function() {
return {};
}
}
},
data: function() {
return {
terminal: null
};
},
mounted: function() {
this.initTerminal(), window.addEventListener("resize", this.handleResize);
},
beforeDestroy: function() {
this.terminal && this.terminal.dispose(), window.removeEventListener("resize", this.handleResize);
},
methods: {
initTerminal: function() {
var u = this, e = this.$refs.terminalContainer;
Promise.all([
Promise.resolve().then(() => we),
Promise.resolve().then(() => me),
Promise.resolve().then(() => fe)
]).then(function(t) {
var r = t[0].createTerminal, n = t[1].default, i = t[2].default, s = i.createContextLogger("Vue2Terminal"), c = {
computingId: u.computingId,
userToken: u.userToken,
wsUrl: u.websocketUrl
}, l = n.validateComputingConfig(c);
if (!l.isValid) {
s.error("Invalid Computing ID config in Vue2 component", { errors: l.errors }), u.$emit("error", {
message: "Invalid Computing ID configuration",
errors: l.errors
});
return;
}
var o = n.validateTerminalOptions(u.options);
if (!o.isValid) {
s.error("Invalid terminal options in Vue2 component", { errors: o.errors }), u.$emit("error", {
message: "Invalid terminal options",
errors: o.errors
});
return;
}
try {
u.terminal = r(e, c, u.options), s.info("Terminal created successfully with Computing ID"), u.$emit("ready", u.terminal);
} catch (g) {
s.error("Failed to create terminal", { error: g.message }), u.$emit("error", {
message: "Failed to create terminal",
error: g.message
});
}
}).catch(function(t) {
console.error("Failed to load terminal dependencies:", t), u.$emit("error", {
message: "Failed to load terminal dependencies",
error: t.message
});
});
},
handleResize: function() {
this.terminal && this.terminal.resize();
}
},
render: function(u) {
return u("div", {
ref: "terminalContainer",
class: "ssh-terminal-container",
style: {
width: "100%",
height: "100%",
minHeight: "300px",
backgroundColor: "#000"
}
});
}
};
const be = (u, e) => {
const t = u.__vccOpts || u;
for (const [r, n] of e)
t[r] = n;
return t;
}, Se = {
__name: "SSHTerminal",
props: {
computingId: { type: String, required: !0 },
userToken: { type: String, required: !0 },
websocketUrl: { type: String, default: "wss://ssh-proxy.dev.longvan.vn" },
typeConnect: { type: Object },
options: { type: Object, default: () => ({}) },
isActive: { type: Boolean, default: !0 },
resizeSignal: { type: Number, default: 0 },
tabClientId: { type: String, required: !0 }
},
emits: ["ready", "error", "rdp-redirect"],
setup(u, { expose: e, emit: t }) {
const r = u, n = t, i = se(null);
let s = null, c = null, l = null;
ae(() => {
g();
}), ce(() => {
s && s.dispose(), window.removeEventListener("resize", o), c && c.disconnect(), l && clearTimeout(l);
}), V(
() => r.resizeSignal,
() => {
N(() => {
setTimeout(o, 150);
});
}
), V(
() => r.isActive,
(w) => {
w && s && N(() => {
s.terminal?.focus();
});
}
);
function o() {
l && clearTimeout(l), l = setTimeout(() => {
p();
}, 250);
}
function g() {
const w = i.value, h = _.createContextLogger("Vue3Terminal"), v = {
computingId: r.computingId,
userToken: r.userToken,
wsUrl: r.websocketUrl,
typeConnect: r.typeConnect,
tabClientId: r.tabClientId
}, E = f.validateComputingConfig(v);
if (!E.isValid) {
n("error", { message: "Invalid Computing ID configuration", errors: E.errors });
return;
}
try {
s = W(w, v, r.options), s._onRdpRedirect = (b) => {
n("rdp-redirect", b);
}, c = new ResizeObserver((b) => {
for (let A of b) {
const { width: C, height: P } = A.contentRect;
C > 0 && P > 0 && o();
}
}), i.value && c.observe(i.value), n("ready", s);
} catch (b) {
h.error("Failed to create terminal", { error: b.message }), n("error", { message: "Failed to create terminal", error: b.message });
}
}
function p() {
s && (console.log("📏 Terminal resizing to fit container..."), s.resize());
}
return e({
getTerminal: () => s,
resize: p
}), (w, h) => (le(), de("div", {
class: K(["terminal-wrapper", `computingId: ${r.computingId}`])
}, [
ue("div", {
ref_key: "terminalContainer",
ref: i,
class: K(`ssh-terminal-container ${r.computingId}`)
}, null, 2)
], 2));
}
}, Te = /* @__PURE__ */ be(Se, [["__scopeId", "data-v-0a812d31"]]);
class ve extends HTMLElement {
constructor() {
super(), this._terminalInstance = null, this._resizeObserver = null, this._shadow = this.attachShadow({ mode: "open" }), this._sshConfig = null, this._wsUrl = null, this._options = {}, this._container = document.createElement("div"), this._container.style.width = "100%", this._container.style.height = "100%", this._container.style.background = "#000", this._container.className = "ssh-terminal-container", this._injectStyles(), this._shadow.appendChild(this._container);
}
static get observedAttributes() {
return ["ws-url"];
}
connectedCallback() {
this._setupResizeObserver();
}
disconnectedCallback() {
this._cleanup();
}
attributeChangedCallback(e, t, r) {
e === "ws-url" && (this._wsUrl = r);
}
// 🔒 Secure method to set SSH configuration với validation
setConfig(e, t = {}) {
const r = _.createContextLogger("WebComponent");
if (this._options = { ...this._options, ...t }, e.computingId && e.userToken) {
const n = f.validateComputingConfig(e);
if (!n.isValid) {
const i = `Invalid computing configuration: ${n.errors.join(", ")}`;
r.error("Invalid computing config in setConfig", { errors: n.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${i}</p>`, this._dispatchEvent("error", { message: i, errors: n.errors });
return;
}
this._sshConfig = {
computingId: f.sanitizeString(e.computingId),
userToken: e.userToken,
// Không sanitize token
wsUrl: e.wsUrl || this._wsUrl || "wss://ssh-proxy.dev.longvan.vn"
};
} else {
const n = f.validateSSHConfig(e);