ssh-terminal
Version:
SSH Terminal component based on xterm.js for multiple frontend frameworks
1,436 lines • 55.7 kB
JavaScript
var ee = Object.defineProperty;
var te = (m, e, t) => e in m ? ee(m, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : m[e] = t;
var D = (m, e, t) => (te(m, 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 N, nextTick as V, 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 o = t.indexOf(r) + r.length, s = t.indexOf(n);
if (o === -1 || s === -1 || o >= s)
throw new Error("Invalid PEM format - malformed headers");
const d = t.substring(o, s).replace(/\s+/g, "");
if (d.length === 0)
throw new Error("Empty PEM content after cleaning");
let i;
try {
i = window.atob(d);
} catch {
throw new Error("Invalid base64 content in PEM");
}
const h = new Uint8Array(i.length);
for (let g = 0; g < i.length; g++)
h[g] = i.charCodeAt(g);
return this.useWebCrypto ? (this.serverPublicKey = await window.crypto.subtle.importKey(
"spki",
h.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 o = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
t,
n
);
console.log("🔧 Encryption successful, result length:", o.byteLength);
const s = btoa(String.fromCharCode(...new Uint8Array(o)));
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 o = n.encrypt(e, "RSA-OAEP", {
md: r.md.sha256.create(),
mgf1: {
md: r.md.sha256.create()
}
}), s = btoa(o);
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 o = new Uint8Array(256);
if (window.crypto && window.crypto.getRandomValues) {
window.crypto.getRandomValues(o);
const l = new TextEncoder().encode(e);
for (let d = 0; d < l.length && d < o.length; d++)
o[d] ^= l[d];
} else {
const l = new TextEncoder().encode(e);
for (let d = 0; d < o.length; d++)
o[d] = (l[d % l.length] + d + 42) % 256;
}
const s = btoa(String.fromCharCode(...o));
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), o = window.crypto.getRandomValues(new Uint8Array(12)), s = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: o
},
t,
n
), l = new Uint8Array(o.length + s.byteLength);
return l.set(o), l.set(new Uint8Array(s), o.length), btoa(String.fromCharCode(...l));
} 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 i = 0; i < r.length; i++)
n[i] = r.charCodeAt(i);
const o = n.slice(0, 12), s = n.slice(12), l = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: o
},
t,
s
);
return new TextDecoder().decode(l);
} 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 o = e.charCodeAt(n);
t = (t << 5) - t + o, 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 y {
/**
* 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, o] of Object.entries(e.theme))
r.includes(n) && typeof o == "string" && (this.isValidColor(o) || 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, o] of Object.entries(e)) {
const s = n.toLowerCase();
t.some((l) => s.includes(l)) ? r[n] = "***HIDDEN***" : typeof o == "object" && o !== null ? r[n] = this.sanitizeForLogging(o) : r[n] = o;
}
return r;
}
}
const 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 = y.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 o = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${e.toUpperCase()}] [CLIENT]`;
if (r) {
const s = this.sanitizeData(r);
return `${o} ${t} ${JSON.stringify(s)}`;
}
return `${o} ${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 M = L;
const T = new M();
class me {
constructor() {
this.connections = /* @__PURE__ */ new Map(), this.contextLogger = T.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 o = n.get(t), s = o.socket;
if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING)
return this.contextLogger.info("Reusing existing WebSocket", { connectionKey: t }), o.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: o, onClose: s, onOpen: l } = r;
this.contextLogger.info("Creating new WebSocket connection", { wsUrl: e, connectionKey: t }), console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), console.log("🔌 Creating WebSocket");
const d = new WebSocket(e), i = this.connections.get(e);
return i.set(t, { socket: d, handlers: r }), d.onopen = () => {
if (console.log(`✅ [${t}] WebSocket OPENED`), this.contextLogger.info("WebSocket opened", { wsUrl: e, connectionKey: t }), l)
try {
l();
} catch (h) {
this.contextLogger.error("Error in onOpen handler", { error: h.message });
}
}, d.onmessage = (h) => {
const g = i.get(t);
if (!g)
return;
const w = g.handlers;
try {
w.onMessage && w.onMessage(h);
} catch (u) {
this.contextLogger.error("Error in onMessage handler", {
connectionKey: t,
error: u.message
});
}
}, d.onerror = (h) => {
console.error(`❌ [${t}] WebSocket ERROR:`, h), this.contextLogger.error("WebSocket error", { wsUrl: e, connectionKey: t, error: h });
const g = i.get(t);
if (g && g.handlers.onError)
try {
g.handlers.onError(h);
} catch (w) {
this.contextLogger.error("Error in onError handler", { error: w.message });
}
}, d.onclose = (h) => {
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), console.log(`🚪 [${t}] WebSocket CLOSED`), console.log("Code:", h.code), console.log("Reason:", h.reason || "(empty)"), console.log("Was Clean:", h.wasClean), console.log("Timestamp:", (/* @__PURE__ */ new Date()).toISOString()), console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), this.contextLogger.info("WebSocket closed", {
wsUrl: e,
connectionKey: t,
code: h.code,
reason: h.reason,
wasClean: h.wasClean
});
const g = i.get(t), w = g ? g.handlers : null;
if (i.delete(t), console.log(`📊 After close - Remaining connections: ${i.size}`), i.size === 0 && (this.connections.delete(e), this.contextLogger.info("All connections closed for URL", { wsUrl: e })), w && w.onClose)
try {
w.onClose(h);
} catch (u) {
this.contextLogger.error("Error in onClose handler", { error: u.message });
}
}, d;
}
/**
* 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 o = n.socket;
o && o.readyState !== WebSocket.CLOSED && o.readyState !== WebSocket.CLOSING && o.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 o = n.get(t);
if (!o)
throw new Error(`No connection found for key: ${t}`);
const s = o.socket;
if (s.readyState !== WebSocket.OPEN)
throw new Error(`Connection not ready (state: ${s.readyState})`);
const l = typeof r == "string" ? r : JSON.stringify(r);
s.send(l);
}
/**
* 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 o = r.socket;
o.readyState !== WebSocket.CLOSED && o.readyState !== WebSocket.CLOSING && o.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, o) => {
e[r].connections.push({
key: o,
state: n.socket.readyState,
stateText: ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][n.socket.readyState]
});
});
}), e;
}
}
const S = new me(), 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 F(m, e, t = {}) {
const r = T.createContextLogger("Terminal");
if (!m)
throw r.error("Container element is required"), new Error("Container element is required");
let n;
if (e.computingId && e.userToken) {
if (n = y.validateComputingConfig(e), !n.isValid)
throw r.error("Invalid computing configuration", { errors: n.errors }), new Error(`Computing configuration invalid: ${n.errors.join(", ")}`);
} else if (n = y.validateSSHConfig(e), !n.isValid)
throw r.error("Invalid SSH configuration", { errors: n.errors }), new Error(`SSH configuration invalid: ${n.errors.join(", ")}`);
const s = y.validateTerminalOptions(t);
if (!s.isValid)
throw r.error("Invalid terminal options", { errors: s.errors }), new Error(`Terminal options invalid: ${s.errors.join(", ")}`);
const l = {
...j,
...t.theme ? { theme: { ...j.theme, ...t.theme } } : {},
...t
}, d = t.showConnectionLogs === !0, i = new re(l), h = new ne(), g = new oe(), w = new ie();
i.loadAddon(h), i.loadAddon(g), i.loadAddon(w), i.open(m), setTimeout(() => {
h.fit();
}, 0);
const u = e.wsUrl || "wss://ssh-proxy.dev.longvan.vn";
if (!y.isValidWebSocketURL(u))
throw r.error("Invalid WebSocket URL", { wsUrl: u }), new Error("Invalid WebSocket URL");
const v = y.validateSecureConnection(u);
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((c) => {
i.writeln(`⚠️ ${c}`);
});
let E = !1, b = null, I = null, C = 0;
const P = l.reconnection?.maxAttempts || 5, H = l.reconnection?.enabled !== !1;
let W = null, $ = !1, z = !1;
const x = e.computingId || `direct-${Date.now()}`, B = e.tabClientId || `direct-${Date.now()}`, A = ge.getTabId(), f = `${A}:${B}`;
r.info("Creating terminal", {
tabId: A,
computingId: x,
connectionKey: f
}), console.log("Creating terminal", {
tabId: A,
computingId: x,
connectionKey: f
});
const G = async () => {
try {
return b = new pe(), await b.generateKeyPair(), !0;
} catch (c) {
return r.error("Failed to initialize encryption", { error: c.message }), !1;
}
}, q = async () => {
try {
const c = y.validateEncryptionReadiness(b, I);
if (!c.isValid)
throw new Error(`Encryption not ready: ${c.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 p = await b.encryptForServer(e.userToken, I);
a = {
type: "encrypted-auth",
computingId: e.computingId,
encryptedToken: p,
clientPublicKey: await b.exportPublicKey(),
// 🔥 Gửi kèm typeConnect để Proxy biết đường phân luồng
typeConnect: k
}, d && i.writeln("🔐 Sent encrypted computing credentials...");
} else {
const p = await b.encryptForServer(e.password, I);
a = {
type: "encrypted-auth",
host: e.host,
username: e.username,
encryptedPassword: p,
clientPublicKey: await b.exportPublicKey(),
typeConnect: k
}, d && i.writeln("🔐 Sent encrypted SSH credentials...");
}
S.send(u, f, a);
} catch (c) {
throw r.error("Failed to send encrypted auth", { error: c.message }), i.writeln(`\r
❌ Encryption failed: ${c.message}\r
`), i.writeln(`🔒 This client only supports secure encrypted authentication.\r
`), S.removeSubscriber(u, f), c;
}
}, Y = () => {
if ($)
return;
$ = !0, C++;
const c = Math.min(1e3 * Math.pow(2, C - 1), 3e4);
i.writeln(
`\r
Attempting to reconnect (${C}/${P}) in ${c / 1e3}s...\r
`
), W = setTimeout(() => {
O();
}, c);
}, Z = async () => {
E = !0, C = 0, $ = !1, d && i.writeln(
C > 0 ? `\r
✅ Reconnected to SSH relay server\r
` : "✅ WebSocket connected to " + u
), d && i.writeln("🔑 Waiting for server public key...");
}, J = async (c) => {
try {
if (!c || !c.data)
return;
let a;
if (typeof c.data == "string")
try {
a = JSON.parse(c.data);
} catch {
i.write(c.data);
return;
}
else
a = c.data;
if (console.log(`📩 Message from Server [${a.type}]:`, a), a.type === "rdp-redirect") {
r.info("🚀 Received RDP redirect command", { url: a.url }), typeof i._onRdpRedirect == "function" && i._onRdpRedirect(a);
return;
}
if (!y.validateMessageSize(c.data, 5e4)) {
r.logSecurity("message_too_large", { size: c.data.length });
return;
}
if (a.type === "welcome") {
try {
if (a.publicKey)
I = await b.importServerPublicKey(a.publicKey), d && i.writeln("🔑 Server public key received"), await q();
else {
i.writeln(`\r
❌ Server does not support encrypted authentication\r
`), i.writeln(`🔒 This client requires RSA encryption for security\r
`), S.removeSubscriber(u, f);
return;
}
} catch (p) {
r.error("Failed to process welcome message", { error: p.message }), i.writeln(`\r
❌ Failed to process server welcome: ${p.message}\r
`), S.removeSubscriber(u, f);
return;
}
return;
}
if (!(!a.computingId || a.computingId === x))
return;
if (a.type === "data")
try {
const p = window.atob(a.data), _ = new Uint8Array(p.length);
for (let R = 0; R < p.length; R++)
_[R] = p.charCodeAt(R);
const U = new TextDecoder().decode(_);
i.write(U);
} catch (p) {
r.error("Failed to decode terminal data", { error: p.message });
try {
i.write(window.atob(a.data));
} catch {
i.write(a.data);
}
}
else
a.type === "error" ? (r.error("Server error received", {
code: a.code,
message: a.message
}), i.writeln(`\r
❌ Error: ${a.message}\r
`)) : a.type === "status" && a.status === "authenticated" ? (z = !0, console.log("Xác thực thành công"), d && i.writeln(`\r
✅ Connected to ${e.host} as ${e.username}\r
`)) : a.type === "status" && a.status === "closed" ? d && i.writeln(`\r
🔌 SSH connection closed\r
`) : a.type === "auth-success" ? (z = !0, console.log("Xác thực thành công"), d && i.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 }), i.writeln(`\r
❌ Authentication failed: ${a.message}\r
`));
} catch {
c && c.data && i.write(c.data);
}
}, X = (c) => {
E = !1, i.writeln(`\r
WebSocket error: ${c?.message || "Unknown error"}\r
`), i.writeln(`\r
Please check if the SSH proxy server is running at ${u}\r
`);
}, Q = (c) => {
if (!c)
return;
if (E = !1, c.code === 1e3) {
i.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"
}[c.code] || `Unknown error (Code: ${c.code})`;
i.writeln(`\r
Connection to SSH relay server closed: ${k}\r
`), c.reason && i.writeln(`Reason: ${c.reason}\r
`), H && !$ && C < P ? Y() : H && C >= P && i.writeln(`\r
Max reconnection attempts reached. Please refresh the page.\r
`);
}, O = async () => {
if (d && (i.writeln(`🔌 Connecting to ${u}...`), i.writeln(`📱 Tab ID: ${A}`), i.writeln(`💻 Computing ID: ${x}`)), !await G()) {
i.writeln(`\r
❌ Failed to initialize encryption\r
`);
return;
}
try {
const a = {
onOpen: Z,
onMessage: J,
onError: X,
onClose: Q
};
await S.getConnection(u, f, a), r.info("Connected to WebSocket pool", {
wsUrl: u,
tabId: A,
computingId: x,
connectionKey: f,
totalConnections: S.getConnectionCount(u)
});
} catch (a) {
r.error("Failed to connect", { error: a.message, wsUrl: u }), i.writeln(`\r
❌ Failed to connect: ${a.message}\r
`);
}
};
return i.onData((c) => {
if (E && S.isConnected(u, f)) {
if (!y.validateMessageSize(c, 1e4)) {
r.warn("Terminal input too large", { size: c.length });
return;
}
let a;
try {
const p = new TextEncoder().encode(c), _ = String.fromCharCode(...p);
a = window.btoa(_);
} catch (p) {
r.error("Failed to encode terminal data", { error: p.message });
try {
a = window.btoa(c);
} catch {
const U = c.replace(/[^\x00-\x7F]/g, "?");
a = window.btoa(U);
}
}
const k = {
type: "data",
data: a,
computingId: x
// 🔥 FIX: Dùng computingId thay vì sshConfig.computingId
};
try {
S.send(u, f, k);
} catch (p) {
r.error("Failed to send terminal data", { error: p.message });
}
}
}), setTimeout(() => {
O();
}, 100), h.fit(), {
terminal: i,
fitAddon: h,
searchAddon: w,
reconnect: async () => {
await O();
},
resize: () => {
if (!(!i || !h))
if (h.fit(), S.isConnected(u, f) && z) {
const c = {
type: "resize",
cols: i.cols,
rows: i.rows
};
try {
S.send(u, f, c), console.log(`📏 Server PTY Resized to: ${i.cols}x${i.rows}`);
} catch {
console.warn("⚠️ Failed to sync resize to server");
}
} else
console.log("⏳ Resize deferred: Waiting for authentication...");
},
dispose: () => {
console.log(`🗑️ Disposing terminal [${f}]`), W && clearTimeout(W), S.removeSubscriber(u, f), i.dispose(), r.info("Terminal disposed", {
wsUrl: u,
tabId: A,
computingId: x,
connectionKey: f,
remainingConnections: S.getConnectionCount(u)
});
},
getConnectionInfo: () => ({
wsUrl: u,
tabId: A,
computingId: x,
connectionKey: f,
connected: S.isConnected(u, f),
totalConnections: S.getConnectionCount(u)
})
};
}
const fe = (m, e) => {
const t = m.__vccOpts || m;
for (const [r, n] of e)
t[r] = n;
return t;
}, ye = {
__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(m, { expose: e, emit: t }) {
const r = m, n = t, o = se(null);
let s = null, l = null, d = null;
ae(() => {
h();
}), ce(() => {
s && s.dispose(), window.removeEventListener("resize", i), l && l.disconnect(), d && clearTimeout(d);
}), N(
() => r.resizeSignal,
() => {
V(() => {
setTimeout(i, 150);
});
}
), N(
() => r.isActive,
(w) => {
w && s && V(() => {
s.terminal?.focus();
});
}
);
function i() {
d && clearTimeout(d), d = setTimeout(() => {
g();
}, 250);
}
function h() {
const w = o.value, u = T.createContextLogger("Vue3Terminal"), v = {
computingId: r.computingId,
userToken: r.userToken,
wsUrl: r.websocketUrl,
typeConnect: r.typeConnect,
tabClientId: r.tabClientId
}, E = y.validateComputingConfig(v);
if (!E.isValid) {
n("error", { message: "Invalid Computing ID configuration", errors: E.errors });
return;
}
try {
s = F(w, v, r.options), s._onRdpRedirect = (b) => {
n("rdp-redirect", b);
}, l = new ResizeObserver((b) => {
for (let I of b) {
const { width: C, height: P } = I.contentRect;
C > 0 && P > 0 && i();
}
}), o.value && l.observe(o.value), n("ready", s);
} catch (b) {
u.error("Failed to create terminal", { error: b.message }), n("error", { message: "Failed to create terminal", error: b.message });
}
}
function g() {
s && (console.log("📏 Terminal resizing to fit container..."), s.resize());
}
return e({
getTerminal: () => s,
resize: g
}), (w, u) => (le(), de("div", {
class: K(["terminal-wrapper", `computingId: ${r.computingId}`])
}, [
ue("div", {
ref_key: "terminalContainer",
ref: o,
class: K(`ssh-terminal-container ${r.computingId}`)
}, null, 2)
], 2));
}
}, Ee = /* @__PURE__ */ fe(ye, [["__scopeId", "data-v-0a812d31"]]);
class we 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 = T.createContextLogger("WebComponent");
if (this._options = { ...this._options, ...t }, e.computingId && e.userToken) {
const n = y.validateComputingConfig(e);
if (!n.isValid) {
const o = `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;">${o}</p>`, this._dispatchEvent("error", { message: o, errors: n.errors });
return;
}
this._sshConfig = {
computingId: y.sanitizeString(e.computingId),
userToken: e.userToken,
// Không sanitize token
wsUrl: e.wsUrl || this._wsUrl || "wss://ssh-proxy.dev.longvan.vn"
};
} else {
const n = y.validateSSHConfig(e);
if (!n.isValid) {
const o = `Invalid SSH configuration: ${n.errors.join(", ")}`;
r.error("Invalid SSH config in setConfig", { errors: n.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${o}</p>`, this._dispatchEvent("error", { message: o, errors: n.errors });
return;
}
this._sshConfig = {
host: y.sanitizeString(e.host),
username: y.sanitizeString(e.username),
password: e.password,
// Không sanitize password
wsUrl: e.wsUrl || this._wsUrl || "wss://ssh-proxy.dev.longvan.vn"
};
}
this._sshConfig.computingId ? this.initComputingTerminal() : this.initTerminal();
}
_injectStyles() {
const e = document.createElement("style");
e.textContent = `
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.ssh-terminal-container[data-v-f4a58236],.ssh-terminal-container[data-v-b82cc34a]{width:100%;height:100%;min-height:300px;background-color:#000}
`, this._shadow.appendChild(e);
}
_setupResizeObserver() {
this._resizeObserver && this._resizeObserver.disconnect(), this._resizeObserver = new ResizeObserver(() => {
this._terminalInstance?.resize && (clearTimeout(this._resizeTimeout), this._resizeTimeout = setTimeout(() => {
this._terminalInstance.resize();
}, 100));
}), this._resizeObserver.observe(this);
}
_cleanup() {
this._resizeObserver && (this._resizeObserver.disconnect(), this._resizeObserver = null), this._resizeTimeout && clearTimeout(this._resizeTimeout), this._terminalInstance?.dispose && (this._terminalInstance.dispose(), this._terminalInstance = null);
}
_dispatchEvent(e, t = {}) {
this.dispatchEvent(
new CustomEvent(e, {
detail: t,
bubbles: !0,
composed: !0
})
);
}
initComputingTerminal() {
const e = T.createContextLogger("WebComponent");
try {
this._terminal = F(this._container, this._sshConfig, this._options), this._dispatchEvent("ready", {
message: "Computing terminal initialized",
computingId: this._sshConfig.computingId
});
} catch (t) {
e.error("Failed to create computing terminal", { error: t.message }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">Failed to create terminal: ${t.message}</p>`, this._dispatchEvent("error", { message: t.message });
}
}
initTerminal() {
const e = T.createContextLogger("WebComponent");
if (!this._sshConfig) {
const l = "SSH configuration not set. Call setConfig() first.";
e.error(l), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${l}</p>`, this._dispatchEvent("error", { message: l });
return;
}
const { host: t, username: r, password: n, wsUrl: o } = this._sshConfig;
let s;
if (this._sshConfig.computingId && this._sshConfig.token) {
if (s = y.validateComputingConfig(this._sshConfig), !s.isValid) {
const l = `Invalid computing configuration: ${s.errors.join(", ")}`;
e.error("Computing config validation failed in initTerminal", { errors: s.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${l}</p>`, this._dispatchEvent("error", { message: l, errors: s.errors });
return;
}
} else if (s = y.validateSSHConfig(this._sshConfig), !s.isValid) {
const l = `Invalid SSH configuration: ${s.errors.join(", ")}`;
e.error("SSH config validation failed in initTerminal", { errors: s.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${l}</p>`, this._dispatchEvent("error", { message: l, errors: s.errors });
return;
}
try {
this._terminalInstance = F(this._container, {
host: t,
username: r,
password: n,
wsUrl: o
}), setTimeout(() => {
this._dispatchEvent("ready", {
host: t,
// Only host is relatively safe to expose
wsUrl: o
});
}, 100);
} catch (l) {
const d = `Failed to create terminal: ${l.message}`;
e.error("Failed to create terminal", { error: l.message }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${d}</p>`, this._dispatchEvent("error", { message: d, error: l });
}
}
// Public API methods
reconnect() {
try {
this._terminalInstance?.reconnect(), this._dispatchEvent("reconnecting");
} catch (e) {
console.error("Reconnect failed:", e), this._dispatchEvent("error", { message: "Reconnect failed", error: e });
}
}
resize() {
try {
this._terminalInstance?.resize();
} catch (e) {
console.error("Resize failed:", e);
}
}
search(e) {
try {
return this._terminalInstance?.search(e);
} catch (t) {
return console.error("Search failed:", t), !1;
}
}
searchPrevious(e) {
try {
return this._terminalInstance?.searchPrevious(e);
} catch (t) {
return console.error("Search previous failed:", t), !1;
}
}
// Get terminal instance for advanced usage
getTerminalInstance() {
return this._terminalInstance;
}
// Check if terminal is connected
isConnected() {
return this._terminalInstance && this._terminalInstance.terminal;
}
}
customElements.define("ssh-terminal", we);
typeof window < "u" && window.customElements && !window.customElements.get("ssh-terminal") && console.log("SSH Terminal Web Component registered for Vue 3 build");
export {
Ee as SSHTerminal,
we as SSHTerminalElement,
Ee as Vue3SSHTerminal,
F as createTerminal
};