feat: SSH 协议增强、文件传输优化及多标签页支持

SSH 协议层:
- 新增 Curve25519 密钥交换算法支持
- 新增 AES-CTR 加密模式 (aes256-ctr, aes192-ctr, aes128-ctr)
- 新增 HMAC 完整性校验 (hmac-sha2-256, hmac-sha2-512, hmac-sha1)
- 优化算法协商和密钥派生流程

前端:
- 文件传输从 Zmodem 迁移到 trzsz,支持拖拽上传和 tmux 兼容
- 新增多标签页 SSH 支持(登录用户可在新标签页打开独立终端)
- 修复 WebSocket 二进制数据处理问题

后端:
- 恢复 Hibernation API 支持长连接
- 兼容 Dropbear 等不发送 shell 确认的 SSH 服务器
- 关闭调试日志

构建:
- 迁移到 pnpm 包管理器
- 添加 GitHub Actions 自动部署
This commit is contained in:
TanXin committed 2026-06-23 13:57:11 +08:00
1 parent 0c16e3ca93
commit 06a79a33c2
27 files changed
+3584 -5648

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
# Cloudflare Workers 构建忽略文件
# 以下文件/目录的变动不会触发重新构建
# 文档文件
README.md
README_en.md
LICENSE
# 图片文件
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.ico
# Git 相关
.git/
.gitignore
# 临时文件
*.log
*.tmp
*.bak
.DS_Store
# 本地开发配置
.dev.vars
.wrangler/
+30
View File
@@ -0,0 +1,30 @@
name: Deploy to Cloudflare Workers
on:
push:
branches: [test]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: pnpm/action-setup@v4
with:
version: 'latest'
- name: Install dependencies
run: |
pnpm install
cd frontend && pnpm install
- name: Build frontend and deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm run deploy
+122 -48
View File
@@ -57,41 +57,86 @@
### 安全可靠
- **端到端加密**:完整的 SSH-2.0 协议实现,包括 ECDH 密钥交换、Ed25519 签名认证以及 AES-256-GCM 数据加密。
- **安全加固体系**:内置针对 IPv6 与保留地址的 SSRF 防护、API 请求频率限制(防爆破),并在本地使用 AES-GCM 算法加密存储您的服务器凭证。
- **端到端加密**:完整的 SSH-2.0 协议实现,支持 Curve25519-SHA256(优先)和 ECDH-NISTP256 密钥交换,AES-256-GCM(优先)/ AES-128-GCM / AES-256-CTR 数据加密,以及 HMAC-SHA2-256/512 完整性校验。
- **多算法主机密钥验证**:支持 Ed25519、ECDSA P-256、RSA 签名验证,首次连接展示 SHA-256 指纹(TOFU 模式)。
- **安全加固体系**:内置针对 IPv6 与保留地址的 SSRF 防护、API 请求频率限制(防爆破),并在本地使用 AES-256-GCM 算法加密存储您的服务器凭证。
- **人机验证**:支持 Cloudflare Turnstile 验证,防止恶意机器人滥用。
- **隔离的会话状态**:借助 Cloudflare Durable Objects 和 Hibernation API,每个终端会话都在沙盒内安全、持久地运行。
- **凭据零暴露**:One-Time-Token 机制确保密码/私钥不经过前端,完全在 Worker 内部安全流转。
<a id="features"></a>
## 核心特性
- **完整的 SSH 握手**:原生 TypeScript 实现 SSH 传输层协议与用户认证协议。
- **纯 TypeScript SSH-2.0 实现**:完全自研的 SSH 协议栈,不依赖任何第三方 SSH 库,基于 Web Crypto API 实现全部加密操作。
- **多算法密钥交换**:支持 Curve25519-SHA256(优先)和 ECDH-NISTP256 两种 KEX 算法,适配各类 SSH 服务器(包括 Dropbear)。
- **IPv4/IPv6 双栈**:完整支持 IPv4 和 IPv6 地址连接,包括 IPv6 方括号格式自动处理。
- **多种认证方式**:支持标准 SSH 密码认证以及基于 Ed25519 的纯文本私钥认证。
- **防范中间人攻击 (TOFU)**:首次连接自动提取服务器 Host Key(SHA-256 指纹)并显示,防止被恶意节点窃听。
- **防范中间人攻击 (TOFU)**:首次连接自动提取服务器 Host Key(SHA-256 指纹)并显示,支持 Ed25519/ECDSA/RSA 签名验证。
- **全功能极客终端**:基于 `@xterm/xterm` 与 `@xterm/addon-webgl` 硬件加速渲染引擎,保证海量日志输出顺滑不卡顿。
- **个性化 UI**:提供 Cyberpunk、Glacier、Gruvbox 等经典终端主题一键切换,支持移动端适配。
- **原生文件传输**:集成 zmodem.js,只需在终端中执行 `rz` / `sz` 命令,即可在浏览器直接与服务器双向拖放/下载文件。
- **原生文件传输**:集成 [trzsz.js](https://github.com/trzsz/trzsz.js),支持 `trz`(上传)/ `tsz`(下载)命令进行文件传输,兼容 tmux 会话。还支持拖拽文件到终端窗口直接上传、目录传输及断点续传等高级功能。(需远程服务器安装 [trzsz](https://trzsz.github.io/))
- **GitHub OAuth 集成**:支持 GitHub 登录,用户可保存和管理常用 SSH 服务器,实现一键连接。
<a id="architecture"></a>
## 架构说明
### 系统架构
```mermaid
flowchart TB
Browser["浏览器客户端<br/>(TypeScript + xterm.js)"]
CF["Cloudflare Edge Network"]
DO["Durable Object<br/>(SSH 会话管理)"]
Server["目标 SSH 服务器<br/>(如 Linux VPS)"]
subgraph "浏览器客户端"
UI["前端 UI<br/>TypeScript + xterm.js"]
Trzsz["trzsz 文件传输"]
end
subgraph "Cloudflare Edge Network"
Worker["Worker<br/>路由 + API"]
SSH_DO["SSHSessionDO<br/>SSH 会话管理"]
User_DO["UserDBDO<br/>用户数据管理"]
end
subgraph "目标服务器"
SSH["SSH 服务器<br/>(OpenSSH/Dropbear)"]
end
Browser <-->|"WebSocket<br/>(前端 UI 输入与终端输出)"| CF
CF <-->|"WebSocket"<br/>路由与长连接| DO
DO <-->|"TCP Socket<br/>@cloudflare/sockets"| Server
UI <-->|"WebSocket<br/>终端 I/O"| Worker
Trzsz <-->|"trzsz 协议"| UI
Worker <-->|"WebSocket"| SSH_DO
Worker <-->|"Internal API"| User_DO
SSH_DO <-->|"TCP Socket<br/>@cloudflare/sockets"| SSH
```
1. 用户在前端输入主机 IP、账号和密码。
### 核心组件
| 组件 | 文件 | 职责 |
|------|------|------|
| **Worker 入口** | `src/worker/index.ts` | HTTP 路由、API 处理、WebSocket 升级 |
| **SSHSessionDO** | `src/worker/durable-object.ts` | SSH 会话生命周期管理、SSRF 防护 |
| **UserDBDO** | `src/worker/user-db.ts` | 用户数据、服务器配置、速率限制(SQLite) |
| **SSHSession** | `src/worker/ssh-session.ts` | SSH 协议状态机(连接→版本→密钥交换→认证→交互) |
| **SSH 协议栈** | `src/ssh/*.ts` | 纯 TypeScript SSH-2.0 实现(传输层、加密、认证、通道) |
| **前端终端** | `frontend/src/terminal.ts` | xterm.js 封装、trzsz 集成、WebSocket 管理 |
### SSH 协议实现
本项目实现了完整的 SSH-2.0 协议栈:
| 层级 | 实现 | 支持算法 |
|------|------|----------|
| **密钥交换** | `kex-curve25519.ts` / `kex-ecdh.ts` | curve25519-sha256, ecdh-sha2-nistp256 |
| **数据加密** | `crypto.ts` | aes256-gcm, aes128-gcm, aes256-ctr, aes192-ctr, aes128-ctr |
| **完整性校验** | `crypto.ts` | hmac-sha2-256, hmac-sha2-512, hmac-sha1 |
| **主机密钥** | `ssh-session.ts` | Ed25519, ECDSA P-256, RSA |
| **用户认证** | `auth.ts` | 密码认证, Ed25519 公钥认证 |
| **通道管理** | `channel.ts` | session channel, PTY, shell, window-change |
### 数据流
1. 用户在前端输入主机 IP、账号和密码(或通过 GitHub OAuth 选择已保存的服务器)。
2. 前端与后端的 Durable Object 建立 WebSocket 连接。
3. DO 接收凭据,使用 `@cloudflare/sockets` 与目标 SSH 服务器建立 TCP 连接。
4. DO 纯代码实现 SSH 协议协商(密钥交换、密码认证等),并将加密后的终端数据通过 WebSocket 转发给前端。
3. SSHSessionDO 接收凭据,使用 `@cloudflare/sockets` 与目标 SSH 服务器建立 TCP 连接。
4. SSHSession 执行完整的 SSH 协议协商(版本交换→密钥交换→认证→打开通道→PTY→Shell)。
5. 加密的终端数据通过 WebSocket 在前端和 SSH 服务器之间双向转发。
<a id="quick-start"></a>
## 快速部署
@@ -107,9 +152,10 @@ flowchart TB
#### 方式一:通过 GitHub 绑定自动部署(推荐)
1. **Fork 本仓库** 到你的 GitHub 账号。
2. **修改域名**:在进行部署前,请先将 `wrangler.toml` 中的自定义域名改成你自己的域名(要求:域名需要先在 Cloudflare 中完成注册或接入)。
3. **一键部署**:登录 Cloudflare,进入 Workers & Pages 绑定你的 GitHub 账号,选择刚才 Fork 的仓库进行应用创建。
4. **填写构建命令**:在部署设置中,请务必将“构建命令”(Build command)填写为 `npm install && npm run build:frontend`,然后点击保存并部署(无需填写构建输出目录)。
2. **一键部署**:登录 Cloudflare,进入 Workers & Pages 绑定你的 GitHub 账号,选择刚才 Fork 的仓库进行应用创建。
3. **填写构建命令**:在部署设置中,请务必将"构建命令"(Build command)填写为 `pnpm run build:frontend`,然后点击保存并部署(无需填写构建输出目录)。
4. **访问应用**:部署成功后,可通过默认域名 `https://cloudssh.<你的子域>.workers.dev` 访问。
5. **绑定自定义域名**(可选):在 Cloudflare Dashboard 的 Workers 设置中,进入 "Triggers" → "Custom Domains" 添加你的域名。
#### 方式二:本地命令行部署
@@ -121,7 +167,8 @@ flowchart TB
2. **安装依赖**
```bash
npm install
npm install -g pnpm
pnpm install
```
3. **登录 Cloudflare**
@@ -131,22 +178,22 @@ flowchart TB
4. **一键部署**
```bash
npm run deploy
pnpm run deploy
```
部署完成后,Wrangler 会输出你的 Worker URL。打开浏览器访问该 URL,即可开始使用你的 Web SSH 终端。
5. **绑定自定义域名**(可选):在 Cloudflare Dashboard 的 Workers 设置中,进入 "Triggers" → "Custom Domains" 添加你的域名。
#### 可选:配置 Turnstile 人机验证
为防止恶意机器人滥用,建议启用 Cloudflare Turnstile 验证:
1. **创建 Turnstile Widget**:登录 [Cloudflare Dashboard](https://dash.cloudflare.com/),进入 Turnstile 页面创建一个新的 Widget。
2. **获取密钥**:创建后会获得一个 **Site Key**(公开)和一个 **Secret Key**(保密)。
3. **配置环境变量**:
- **方式一部署**:在 Cloudflare Dashboard 的 Workers 设置中,添加以下环境变量:
- `TURNSTILE_SECRET` = 你的 Secret Key
- `TURNSTILE_SITEKEY` = 你的 Site Key
- **方式二部署**:取消 `wrangler.toml` 中 `TURNSTILE_SECRET` 和 `TURNSTILE_SITEKEY` 的注释,填入对应密钥。
3. **配置环境变量**:在 Cloudflare Dashboard 的 Workers 设置中,进入 "Settings" → "Variables and Secrets",添加以下环境变量:
- `TURNSTILE_SECRET` = 你的 Secret Key(类型选择 Secret)
- `TURNSTILE_SITEKEY` = 你的 Site Key(类型选择 Text)
4. **重新部署**:运行部署命令使配置生效。
> **说明**:Turnstile 验证为会话级别,用户通过验证后当前会话内所有功能可用,关闭浏览器后需重新验证。
@@ -162,38 +209,66 @@ flowchart TB
- **Authorization callback URL**:`https://your-domain.com/api/auth/callback`
- 创建后获得 **Client ID**,点击 **Generate a new client secret** 生成 **Client Secret**(仅显示一次,请立即保存)
2. **配置环境变量**:
- **方式一部署**:在 Cloudflare Dashboard 的 Workers 设置中,添加以下环境变量:
- `GITHUB_CLIENT_ID` = 你的 Client ID
- `BASE_URL` = `https://your-domain.com`(你的部署域名)
- **方式二部署**:取消 `wrangler.toml` 中 `GITHUB_CLIENT_ID` 和 `BASE_URL` 的注释,填入对应值。
2. **配置环境变量**:在 Cloudflare Dashboard 的 Workers 设置中,进入 "Settings" → "Variables and Secrets",添加以下环境变量:
- `GITHUB_CLIENT_ID` = 你的 Client ID(类型选择 Text)
- `BASE_URL` = `https://your-domain.com`(你的部署域名,类型选择 Text)
- `GITHUB_CLIENT_SECRET` = 你的 Client Secret(类型选择 Secret)
3. **设置 Secrets(敏感密钥)**:
- 部署项目后,进入 Cloudflare Dashboard → 你的 Worker 项目 (`cloudssh`) → **Settings (设置)** → **Variables and Secrets (变量和机密)**。
- 点击 **Add (添加)** 环境变量:
- **Type (类型)**:选择 **Secret (机密)**(非常重要,不要选 Text)
- **变量名**:`GITHUB_CLIENT_SECRET`
- **值**:粘贴你获取到的 Client Secret
- 随后点击 **Save and deploy (保存并部署)**。
4. **重新部署**:如果你是刚刚修改了环境变量,且是首次启用该功能,请务必删除旧版并全新部署以初始化数据库。
3. **重新部署**:如果你是刚刚修改了环境变量,且是首次启用该功能,请务必删除旧版并全新部署以初始化数据库。
> **说明**:服务器凭据(密码/私钥)在数据库中使用 AES-256-GCM 加密存储,本地加密密钥将自动生成并安全地存储在数据库中(也可在环境变量中手动设置 `SESSION_SECRET` 来指定)。连接时凭据不经过前端,通过 one-time-token 机制安全传递。
> **注意**:首次启用此功能需要从零部署(删除旧 Worker 后重新部署),因为需要初始化新的 Durable Object。可通过 `npx wrangler delete cloudssh` 删除旧 Worker,然后运行 `npm run deploy` 重新部署。
> **注意**:首次启用此功能需要从零部署(删除旧 Worker 后重新部署),因为需要初始化新的 Durable Object。可通过 `npx wrangler delete cloudssh` 删除旧 Worker,然后运行 `pnpm run deploy` 重新部署。
<a id="development"></a>
## 开发说明
本项目分为两部分:
1. **Frontend (前端)**:在 `frontend/` 目录下,使用 Vite 构建。
2. **Worker (后端)**:在 `src/` 目录下,包含 Cloudflare Worker 入口与 SSH 协议的核心实现。
### 项目结构
本项目采用 pnpm monorepo 工作区结构:
在本地开发时,可以运行:
```bash
npm run dev
```
此命令将启动 Wrangler 的本地开发环境服务器。
CloudSSH/
├── src/ # 后端源码 (Cloudflare Worker)
│ ├── ssh/ # SSH 协议纯实现层
│ └── worker/ # Worker 入口和 Durable Objects
├── frontend/ # 前端源码 (独立 workspace)
│ ├── src/ # TypeScript + xterm.js + trzsz
│ └── package.json # 前端依赖
├── scripts/ # 构建脚本
├── pnpm-workspace.yaml # pnpm 工作区配置
└── wrangler.toml # Cloudflare 部署配置
```
### 本地开发
1. **安装依赖**
```bash
pnpm install
```
2. **启动开发服务器**
```bash
pnpm run dev
```
此命令将构建前端并启动 Wrangler 本地开发环境。
3. **仅构建前端**
```bash
pnpm run build:frontend
```
### 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| **前端** | TypeScript + Vite + xterm.js | Web 终端模拟器,WebGL 硬件加速 |
| **UI 框架** | Tailwind CSS (CDN) | 赛博朋克风格暗色主题 |
| **文件传输** | trzsz.js | 支持 trz/tsz 命令、拖拽上传、断点续传 |
| **后端** | Cloudflare Workers | Serverless 边缘计算 |
| **会话管理** | Durable Objects | SSH 会话隔离、Hibernation API |
| **数据存储** | Durable Objects SQLite | 用户数据、服务器配置 |
| **包管理** | pnpm (workspace) | Monorepo 依赖管理 |
<a id="license"></a>
## 开源协议
@@ -213,4 +288,3 @@ npm run dev
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=newbietan/CloudSSH&type=date&legend=top-left" />
</picture>
</a>
+122 -47
View File
@@ -57,41 +57,86 @@
### Secure and Reliable
- **End-to-End Encryption**: Complete implementation of the SSH-2.0 protocol, including ECDH key exchange, Ed25519 signature authentication, and AES-256-GCM data encryption.
- **Security Hardening**: Built-in SSRF protection against IPv6 and reserved IPs, API rate limiting (anti-brute force), and local AES-GCM encryption for your stored server credentials.
- **End-to-End Encryption**: Complete SSH-2.0 protocol implementation supporting Curve25519-SHA256 (preferred) and ECDH-NISTP256 key exchange, AES-256-GCM (preferred) / AES-128-GCM / AES-256-CTR data encryption, and HMAC-SHA2-256/512 integrity verification.
- **Multi-Algorithm Host Key Verification**: Supports Ed25519, ECDSA P-256, and RSA signature verification, with SHA-256 fingerprint display on first connection (TOFU mode).
- **Security Hardening**: Built-in SSRF protection against IPv6 and reserved IPs, API rate limiting (anti-brute force), and local AES-256-GCM encryption for your stored server credentials.
- **Human Verification**: Supports Cloudflare Turnstile verification to prevent malicious bot abuse.
- **Isolated Session State**: Leveraging Cloudflare Durable Objects and the Hibernation API, every terminal session runs securely and persistently within its sandbox.
- **Zero Credential Exposure**: One-Time-Token mechanism ensures passwords/private keys never pass through the frontend, securely flowing entirely within the Worker.
<a id="features"></a>
## Features
- **Full SSH Handshake**: Native TypeScript implementation of the SSH transport layer and user authentication protocols.
- **Pure TypeScript SSH-2.0 Implementation**: Fully self-developed SSH protocol stack, with no dependency on any third-party SSH libraries, implementing all cryptographic operations based on Web Crypto API.
- **Multi-Algorithm Key Exchange**: Supports Curve25519-SHA256 (preferred) and ECDH-NISTP256 KEX algorithms, compatible with various SSH servers (including Dropbear).
- **IPv4/IPv6 Dual Stack**: Full support for both IPv4 and IPv6 address connections, including automatic handling of IPv6 bracket notation.
- **Multiple Auth Methods**: Supports standard SSH password authentication as well as Ed25519 plaintext private key authentication.
- **MitM Protection (TOFU)**: Automatically extracts and prints the server's Host Key (SHA-256 fingerprint) on the first connection, preventing eavesdropping by malicious nodes.
- **MitM Protection (TOFU)**: Automatically extracts and prints the server's Host Key (SHA-256 fingerprint) on the first connection, supporting Ed25519/ECDSA/RSA signature verification.
- **Geek Terminal Experience**: Powered by `@xterm/xterm` and the `@xterm/addon-webgl` hardware acceleration rendering engine, ensuring silky smooth scrolling even with massive log outputs.
- **Customizable UI**: Switch seamlessly between classic terminal themes like Cyberpunk, Glacier, and Gruvbox, fully optimized for mobile devices.
- **Native File Transfer**: Integrated with `zmodem.js`. Just run `rz` or `sz` commands in the terminal to easily upload/download files via drag & drop directly in the browser.
- **Native File Transfer**: Integrated with [trzsz.js](https://github.com/trzsz/trzsz.js), supporting `trz` (upload) / `tsz` (download) commands for file transfer, fully compatible with tmux sessions. Also supports drag-and-drop file upload to the terminal, directory transfer, and resumable transfers. (Requires [trzsz](https://trzsz.github.io/) installed on the remote server)
- **GitHub OAuth Integration**: Supports GitHub login, allowing users to save and manage frequently used SSH servers for one-click connections.
<a id="architecture"></a>
## Architecture
### System Architecture
```mermaid
flowchart TB
Browser["Browser Client<br/>(TypeScript + xterm.js)"]
CF["Cloudflare Edge Network"]
DO["Durable Object<br/>(SSH Session Management)"]
Server["Target SSH Server<br/>(e.g., Linux VPS)"]
subgraph "Browser Client"
UI["Frontend UI<br/>TypeScript + xterm.js"]
Trzsz["trzsz File Transfer"]
end
subgraph "Cloudflare Edge Network"
Worker["Worker<br/>Routing + API"]
SSH_DO["SSHSessionDO<br/>SSH Session Management"]
User_DO["UserDBDO<br/>User Data Management"]
end
subgraph "Target Server"
SSH["SSH Server<br/>(OpenSSH/Dropbear)"
end
Browser <-->|"WebSocket<br/>(Frontend UI Input & Terminal Output)"| CF
CF <-->|"WebSocket"<br/>Routing & Long-lived Connection| DO
DO <-->|"TCP Socket<br/>@cloudflare/sockets"| Server
UI <-->|"WebSocket<br/>Terminal I/O"| Worker
Trzsz <-->|"trzsz Protocol"| UI
Worker <-->|"WebSocket"| SSH_DO
Worker <-->|"Internal API"| User_DO
SSH_DO <-->|"TCP Socket<br/>@cloudflare/sockets"| SSH
```
1. The user enters the host IP, username, and password on the frontend.
### Core Components
| Component | File | Responsibility |
|-----------|------|----------------|
| **Worker Entry** | `src/worker/index.ts` | HTTP routing, API handling, WebSocket upgrade |
| **SSHSessionDO** | `src/worker/durable-object.ts` | SSH session lifecycle management, SSRF protection |
| **UserDBDO** | `src/worker/user-db.ts` | User data, server configs, rate limiting (SQLite) |
| **SSHSession** | `src/worker/ssh-session.ts` | SSH protocol state machine (connect→version→kex→auth→interactive) |
| **SSH Protocol Stack** | `src/ssh/*.ts` | Pure TypeScript SSH-2.0 implementation (transport, crypto, auth, channels) |
| **Frontend Terminal** | `frontend/src/terminal.ts` | xterm.js wrapper, trzsz integration, WebSocket management |
### SSH Protocol Implementation
This project implements a complete SSH-2.0 protocol stack:
| Layer | Implementation | Supported Algorithms |
|-------|----------------|---------------------|
| **Key Exchange** | `kex-curve25519.ts` / `kex-ecdh.ts` | curve25519-sha256, ecdh-sha2-nistp256 |
| **Data Encryption** | `crypto.ts` | aes256-gcm, aes128-gcm, aes256-ctr, aes192-ctr, aes128-ctr |
| **Integrity** | `crypto.ts` | hmac-sha2-256, hmac-sha2-512, hmac-sha1 |
| **Host Keys** | `ssh-session.ts` | Ed25519, ECDSA P-256, RSA |
| **User Auth** | `auth.ts` | Password authentication, Ed25519 public key authentication |
| **Channel Management** | `channel.ts` | Session channel, PTY, shell, window-change |
### Data Flow
1. The user enters the host IP, username, and password on the frontend (or selects a saved server via GitHub OAuth).
2. The frontend establishes a WebSocket connection with the backend Durable Object.
3. The DO receives the credentials and establishes a TCP connection with the target SSH server using `@cloudflare/sockets`.
4. The DO handles the entire SSH protocol negotiation (key exchange, password auth, etc.) in pure code and forwards the encrypted terminal data to the frontend via WebSocket.
3. SSHSessionDO receives the credentials and establishes a TCP connection with the target SSH server using `@cloudflare/sockets`.
4. SSHSession executes the complete SSH protocol negotiation (version exchange → key exchange → authentication → channel open → PTY → Shell).
5. Encrypted terminal data is bidirectionally forwarded between the frontend and SSH server via WebSocket.
<a id="quick-start"></a>
## Quick Deployment
@@ -107,9 +152,10 @@ flowchart TB
#### Method 1: Deploy via GitHub Integration (Recommended)
1. **Fork this repository** to your GitHub account.
2. **Configure Domain**: Before deploying, you must modify the custom domain in `wrangler.toml` to your own domain (Note: The domain must be registered or active in Cloudflare beforehand).
3. **Setup Deployment**: Log in to the Cloudflare dashboard, go to Workers & Pages to connect your GitHub account, and select the forked repository.
4. **Build Command**: During the deployment configuration, make sure to enter `npm install && npm run build:frontend` as the Build command, then deploy with one click (the build output directory can be left blank).
2. **Setup Deployment**: Log in to the Cloudflare dashboard, go to Workers & Pages to connect your GitHub account, and select the forked repository.
3. **Build Command**: During the deployment configuration, make sure to enter `pnpm run build:frontend` as the Build command, then deploy with one click (the build output directory can be left blank).
4. **Access the App**: After successful deployment, you can access it via the default domain `https://cloudssh.<your-subdomain>.workers.dev`.
5. **Bind Custom Domain** (Optional): In the Cloudflare Dashboard Workers settings, go to "Triggers" → "Custom Domains" to add your domain.
#### Method 2: Local CLI Deployment
@@ -121,7 +167,8 @@ flowchart TB
2. **Install Dependencies**
```bash
npm install
npm install -g pnpm
pnpm install
```
3. **Login to Cloudflare**
@@ -131,22 +178,22 @@ flowchart TB
4. **One-Click Deploy**
```bash
npm run deploy
pnpm run deploy
```
Once deployed, Wrangler will output your Worker URL. Open that URL in your browser to start using your Web SSH terminal.
5. **Bind Custom Domain** (Optional): In the Cloudflare Dashboard Workers settings, go to "Triggers" → "Custom Domains" to add your domain.
#### Optional: Configure Turnstile Human Verification
To prevent malicious bot abuse, it is recommended to enable Cloudflare Turnstile verification:
1. **Create Turnstile Widget**: Log in to [Cloudflare Dashboard](https://dash.cloudflare.com/), go to the Turnstile page and create a new Widget.
2. **Get Keys**: After creation, you will receive a **Site Key** (public) and a **Secret Key** (private).
3. **Configure Environment Variables**:
- **Method 1 (GitHub)**: In the Cloudflare Dashboard Workers settings, add the following environment variables:
- `TURNSTILE_SECRET` = your Secret Key
- `TURNSTILE_SITEKEY` = your Site Key
- **Method 2 (CLI)**: Uncomment `TURNSTILE_SECRET` and `TURNSTILE_SITEKEY` in `wrangler.toml` and enter your keys.
3. **Configure Environment Variables**: In the Cloudflare Dashboard Workers settings, go to "Settings" → "Variables and Secrets", add the following environment variables:
- `TURNSTILE_SECRET` = your Secret Key (Type: Secret)
- `TURNSTILE_SITEKEY` = your Site Key (Type: Text)
4. **Redeploy**: Run the deployment command to apply the configuration.
> **Note**: Turnstile verification is session-level. After verification, all features are available for the current session. Closing the browser will require re-verification.
@@ -162,38 +209,66 @@ With GitHub OAuth enabled, users can log in with their GitHub account and save/m
- **Authorization callback URL**: `https://your-domain.com/api/auth/callback`
- After creation, note the **Client ID**, then click **Generate a new client secret** to get the **Client Secret** (shown only once, save it immediately)
2. **Configure Environment Variables**:
- **Method 1 (GitHub)**: In the Cloudflare Dashboard Workers settings, add:
- `GITHUB_CLIENT_ID` = your Client ID
- `BASE_URL` = `https://your-domain.com` (your deployed domain)
- **Method 2 (CLI)**: Uncomment `GITHUB_CLIENT_ID` and `BASE_URL` in `wrangler.toml` and enter your values.
2. **Configure Environment Variables**: In the Cloudflare Dashboard Workers settings, go to "Settings" → "Variables and Secrets", add the following environment variables:
- `GITHUB_CLIENT_ID` = your Client ID (Type: Text)
- `BASE_URL` = `https://your-domain.com` (your deployed domain, Type: Text)
- `GITHUB_CLIENT_SECRET` = your Client Secret (Type: Secret)
3. **Set Secrets (Sensitive Information)**:
- After deploying the project, go to Cloudflare Dashboard → your Worker project (`cloudssh`) → **Settings** → **Variables and Secrets**.
- Click **Add** under Environment Variables:
- **Type**: Select **Secret** (Important: Do not select Text)
- **Variable name**: `GITHUB_CLIENT_SECRET`
- **Value**: Paste your Client Secret
- Click **Save and deploy**.
4. **Redeploy**: If you just added the variables and are enabling the feature for the first time, you must delete the old deployment and redeploy to initialize the database.
3. **Redeploy**: If you just added the variables and are enabling the feature for the first time, you must delete the old deployment and redeploy to initialize the database.
> **Note**: Server credentials (passwords/private keys) are encrypted with AES-256-GCM before storage. The local encryption key is automatically generated and safely stored in the database (you can also manually override it by setting `SESSION_SECRET` in environment variables). During connection, credentials never pass through the frontend — they are securely transmitted via a one-time-token mechanism.
> **Important**: Enabling this feature for the first time requires a clean deployment (delete the old Worker first, then redeploy) to initialize the new Durable Object. Use `npx wrangler delete cloudssh` to remove the old Worker, then run `npm run deploy` to redeploy.
> **Important**: Enabling this feature for the first time requires a clean deployment (delete the old Worker first, then redeploy) to initialize the new Durable Object. Use `npx wrangler delete cloudssh` to remove the old Worker, then run `pnpm run deploy` to redeploy.
<a id="development"></a>
## Development
This project consists of two parts:
1. **Frontend**: Located in the `frontend/` directory, built with Vite.
2. **Worker**: Located in the `src/` directory, containing the Cloudflare Worker entry point and the core SSH protocol implementation.
### Project Structure
This project uses pnpm monorepo workspace structure:
For local development, you can run:
```bash
npm run dev
```
This command starts Wrangler's local development environment server.
CloudSSH/
├── src/ # Backend source (Cloudflare Worker)
│ ├── ssh/ # SSH protocol pure implementation layer
│ └── worker/ # Worker entry and Durable Objects
├── frontend/ # Frontend source (independent workspace)
│ ├── src/ # TypeScript + xterm.js + trzsz
│ └── package.json # Frontend dependencies
├── scripts/ # Build scripts
├── pnpm-workspace.yaml # pnpm workspace configuration
└── wrangler.toml # Cloudflare deployment configuration
```
### Local Development
1. **Install Dependencies**
```bash
pnpm install
```
2. **Start Development Server**
```bash
pnpm run dev
```
This command builds the frontend and starts the Wrangler local development environment.
3. **Build Frontend Only**
```bash
pnpm run build:frontend
```
### Tech Stack
| Layer | Technology | Description |
|-------|------------|-------------|
| **Frontend** | TypeScript + Vite + xterm.js | Web terminal emulator, WebGL hardware acceleration |
| **UI Framework** | Tailwind CSS (CDN) | Cyberpunk-style dark theme |
| **File Transfer** | trzsz.js | Supports trz/tsz commands, drag-and-drop upload, resumable transfers |
| **Backend** | Cloudflare Workers | Serverless edge computing |
| **Session Management** | Durable Objects | SSH session isolation, Hibernation API |
| **Data Storage** | Durable Objects SQLite | User data, server configurations |
| **Package Manager** | pnpm (workspace) | Monorepo dependency management |
<a id="license"></a>
## License
-1929
View File
File diff suppressed because it is too large. Load diff
+2 -4
View File
@@ -5,18 +5,16 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"postinstall": "patch-package"
"preview": "vite preview"
},
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^5.5.0",
"zmodem.js": "^0.1.10"
"trzsz": "^1.1.6"
},
"devDependencies": {
"patch-package": "^8.0.1",
"typescript": "^5.7.0",
"vite": "^6.0.0"
}
-13
View File
@@ -1,13 +0,0 @@
diff --git a/node_modules/zmodem.js/src/zmodem_browser.js b/node_modules/zmodem.js/src/zmodem_browser.js
index 7405f54..32a6c29 100644
--- a/node_modules/zmodem.js/src/zmodem_browser.js
+++ b/node_modules/zmodem.js/src/zmodem_browser.js
@@ -123,7 +123,7 @@ Zmodem.Browser = {
};
reader.onload = function reader_onload(e) {
- piece = new Uint8Array(e.target.result, xfer, piece)
+ piece = new Uint8Array(e.target.result, xfer.get_offset())
_check_aborted(session);
+44 -4
View File
@@ -9,6 +9,43 @@ let connectionForm: ConnectionForm | null = null;
let serverList: ServerList | null = null;
let isLoggedIn = false;
// ==================== 独立终端标签页模式 ====================
function isTerminalTab(): boolean {
const params = new URLSearchParams(window.location.search);
return params.has('wsUrl');
}
function initTerminalTab(): void {
const params = new URLSearchParams(window.location.search);
const wsUrl = params.get('wsUrl')!;
const serverName = params.get('name') || 'Server';
// 隐藏所有非终端元素
document.getElementById('auth-section')!.classList.add('hidden');
document.getElementById('user-space-section')!.classList.add('hidden');
document.getElementById('user-space-section')!.classList.remove('flex');
document.getElementById('terminal-section')!.classList.remove('hidden');
document.getElementById('terminal-section')!.classList.add('flex');
// 更新终端状态栏
document.getElementById('term-host')!.textContent = `Server: ${serverName}`;
document.getElementById('term-user')!.textContent = '';
document.getElementById('term-port')!.textContent = '';
terminal.mount();
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
terminal.connectWithWebSocket(ws);
// 断开连接时关闭标签页
document.getElementById('disconnect-btn')?.addEventListener('click', () => {
terminal.disconnect();
window.close();
});
}
// ==================== 页面切换 ====================
function showAuthSection(): void {
@@ -35,10 +72,6 @@ function showUserSpace(user: { id: number; github_id: number; username: string;
serverList = new ServerList(
user,
// onConnect 回调
(wsUrl: string, serverName: string) => {
showTerminalFromServer(wsUrl, serverName);
},
// onLogout 回调
() => {
isLoggedIn = false;
@@ -64,6 +97,7 @@ function showTerminalFromServer(wsUrl: string, serverName: string): void {
// 通过 wsUrl(含 one-time-token)建立连接
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
terminal.connectWithWebSocket(ws);
}
@@ -103,6 +137,12 @@ async function init(): Promise<void> {
copyrightYearSpan.textContent = new Date().getFullYear().toString();
}
// 独立终端标签页模式:URL 包含 wsUrl 参数
if (isTerminalTab()) {
initTerminalTab();
return;
}
try {
// 检查是否已登录
const meRes = await fetch('/api/auth/me');
+5 -7
View File
@@ -1,5 +1,3 @@
import { SSHTerminal } from './terminal';
interface UserInfo {
id: number;
github_id: number;
@@ -25,18 +23,15 @@ interface ServerConfig {
export class ServerList {
private user: UserInfo;
private servers: ServerConfig[] = [];
private onConnect: (wsUrl: string, serverName: string) => void;
private onLogout: () => void;
private editingServerId: number | null = null;
private modalAuthMode: 'password' | 'key' = 'password';
constructor(
user: UserInfo,
onConnect: (wsUrl: string, serverName: string) => void,
onLogout: () => void
) {
this.user = user;
this.onConnect = onConnect;
this.onLogout = onLogout;
this.init();
}
@@ -209,9 +204,12 @@ export class ServerList {
}
const { wsUrl } = await res.json() as { wsUrl: string };
this.onConnect(wsUrl, server.name);
// 连接成功跳转终端后,立即恢复按钮的原始状态,以便用户断开连接返回后看到的是正常按钮
// 在新标签页打开终端
const terminalUrl = `/?wsUrl=${encodeURIComponent(wsUrl)}&name=${encodeURIComponent(server.name)}`;
window.open(terminalUrl, '_blank');
// 恢复按钮状态
if (connectBtn) {
connectBtn.innerHTML = `
<span class="material-symbols-outlined" style="font-size: 14px;">power_settings_new</span>
+80 -26
View File
@@ -2,7 +2,7 @@ import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { WebglAddon } from '@xterm/addon-webgl';
import { ZmodemHandler } from './zmodem-handler';
import { TrzszFilter } from 'trzsz';
import '@xterm/xterm/css/xterm.css';
export interface SSHConnectionConfig {
@@ -46,6 +46,7 @@ export class SSHTerminal {
private container: HTMLElement;
private disposables: { dispose(): void }[] = [];
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
private trzszFilter: TrzszFilter | null = null;
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -82,6 +83,21 @@ export class SSHTerminal {
console.error('Failed to read clipboard', err);
}
});
// Drag-and-drop file upload support (trzsz)
this.container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
this.container.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
if (this.trzszFilter && e.dataTransfer?.items) {
this.trzszFilter.uploadFiles(e.dataTransfer.items)
.then(() => console.log('[trzsz] Drag-drop upload success'))
.catch((err: any) => console.error('[trzsz] Drag-drop upload error:', err));
}
});
}
setTheme(themeName: keyof typeof THEMES): void {
@@ -112,6 +128,7 @@ export class SSHTerminal {
}
async connect(config: SSHConnectionConfig): Promise<void> {
this.resetActiveConnection();
this.lastConfig = config;
this.terminal.clear();
@@ -124,6 +141,7 @@ export class SSHTerminal {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(wsUrl.toString());
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.terminal.writeln('\x1b[32m[+] WebSocket connected, sending credentials...\x1b[0m');
@@ -159,8 +177,10 @@ export class SSHTerminal {
* 服务器已通过 token 获取凭据,无需前端发送
*/
connectWithWebSocket(ws: WebSocket): void {
this.resetActiveConnection();
this.lastConfig = null;
this.ws = ws;
ws.binaryType = 'arraybuffer';
this.terminal.clear();
const termStatus = document.getElementById('term-status');
@@ -177,15 +197,26 @@ export class SSHTerminal {
private setupWebSocketHandlers(rejectFn?: (reason?: any) => void): void {
if (!this.ws) return;
// Zmodem support
const zmodemHandler = new ZmodemHandler(
(data) => this.terminal.write(data),
(data) => {
// Trzsz file transfer support
this.trzszFilter = new TrzszFilter({
writeToTerminal: (data: string | ArrayBuffer | Uint8Array | Blob) => {
if (typeof data === 'string') {
this.terminal.write(data);
} else if (data instanceof Uint8Array) {
this.terminal.write(data);
} else if (data instanceof ArrayBuffer) {
this.terminal.write(new Uint8Array(data));
} else if (data instanceof Blob) {
data.arrayBuffer().then(buf => this.terminal.write(new Uint8Array(buf)));
}
},
sendToServer: (data: string | Uint8Array) => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
);
},
terminalColumns: this.terminal.cols,
});
this.ws.onmessage = (event) => {
if (typeof event.data === 'string') {
@@ -203,16 +234,16 @@ export class SSHTerminal {
case 'error':
this.terminal.writeln(`\x1b[31m[!] ${msg.message}\x1b[0m`);
break;
case 'pong':
break;
}
} catch {
this.terminal.write(event.data);
// Non-JSON string data — pass through trzsz filter
this.trzszFilter!.processServerOutput(event.data);
}
} else {
const reader = new FileReader();
reader.onload = () => {
zmodemHandler.consume(reader.result as ArrayBuffer);
};
reader.readAsArrayBuffer(event.data);
// Binary data — pass through trzsz filter
this.trzszFilter!.processServerOutput(event.data);
}
};
@@ -236,14 +267,21 @@ export class SSHTerminal {
if (rejectFn) rejectFn(new Error('WebSocket connection failed'));
};
// User input goes through trzsz filter
this.disposables.push(
this.terminal.onData((data) => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
this.trzszFilter!.processTerminalInput(data);
})
);
// Binary input support
this.disposables.push(
this.terminal.onBinary((data) => {
this.trzszFilter!.processBinaryInput(data);
})
);
// Terminal resize: send to server + update trzsz column count
this.disposables.push(
this.terminal.onResize(({ cols, rows }) => {
if (this.ws?.readyState === WebSocket.OPEN) {
@@ -253,6 +291,7 @@ export class SSHTerminal {
rows,
}));
}
this.trzszFilter?.setTerminalColumns(cols);
})
);
}
@@ -277,10 +316,32 @@ export class SSHTerminal {
}
}
private scheduleReconnect(): void {
private disposeConnectionDisposables(): void {
this.disposables.forEach(d => d.dispose());
this.disposables = [];
}
private clearReconnectTimeout(): void {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
}
private resetActiveConnection(): void {
this.stopHeartbeat();
this.clearReconnectTimeout();
this.disposeConnectionDisposables();
if (this.ws && this.ws.readyState !== WebSocket.CLOSED && this.ws.readyState !== WebSocket.CLOSING) {
this.ws.close(1000);
}
this.ws = null;
this.trzszFilter = null;
}
private scheduleReconnect(): void {
this.clearReconnectTimeout();
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
@@ -288,6 +349,7 @@ export class SSHTerminal {
this.terminal.writeln(`\x1b[33m[*] Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...\x1b[0m`);
this.reconnectTimeout = setTimeout(async () => {
this.reconnectTimeout = null;
if (this.lastConfig) {
this.terminal.writeln('\x1b[32m[+] Reconnecting...\x1b[0m');
try {
@@ -300,16 +362,8 @@ export class SSHTerminal {
}
disconnect(): void {
this.stopHeartbeat();
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
this.reconnectAttempts = this.maxReconnectAttempts;
this.ws?.close(1000);
this.ws = null;
this.disposables.forEach(d => d.dispose());
this.disposables = [];
this.resetActiveConnection();
this.lastConfig = null;
this.terminal.clear();
}
-115
View File
@@ -1,115 +0,0 @@
// @ts-ignore
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
export class ZmodemHandler {
private sentry: any;
private session: any | null = null;
private onTerminalWrite: (data: Uint8Array) => void;
private onWsSend: (data: Uint8Array) => void;
constructor(
onTerminalWrite: (data: Uint8Array) => void,
onWsSend: (data: Uint8Array) => void
) {
this.onTerminalWrite = onTerminalWrite;
this.onWsSend = onWsSend;
this.init();
}
private init() {
this.sentry = new Zmodem.Sentry({
to_terminal: (octets: Array<number>) => {
this.onTerminalWrite(new Uint8Array(octets));
},
sender: (octets: Array<number>) => {
this.onWsSend(new Uint8Array(octets));
},
on_detect: (detection: any) => {
const zsession = detection.confirm();
this.session = zsession;
if (zsession.type === 'receive') {
this.handleReceive(zsession);
} else {
this.handleSend(zsession);
}
},
on_retract: () => {
this.session = null;
}
});
}
public consume(data: ArrayBuffer) {
this.sentry.consume(data);
}
private handleReceive(zsession: any) {
zsession.on('offer', (xfer: any) => {
const name = xfer.get_details().name;
const size = xfer.get_details().size;
const accept = confirm("接受来自服务器的文件下载?\n" + name + " (" + size + " bytes)");
if (!accept) {
xfer.skip();
return;
}
const packets: Uint8Array[] = [];
xfer.on('input', (payload: number[]) => {
packets.push(new Uint8Array(payload));
});
xfer.accept().then(() => {
Zmodem.Browser.save_to_disk(packets, name);
});
});
zsession.start();
}
private handleSend(zsession: any) {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.style.display = 'none';
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) {
Zmodem.Browser.send_files(zsession, files, {
on_offer_response: (file: any, xfer: any) => {
if (!xfer) console.warn('Server rejected file:', file.name);
},
on_progress: (file: any, xfer: any, chunk: any) => {
// Can implement progress bar here
},
on_file_complete: (file: any) => {
console.log('Finished uploading:', file.name);
}
}).then(() => {
zsession.close();
input.remove();
});
} else {
zsession.close();
input.remove();
}
};
// Handle cancel (focus returns without file selection)
const onFocusBack = () => {
setTimeout(() => {
if (input.files?.length === 0) {
zsession.close();
input.remove();
}
}, 1000);
window.removeEventListener('focus', onFocusBack);
};
window.addEventListener('focus', onFocusBack);
document.body.appendChild(input);
input.click();
}
}
-3046
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -4,8 +4,8 @@
"description": "纯 Cloudflare 架构 Web SSH 工具",
"main": "src/worker/index.ts",
"scripts": {
"dev": "node scripts/build-html.js && npx wrangler dev",
"deploy": "node scripts/build-html.js && npx wrangler deploy",
"dev": "node scripts/build-html.js && wrangler dev",
"deploy": "node scripts/build-html.js && wrangler deploy",
"build:frontend": "node scripts/build-html.js",
"test": "vitest run"
},
+2234
View File
File diff suppressed because it is too large. Load diff
+6
View File
@@ -0,0 +1,6 @@
packages:
- '.'
- 'frontend'
allowBuilds:
esbuild: true
workerd: true
+2 -2
View File
@@ -11,8 +11,8 @@ const htmlTsFile = path.join(workerDir, 'html.ts');
console.log('Building frontend...');
try {
// 1. Install and build frontend
execSync('npm install', { cwd: frontendDir, stdio: 'inherit' });
execSync('npm run build', { cwd: frontendDir, stdio: 'inherit' });
execSync('pnpm install', { cwd: frontendDir, stdio: 'inherit' });
execSync('pnpm run build', { cwd: frontendDir, stdio: 'inherit' });
console.log('Inlining assets...');
// 2. Read dist/index.html
+70
View File
@@ -0,0 +1,70 @@
export const KEX_ALGORITHM_CURVE25519_SHA256 = 'curve25519-sha256';
export const KEX_ALGORITHM_ECDH_NISTP256 = 'ecdh-sha2-nistp256';
export const SUPPORTED_KEX_ALGORITHMS = [
KEX_ALGORITHM_CURVE25519_SHA256,
KEX_ALGORITHM_ECDH_NISTP256,
];
export const SUPPORTED_ENCRYPTION_ALGORITHMS = [
'aes256-gcm@openssh.com',
'aes128-gcm@openssh.com',
'aes256-ctr',
'aes192-ctr',
'aes128-ctr',
];
export const SUPPORTED_MAC_ALGORITHMS = [
'hmac-sha2-256',
'hmac-sha2-512',
'hmac-sha1',
];
export interface CipherSpec {
mode: 'gcm' | 'ctr';
blockSize: number;
ivLength: number;
keyLength: number;
aead: boolean;
}
export interface MacSpec {
length: number;
keyLength: number;
}
const CIPHER_SPECS: Record<string, CipherSpec> = {
'aes256-gcm@openssh.com': { mode: 'gcm', blockSize: 16, ivLength: 12, keyLength: 32, aead: true },
'aes128-gcm@openssh.com': { mode: 'gcm', blockSize: 16, ivLength: 12, keyLength: 16, aead: true },
'aes256-ctr': { mode: 'ctr', blockSize: 16, ivLength: 16, keyLength: 32, aead: false },
'aes192-ctr': { mode: 'ctr', blockSize: 16, ivLength: 16, keyLength: 24, aead: false },
'aes128-ctr': { mode: 'ctr', blockSize: 16, ivLength: 16, keyLength: 16, aead: false },
};
const MAC_SPECS: Record<string, MacSpec> = {
none: { length: 0, keyLength: 32 },
'hmac-sha1': { length: 20, keyLength: 20 },
'hmac-sha2-256': { length: 32, keyLength: 32 },
'hmac-sha2-512': { length: 64, keyLength: 64 },
};
export function isCurve25519KEXAlgorithm(algorithm: string): boolean {
return algorithm === KEX_ALGORITHM_CURVE25519_SHA256;
}
export function getCipherSpec(algorithm: string): CipherSpec {
const spec = CIPHER_SPECS[algorithm];
if (!spec) throw new Error(`Unsupported cipher: ${algorithm}`);
return spec;
}
export function getMacSpec(algorithm: string): MacSpec {
const spec = MAC_SPECS[algorithm];
if (!spec) throw new Error(`Unsupported MAC algorithm: ${algorithm}`);
return spec;
}
export function getMacAlgorithmsForCipher(cipher: string): string[] {
if (getCipherSpec(cipher).aead) return ['none'];
return SUPPORTED_MAC_ALGORITHMS;
}
+121 -4
View File
@@ -1,3 +1,5 @@
import { concat, encodeUint32 } from './utils';
export class SSHAESGCMCipher {
private key: CryptoKey | null = null;
private iv: Uint8Array;
@@ -32,7 +34,7 @@ export class SSHAESGCMCipher {
}
}
async encrypt(plaintext: Uint8Array, _seqNum?: number, aad?: Uint8Array): Promise<Uint8Array> {
async encrypt(plaintext: Uint8Array, _seqNum?: number, aad?: Uint8Array, _commit: boolean = true): Promise<Uint8Array> {
if (!this.key) throw new Error('Cipher not initialized');
const nonce = new Uint8Array(this.iv);
@@ -40,14 +42,14 @@ export class SSHAESGCMCipher {
if (aad) alg.additionalData = aad;
const encrypted = new Uint8Array(
await crypto.subtle.encrypt(alg as AesGcmParams, this.key, plaintext)
await crypto.subtle.encrypt(alg as unknown as SubtleCryptoEncryptAlgorithm, this.key, plaintext)
);
this.incIV();
return encrypted;
}
async decrypt(ciphertext: Uint8Array, _seqNum?: number, aad?: Uint8Array): Promise<Uint8Array | null> {
async decrypt(ciphertext: Uint8Array, _seqNum?: number, aad?: Uint8Array, _commit: boolean = true): Promise<Uint8Array | null> {
if (!this.key) throw new Error('Cipher not initialized');
const nonce = new Uint8Array(this.iv);
@@ -56,7 +58,7 @@ export class SSHAESGCMCipher {
try {
const decrypted = new Uint8Array(
await crypto.subtle.decrypt(alg as AesGcmParams, this.key, ciphertext)
await crypto.subtle.decrypt(alg as unknown as SubtleCryptoEncryptAlgorithm, this.key, ciphertext)
);
this.incIV();
return decrypted;
@@ -67,6 +69,121 @@ export class SSHAESGCMCipher {
}
}
export class SSHAESCTRCipher {
private key: CryptoKey | null = null;
private counter: Uint8Array;
private rawKey: Uint8Array;
constructor(rawKey: Uint8Array, iv: Uint8Array) {
if (iv.length !== 16) {
throw new Error(`AES-CTR requires a 16-byte IV, got ${iv.length}`);
}
this.counter = new Uint8Array(iv);
this.rawKey = rawKey;
}
async init(): Promise<void> {
this.key = await crypto.subtle.importKey(
'raw',
this.rawKey,
{ name: 'AES-CTR', length: this.rawKey.length * 8 },
false,
['encrypt', 'decrypt']
);
}
private incCounter(blocks: number): void {
for (let block = 0; block < blocks; block++) {
for (let i = 15; i >= 0; i--) {
this.counter[i]++;
if (this.counter[i] !== 0) break;
}
}
}
async encrypt(plaintext: Uint8Array, _seqNum?: number, _aad?: Uint8Array, commit: boolean = true): Promise<Uint8Array> {
if (!this.key) throw new Error('Cipher not initialized');
const counter = new Uint8Array(this.counter);
const encrypted = new Uint8Array(
await crypto.subtle.encrypt(
{ name: 'AES-CTR', counter, length: 128 } as SubtleCryptoEncryptAlgorithm,
this.key,
plaintext
)
);
if (commit) {
this.incCounter(Math.ceil(plaintext.length / 16));
}
return encrypted;
}
async decrypt(ciphertext: Uint8Array, _seqNum?: number, _aad?: Uint8Array, commit: boolean = true): Promise<Uint8Array | null> {
if (!this.key) throw new Error('Cipher not initialized');
const counter = new Uint8Array(this.counter);
try {
const decrypted = new Uint8Array(
await crypto.subtle.decrypt(
{ name: 'AES-CTR', counter, length: 128 } as SubtleCryptoEncryptAlgorithm,
this.key,
ciphertext
)
);
if (commit) {
this.incCounter(Math.ceil(ciphertext.length / 16));
}
return decrypted;
} catch (e) {
console.error('[CRYPTO] AES-CTR decrypt failed, ciphertextLen:', ciphertext.length, 'error:', e instanceof Error ? e.message : String(e));
return null;
}
}
}
export class SSHHMAC {
private key: CryptoKey | null = null;
private rawKey: Uint8Array;
private hash: 'SHA-1' | 'SHA-256' | 'SHA-512';
readonly length: number;
constructor(algorithm: string, rawKey: Uint8Array) {
this.rawKey = rawKey;
if (algorithm === 'hmac-sha1') {
this.hash = 'SHA-1';
this.length = 20;
} else if (algorithm === 'hmac-sha2-256') {
this.hash = 'SHA-256';
this.length = 32;
} else if (algorithm === 'hmac-sha2-512') {
this.hash = 'SHA-512';
this.length = 64;
} else {
throw new Error(`Unsupported MAC algorithm: ${algorithm}`);
}
}
async init(): Promise<void> {
this.key = await crypto.subtle.importKey(
'raw',
this.rawKey,
{ name: 'HMAC', hash: this.hash },
false,
['sign', 'verify']
);
}
async sign(packet: Uint8Array, seqNum: number): Promise<Uint8Array> {
if (!this.key) throw new Error('MAC not initialized');
const data = concat(encodeUint32(seqNum), packet);
return new Uint8Array(await crypto.subtle.sign('HMAC', this.key, data));
}
async verify(packet: Uint8Array, seqNum: number, expected: Uint8Array): Promise<boolean> {
if (!this.key) throw new Error('MAC not initialized');
const data = concat(encodeUint32(seqNum), packet);
return crypto.subtle.verify('HMAC', this.key, expected, data);
}
}
export const REKEY_THRESHOLD = 1 << 30;
export function shouldRekey(seqNum: number): boolean {
+140
View File
@@ -0,0 +1,140 @@
import { SSH_MSG_KEX_ECDH_INIT } from '../types';
import { concat, readUint32, encodeString, toSSHMPInt } from './utils';
type X25519SubtleCrypto = {
generateKey(
algorithm: { name: 'X25519' },
extractable: boolean,
keyUsages: string[]
): Promise<CryptoKeyPair>;
exportKey(format: 'raw', key: CryptoKey): Promise<ArrayBuffer>;
importKey(
format: 'raw',
keyData: ArrayBuffer | ArrayBufferView,
algorithm: { name: 'X25519' },
extractable: boolean,
keyUsages: string[]
): Promise<CryptoKey>;
deriveBits(
algorithm: { name: 'X25519'; public: CryptoKey },
baseKey: CryptoKey,
length: number
): Promise<ArrayBuffer>;
};
function x25519Subtle(): X25519SubtleCrypto {
return crypto.subtle as unknown as X25519SubtleCrypto;
}
function isAllZero(bytes: Uint8Array): boolean {
for (const byte of bytes) {
if (byte !== 0) return false;
}
return true;
}
export type Curve25519KeyPair = CryptoKeyPair;
export class Curve25519KeyExchange {
static async generateKeyPair(): Promise<Curve25519KeyPair> {
return x25519Subtle().generateKey(
{ name: 'X25519' },
true,
['deriveBits']
);
}
static async exportRawPublicKey(keyPair: Curve25519KeyPair): Promise<Uint8Array> {
return new Uint8Array(
await x25519Subtle().exportKey('raw', keyPair.publicKey)
);
}
static buildInit(clientRawPublicKey: Uint8Array): Uint8Array {
if (clientRawPublicKey.length !== 32) {
throw new Error(`Invalid Curve25519 client public key length: ${clientRawPublicKey.length}`);
}
return concat(
new Uint8Array([SSH_MSG_KEX_ECDH_INIT]),
encodeString(clientRawPublicKey)
);
}
static parseReply(data: Uint8Array): {
hostKey: Uint8Array;
serverRawPublicKey: Uint8Array;
signature: Uint8Array;
} {
let offset = 1;
const hostKeyLen = readUint32(data, offset);
offset += 4;
const hostKey = data.slice(offset, offset + hostKeyLen);
offset += hostKeyLen;
const qSLen = readUint32(data, offset);
offset += 4;
const serverRawPublicKey = data.slice(offset, offset + qSLen);
offset += qSLen;
const sigLen = readUint32(data, offset);
offset += 4;
const signature = data.slice(offset, offset + sigLen);
return { hostKey, serverRawPublicKey, signature };
}
static async computeSharedSecret(
privateKey: CryptoKey,
serverRawPublicKey: Uint8Array
): Promise<Uint8Array> {
if (serverRawPublicKey.length !== 32) {
throw new Error(`Invalid Curve25519 server public key length: ${serverRawPublicKey.length}`);
}
const serverKey = await x25519Subtle().importKey(
'raw',
serverRawPublicKey,
{ name: 'X25519' },
false,
[]
);
const sharedSecret = new Uint8Array(
await x25519Subtle().deriveBits(
{ name: 'X25519', public: serverKey },
privateKey,
256
)
);
if (isAllZero(sharedSecret)) {
throw new Error('Curve25519 key exchange failed: all-zero shared secret');
}
return toSSHMPInt(sharedSecret);
}
static async computeExchangeHash(
clientVersion: string,
serverVersion: string,
clientKEXInit: Uint8Array,
serverKEXInit: Uint8Array,
hostKey: Uint8Array,
clientRawPublicKey: Uint8Array,
serverRawPublicKey: Uint8Array,
sharedSecret: Uint8Array
): Promise<Uint8Array> {
const v_c = encodeString(clientVersion);
const v_s = encodeString(serverVersion);
const i_c = encodeString(clientKEXInit);
const i_s = encodeString(serverKEXInit);
const k_s = encodeString(hostKey);
const e = encodeString(clientRawPublicKey);
const f = encodeString(serverRawPublicKey);
const k = sharedSecret;
const data = concat(v_c, v_s, i_c, i_s, k_s, e, f, k);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return new Uint8Array(hashBuffer);
}
}
+29 -12
View File
@@ -1,9 +1,34 @@
import { SSH_MSG_KEX_ECDH_INIT } from '../types';
import { concat, readUint32, encodeString, toSSHMPInt } from './utils';
type ECDHSubtleCrypto = {
generateKey(
algorithm: { name: 'ECDH'; namedCurve: 'P-256' },
extractable: boolean,
keyUsages: string[]
): Promise<CryptoKeyPair>;
exportKey(format: 'raw', key: CryptoKey): Promise<ArrayBuffer>;
importKey(
format: 'raw',
keyData: ArrayBuffer | ArrayBufferView,
algorithm: { name: 'ECDH'; namedCurve: 'P-256' },
extractable: boolean,
keyUsages: string[]
): Promise<CryptoKey>;
deriveBits(
algorithm: { name: 'ECDH'; public: CryptoKey },
baseKey: CryptoKey,
length: number
): Promise<ArrayBuffer>;
};
function ecdhSubtle(): ECDHSubtleCrypto {
return crypto.subtle as unknown as ECDHSubtleCrypto;
}
export class ECDHKeyExchange {
static async generateKeyPair(): Promise<CryptoKeyPair> {
return crypto.subtle.generateKey(
return ecdhSubtle().generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveBits']
@@ -12,7 +37,7 @@ export class ECDHKeyExchange {
static async exportRawPublicKey(keyPair: CryptoKeyPair): Promise<Uint8Array> {
return new Uint8Array(
await crypto.subtle.exportKey('raw', keyPair.publicKey)
await ecdhSubtle().exportKey('raw', keyPair.publicKey)
);
}
@@ -51,7 +76,7 @@ export class ECDHKeyExchange {
privateKey: CryptoKey,
serverRawPublicKey: Uint8Array
): Promise<Uint8Array> {
const serverKey = await crypto.subtle.importKey(
const serverKey = await ecdhSubtle().importKey(
'raw',
serverRawPublicKey,
{ name: 'ECDH', namedCurve: 'P-256' },
@@ -59,7 +84,7 @@ export class ECDHKeyExchange {
[]
);
const sharedBits = await crypto.subtle.deriveBits(
const sharedBits = await ecdhSubtle().deriveBits(
{ name: 'ECDH', public: serverKey },
privateKey,
256
@@ -87,15 +112,7 @@ export class ECDHKeyExchange {
const f = encodeString(serverRawPublicKey);
const k = sharedSecret;
console.log('[KEX] Hash input lengths: V_C=' + v_c.length + ' V_S=' + v_s.length +
' I_C=' + i_c.length + ' I_S=' + i_s.length +
' K_S=' + k_s.length + ' e=' + e.length + ' f=' + f.length + ' K=' + k.length);
console.log('[KEX] V_C hex=' + Array.from(v_c).map(b => b.toString(16).padStart(2, '0')).join(''));
console.log('[KEX] V_S hex=' + Array.from(v_s).map(b => b.toString(16).padStart(2, '0')).join(''));
const data = concat(v_c, v_s, i_c, i_s, k_s, e, f, k);
console.log('[KEX] Hash input total length: ' + data.length);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return new Uint8Array(hashBuffer);
}
+13 -8
View File
@@ -1,5 +1,10 @@
import { SSH_MSG_KEXINIT, KEXInitMessage } from '../types';
import { encodeUint32, concat } from './utils';
import {
SUPPORTED_ENCRYPTION_ALGORITHMS,
SUPPORTED_KEX_ALGORITHMS,
SUPPORTED_MAC_ALGORITHMS
} from './algorithms';
import { concat } from './utils';
export class KEXInitBuilder {
static build(): Uint8Array {
@@ -12,12 +17,12 @@ export class KEXInitBuilder {
parts.push(cookie);
const algorithmLists = [
'ecdh-sha2-nistp256',
SUPPORTED_KEX_ALGORITHMS.join(','),
'ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256,ssh-rsa',
'aes256-gcm@openssh.com,aes128-gcm@openssh.com',
'aes256-gcm@openssh.com,aes128-gcm@openssh.com',
'none',
'none',
SUPPORTED_ENCRYPTION_ALGORITHMS.join(','),
SUPPORTED_ENCRYPTION_ALGORITHMS.join(','),
SUPPORTED_MAC_ALGORITHMS.join(','),
SUPPORTED_MAC_ALGORITHMS.join(','),
'none',
'none',
'',
@@ -68,9 +73,9 @@ export function parseKEXInit(data: Uint8Array): KEXInitMessage {
};
}
export function negotiate(clientList: string[], serverList: string[]): string {
export function negotiate(clientList: string[], serverList: string[], category: string = 'algorithm'): string {
for (const algo of clientList) {
if (serverList.includes(algo)) return algo;
}
throw new Error(`No common algorithm: ${clientList} vs ${serverList}`);
throw new Error(`No common ${category}: client=[${clientList.join(',')}] server=[${serverList.join(',')}]`);
}
+9 -5
View File
@@ -5,14 +5,18 @@ export class KeyDerivation {
static async deriveKeys(
sharedSecret: Uint8Array,
exchangeHash: Uint8Array,
sessionID: Uint8Array
sessionID: Uint8Array,
ivLengthC2S: number = 12,
ivLengthS2C: number = 12,
integrityKeyLengthC2S: number = 32,
integrityKeyLengthS2C: number = 32
): Promise<SessionKeys> {
const ivC2S = await this.expandKey(sharedSecret, exchangeHash, 'A', sessionID, 12);
const ivS2C = await this.expandKey(sharedSecret, exchangeHash, 'B', sessionID, 12);
const ivC2S = await this.expandKey(sharedSecret, exchangeHash, 'A', sessionID, ivLengthC2S);
const ivS2C = await this.expandKey(sharedSecret, exchangeHash, 'B', sessionID, ivLengthS2C);
const keyC2S = await this.expandKey(sharedSecret, exchangeHash, 'C', sessionID, 32);
const keyS2C = await this.expandKey(sharedSecret, exchangeHash, 'D', sessionID, 32);
const intKeyC2S = await this.expandKey(sharedSecret, exchangeHash, 'E', sessionID, 32);
const intKeyS2C = await this.expandKey(sharedSecret, exchangeHash, 'F', sessionID, 32);
const intKeyC2S = await this.expandKey(sharedSecret, exchangeHash, 'E', sessionID, integrityKeyLengthC2S);
const intKeyS2C = await this.expandKey(sharedSecret, exchangeHash, 'F', sessionID, integrityKeyLengthS2C);
return {
ivClientToServer: ivC2S,
+29 -10
View File
@@ -13,8 +13,10 @@ export class SSHPacketParser {
}
async nextPacket(blockSize: number, decrypt: (
data: Uint8Array, seq: number, aad?: Uint8Array
) => Uint8Array | Promise<Uint8Array | null> | null, hasAuthTag: boolean = false): Promise<SSHPacket | null> {
data: Uint8Array, seq: number, aad?: Uint8Array, commit?: boolean
) => Uint8Array | Promise<Uint8Array | null> | null, hasAuthTag: boolean = false,
macLength: number = 0,
verifyMac?: (packet: Uint8Array, mac: Uint8Array, seq: number) => boolean | Promise<boolean>): Promise<SSHPacket | null> {
if (hasAuthTag) {
if (this.buffer.length < 4) return null;
const packetLength = readUint32(this.buffer, 0);
@@ -27,7 +29,7 @@ export class SSHPacketParser {
const lengthField = raw.slice(0, 4);
const dataToDecrypt = raw.slice(4);
const decrypted = await decrypt(dataToDecrypt, this.seqNum, lengthField);
const decrypted = await decrypt(dataToDecrypt, this.seqNum, lengthField, true);
if (!decrypted) return null;
const paddingLength = decrypted[0];
@@ -46,7 +48,7 @@ export class SSHPacketParser {
if (this.buffer.length < blockSize) return null;
const header = await decrypt(
this.buffer.slice(0, blockSize), this.seqNum
this.buffer.slice(0, blockSize), this.seqNum, undefined, false
);
if (!header) return null;
@@ -56,14 +58,22 @@ export class SSHPacketParser {
const totalBlocks = Math.ceil((4 + packetLength) / blockSize);
const totalSize = totalBlocks * blockSize;
if (this.buffer.length < totalSize) return null;
if (this.buffer.length < totalSize + macLength) return null;
const encryptedPacket = this.buffer.slice(0, totalSize);
this.buffer = this.buffer.slice(totalSize);
const mac = this.buffer.slice(totalSize, totalSize + macLength);
this.buffer = this.buffer.slice(totalSize + macLength);
const decrypted = await decrypt(encryptedPacket, this.seqNum);
const decrypted = await decrypt(encryptedPacket, this.seqNum, undefined, true);
if (!decrypted) return null;
if (verifyMac && macLength > 0) {
const macValid = await verifyMac(decrypted, mac, this.seqNum);
if (!macValid) {
throw new Error('Invalid packet MAC');
}
}
const paddingLength = decrypted[4];
const payload = decrypted.slice(5, 5 + packetLength - 1 - paddingLength);
@@ -73,6 +83,7 @@ export class SSHPacketParser {
length: packetLength,
paddingLength,
payload,
mac,
};
}
@@ -95,7 +106,8 @@ export class SSHPacketBuilder {
blockSize: number,
encrypt: ((data: Uint8Array, seq: number, aad?: Uint8Array) => Uint8Array | Promise<Uint8Array>) | null,
seqNum: number,
hasAuthTag: boolean = false
hasAuthTag: boolean = false,
mac?: (packet: Uint8Array, seq: number) => Uint8Array | Promise<Uint8Array>
): Promise<Uint8Array> {
const packetLength = 1 + payload.length;
// For AES-GCM (hasAuthTag), padding aligns the encrypted portion
@@ -123,7 +135,6 @@ export class SSHPacketBuilder {
packet.set(payload, 5);
const crypto = globalThis.crypto;
const randomPadding = new Uint8Array(paddingLength);
crypto.getRandomValues(randomPadding);
packet.set(randomPadding, 5 + payload.length);
@@ -138,7 +149,15 @@ export class SSHPacketBuilder {
result.set(encryptedData, 4);
return result;
}
return await encrypt(packet, seqNum);
const encryptedPacket = await encrypt(packet, seqNum);
if (mac) {
const macBytes = await mac(packet, seqNum);
const result = new Uint8Array(encryptedPacket.length + macBytes.length);
result.set(encryptedPacket, 0);
result.set(macBytes, encryptedPacket.length);
return result;
}
return encryptedPacket;
}
return packet;
-10
View File
@@ -50,7 +50,6 @@ export class SSHSessionDO {
return new Response('Expected WebSocket', { status: 400 });
}
// 检查是否有预填充配置(来自 one-time-token 连接)
const url = new URL(request.url);
const configParam = url.searchParams.get('config');
let prefilledConfig: SSHConnectionConfig | null = null;
@@ -66,13 +65,10 @@ export class SSHSessionDO {
const pair = new WebSocketPair();
const [client, server] = [pair[0], pair[1]];
// Use Hibernation API for long-lived WebSocket connections
this.state.acceptWebSocket(server);
if (prefilledConfig) {
// 预填充模式:直接发起 SSH 连接,不等待前端凭据
server.serializeAttachment({ state: 'prefilled' });
// 使用 queueMicrotask 确保 WebSocket 就绪后再连接
queueMicrotask(async () => {
try {
await this.initSSHSession(server, prefilledConfig!);
@@ -85,8 +81,6 @@ export class SSHSessionDO {
}
});
} else {
// 匿名模式:等待前端发送凭据
// Set a timeout for receiving credentials
const timeout = setTimeout(() => {
try {
server.send(JSON.stringify({ type: 'error', message: 'Connection timeout' }));
@@ -104,7 +98,6 @@ export class SSHSessionDO {
} as any);
}
// Hibernation API: called when a WebSocket receives a message
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const session = this.sessions.get(ws);
if (session) {
@@ -112,7 +105,6 @@ export class SSHSessionDO {
return;
}
// This is the first message (credentials)
const timeout = this._pendingTimeouts.get(ws);
if (timeout) {
clearTimeout(timeout);
@@ -135,7 +127,6 @@ export class SSHSessionDO {
}
}
// Hibernation API: called when a WebSocket is closed
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
const session = this.sessions.get(ws);
if (session) {
@@ -149,7 +140,6 @@ export class SSHSessionDO {
}
}
// Hibernation API: called when a WebSocket error occurs
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
await this.webSocketClose(ws, 1011, 'Error', false);
}
+282 -274
View File
@@ -1,55 +1,55 @@
// Automatically generated by scripts/build-html.js. Do not edit directly.
export const HTML = `<!DOCTYPE html>
<html class="dark" lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CloudSSH</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"background": "#131313",
"on-surface": "#e5e2e1",
"on-surface-variant": "#bbccb0",
"outline": "#86957d",
"outline-variant": "#3c4b36",
"primary-container": "#4af626",
"on-primary-fixed": "#022100",
"surface": "#131313",
"surface-variant": "#353534",
"secondary-container": "#14d1ff",
"secondary-fixed": "#b7eaff",
"error": "#ffb4ab",
"error-container": "#93000a",
"surface-container-lowest": "#0e0e0e"
},
fontFamily: {
"body": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"label": ["JetBrains Mono"],
"code": ["JetBrains Mono"]
}
}
}
}
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module">var Ws=Object.defineProperty;var $s=(r,e,_)=>e in r?Ws(r,e,{enumerable:!0,configurable:!0,writable:!0,value:_}):r[e]=_;var Y=(r,e,_)=>$s(r,typeof e!="symbol"?e+"":e,_);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const g of document.querySelectorAll('link[rel="modulepreload"]'))C(g);new MutationObserver(g=>{for(const k of g)if(k.type==="childList")for(const w of k.addedNodes)w.tagName==="LINK"&&w.rel==="modulepreload"&&C(w)}).observe(document,{childList:!0,subtree:!0});function _(g){const k={};return g.integrity&&(k.integrity=g.integrity),g.referrerPolicy&&(k.referrerPolicy=g.referrerPolicy),g.crossOrigin==="use-credentials"?k.credentials="include":g.crossOrigin==="anonymous"?k.credentials="omit":k.credentials="same-origin",k}function C(g){if(g.ep)return;g.ep=!0;const k=_(g);fetch(g.href,k)}})();var wt={exports:{}},di;function Us(){return di||(di=1,(function(r,e){(function(_,C){r.exports=C()})(globalThis,(()=>(()=>{var _={4567:function(w,s,a){var l=this&&this.__decorate||function(i,n,c,m){var S,y=arguments.length,f=y<3?n:m===null?m=Object.getOwnPropertyDescriptor(n,c):m;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")f=Reflect.decorate(i,n,c,m);else for(var L=i.length-1;L>=0;L--)(S=i[L])&&(f=(y<3?S(f):y>3?S(n,c,f):S(n,c))||f);return y>3&&f&&Object.defineProperty(n,c,f),f},d=this&&this.__param||function(i,n){return function(c,m){n(c,m,i)}};Object.defineProperty(s,"__esModule",{value:!0}),s.AccessibilityManager=void 0;const h=a(9042),u=a(9924),v=a(844),b=a(4725),p=a(2585),t=a(3656);let o=s.AccessibilityManager=class extends v.Disposable{constructor(i,n,c,m){super(),this._terminal=i,this._coreBrowserService=c,this._renderService=m,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let S=0;S<this._terminal.rows;S++)this._rowElements[S]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[S]);if(this._topBoundaryFocusListener=S=>this._handleBoundaryFocus(S,0),this._bottomBoundaryFocusListener=S=>this._handleBoundaryFocus(S,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new u.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((S=>this._handleResize(S.rows)))),this.register(this._terminal.onRender((S=>this._refreshRows(S.start,S.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((S=>this._handleChar(S)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\`
\`)))),this.register(this._terminal.onA11yTab((S=>this._handleTab(S)))),this.register(this._terminal.onKey((S=>this._handleKey(S.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,t.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,v.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(i){for(let n=0;n<i;n++)this._handleChar(" ")}_handleChar(i){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==i&&(this._charsToAnnounce+=i):this._charsToAnnounce+=i,i===\`
\`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=h.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(i){this._clearLiveRegion(),new RegExp("\\\\p{Control}","u").test(i)||this._charsToConsume.push(i)}_refreshRows(i,n){this._liveRegionDebouncer.refresh(i,n,this._terminal.rows)}_renderRows(i,n){const c=this._terminal.buffer,m=c.lines.length.toString();for(let S=i;S<=n;S++){const y=c.lines.get(c.ydisp+S),f=[],L=(y==null?void 0:y.translateToString(!0,void 0,void 0,f))||"",M=(c.ydisp+S+1).toString(),T=this._rowElements[S];T&&(L.length===0?(T.innerText=" ",this._rowColumns.set(T,[0,1])):(T.textContent=L,this._rowColumns.set(T,f)),T.setAttribute("aria-posinset",M),T.setAttribute("aria-setsize",m))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(i,n){const c=i.target,m=this._rowElements[n===0?1:this._rowElements.length-2];if(c.getAttribute("aria-posinset")===(n===0?"1":\`\${this._terminal.buffer.lines.length}\`)||i.relatedTarget!==m)return;let S,y;if(n===0?(S=c,y=this._rowElements.pop(),this._rowContainer.removeChild(y)):(S=this._rowElements.shift(),y=c,this._rowContainer.removeChild(S)),S.removeEventListener("focus",this._topBoundaryFocusListener),y.removeEventListener("focus",this._bottomBoundaryFocusListener),n===0){const f=this._createAccessibilityTreeNode();this._rowElements.unshift(f),this._rowContainer.insertAdjacentElement("afterbegin",f)}else{const f=this._createAccessibilityTreeNode();this._rowElements.push(f),this._rowContainer.appendChild(f)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(n===0?-1:1),this._rowElements[n===0?1:this._rowElements.length-2].focus(),i.preventDefault(),i.stopImmediatePropagation()}_handleSelectionChange(){var L;if(this._rowElements.length===0)return;const i=document.getSelection();if(!i)return;if(i.isCollapsed)return void(this._rowContainer.contains(i.anchorNode)&&this._terminal.clearSelection());if(!i.anchorNode||!i.focusNode)return void console.error("anchorNode and/or focusNode are null");let n={node:i.anchorNode,offset:i.anchorOffset},c={node:i.focusNode,offset:i.focusOffset};if((n.node.compareDocumentPosition(c.node)&Node.DOCUMENT_POSITION_PRECEDING||n.node===c.node&&n.offset>c.offset)&&([n,c]=[c,n]),n.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(n={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(n.node))return;const m=this._rowElements.slice(-1)[0];if(c.node.compareDocumentPosition(m)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(c={node:m,offset:((L=m.textContent)==null?void 0:L.length)??0}),!this._rowContainer.contains(c.node))return;const S=({node:M,offset:T})=>{const B=M instanceof Text?M.parentNode:M;let R=parseInt(B==null?void 0:B.getAttribute("aria-posinset"),10)-1;if(isNaN(R))return console.warn("row is invalid. Race condition?"),null;const O=this._rowColumns.get(B);if(!O)return console.warn("columns is null. Race condition?"),null;let F=T<O.length?O[T]:O.slice(-1)[0]+1;return F>=this._terminal.cols&&(++R,F=0),{row:R,column:F}},y=S(n),f=S(c);if(y&&f){if(y.row>f.row||y.row===f.row&&y.column>=f.column)throw new Error("invalid range");this._terminal.select(y.column,y.row,(f.row-y.row)*this._terminal.cols-y.column+f.column)}}_handleResize(i){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let n=this._rowContainer.children.length;n<this._terminal.rows;n++)this._rowElements[n]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[n]);for(;this._rowElements.length>i;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const i=this._coreBrowserService.mainDocument.createElement("div");return i.setAttribute("role","listitem"),i.tabIndex=-1,this._refreshRowDimensions(i),i}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=\`\${this._renderService.dimensions.css.canvas.width}px\`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let i=0;i<this._terminal.rows;i++)this._refreshRowDimensions(this._rowElements[i])}}_refreshRowDimensions(i){i.style.height=\`\${this._renderService.dimensions.css.cell.height}px\`}};s.AccessibilityManager=o=l([d(1,p.IInstantiationService),d(2,b.ICoreBrowserService),d(3,b.IRenderService)],o)},3614:(w,s)=>{function a(u){return u.replace(/\\r?\\n/g,"\\Line truncated
export const HTML = `<!DOCTYPE html>
<html class="dark" lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+CiAgPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzEzMTMxMyIvPgogIDxyZWN0IHg9IjIiIHk9IjIiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgcng9IjMiIGZpbGw9IiMxYTFhMWEiIHN0cm9rZT0iIzRhZjYyNiIgc3Ryb2tlLXdpZHRoPSIxIi8+CiAgPHBhdGggZD0iTTggMTFMMTQgMTZMOCAyMSIgc3Ryb2tlPSIjNGFmNjI2IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+CiAgPGxpbmUgeDE9IjE2IiB5MT0iMjEiIHgyPSIyNCIgeTI9IjIxIiBzdHJva2U9IiM0YWY2MjYiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+" />
<title>CloudSSH</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"background": "#131313",
"on-surface": "#e5e2e1",
"on-surface-variant": "#bbccb0",
"outline": "#86957d",
"outline-variant": "#3c4b36",
"primary-container": "#4af626",
"on-primary-fixed": "#022100",
"surface": "#131313",
"surface-variant": "#353534",
"secondary-container": "#14d1ff",
"secondary-fixed": "#b7eaff",
"error": "#ffb4ab",
"error-container": "#93000a",
"surface-container-lowest": "#0e0e0e"
},
fontFamily: {
"body": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"label": ["JetBrains Mono"],
"code": ["JetBrains Mono"]
}
}
}
}
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module">var ao=Object.defineProperty;var ho=(i,e,t)=>e in i?ao(i,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):i[e]=t;var Q=(i,e,t)=>ho(i,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))r(n);new MutationObserver(n=>{for(const o of n)if(o.type==="childList")for(const _ of o.addedNodes)_.tagName==="LINK"&&_.rel==="modulepreload"&&r(_)}).observe(document,{childList:!0,subtree:!0});function t(n){const o={};return n.integrity&&(o.integrity=n.integrity),n.referrerPolicy&&(o.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?o.credentials="include":n.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(n){if(n.ep)return;n.ep=!0;const o=t(n);fetch(n.href,o)}})();var Hi={exports:{}},$s;function lo(){return $s||($s=1,(function(i,e){(function(t,r){i.exports=r()})(globalThis,(()=>(()=>{var t={4567:function(_,s,h){var a=this&&this.__decorate||function(l,c,v,y){var S,x=arguments.length,b=x<3?c:y===null?y=Object.getOwnPropertyDescriptor(c,v):y;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")b=Reflect.decorate(l,c,v,y);else for(var E=l.length-1;E>=0;E--)(S=l[E])&&(b=(x<3?S(b):x>3?S(c,v,b):S(c,v))||b);return x>3&&b&&Object.defineProperty(c,v,b),b},d=this&&this.__param||function(l,c){return function(v,y){c(v,y,l)}};Object.defineProperty(s,"__esModule",{value:!0}),s.AccessibilityManager=void 0;const u=h(9042),p=h(9924),m=h(844),C=h(4725),w=h(2585),f=h(3656);let g=s.AccessibilityManager=class extends m.Disposable{constructor(l,c,v,y){super(),this._terminal=l,this._coreBrowserService=v,this._renderService=y,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let S=0;S<this._terminal.rows;S++)this._rowElements[S]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[S]);if(this._topBoundaryFocusListener=S=>this._handleBoundaryFocus(S,0),this._bottomBoundaryFocusListener=S=>this._handleBoundaryFocus(S,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new p.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((S=>this._handleResize(S.rows)))),this.register(this._terminal.onRender((S=>this._refreshRows(S.start,S.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((S=>this._handleChar(S)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\`
\`)))),this.register(this._terminal.onA11yTab((S=>this._handleTab(S)))),this.register(this._terminal.onKey((S=>this._handleKey(S.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,f.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,m.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(l){for(let c=0;c<l;c++)this._handleChar(" ")}_handleChar(l){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==l&&(this._charsToAnnounce+=l):this._charsToAnnounce+=l,l===\`
\`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=u.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(l){this._clearLiveRegion(),new RegExp("\\\\p{Control}","u").test(l)||this._charsToConsume.push(l)}_refreshRows(l,c){this._liveRegionDebouncer.refresh(l,c,this._terminal.rows)}_renderRows(l,c){const v=this._terminal.buffer,y=v.lines.length.toString();for(let S=l;S<=c;S++){const x=v.lines.get(v.ydisp+S),b=[],E=(x==null?void 0:x.translateToString(!0,void 0,void 0,b))||"",B=(v.ydisp+S+1).toString(),T=this._rowElements[S];T&&(E.length===0?(T.innerText=" ",this._rowColumns.set(T,[0,1])):(T.textContent=E,this._rowColumns.set(T,b)),T.setAttribute("aria-posinset",B),T.setAttribute("aria-setsize",y))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(l,c){const v=l.target,y=this._rowElements[c===0?1:this._rowElements.length-2];if(v.getAttribute("aria-posinset")===(c===0?"1":\`\${this._terminal.buffer.lines.length}\`)||l.relatedTarget!==y)return;let S,x;if(c===0?(S=v,x=this._rowElements.pop(),this._rowContainer.removeChild(x)):(S=this._rowElements.shift(),x=v,this._rowContainer.removeChild(S)),S.removeEventListener("focus",this._topBoundaryFocusListener),x.removeEventListener("focus",this._bottomBoundaryFocusListener),c===0){const b=this._createAccessibilityTreeNode();this._rowElements.unshift(b),this._rowContainer.insertAdjacentElement("afterbegin",b)}else{const b=this._createAccessibilityTreeNode();this._rowElements.push(b),this._rowContainer.appendChild(b)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(c===0?-1:1),this._rowElements[c===0?1:this._rowElements.length-2].focus(),l.preventDefault(),l.stopImmediatePropagation()}_handleSelectionChange(){var E;if(this._rowElements.length===0)return;const l=document.getSelection();if(!l)return;if(l.isCollapsed)return void(this._rowContainer.contains(l.anchorNode)&&this._terminal.clearSelection());if(!l.anchorNode||!l.focusNode)return void console.error("anchorNode and/or focusNode are null");let c={node:l.anchorNode,offset:l.anchorOffset},v={node:l.focusNode,offset:l.focusOffset};if((c.node.compareDocumentPosition(v.node)&Node.DOCUMENT_POSITION_PRECEDING||c.node===v.node&&c.offset>v.offset)&&([c,v]=[v,c]),c.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(c={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(c.node))return;const y=this._rowElements.slice(-1)[0];if(v.node.compareDocumentPosition(y)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(v={node:y,offset:((E=y.textContent)==null?void 0:E.length)??0}),!this._rowContainer.contains(v.node))return;const S=({node:B,offset:T})=>{const M=B instanceof Text?B.parentNode:B;let k=parseInt(M==null?void 0:M.getAttribute("aria-posinset"),10)-1;if(isNaN(k))return console.warn("row is invalid. Race condition?"),null;const I=this._rowColumns.get(M);if(!I)return console.warn("columns is null. Race condition?"),null;let F=T<I.length?I[T]:I.slice(-1)[0]+1;return F>=this._terminal.cols&&(++k,F=0),{row:k,column:F}},x=S(c),b=S(v);if(x&&b){if(x.row>b.row||x.row===b.row&&x.column>=b.column)throw new Error("invalid range");this._terminal.select(x.column,x.row,(b.row-x.row)*this._terminal.cols-x.column+b.column)}}_handleResize(l){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let c=this._rowContainer.children.length;c<this._terminal.rows;c++)this._rowElements[c]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[c]);for(;this._rowElements.length>l;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const l=this._coreBrowserService.mainDocument.createElement("div");return l.setAttribute("role","listitem"),l.tabIndex=-1,this._refreshRowDimensions(l),l}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=\`\${this._renderService.dimensions.css.canvas.width}px\`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let l=0;l<this._terminal.rows;l++)this._refreshRowDimensions(this._rowElements[l])}}_refreshRowDimensions(l){l.style.height=\`\${this._renderService.dimensions.css.cell.height}px\`}};s.AccessibilityManager=g=a([d(1,w.IInstantiationService),d(2,C.ICoreBrowserService),d(3,C.IRenderService)],g)},3614:(_,s)=>{function h(p){return p.replace(/\\r?\\n/g,"\\Line truncated
WARNING: This link could potentially be dangerous\`)){const o=window.open();if(o){try{o.opener=null}catch{}o.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}}s.OscLinkProvider=v=l([d(0,u.IBufferService),d(1,u.IOptionsService),d(2,u.IOscLinkService)],v)},6193:(w,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.RenderDebouncer=void 0,s.RenderDebouncer=class{constructor(a,l){this._renderCallback=a,this._coreBrowserService=l,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(a){return this._refreshCallbacks.push(a),this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh()))),this._animationFrame}refresh(a,l,d){this._rowCount=d,a=a!==void 0?a:0,l=l!==void 0?l:this._rowCount-1,this._rowStart=this._rowStart!==void 0?Math.min(this._rowStart,a):a,this._rowEnd=this._rowEnd!==void 0?Math.max(this._rowEnd,l):l,this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return void this._runRefreshCallbacks();const a=Math.max(this._rowStart,0),l=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(a,l),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(const a of this._refreshCallbacks)a(0);this._refreshCallbacks=[]}}},3236:(w,s,a)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Terminal=void 0;const l=a(3614),d=a(3656),h=a(3551),u=a(9042),v=a(3730),b=a(1680),p=a(3107),t=a(5744),o=a(2950),i=a(1296),n=a(428),c=a(4269),m=a(5114),S=a(8934),y=a(3230),f=a(9312),L=a(4725),M=a(6731),T=a(8055),B=a(8969),R=a(8460),O=a(844),F=a(6114),U=a(8437),$=a(2584),W=a(7399),x=a(5941),A=a(9074),D=a(2585),P=a(5435),z=a(4567),q=a(779);class K extends B.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(I={}){super(I),this.browser=F,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new O.MutableDisposable),this._onCursorMove=this.register(new R.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new R.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new R.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new R.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new R.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new R.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new R.EventEmitter),this._onBlur=this.register(new R.EventEmitter),this._onA11yCharEmitter=this.register(new R.EventEmitter),this._onA11yTabEmitter=this.register(new R.EventEmitter),this._onWillOpen=this.register(new R.EventEmitter),this._setup(),this._decorationService=this._instantiationService.createInstance(A.DecorationService),this._instantiationService.setService(D.IDecorationService,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(q.LinkProviderService),this._instantiationService.setService(L.ILinkProviderService,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(v.OscLinkProvider)),this.register(this._inputHandler.onRequestBell((()=>this._onBell.fire()))),this.register(this._inputHandler.onRequestRefreshRows(((E,N)=>this.refresh(E,N)))),this.register(this._inputHandler.onRequestSendFocus((()=>this._reportFocus()))),this.register(this._inputHandler.onRequestReset((()=>this.reset()))),this.register(this._inputHandler.onRequestWindowsOptionsReport((E=>this._reportWindowsOptions(E)))),this.register(this._inputHandler.onColor((E=>this._handleColorEvent(E)))),this.register((0,R.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,R.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,R.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,R.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize((E=>this._afterResize(E.cols,E.rows)))),this.register((0,O.toDisposable)((()=>{var E,N;this._customKeyEventHandler=void 0,(N=(E=this.element)==null?void 0:E.parentNode)==null||N.removeChild(this.element)})))}_handleColorEvent(I){if(this._themeService)for(const E of I){let N,H="";switch(E.index){case 256:N="foreground",H="10";break;case 257:N="background",H="11"; Line truncated
WARNING: This link could potentially be dangerous\`)){const g=window.open();if(g){try{g.opener=null}catch{}g.location.href=f}else console.warn("Opening link blocked as opener could not be cleared")}}s.OscLinkProvider=m=a([d(0,p.IBufferService),d(1,p.IOptionsService),d(2,p.IOscLinkService)],m)},6193:(_,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.RenderDebouncer=void 0,s.RenderDebouncer=class{constructor(h,a){this._renderCallback=h,this._coreBrowserService=a,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(h){return this._refreshCallbacks.push(h),this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh()))),this._animationFrame}refresh(h,a,d){this._rowCount=d,h=h!==void 0?h:0,a=a!==void 0?a:this._rowCount-1,this._rowStart=this._rowStart!==void 0?Math.min(this._rowStart,h):h,this._rowEnd=this._rowEnd!==void 0?Math.max(this._rowEnd,a):a,this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return void this._runRefreshCallbacks();const h=Math.max(this._rowStart,0),a=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(h,a),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(const h of this._refreshCallbacks)h(0);this._refreshCallbacks=[]}}},3236:(_,s,h)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Terminal=void 0;const a=h(3614),d=h(3656),u=h(3551),p=h(9042),m=h(3730),C=h(1680),w=h(3107),f=h(5744),g=h(2950),l=h(1296),c=h(428),v=h(4269),y=h(5114),S=h(8934),x=h(3230),b=h(9312),E=h(4725),B=h(6731),T=h(8055),M=h(8969),k=h(8460),I=h(844),F=h(6114),H=h(8437),z=h(2584),$=h(7399),L=h(5941),R=h(9074),D=h(2585),P=h(5435),U=h(4567),j=h(779);class G extends M.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(O={}){super(O),this.browser=F,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new I.MutableDisposable),this._onCursorMove=this.register(new k.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new k.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new k.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new k.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new k.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new k.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new k.EventEmitter),this._onBlur=this.register(new k.EventEmitter),this._onA11yCharEmitter=this.register(new k.EventEmitter),this._onA11yTabEmitter=this.register(new k.EventEmitter),this._onWillOpen=this.register(new k.EventEmitter),this._setup(),this._decorationService=this._instantiationService.createInstance(R.DecorationService),this._instantiationService.setService(D.IDecorationService,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(j.LinkProviderService),this._instantiationService.setService(E.ILinkProviderService,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(m.OscLinkProvider)),this.register(this._inputHandler.onRequestBell((()=>this._onBell.fire()))),this.register(this._inputHandler.onRequestRefreshRows(((A,N)=>this.refresh(A,N)))),this.register(this._inputHandler.onRequestSendFocus((()=>this._reportFocus()))),this.register(this._inputHandler.onRequestReset((()=>this.reset()))),this.register(this._inputHandler.onRequestWindowsOptionsReport((A=>this._reportWindowsOptions(A)))),this.register(this._inputHandler.onColor((A=>this._handleColorEvent(A)))),this.register((0,k.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,k.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,k.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,k.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize((A=>this._afterResize(A.cols,A.rows)))),this.register((0,I.toDisposable)((()=>{var A,N;this._customKeyEventHandler=void 0,(N=(A=this.element)==null?void 0:A.parentNode)==null||N.removeChild(this.element)})))}_handleColorEvent(O){if(this._themeService)for(const A of O){let N,W="";switch(A.index){case 256:N="foreground",W="10";break;case 257:N="background",W="11"; Line truncated
\`:\`
\`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(f){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._refresh()))),o.isLinux&&f&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(f){const L=this._getMouseBufferCoords(f),M=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!!(M&&T&&L)&&this._areCoordsInSelection(L,M,T)}isCellInSelection(f,L){const M=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!(!M||!T)&&this._areCoordsInSelection([f,L],M,T)}_areCoordsInSelection(f,L,M){return f[1]>L[1]&&f[1]<M[1]||L[1]===M[1]&&f[1]===L[1]&&f[0]>=L[0]&&f[0]<M[0]||L[1]<M[1]&&f[1]===M[1]&&f[0]<M[0]||L[1]<M[1]&&f[1]===L[1]&&f[0]>=L[0]}_selectWordAtCursor(f,L){var B,R;const M=(R=(B=this._linkifier.currentLink)==null?void 0:B.link)==null?void 0:R.range;if(M)return this._model.selectionStart=[M.start.x-1,M.start.y-1],this._model.selectionStartLength=(0,i.getRangeLength)(M,this._bufferService.cols),this._model.selectionEnd=void 0,!0;const T=this._getMouseBufferCoords(f);return!!T&&(this._selectWordAt(T,L),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(f,L){this._model.clearSelection(),f=Math.max(f,0),L=Math.min(L,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,f],this._model.selectionEnd=[this._bufferService.cols,L],this.refresh(),this._onSelectionChange.fire()}_handleTrim(f){this._model.handleTrim(f)&&this.refresh()}_getMouseBufferCoords(f){const L=this._mouseService.getCoords(f,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(L)return L[0]--,L[1]--,L[1]+=this._bufferService.buffer.ydisp,L}_getMouseEventScrollAmount(f){let L=(0,h.getCoordsRelativeToElement)(this._coreBrowserService.window,f,this._screenElement)[1];const M=this._renderService.dimensions.css.canvas.height;return L>=0&&L<=M?0:(L>M&&(L-=M),L=Math.min(Math.max(L,-50),50),L/=50,L/Math.abs(L)+Math.round(14*L))}shouldForceSelection(f){return o.isMac?f.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:f.shiftKey}handleMouseDown(f){if(this._mouseDownTimeStamp=f.timeStamp,(f.button!==2||!this.hasSelection)&&f.button===0){if(!this._enabled){if(!this.shouldForceSelection(f))return;f.stopPropagation()}f.preventDefault(),this._dragScrollAmount=0,this._enabled&&f.shiftKey?this._handleIncrementalClick(f):f.detail===1?this._handleSingleClick(f):f.detail===2?this._handleDoubleClick(f):f.detail===3&&this._handleTripleClick(f),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval((()=>this._dragScroll()),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(f){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(f))}_handleSingleClick(f){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(f)?3:0,this._model.selectionStart=this._getMouseBufferCoords(f),!this._model.selectionStart)return;this._model.selectionEnd=void 0;const L=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);L&&L.length!==this._model.selectionStart[0]&&L.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(f){this._selectWordAtCursor(f,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(f){const L=this._getMouseBufferCoords(f);L&&(this._activeSelectionMode=2,this._selectLineAt(L[1]))}shouldColumnSelect(f){return f.altKey&&!(o.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(f){if(f.stopImmediatePropagation(),!this._model.selectionStart)return;const L=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(f),!this._model.selectionEnd)return void this.refresh(!0);this._activeSelectionMode===2?this._model.selectionEnd[1]<this._model.selectionStart[1]?this._model.selectionEnd[0]=0:this._model.selectionEnd[0]=this._buf Line truncated
\`,h.VT="\\v",h.FF="\\f",h.CR="\\r",h.SO="",h.SI="",h.DLE="",h.DC1="",h.DC2="",h.DC3="",h.DC4="",h.NAK="",h.SYN="",h.ETB="",h.CAN="",h.EM="",h.SUB="",h.ESC="\\x1B",h.FS="",h.GS="",h.RS="",h.US="",h.SP=" ",h.DEL=""})(a||(s.C0=a={})),(function(h){h.PAD="€",h.HOP="",h.BPH="‚",h.NBH="ƒ",h.IND="„",h.NEL="…",h.SSA="†",h.ESA="‡",h.HTS="ˆ",h.HTJ="‰",h.VTS="Š",h.PLD="‹",h.PLU="Œ",h.RI="",h.SS2="Ž",h.SS3="",h.DCS="",h.PU1="‘",h.PU2="’",h.STS="“",h.CCH="”",h.MW="•",h.SPA="–",h.EPA="—",h.SOS="˜",h.SGCI="™",h.SCI="š",h.CSI="›",h.ST="œ",h.OSC="",h.PM="ž",h.APC="Ÿ"})(l||(s.C1=l={})),(function(h){h.ST=\`\${a.ESC}\\\\\`})(d||(s.C1_ESCAPED=d={}))},7399:(w,s,a)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.evaluateKeyboardEvent=void 0;const l=a(2584),d={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["\`","~"],219:["[","{"],220:["\\\\","|"],221:["]","}"],222:["'",'"']};s.evaluateKeyboardEvent=function(h,u,v,b){const p={type:0,cancel:!1,key:void 0},t=(h.shiftKey?1:0)|(h.altKey?2:0)|(h.ctrlKey?4:0)|(h.metaKey?8:0);switch(h.keyCode){case 0:h.key==="UIKeyInputUpArrow"?p.key=u?l.C0.ESC+"OA":l.C0.ESC+"[A":h.key==="UIKeyInputLeftArrow"?p.key=u?l.C0.ESC+"OD":l.C0.ESC+"[D":h.key==="UIKeyInputRightArrow"?p.key=u?l.C0.ESC+"OC":l.C0.ESC+"[C":h.key==="UIKeyInputDownArrow"&&(p.key=u?l.C0.ESC+"OB":l.C0.ESC+"[B");break;case 8:p.key=h.ctrlKey?"\\b":l.C0.DEL,h.altKey&&(p.key=l.C0.ESC+p.key);break;case 9:if(h.shiftKey){p.key=l.C0.ESC+"[Z";break}p.key=l.C0.HT,p.cancel=!0;break;case 13:p.key=h.altKey?l.C0.ESC+l.C0.CR:l.C0.CR,p.cancel=!0;break;case 27:p.key=l.C0.ESC,h.altKey&&(p.key=l.C0.ESC+l.C0.ESC),p.cancel=!0;break;case 37:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"D",p.key===l.C0.ESC+"[1;3D"&&(p.key=l.C0.ESC+(v?"b":"[1;5D"))):p.key=u?l.C0.ESC+"OD":l.C0.ESC+"[D";break;case 39:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"C",p.key===l.C0.ESC+"[1;3C"&&(p.key=l.C0.ESC+(v?"f":"[1;5C"))):p.key=u?l.C0.ESC+"OC":l.C0.ESC+"[C";break;case 38:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"A",v||p.key!==l.C0.ESC+"[1;3A"||(p.key=l.C0.ESC+"[1;5A")):p.key=u?l.C0.ESC+"OA":l.C0.ESC+"[A";break;case 40:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"B",v||p.key!==l.C0.ESC+"[1;3B"||(p.key=l.C0.ESC+"[1;5B")):p.key=u?l.C0.ESC+"OB":l.C0.ESC+"[B";break;case 45:h.shiftKey||h.ctrlKey||(p.key=l.C0.ESC+"[2~");break;case 46:p.key=t?l.C0.ESC+"[3;"+(t+1)+"~":l.C0.ESC+"[3~";break;case 36:p.key=t?l.C0.ESC+"[1;"+(t+1)+"H":u?l.C0.ESC+"OH":l.C0.ESC+"[H";break;case 35:p.key=t?l.C0.ESC+"[1;"+(t+1)+"F":u?l.C0.ESC+"OF":l.C0.ESC+"[F";break;case 33:h.shiftKey?p.type=2:h.ctrlKey?p.key=l.C0.ESC+"[5;"+(t+1)+"~":p.key=l.C0.ESC+"[5~";break;case 34:h.shiftKey?p.type=3:h.ctrlKey?p.key=l.C0.ESC+"[6;"+(t+1)+"~":p.key=l.C0.ESC+"[6~";break;case 112:p.key=t?l.C0.ESC+"[1;"+(t+1)+"P":l.C0.ESC+"OP";break;case 113:p.key=t?l.C0.ESC+"[1;"+(t+1)+"Q":l.C0.ESC+"OQ";break;case 114:p.key=t?l.C0.ESC+"[1;"+(t+1)+"R":l.C0.ESC+"OR";break;case 115:p.key=t?l.C0.ESC+"[1;"+(t+1)+"S":l.C0.ESC+"OS";break;case 116:p.key=t?l.C0.ESC+"[15;"+(t+1)+"~":l.C0.ESC+"[15~";break;case 117:p.key=t?l.C0.ESC+"[17;"+(t+1)+"~":l.C0.ESC+"[17~";break;case 118:p.key=t?l.C0.ESC+"[18;"+(t+1)+"~":l.C0.ESC+"[18~";break;case 119:p.key=t?l.C0.ESC+"[19;"+(t+1)+"~":l.C0.ESC+"[19~";break;case 120:p.key=t?l.C0.ESC+"[20;"+(t+1)+"~":l.C0.ESC+"[20~";break;case 121:p.key=t?l.C0.ESC+"[21;"+(t+1)+"~":l.C0.ESC+"[21~";break;case 122:p.key=t?l.C0.ESC+"[23;"+(t+1)+"~":l.C0.ESC+"[23~";break;case 123:p.key=t?l.C0.ESC+"[24;"+(t+1)+"~":l.C0.ESC+"[24~";break;default:if(!h.ctrlKey||h.shiftKey||h.altKey||h.metaKey)if(v&&!b||!h.altKey||h.metaKey)!v||h.altKey||h.ctrlKey||h.shiftKey||!h.metaKey?h.key&&!h.ctrlKey&&!h.altKey&&!h.metaKey&&h.keyCode>=48&&h.key.length===1?p.key=h.key:h.key&&h.ctrlKey&&(h.key==="_"&&(p.key=l.C0.US),h.key==="@"&&(p.key=l.C0.NUL)):h.keyCode===65&&(p.type=1);else{const o=d[h.keyCode],i=o==null?void 0:o[h.shiftKey?1:0];if(i)p.key=l.C0.ESC+i;else if(h.keyCode>=65&&h.keyCode<=90){const n=h.ctrlKey?h.keyCode-64:h.keyCode+32;let c=String.fromCharCode(n);h.shiftKey&&(c=c.toUpperCase()),p.key=l.C0.ESC+c}else if(h.keyCode===32)p.key=l.C0.ESC+(h.ctrlKey?l.C0.NUL:" ");else if(h.key==="Dead"&&h.code.startsWith("Key")){let n=h.code.slice(3,4);h.shiftKey||(n=n.toLowerCase()),p.key=l.C0.ESC+n,p.cancel=!0}}else h.keyCode>=65&&h.keyCode<=90?p.key=String.fromCharCode(h.keyCode-64):h.keyCode===32?p.key=l.C0.NUL:h.keyCode>=51&&h.keyCode<=55?p.key=String.fromCharCode(h.keyCode-51+27):h.keyCode===56?p.key=l.C0.DEL:h.keyCode===219?p.key=l.C0.ESC:h.keyCode===220?p.key=l.C0.FS:h.keyCode===221&&(p.key=l.C0.GS)}return p}},482:(w,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Utf8ToUtf32=s.StringToUtf32=s.utf32ToString=s.stringFromCodePoint=void 0,s.stringFromCodePoint=function(a){return a>65535?(a-=65536,String.f Line truncated
\`,i)}paste(o){this._core.paste(o)}refresh(o,i){this._verifyIntegers(o,i),this._core.refresh(o,i)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(o){this._addonManager.loadAddon(this,o)}static get strings(){return s}_verifyIntegers(...o){for(const i of o)if(i===1/0||isNaN(i)||i%1!=0)throw new Error("This API only accepts integers")}_verifyPositiveIntegers(...o){for(const i of o)if(i&&(i===1/0||isNaN(i)||i%1!=0||i<0))throw new Error("This API only accepts positive integers")}}w.Terminal=p})(),k})()))})(wt)),wt.exports}var zs=Us(),yt={exports:{}},ui;function js(){return ui||(ui=1,(function(r,e){(function(_,C){r.exports=C()})(self,(()=>(()=>{var _={};return(()=>{var C=_;Object.defineProperty(C,"__esModule",{value:!0}),C.FitAddon=void 0,C.FitAddon=class{activate(g){this._terminal=g}dispose(){}fit(){const g=this.proposeDimensions();if(!g||!this._terminal||isNaN(g.cols)||isNaN(g.rows))return;const k=this._terminal._core;this._terminal.rows===g.rows&&this._terminal.cols===g.cols||(k._renderService.clear(),this._terminal.resize(g.cols,g.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;const g=this._terminal._core,k=g._renderService.dimensions;if(k.css.cell.width===0||k.css.cell.height===0)return;const w=this._terminal.options.scrollback===0?0:g.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),a=parseInt(s.getPropertyValue("height")),l=Math.max(0,parseInt(s.getPropertyValue("width"))),d=window.getComputedStyle(this._terminal.element),h=a-(parseInt(d.getPropertyValue("padding-top"))+parseInt(d.getPropertyValue("padding-bottom"))),u=l-(parseInt(d.getPropertyValue("padding-right"))+parseInt(d.getPropertyValue("padding-left")))-w;return{cols:Math.max(2,Math.floor(u/k.css.cell.width)),rows:Math.max(1,Math.floor(h/k.css.cell.height))}}}})(),_})()))})(yt)),yt.exports}var Zs=js(),xt={exports:{}},_i;function qs(){return _i||(_i=1,(function(r,e){(function(_,C){r.exports=C()})(self,(()=>(()=>{var _={6:(w,s)=>{function a(d){try{const h=new URL(d),u=h.password&&h.username?\`\${h.protocol}//\${h.username}:\${h.password}@\${h.host}\`:h.username?\`\${h.protocol}//\${h.username}@\${h.host}\`:\`\${h.protocol}//\${h.host}\`;return d.toLocaleLowerCase().startsWith(u.toLocaleLowerCase())}catch{return!1}}Object.defineProperty(s,"__esModule",{value:!0}),s.LinkComputer=s.WebLinkProvider=void 0,s.WebLinkProvider=class{constructor(d,h,u,v={}){this._terminal=d,this._regex=h,this._handler=u,this._options=v}provideLinks(d,h){const u=l.computeLink(d,this._regex,this._terminal,this._handler);h(this._addCallbacks(u))}_addCallbacks(d){return d.map((h=>(h.leave=this._options.leave,h.hover=(u,v)=>{if(this._options.hover){const{range:b}=h;this._options.hover(u,v,b)}},h)))}};class l{static computeLink(h,u,v,b){const p=new RegExp(u.source,(u.flags||"")+"g"),[t,o]=l._getWindowedLineStrings(h-1,v),i=t.join("");let n;const c=[];for(;n=p.exec(i);){const m=n[0];if(!a(m))continue;const[S,y]=l._mapStrIdx(v,o,0,n.index),[f,L]=l._mapStrIdx(v,S,y,m.length);if(S===-1||y===-1||f===-1||L===-1)continue;const M={start:{x:y+1,y:S+1},end:{x:L,y:f+1}};c.push({range:M,text:m,activate:b})}return c}static _getWindowedLineStrings(h,u){let v,b=h,p=h,t=0,o="";const i=[];if(v=u.buffer.active.getLine(h)){const n=v.translateToString(!0);if(v.isWrapped&&n[0]!==" "){for(t=0;(v=u.buffer.active.getLine(--b))&&t<2048&&(o=v.translateToString(!0),t+=o.length,i.push(o),v.isWrapped&&o.indexOf(" ")===-1););i.reverse()}for(i.push(n),t=0;(v=u.buffer.active.getLine(++p))&&v.isWrapped&&t<2048&&(o=v.translateToString(!0),t+=o.length,i.push(o),o.indexOf(" ")===-1););}return[i,b]}static _mapStrIdx(h,u,v,b){const p=h.buffer.active,t=p.getNullCell();let o=v;for(;b;){const i=p.getLine(u);if(!i)return[-1,-1];for(let n=o;n<i.length;++n){i.getCell(n,t);const c=t.getChars();if(t.getWidth()&&(b-=c.length||1,n===i.length-1&&c==="")){const m=p.getLine(u+1);m&&m.isWrapped&&(m.getCell(0,t),t.getWidth()===2&&(b+=1))}if(b<0)return[u,n]}u++,o=0}return[u,o]}}s.LinkComputer=l}},C={};function g(w){var s=C[w];if(s!==void 0)return s.exports;var a=C[w]={exports:{}};return _[w](a,a.exports,g),a.exports}var k={};return(()=>{var w=k;Object.defineProperty(w,"__esModule",{value:!0}),w.WebLinksAddon=void 0;const s=g(6),a=/(https?|HTTPS?):[/]{2}[^\\s"'!*(){}|\\\\\\^<>\`]*[^\\s"':,.!?{}|\\\\\\^~\\[\\]\`()<>]/;function l(d,h){const u=window.open();if(u){try{u.opener=null}catch{}u.location.href=h}else console.warn("Opening link blocked as opener could not be cleared")}w.WebLinksAddon=class{constructor(d=l,h={}){this._handler=d,this._options=h}activate(d){this._terminal=d;const h=this._options,u=h.urlRegex||a;this._linkProvider=this._terminal.registerLinkProvider(new s.WebLinkProvider(this._terminal,u,this._handler,h))}dispose(){var d;(d=this._linkProvider)==null||d.dispose()}}})(),k})()))})(xt)),xt.exports}var Ks=qs();/**
\`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(b){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._refresh()))),g.isLinux&&b&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(b){const E=this._getMouseBufferCoords(b),B=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!!(B&&T&&E)&&this._areCoordsInSelection(E,B,T)}isCellInSelection(b,E){const B=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!(!B||!T)&&this._areCoordsInSelection([b,E],B,T)}_areCoordsInSelection(b,E,B){return b[1]>E[1]&&b[1]<B[1]||E[1]===B[1]&&b[1]===E[1]&&b[0]>=E[0]&&b[0]<B[0]||E[1]<B[1]&&b[1]===B[1]&&b[0]<B[0]||E[1]<B[1]&&b[1]===E[1]&&b[0]>=E[0]}_selectWordAtCursor(b,E){var M,k;const B=(k=(M=this._linkifier.currentLink)==null?void 0:M.link)==null?void 0:k.range;if(B)return this._model.selectionStart=[B.start.x-1,B.start.y-1],this._model.selectionStartLength=(0,l.getRangeLength)(B,this._bufferService.cols),this._model.selectionEnd=void 0,!0;const T=this._getMouseBufferCoords(b);return!!T&&(this._selectWordAt(T,E),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(b,E){this._model.clearSelection(),b=Math.max(b,0),E=Math.min(E,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,b],this._model.selectionEnd=[this._bufferService.cols,E],this.refresh(),this._onSelectionChange.fire()}_handleTrim(b){this._model.handleTrim(b)&&this.refresh()}_getMouseBufferCoords(b){const E=this._mouseService.getCoords(b,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(E)return E[0]--,E[1]--,E[1]+=this._bufferService.buffer.ydisp,E}_getMouseEventScrollAmount(b){let E=(0,u.getCoordsRelativeToElement)(this._coreBrowserService.window,b,this._screenElement)[1];const B=this._renderService.dimensions.css.canvas.height;return E>=0&&E<=B?0:(E>B&&(E-=B),E=Math.min(Math.max(E,-50),50),E/=50,E/Math.abs(E)+Math.round(14*E))}shouldForceSelection(b){return g.isMac?b.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:b.shiftKey}handleMouseDown(b){if(this._mouseDownTimeStamp=b.timeStamp,(b.button!==2||!this.hasSelection)&&b.button===0){if(!this._enabled){if(!this.shouldForceSelection(b))return;b.stopPropagation()}b.preventDefault(),this._dragScrollAmount=0,this._enabled&&b.shiftKey?this._handleIncrementalClick(b):b.detail===1?this._handleSingleClick(b):b.detail===2?this._handleDoubleClick(b):b.detail===3&&this._handleTripleClick(b),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval((()=>this._dragScroll()),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(b){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(b))}_handleSingleClick(b){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(b)?3:0,this._model.selectionStart=this._getMouseBufferCoords(b),!this._model.selectionStart)return;this._model.selectionEnd=void 0;const E=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);E&&E.length!==this._model.selectionStart[0]&&E.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(b){this._selectWordAtCursor(b,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(b){const E=this._getMouseBufferCoords(b);E&&(this._activeSelectionMode=2,this._selectLineAt(E[1]))}shouldColumnSelect(b){return b.altKey&&!(g.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(b){if(b.stopImmediatePropagation(),!this._model.selectionStart)return;const E=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(b),!this._model.selectionEnd)return void this.refresh(!0);this._activeSelectionMode===2?this._model.selectionEnd[1]<this._model.selectionStart[1]?this._model.selectionEnd[0]=0:this._model.selectionEnd[0]=this._buf Line truncated
\`,u.VT="\\v",u.FF="\\f",u.CR="\\r",u.SO="",u.SI="",u.DLE="",u.DC1="",u.DC2="",u.DC3="",u.DC4="",u.NAK="",u.SYN="",u.ETB="",u.CAN="",u.EM="",u.SUB="",u.ESC="\\x1B",u.FS="",u.GS="",u.RS="",u.US="",u.SP=" ",u.DEL=""})(h||(s.C0=h={})),(function(u){u.PAD="€",u.HOP="",u.BPH="‚",u.NBH="ƒ",u.IND="„",u.NEL="…",u.SSA="†",u.ESA="‡",u.HTS="ˆ",u.HTJ="‰",u.VTS="Š",u.PLD="‹",u.PLU="Œ",u.RI="",u.SS2="Ž",u.SS3="",u.DCS="",u.PU1="‘",u.PU2="’",u.STS="“",u.CCH="”",u.MW="•",u.SPA="–",u.EPA="—",u.SOS="˜",u.SGCI="™",u.SCI="š",u.CSI="›",u.ST="œ",u.OSC="",u.PM="ž",u.APC="Ÿ"})(a||(s.C1=a={})),(function(u){u.ST=\`\${h.ESC}\\\\\`})(d||(s.C1_ESCAPED=d={}))},7399:(_,s,h)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.evaluateKeyboardEvent=void 0;const a=h(2584),d={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["\`","~"],219:["[","{"],220:["\\\\","|"],221:["]","}"],222:["'",'"']};s.evaluateKeyboardEvent=function(u,p,m,C){const w={type:0,cancel:!1,key:void 0},f=(u.shiftKey?1:0)|(u.altKey?2:0)|(u.ctrlKey?4:0)|(u.metaKey?8:0);switch(u.keyCode){case 0:u.key==="UIKeyInputUpArrow"?w.key=p?a.C0.ESC+"OA":a.C0.ESC+"[A":u.key==="UIKeyInputLeftArrow"?w.key=p?a.C0.ESC+"OD":a.C0.ESC+"[D":u.key==="UIKeyInputRightArrow"?w.key=p?a.C0.ESC+"OC":a.C0.ESC+"[C":u.key==="UIKeyInputDownArrow"&&(w.key=p?a.C0.ESC+"OB":a.C0.ESC+"[B");break;case 8:w.key=u.ctrlKey?"\\b":a.C0.DEL,u.altKey&&(w.key=a.C0.ESC+w.key);break;case 9:if(u.shiftKey){w.key=a.C0.ESC+"[Z";break}w.key=a.C0.HT,w.cancel=!0;break;case 13:w.key=u.altKey?a.C0.ESC+a.C0.CR:a.C0.CR,w.cancel=!0;break;case 27:w.key=a.C0.ESC,u.altKey&&(w.key=a.C0.ESC+a.C0.ESC),w.cancel=!0;break;case 37:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"D",w.key===a.C0.ESC+"[1;3D"&&(w.key=a.C0.ESC+(m?"b":"[1;5D"))):w.key=p?a.C0.ESC+"OD":a.C0.ESC+"[D";break;case 39:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"C",w.key===a.C0.ESC+"[1;3C"&&(w.key=a.C0.ESC+(m?"f":"[1;5C"))):w.key=p?a.C0.ESC+"OC":a.C0.ESC+"[C";break;case 38:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"A",m||w.key!==a.C0.ESC+"[1;3A"||(w.key=a.C0.ESC+"[1;5A")):w.key=p?a.C0.ESC+"OA":a.C0.ESC+"[A";break;case 40:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"B",m||w.key!==a.C0.ESC+"[1;3B"||(w.key=a.C0.ESC+"[1;5B")):w.key=p?a.C0.ESC+"OB":a.C0.ESC+"[B";break;case 45:u.shiftKey||u.ctrlKey||(w.key=a.C0.ESC+"[2~");break;case 46:w.key=f?a.C0.ESC+"[3;"+(f+1)+"~":a.C0.ESC+"[3~";break;case 36:w.key=f?a.C0.ESC+"[1;"+(f+1)+"H":p?a.C0.ESC+"OH":a.C0.ESC+"[H";break;case 35:w.key=f?a.C0.ESC+"[1;"+(f+1)+"F":p?a.C0.ESC+"OF":a.C0.ESC+"[F";break;case 33:u.shiftKey?w.type=2:u.ctrlKey?w.key=a.C0.ESC+"[5;"+(f+1)+"~":w.key=a.C0.ESC+"[5~";break;case 34:u.shiftKey?w.type=3:u.ctrlKey?w.key=a.C0.ESC+"[6;"+(f+1)+"~":w.key=a.C0.ESC+"[6~";break;case 112:w.key=f?a.C0.ESC+"[1;"+(f+1)+"P":a.C0.ESC+"OP";break;case 113:w.key=f?a.C0.ESC+"[1;"+(f+1)+"Q":a.C0.ESC+"OQ";break;case 114:w.key=f?a.C0.ESC+"[1;"+(f+1)+"R":a.C0.ESC+"OR";break;case 115:w.key=f?a.C0.ESC+"[1;"+(f+1)+"S":a.C0.ESC+"OS";break;case 116:w.key=f?a.C0.ESC+"[15;"+(f+1)+"~":a.C0.ESC+"[15~";break;case 117:w.key=f?a.C0.ESC+"[17;"+(f+1)+"~":a.C0.ESC+"[17~";break;case 118:w.key=f?a.C0.ESC+"[18;"+(f+1)+"~":a.C0.ESC+"[18~";break;case 119:w.key=f?a.C0.ESC+"[19;"+(f+1)+"~":a.C0.ESC+"[19~";break;case 120:w.key=f?a.C0.ESC+"[20;"+(f+1)+"~":a.C0.ESC+"[20~";break;case 121:w.key=f?a.C0.ESC+"[21;"+(f+1)+"~":a.C0.ESC+"[21~";break;case 122:w.key=f?a.C0.ESC+"[23;"+(f+1)+"~":a.C0.ESC+"[23~";break;case 123:w.key=f?a.C0.ESC+"[24;"+(f+1)+"~":a.C0.ESC+"[24~";break;default:if(!u.ctrlKey||u.shiftKey||u.altKey||u.metaKey)if(m&&!C||!u.altKey||u.metaKey)!m||u.altKey||u.ctrlKey||u.shiftKey||!u.metaKey?u.key&&!u.ctrlKey&&!u.altKey&&!u.metaKey&&u.keyCode>=48&&u.key.length===1?w.key=u.key:u.key&&u.ctrlKey&&(u.key==="_"&&(w.key=a.C0.US),u.key==="@"&&(w.key=a.C0.NUL)):u.keyCode===65&&(w.type=1);else{const g=d[u.keyCode],l=g==null?void 0:g[u.shiftKey?1:0];if(l)w.key=a.C0.ESC+l;else if(u.keyCode>=65&&u.keyCode<=90){const c=u.ctrlKey?u.keyCode-64:u.keyCode+32;let v=String.fromCharCode(c);u.shiftKey&&(v=v.toUpperCase()),w.key=a.C0.ESC+v}else if(u.keyCode===32)w.key=a.C0.ESC+(u.ctrlKey?a.C0.NUL:" ");else if(u.key==="Dead"&&u.code.startsWith("Key")){let c=u.code.slice(3,4);u.shiftKey||(c=c.toLowerCase()),w.key=a.C0.ESC+c,w.cancel=!0}}else u.keyCode>=65&&u.keyCode<=90?w.key=String.fromCharCode(u.keyCode-64):u.keyCode===32?w.key=a.C0.NUL:u.keyCode>=51&&u.keyCode<=55?w.key=String.fromCharCode(u.keyCode-51+27):u.keyCode===56?w.key=a.C0.DEL:u.keyCode===219?w.key=a.C0.ESC:u.keyCode===220?w.key=a.C0.FS:u.keyCode===221&&(w.key=a.C0.GS)}return w}},482:(_,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Utf8ToUtf32=s.StringToUtf32=s.utf32ToString=s.stringFromCodePoint=void 0,s.stringFromCodePoint=function(h){return h>65535?(h-=65536,String.f Line truncated
\`,l)}paste(g){this._core.paste(g)}refresh(g,l){this._verifyIntegers(g,l),this._core.refresh(g,l)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(g){this._addonManager.loadAddon(this,g)}static get strings(){return s}_verifyIntegers(...g){for(const l of g)if(l===1/0||isNaN(l)||l%1!=0)throw new Error("This API only accepts integers")}_verifyPositiveIntegers(...g){for(const l of g)if(l&&(l===1/0||isNaN(l)||l%1!=0||l<0))throw new Error("This API only accepts positive integers")}}_.Terminal=w})(),o})()))})(Hi)),Hi.exports}var co=lo(),zi={exports:{}},Hs;function uo(){return Hs||(Hs=1,(function(i,e){(function(t,r){i.exports=r()})(self,(()=>(()=>{var t={};return(()=>{var r=t;Object.defineProperty(r,"__esModule",{value:!0}),r.FitAddon=void 0,r.FitAddon=class{activate(n){this._terminal=n}dispose(){}fit(){const n=this.proposeDimensions();if(!n||!this._terminal||isNaN(n.cols)||isNaN(n.rows))return;const o=this._terminal._core;this._terminal.rows===n.rows&&this._terminal.cols===n.cols||(o._renderService.clear(),this._terminal.resize(n.cols,n.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;const n=this._terminal._core,o=n._renderService.dimensions;if(o.css.cell.width===0||o.css.cell.height===0)return;const _=this._terminal.options.scrollback===0?0:n.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),h=parseInt(s.getPropertyValue("height")),a=Math.max(0,parseInt(s.getPropertyValue("width"))),d=window.getComputedStyle(this._terminal.element),u=h-(parseInt(d.getPropertyValue("padding-top"))+parseInt(d.getPropertyValue("padding-bottom"))),p=a-(parseInt(d.getPropertyValue("padding-right"))+parseInt(d.getPropertyValue("padding-left")))-_;return{cols:Math.max(2,Math.floor(p/o.css.cell.width)),rows:Math.max(1,Math.floor(u/o.css.cell.height))}}}})(),t})()))})(zi)),zi.exports}var fo=uo(),Wi={exports:{}},zs;function _o(){return zs||(zs=1,(function(i,e){(function(t,r){i.exports=r()})(self,(()=>(()=>{var t={6:(_,s)=>{function h(d){try{const u=new URL(d),p=u.password&&u.username?\`\${u.protocol}//\${u.username}:\${u.password}@\${u.host}\`:u.username?\`\${u.protocol}//\${u.username}@\${u.host}\`:\`\${u.protocol}//\${u.host}\`;return d.toLocaleLowerCase().startsWith(p.toLocaleLowerCase())}catch{return!1}}Object.defineProperty(s,"__esModule",{value:!0}),s.LinkComputer=s.WebLinkProvider=void 0,s.WebLinkProvider=class{constructor(d,u,p,m={}){this._terminal=d,this._regex=u,this._handler=p,this._options=m}provideLinks(d,u){const p=a.computeLink(d,this._regex,this._terminal,this._handler);u(this._addCallbacks(p))}_addCallbacks(d){return d.map((u=>(u.leave=this._options.leave,u.hover=(p,m)=>{if(this._options.hover){const{range:C}=u;this._options.hover(p,m,C)}},u)))}};class a{static computeLink(u,p,m,C){const w=new RegExp(p.source,(p.flags||"")+"g"),[f,g]=a._getWindowedLineStrings(u-1,m),l=f.join("");let c;const v=[];for(;c=w.exec(l);){const y=c[0];if(!h(y))continue;const[S,x]=a._mapStrIdx(m,g,0,c.index),[b,E]=a._mapStrIdx(m,S,x,y.length);if(S===-1||x===-1||b===-1||E===-1)continue;const B={start:{x:x+1,y:S+1},end:{x:E,y:b+1}};v.push({range:B,text:y,activate:C})}return v}static _getWindowedLineStrings(u,p){let m,C=u,w=u,f=0,g="";const l=[];if(m=p.buffer.active.getLine(u)){const c=m.translateToString(!0);if(m.isWrapped&&c[0]!==" "){for(f=0;(m=p.buffer.active.getLine(--C))&&f<2048&&(g=m.translateToString(!0),f+=g.length,l.push(g),m.isWrapped&&g.indexOf(" ")===-1););l.reverse()}for(l.push(c),f=0;(m=p.buffer.active.getLine(++w))&&m.isWrapped&&f<2048&&(g=m.translateToString(!0),f+=g.length,l.push(g),g.indexOf(" ")===-1););}return[l,C]}static _mapStrIdx(u,p,m,C){const w=u.buffer.active,f=w.getNullCell();let g=m;for(;C;){const l=w.getLine(p);if(!l)return[-1,-1];for(let c=g;c<l.length;++c){l.getCell(c,f);const v=f.getChars();if(f.getWidth()&&(C-=v.length||1,c===l.length-1&&v==="")){const y=w.getLine(p+1);y&&y.isWrapped&&(y.getCell(0,f),f.getWidth()===2&&(C+=1))}if(C<0)return[p,c]}p++,g=0}return[p,g]}}s.LinkComputer=a}},r={};function n(_){var s=r[_];if(s!==void 0)return s.exports;var h=r[_]={exports:{}};return t[_](h,h.exports,n),h.exports}var o={};return(()=>{var _=o;Object.defineProperty(_,"__esModule",{value:!0}),_.WebLinksAddon=void 0;const s=n(6),h=/(https?|HTTPS?):[/]{2}[^\\s"'!*(){}|\\\\\\^<>\`]*[^\\s"':,.!?{}|\\\\\\^~\\[\\]\`()<>]/;function a(d,u){const p=window.open();if(p){try{p.opener=null}catch{}p.location.href=u}else console.warn("Opening link blocked as opener could not be cleared")}_.WebLinksAddon=class{constructor(d=a,u={}){this._handler=d,this._options=u}activate(d){this._terminal=d;const u=this._options,p=u.urlRegex||h;this._linkProvider=this._terminal.registerLinkProvider(new s.WebLinkProvider(this._terminal,p,this._handler,u))}dispose(){var d;(d=this._linkProvider)==null||d.dispose()}}})(),o})()))})(Wi)),Wi.exports}var go=_o();/**
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
* @license MIT
*
@@ -60,13 +60,13 @@ WARNING: This link could potentially be dangerous\`)){const o=window.open();if(o
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
*/var Vs=(r,e,_,C)=>{for(var g=e,k=r.length-1,w;k>=0;k--)(w=r[k])&&(g=w(g)||g);return g},Gs=(r,e)=>(_,C)=>e(_,C,r),Xs=class{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(r){setTimeout(()=>{throw r.stack?fi.isErrorNoTelemetry(r)?new fi(r.message+\`
*/var po=(i,e,t,r)=>{for(var n=e,o=i.length-1,_;o>=0;o--)(_=i[o])&&(n=_(n)||n);return n},vo=(i,e)=>(t,r)=>e(t,r,i),mo=class{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(e){setTimeout(()=>{throw e.stack?Ws.isErrorNoTelemetry(e)?new Ws(e.message+\`
\`+r.stack):new Error(r.message+\`
\`+e.stack):new Error(e.message+\`
\`+r.stack):r},0)}}addListener(r){return this.listeners.push(r),()=>{this._removeListener(r)}}emit(r){this.listeners.forEach(e=>{e(r)})}_removeListener(r){this.listeners.splice(this.listeners.indexOf(r),1)}setUnexpectedErrorHandler(r){this.unexpectedErrorHandler=r}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(r){this.unexpectedErrorHandler(r),this.emit(r)}onUnexpectedExternalError(r){this.unexpectedErrorHandler(r)}},Ys=new Xs;function Et(r){Js(r)||Ys.onUnexpectedError(r)}var Jt="Canceled";function Js(r){return r instanceof Qs?!0:r instanceof Error&&r.name===Jt&&r.message===Jt}var Qs=class extends Error{constructor(){super(Jt),this.name=this.message}},fi=class Qt extends Error{constructor(e){super(e),this.name="CodeExpectedError"}static fromError(e){if(e instanceof Qt)return e;let _=new Qt;return _.message=e.message,_.stack=e.stack,_}static isErrorNoTelemetry(e){return e.name==="CodeExpectedError"}},er;(r=>{function e(k){return k<0}r.isLessThan=e;function _(k){return k<=0}r.isLessThanOrEqual=_;function C(k){return k>0}r.isGreaterThan=C;function g(k){return k===0}r.isNeitherLessOrGreaterThan=g,r.greaterThan=1,r.lessThan=-1,r.neitherLessOrGreaterThan=0})(er||(er={}));function tr(r,e){let _=this,C=!1,g;return function(){return C||(C=!0,e||(g=r.apply(_,arguments))),g}}var ls;(r=>{function e(c){return c&&typeof c=="object"&&typeof c[Symbol.iterator]=="function"}r.is=e;let _=Object.freeze([]);function C(){return _}r.empty=C;function*g(c){yield c}r.single=g;function k(c){return e(c)?c:g(c)}r.wrap=k;function w(c){return c||_}r.from=w;function*s(c){for(let m=c.length-1;m>=0;m--)yield c[m]}r.reverse=s;function a(c){return!c||c[Symbol.iterator]().next().done===!0}r.isEmpty=a;function l(c){return c[Symbol.iterator]().next().value}r.first=l;function d(c,m){let S=0;for(let y of c)if(m(y,S++))return!0;return!1}r.some=d;function h(c,m){for(let S of c)if(m(S))return S}r.find=h;function*u(c,m){for(let S of c)m(S)&&(yield S)}r.filter=u;function*v(c,m){let S=0;for(let y of c)yield m(y,S++)}r.map=v;function*b(c,m){let S=0;for(let y of c)yield*m(y,S++)}r.flatMap=b;function*p(...c){for(let m of c)yield*m}r.concat=p;function t(c,m,S){let y=S;for(let f of c)y=m(y,f);return y}r.reduce=t;function*o(c,m,S=c.length){for(m<0&&(m+=c.length),S<0?S+=c.length:S>c.length&&(S=c.length);m<S;m++)yield c[m]}r.slice=o;function i(c,m=Number.POSITIVE_INFINITY){let S=[];if(m===0)return[S,c];let y=c[Symbol.iterator]();for(let f=0;f<m;f++){let L=y.next();if(L.done)return[S,r.empty()];S.push(L.value)}return[S,{[Symbol.iterator](){return y}}]}r.consume=i;async function n(c){let m=[];for await(let S of c)m.push(S);return Promise.resolve(m)}r.asyncToArray=n})(ls||(ls={}));function cs(r){if(ls.is(r)){let e=[];for(let _ of r)if(_)try{_.dispose()}catch(C){e.push(C)}if(e.length===1)throw e[0];if(e.length>1)throw new AggregateError(e,"Encountered errors while disposing of store");return Array.isArray(r)?[]:r}else if(r)return r.dispose(),r}function ds(...r){return _e(()=>cs(r))}function _e(r){return{dispose:tr(()=>{r()})}}var us=class _s{constructor(){this._toDispose=new Set,this._isDisposed=!1}dispose(){this._isDisposed||(this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(this._toDispose.size!==0)try{cs(this._toDispose)}finally{this._toDispose.clear()}}add(e){if(!e)return e;if(e===this)throw new Error("Cannot register a disposable on itself!");return this._isDisposed?_s.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(e),e}delete(e){if(e){if(e===this)throw new Error("Cannot dispose a disposable on itself!");this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),void 0)}};us.DISABLE_DISPOSED_WARNING=!1;var Ve=us,we=class{constructor(){this._store=new Ve,this._store}dispose(){this._store.dispose()}_register(r){if(r===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(r)}};we.None=Object.freeze({dispose(){}});var et=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(r){var e;this._isDisposed||r===this._value||((e=this._value)==null||e.dispose(),this._value=r)}clear(){this.value=void 0}dispose(){var r;this._isDisposed=!0,(r=this._value)==null||r.dispose(),this._value=void 0}clearAndLeak(){let r=this._value;return this._value=void 0,r}},oi=typeof process<"u"&&"title"in process,St=oi?"node":navigator.userAgent,ir=oi?"node":navigator.platform,sr=St.includes("Firefox"),rr=St.includes("Edge"),fs=/^((?!chrome|android).)*safari/i.test(St);function nr(){if(!fs)return 0;let r=St.match(/Version\\/(\\d+)/);return r===null||r.length<2?0:parseInt(r[1])}ir.indexOf("Linux")>=0;var or="",ae=0,he=0,le=0,re=0,me={css:"#00000000",rgba:0},pe;(r=>{function e(g,k,w,s){return s!==void 0?\`#\${He(g)}\${He(k)}\${He(w)}\${He(s)}\`:\`#\Line truncated
\`+e.stack):e},0)}}addListener(e){return this.listeners.push(e),()=>{this._removeListener(e)}}emit(e){this.listeners.forEach(t=>{t(e)})}_removeListener(e){this.listeners.splice(this.listeners.indexOf(e),1)}setUnexpectedErrorHandler(e){this.unexpectedErrorHandler=e}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(e){this.unexpectedErrorHandler(e),this.emit(e)}onUnexpectedExternalError(e){this.unexpectedErrorHandler(e)}},wo=new mo;function Ui(i){bo(i)||wo.onUnexpectedError(i)}var ws="Canceled";function bo(i){return i instanceof So?!0:i instanceof Error&&i.name===ws&&i.message===ws}var So=class extends Error{constructor(){super(ws),this.name=this.message}},Ws=class bs extends Error{constructor(e){super(e),this.name="CodeExpectedError"}static fromError(e){if(e instanceof bs)return e;let t=new bs;return t.message=e.message,t.stack=e.stack,t}static isErrorNoTelemetry(e){return e.name==="CodeExpectedError"}},yo;(i=>{function e(o){return o<0}i.isLessThan=e;function t(o){return o<=0}i.isLessThanOrEqual=t;function r(o){return o>0}i.isGreaterThan=r;function n(o){return o===0}i.isNeitherLessOrGreaterThan=n,i.greaterThan=1,i.lessThan=-1,i.neitherLessOrGreaterThan=0})(yo||(yo={}));function Co(i,e){let t=this,r=!1,n;return function(){return r||(r=!0,e||(n=i.apply(t,arguments))),n}}var sn;(i=>{function e(v){return v&&typeof v=="object"&&typeof v[Symbol.iterator]=="function"}i.is=e;let t=Object.freeze([]);function r(){return t}i.empty=r;function*n(v){yield v}i.single=n;function o(v){return e(v)?v:n(v)}i.wrap=o;function _(v){return v||t}i.from=_;function*s(v){for(let y=v.length-1;y>=0;y--)yield v[y]}i.reverse=s;function h(v){return!v||v[Symbol.iterator]().next().done===!0}i.isEmpty=h;function a(v){return v[Symbol.iterator]().next().value}i.first=a;function d(v,y){let S=0;for(let x of v)if(y(x,S++))return!0;return!1}i.some=d;function u(v,y){for(let S of v)if(y(S))return S}i.find=u;function*p(v,y){for(let S of v)y(S)&&(yield S)}i.filter=p;function*m(v,y){let S=0;for(let x of v)yield y(x,S++)}i.map=m;function*C(v,y){let S=0;for(let x of v)yield*y(x,S++)}i.flatMap=C;function*w(...v){for(let y of v)yield*y}i.concat=w;function f(v,y,S){let x=S;for(let b of v)x=y(x,b);return x}i.reduce=f;function*g(v,y,S=v.length){for(y<0&&(y+=v.length),S<0?S+=v.length:S>v.length&&(S=v.length);y<S;y++)yield v[y]}i.slice=g;function l(v,y=Number.POSITIVE_INFINITY){let S=[];if(y===0)return[S,v];let x=v[Symbol.iterator]();for(let b=0;b<y;b++){let E=x.next();if(E.done)return[S,i.empty()];S.push(E.value)}return[S,{[Symbol.iterator](){return x}}]}i.consume=l;async function c(v){let y=[];for await(let S of v)y.push(S);return Promise.resolve(y)}i.asyncToArray=c})(sn||(sn={}));function rn(i){if(sn.is(i)){let e=[];for(let t of i)if(t)try{t.dispose()}catch(r){e.push(r)}if(e.length===1)throw e[0];if(e.length>1)throw new AggregateError(e,"Encountered errors while disposing of store");return Array.isArray(i)?[]:i}else if(i)return i.dispose(),i}function nn(...i){return Se(()=>rn(i))}function Se(i){return{dispose:Co(()=>{i()})}}var on=class an{constructor(){this._toDispose=new Set,this._isDisposed=!1}dispose(){this._isDisposed||(this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(this._toDispose.size!==0)try{rn(this._toDispose)}finally{this._toDispose.clear()}}add(e){if(!e)return e;if(e===this)throw new Error("Cannot register a disposable on itself!");return this._isDisposed?an.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(e),e}delete(e){if(e){if(e===this)throw new Error("Cannot dispose a disposable on itself!");this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),void 0)}};on.DISABLE_DISPOSED_WARNING=!1;var kt=on,Pe=class{constructor(){this._store=new kt,this._store}dispose(){this._store.dispose()}_register(e){if(e===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(e)}};Pe.None=Object.freeze({dispose(){}});var Ot=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){var t;this._isDisposed||e===this._value||((t=this._value)==null||t.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){var e;this._isDisposed=!0,(e=this._value)==null||e.dispose(),this._value=void 0}clearAndLeak(){let e=this._value;return this._value=void 0,e}},Ts=typeof process<"u"&&"title"in process,Ii=Ts?"node":navigator.userAgent,xo=Ts?"node":navigator.platform,Eo=Ii.includes("Firefox"),Lo=Ii.includes("Edge"),hn=/^((?!chrome|android).)*safari/i.test(Ii);function ko(){if(!hn)return 0;let i=Ii.match(/Version\\/(\\d+)/);return i===null||i.length<2?0:parseInt(i[1])}xo.indexOf("Linux")>=0;var Ao="",_e=0,ge=0,pe=0,le=0,Re={css:"#00000000",rgba:0},Ce;(i=>{function e(n,o,_,s){return s!==void 0?\`#\${lt(n)}\${lt(o)}\${lt(_)}\${lt(s)}\`:\`#\Line truncated
\`).slice(2).join(\`
\`))}},Ir=class extends Error{constructor(r,e){super(r),this.name="ListenerLeakError",this.stack=e}},Or=class extends Error{constructor(r,e){super(r),this.name="ListenerRefusalError",this.stack=e}},Pr=0,kt=class{constructor(r){this.value=r,this.id=Pr++}},Fr=2,Hr,ee=class{constructor(r){var e,_,C,g;this._size=0,this._options=r,this._leakageMon=(e=this._options)!=null&&e.leakWarningThreshold?new Tr((r==null?void 0:r.onListenerError)??Et,((_=this._options)==null?void 0:_.leakWarningThreshold)??Br):void 0,this._perfMon=(C=this._options)!=null&&C._profName?new Mr(this._options._profName):void 0,this._deliveryQueue=(g=this._options)==null?void 0:g.deliveryQueue}dispose(){var r,e,_,C;this._disposed||(this._disposed=!0,((r=this._deliveryQueue)==null?void 0:r.current)===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),(_=(e=this._options)==null?void 0:e.onDidRemoveLastListener)==null||_.call(e),(C=this._leakageMon)==null||C.dispose())}get event(){return this._event??(this._event=(r,e,_)=>{var w,s,a,l,d;if(this._leakageMon&&this._size>this._leakageMon.threshold**2){let h=\`[\${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (\${this._size} vs \${this._leakageMon.threshold})\`;console.warn(h);let u=this._leakageMon.getMostFrequentStack()??["UNKNOWN stack",-1],v=new Or(\`\${h}. HINT: Stack shows most frequent listener (\${u[1]}-times)\`,u[0]);return(((w=this._options)==null?void 0:w.onListenerError)||Et)(v),we.None}if(this._disposed)return we.None;e&&(r=r.bind(e));let C=new kt(r),g;this._leakageMon&&this._size>=Math.ceil(this._leakageMon.threshold*.2)&&(C.stack=Dr.create(),g=this._leakageMon.check(C.stack,this._size+1)),this._listeners?this._listeners instanceof kt?(this._deliveryQueue??(this._deliveryQueue=new Nr),this._listeners=[this._listeners,C]):this._listeners.push(C):((a=(s=this._options)==null?void 0:s.onWillAddFirstListener)==null||a.call(s,this),this._listeners=C,(d=(l=this._options)==null?void 0:l.onDidAddFirstListener)==null||d.call(l,this)),this._size++;let k=_e(()=>{g==null||g(),this._removeListener(C)});return _ instanceof Ve?_.add(k):Array.isArray(_)&&_.push(k),k}),this._event}_removeListener(r){var g,k,w,s;if((k=(g=this._options)==null?void 0:g.onWillRemoveListener)==null||k.call(g,this),!this._listeners)return;if(this._size===1){this._listeners=void 0,(s=(w=this._options)==null?void 0:w.onDidRemoveLastListener)==null||s.call(w,this),this._size=0;return}let e=this._listeners,_=e.indexOf(r);if(_===-1)throw console.log("disposed?",this._disposed),console.log("size?",this._size),console.log("arr?",JSON.stringify(this._listeners)),new Error("Attempted to dispose unknown listener");this._size--,e[_]=void 0;let C=this._deliveryQueue.current===this;if(this._size*Fr<=e.length){let a=0;for(let l=0;l<e.length;l++)e[l]?e[a++]=e[l]:C&&(this._deliveryQueue.end--,a<this._deliveryQueue.i&&this._deliveryQueue.i--);e.length=a}}_deliver(r,e){var C;if(!r)return;let _=((C=this._options)==null?void 0:C.onListenerError)||Et;if(!_){r.value(e);return}try{r.value(e)}catch(g){_(g)}}_deliverQueue(r){let e=r.current._listeners;for(;r.i<r.end;)this._deliver(e[r.i++],r.value);r.reset()}fire(r){var e,_,C,g;if((e=this._deliveryQueue)!=null&&e.current&&(this._deliverQueue(this._deliveryQueue),(_=this._perfMon)==null||_.stop()),(C=this._perfMon)==null||C.start(this._size),this._listeners)if(this._listeners instanceof kt)this._deliver(this._listeners,r);else{let k=this._deliveryQueue;k.enqueue(this,r,this._listeners.length),this._deliverQueue(k)}(g=this._perfMon)==null||g.stop()}hasListeners(){return this._size>0}},Nr=class{constructor(){this.i=-1,this.end=0}enqueue(r,e,_){this.i=0,this.end=_,this.current=r,this.value=e}reset(){this.i=this.end,this.current=void 0,this.value=void 0}},Si={texturePage:0,texturePosition:{x:0,y:0},texturePositionClipSpace:{x:0,y:0},offset:{x:0,y:0},size:{x:0,y:0},sizeClipSpace:{x:0,y:0}},tt=2,it,Pe=class Ze{constructor(e,_,C){this._document=e,this._config=_,this._unicodeService=C,this._didWarmUp=!1,this._cacheMap=new mi,this._cacheMapCombined=new mi,this._pages=[],this._activePages=[],this._workBoundingBox={top:0,left:0,bottom:0,right:0},this._workAttributeData=new qe,this._textureSize=512,this._onAddTextureAtlasCanvas=new ee,this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event,this._onRemoveTextureAtlasCanvas=new ee,this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event,this._requestClearModel=!1,this._createNewPage(),this._tmpCanvas=ks(e,this._config.deviceCellWidth*4+tt*2,this._config.deviceCellHeight+tt*2),this._tmpCtx=ne(this._tmpCanvas.getContext("2d",{alpha:this._config.allowTransparency,willReadFrequently:!0}))}get pages(){return this._pages}dispose(){this._tmpCanvas.remove();for(let e of this.pages)e.canvas.remove();this._onAddTextureAtlasCanvas.dispose()}warmUp(){this._didWarmUp||(this._doWarmUp(),this._didWarmUp=!0)}_doWarmUp(){let e=new kr;for(let _=33;_<126;_++)e.enqueue(()=>{if Line truncated
\`))}},ia=class extends Error{constructor(e,t){super(e),this.name="ListenerLeakError",this.stack=t}},sa=class extends Error{constructor(e,t){super(e),this.name="ListenerRefusalError",this.stack=t}},ra=0,ji=class{constructor(e){this.value=e,this.id=ra++}},na=2,oa,re=class{constructor(e){var t,r,n,o;this._size=0,this._options=e,this._leakageMon=(t=this._options)!=null&&t.leakWarningThreshold?new ea((e==null?void 0:e.onListenerError)??Ui,((r=this._options)==null?void 0:r.leakWarningThreshold)??Qo):void 0,this._perfMon=(n=this._options)!=null&&n._profName?new Jo(this._options._profName):void 0,this._deliveryQueue=(o=this._options)==null?void 0:o.deliveryQueue}dispose(){var e,t,r,n;this._disposed||(this._disposed=!0,((e=this._deliveryQueue)==null?void 0:e.current)===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),(r=(t=this._options)==null?void 0:t.onDidRemoveLastListener)==null||r.call(t),(n=this._leakageMon)==null||n.dispose())}get event(){return this._event??(this._event=(e,t,r)=>{var s,h,a,d,u;if(this._leakageMon&&this._size>this._leakageMon.threshold**2){let p=\`[\${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (\${this._size} vs \${this._leakageMon.threshold})\`;console.warn(p);let m=this._leakageMon.getMostFrequentStack()??["UNKNOWN stack",-1],C=new sa(\`\${p}. HINT: Stack shows most frequent listener (\${m[1]}-times)\`,m[0]);return(((s=this._options)==null?void 0:s.onListenerError)||Ui)(C),Pe.None}if(this._disposed)return Pe.None;t&&(e=e.bind(t));let n=new ji(e),o;this._leakageMon&&this._size>=Math.ceil(this._leakageMon.threshold*.2)&&(n.stack=ta.create(),o=this._leakageMon.check(n.stack,this._size+1)),this._listeners?this._listeners instanceof ji?(this._deliveryQueue??(this._deliveryQueue=new aa),this._listeners=[this._listeners,n]):this._listeners.push(n):((a=(h=this._options)==null?void 0:h.onWillAddFirstListener)==null||a.call(h,this),this._listeners=n,(u=(d=this._options)==null?void 0:d.onDidAddFirstListener)==null||u.call(d,this)),this._size++;let _=Se(()=>{o==null||o(),this._removeListener(n)});return r instanceof kt?r.add(_):Array.isArray(r)&&r.push(_),_}),this._event}_removeListener(e){var o,_,s,h;if((_=(o=this._options)==null?void 0:o.onWillRemoveListener)==null||_.call(o,this),!this._listeners)return;if(this._size===1){this._listeners=void 0,(h=(s=this._options)==null?void 0:s.onDidRemoveLastListener)==null||h.call(s,this),this._size=0;return}let t=this._listeners,r=t.indexOf(e);if(r===-1)throw console.log("disposed?",this._disposed),console.log("size?",this._size),console.log("arr?",JSON.stringify(this._listeners)),new Error("Attempted to dispose unknown listener");this._size--,t[r]=void 0;let n=this._deliveryQueue.current===this;if(this._size*na<=t.length){let a=0;for(let d=0;d<t.length;d++)t[d]?t[a++]=t[d]:n&&(this._deliveryQueue.end--,a<this._deliveryQueue.i&&this._deliveryQueue.i--);t.length=a}}_deliver(e,t){var n;if(!e)return;let r=((n=this._options)==null?void 0:n.onListenerError)||Ui;if(!r){e.value(t);return}try{e.value(t)}catch(o){r(o)}}_deliverQueue(e){let t=e.current._listeners;for(;e.i<e.end;)this._deliver(t[e.i++],e.value);e.reset()}fire(e){var t,r,n,o;if((t=this._deliveryQueue)!=null&&t.current&&(this._deliverQueue(this._deliveryQueue),(r=this._perfMon)==null||r.stop()),(n=this._perfMon)==null||n.start(this._size),this._listeners)if(this._listeners instanceof ji)this._deliver(this._listeners,e);else{let _=this._deliveryQueue;_.enqueue(this,e,this._listeners.length),this._deliverQueue(_)}(o=this._perfMon)==null||o.stop()}hasListeners(){return this._size>0}},aa=class{constructor(){this.i=-1,this.end=0}enqueue(e,t,r){this.i=0,this.end=r,this.current=e,this.value=t}reset(){this.i=this.end,this.current=void 0,this.value=void 0}},Ks={texturePage:0,texturePosition:{x:0,y:0},texturePositionClipSpace:{x:0,y:0},offset:{x:0,y:0},size:{x:0,y:0},sizeClipSpace:{x:0,y:0}},Ft=2,$t,st=class Ct{constructor(e,t,r){this._document=e,this._config=t,this._unicodeService=r,this._didWarmUp=!1,this._cacheMap=new qs,this._cacheMapCombined=new qs,this._pages=[],this._activePages=[],this._workBoundingBox={top:0,left:0,bottom:0,right:0},this._workAttributeData=new xt,this._textureSize=512,this._onAddTextureAtlasCanvas=new re,this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event,this._onRemoveTextureAtlasCanvas=new re,this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event,this._requestClearModel=!1,this._createNewPage(),this._tmpCanvas=Sn(e,this._config.deviceCellWidth*4+Ft*2,this._config.deviceCellHeight+Ft*2),this._tmpCtx=ce(this._tmpCanvas.getContext("2d",{alpha:this._config.allowTransparency,willReadFrequently:!0}))}get pages(){return this._pages}dispose(){this._tmpCanvas.remove();for(let e of this.pages)e.canvas.remove();this._onAddTextureAtlasCanvas.dispose()}warmUp(){this._didWarmUp||(this._doWarmUp(),this._didWarmUp=!0)}_doWarmUp(){let e=new Zo;for(let t=33;t<126;t++)e.enqueue(()=>{if Line truncated
layout (location = 0) in vec2 a_unitquad;
layout (location = 1) in vec2 a_cellpos;
layout (location = 2) in vec2 a_offset;
@@ -86,13 +86,13 @@ void main() {
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_texpage = int(a_texpage);
v_texcoord = a_texcoord + a_unitquad * a_texsize;
}\`;function Vr(r){let e="";for(let _=1;_<r;_++)e+=\` else if (v_texpage == \${_}) { outColor = texture(u_texture[\${_}], v_texcoord); }\`;return\`#version 300 es
}\`;function pa(i){let e="";for(let t=1;t<i;t++)e+=\` else if (v_texpage == \${t}) { outColor = texture(u_texture[\${t}], v_texcoord); }\`;return\`#version 300 es
precision lowp float;
in vec2 v_texcoord;
flat in int v_texpage;
uniform sampler2D u_texture[\${r}];
uniform sampler2D u_texture[\${i}];
out vec4 outColor;
@@ -100,7 +100,7 @@ void main() {
if (v_texpage == 0) {
outColor = texture(u_texture[0], v_texcoord);
} \${e}
}\`}var Fe=11,ze=Fe*Float32Array.BYTES_PER_ELEMENT,Gr=2,se=0,Q,Mt=0,st=0,Xr=class extends we{constructor(r,e,_,C){super(),this._terminal=r,this._gl=e,this._dimensions=_,this._optionsService=C,this._activeBuffer=0,this._vertices={count:0,attributes:new Float32Array(0),attributesBuffers:[new Float32Array(0),new Float32Array(0)]};let g=this._gl;Pe.maxAtlasPages===void 0&&(Pe.maxAtlasPages=Math.min(32,ne(g.getParameter(g.MAX_TEXTURE_IMAGE_UNITS))),Pe.maxTextureSize=ne(g.getParameter(g.MAX_TEXTURE_SIZE))),this._program=ne(Bs(g,Kr,Vr(Pe.maxAtlasPages))),this._register(_e(()=>g.deleteProgram(this._program))),this._projectionLocation=ne(g.getUniformLocation(this._program,"u_projection")),this._resolutionLocation=ne(g.getUniformLocation(this._program,"u_resolution")),this._textureLocation=ne(g.getUniformLocation(this._program,"u_texture")),this._vertexArrayObject=g.createVertexArray(),g.bindVertexArray(this._vertexArrayObject);let k=new Float32Array([0,0,1,0,0,1,1,1]),w=g.createBuffer();this._register(_e(()=>g.deleteBuffer(w))),g.bindBuffer(g.ARRAY_BUFFER,w),g.bufferData(g.ARRAY_BUFFER,k,g.STATIC_DRAW),g.enableVertexAttribArray(0),g.vertexAttribPointer(0,2,this._gl.FLOAT,!1,0,0);let s=new Uint8Array([0,1,2,3]),a=g.createBuffer();this._register(_e(()=>g.deleteBuffer(a))),g.bindBuffer(g.ELEMENT_ARRAY_BUFFER,a),g.bufferData(g.ELEMENT_ARRAY_BUFFER,s,g.STATIC_DRAW),this._attributesBuffer=ne(g.createBuffer()),this._register(_e(()=>g.deleteBuffer(this._attributesBuffer))),g.bindBuffer(g.ARRAY_BUFFER,this._attributesBuffer),g.enableVertexAttribArray(2),g.vertexAttribPointer(2,2,g.FLOAT,!1,ze,0),g.vertexAttribDivisor(2,1),g.enableVertexAttribArray(3),g.vertexAttribPointer(3,2,g.FLOAT,!1,ze,2*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(3,1),g.enableVertexAttribArray(4),g.vertexAttribPointer(4,1,g.FLOAT,!1,ze,4*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(4,1),g.enableVertexAttribArray(5),g.vertexAttribPointer(5,2,g.FLOAT,!1,ze,5*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(5,1),g.enableVertexAttribArray(6),g.vertexAttribPointer(6,2,g.FLOAT,!1,ze,7*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(6,1),g.enableVertexAttribArray(1),g.vertexAttribPointer(1,2,g.FLOAT,!1,ze,9*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(1,1),g.useProgram(this._program);let l=new Int32Array(Pe.maxAtlasPages);for(let d=0;d<Pe.maxAtlasPages;d++)l[d]=d;g.uniform1iv(this._textureLocation,l),g.uniformMatrix4fv(this._projectionLocation,!1,Ms),this._atlasTextures=[];for(let d=0;d<Pe.maxAtlasPages;d++){let h=new qr(ne(g.createTexture()));this._register(_e(()=>g.deleteTexture(h.texture))),g.activeTexture(g.TEXTURE0+d),g.bindTexture(g.TEXTURE_2D,h.texture),g.texParameteri(g.TEXTURE_2D,g.TEXTURE_WRAP_S,g.CLAMP_TO_EDGE),g.texParameteri(g.TEXTURE_2D,g.TEXTURE_WRAP_T,g.CLAMP_TO_EDGE),g.texImage2D(g.TEXTURE_2D,0,g.RGBA,1,1,0,g.RGBA,g.UNSIGNED_BYTE,new Uint8Array([255,0,0,255])),this._atlasTextures[d]=h}g.enable(g.BLEND),g.blendFunc(g.SRC_ALPHA,g.ONE_MINUS_SRC_ALPHA),this.handleResize()}beginFrame(){return this._atlas?this._atlas.beginFrame():!0}updateCell(r,e,_,C,g,k,w,s,a){this._updateCell(this._vertices.attributes,r,e,_,C,g,k,w,s,a)}_updateCell(r,e,_,C,g,k,w,s,a,l){if(se=(_*this._terminal.cols+e)*Fe,C===0||C===void 0){r.fill(0,se,se+Fe-1-Gr);return}this._atlas&&(s&&s.length>1?Q=this._atlas.getRasterizedGlyphCombinedChar(s,g,k,w,!1,this._terminal.element):Q=this._atlas.getRasterizedGlyph(C,g,k,w,!1,this._terminal.element),Mt=Math.floor((this._dimensions.device.cell.width-this._dimensions.device.char.width)/2),g!==l&&Q.offset.x>Mt?(st=Q.offset.x-Mt,r[se]=-(Q.offset.x-st)+this._dimensions.device.char.left,r[se+1]=-Q.offset.y+this._dimensions.device.char.top,r[se+2]=(Q.size.x-st)/this._dimensions.device.canvas.width,r[se+3]=Q.size.y/this._dimensions.device.canvas.height,r[se+4]=Q.texturePage,r[se+5]=Q.texturePositionClipSpace.x+st/this._atlas.pages[Q.texturePage].canvas.width,r[se+6]=Q.texturePositionClipSpace.y,r[se+7]=Q.sizeClipSpace.x-st/this._atlas.pages[Q.texturePage].canvas.width,r[se+8]=Q.sizeClipSpace.y):(r[se]=-Q.offset.x+this._dimensions.device.char.left,r[se+1]=-Q.offset.y+this._dimensions.device.char.top,r[se+2]=Q.size.x/this._dimensions.device.canvas.width,r[se+3]=Q.size.y/this._dimensions.device.canvas.height,r[se+4]=Q.texturePage,r[se+5]=Q.texturePositionClipSpace.x,r[se+6]=Q.texturePositionClipSpace.y,r[se+7]=Q.sizeClipSpace.x,r[se+8]=Q.sizeClipSpace.y),this._optionsService.rawOptions.rescaleOverlappingGlyphs&&ur(C,a,Q.size.x,this._dimensions.device.cell.width)&&(r[se+2]=(this._dimensions.device.cell.width-1)/this._dimensions.device.canvas.width))}clear(){let r=this._terminal,e=r.cols*r.rows*Fe;this._vertices.count!==e?this._vertices.attributes=new Float32Array(e):this._vertices.attributes.fill(0);let _=0;for(;_<this._vertices.attributesBuffers.length;_++)this._vertices.count!==e?this._vertices.attributesBuffers[_]=new Float32Array(e):this._vertices.attributesBuffers[_].fill(0);this._vertices.count=e,_=0;for Line truncated
}\`}var rt=11,bt=rt*Float32Array.BYTES_PER_ELEMENT,va=2,he=0,se,Vi=0,Ht=0,ma=class extends Pe{constructor(e,t,r,n){super(),this._terminal=e,this._gl=t,this._dimensions=r,this._optionsService=n,this._activeBuffer=0,this._vertices={count:0,attributes:new Float32Array(0),attributesBuffers:[new Float32Array(0),new Float32Array(0)]};let o=this._gl;st.maxAtlasPages===void 0&&(st.maxAtlasPages=Math.min(32,ce(o.getParameter(o.MAX_TEXTURE_IMAGE_UNITS))),st.maxTextureSize=ce(o.getParameter(o.MAX_TEXTURE_SIZE))),this._program=ce(En(o,ga,pa(st.maxAtlasPages))),this._register(Se(()=>o.deleteProgram(this._program))),this._projectionLocation=ce(o.getUniformLocation(this._program,"u_projection")),this._resolutionLocation=ce(o.getUniformLocation(this._program,"u_resolution")),this._textureLocation=ce(o.getUniformLocation(this._program,"u_texture")),this._vertexArrayObject=o.createVertexArray(),o.bindVertexArray(this._vertexArrayObject);let _=new Float32Array([0,0,1,0,0,1,1,1]),s=o.createBuffer();this._register(Se(()=>o.deleteBuffer(s))),o.bindBuffer(o.ARRAY_BUFFER,s),o.bufferData(o.ARRAY_BUFFER,_,o.STATIC_DRAW),o.enableVertexAttribArray(0),o.vertexAttribPointer(0,2,this._gl.FLOAT,!1,0,0);let h=new Uint8Array([0,1,2,3]),a=o.createBuffer();this._register(Se(()=>o.deleteBuffer(a))),o.bindBuffer(o.ELEMENT_ARRAY_BUFFER,a),o.bufferData(o.ELEMENT_ARRAY_BUFFER,h,o.STATIC_DRAW),this._attributesBuffer=ce(o.createBuffer()),this._register(Se(()=>o.deleteBuffer(this._attributesBuffer))),o.bindBuffer(o.ARRAY_BUFFER,this._attributesBuffer),o.enableVertexAttribArray(2),o.vertexAttribPointer(2,2,o.FLOAT,!1,bt,0),o.vertexAttribDivisor(2,1),o.enableVertexAttribArray(3),o.vertexAttribPointer(3,2,o.FLOAT,!1,bt,2*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(3,1),o.enableVertexAttribArray(4),o.vertexAttribPointer(4,1,o.FLOAT,!1,bt,4*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(4,1),o.enableVertexAttribArray(5),o.vertexAttribPointer(5,2,o.FLOAT,!1,bt,5*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(5,1),o.enableVertexAttribArray(6),o.vertexAttribPointer(6,2,o.FLOAT,!1,bt,7*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(6,1),o.enableVertexAttribArray(1),o.vertexAttribPointer(1,2,o.FLOAT,!1,bt,9*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(1,1),o.useProgram(this._program);let d=new Int32Array(st.maxAtlasPages);for(let u=0;u<st.maxAtlasPages;u++)d[u]=u;o.uniform1iv(this._textureLocation,d),o.uniformMatrix4fv(this._projectionLocation,!1,xn),this._atlasTextures=[];for(let u=0;u<st.maxAtlasPages;u++){let p=new _a(ce(o.createTexture()));this._register(Se(()=>o.deleteTexture(p.texture))),o.activeTexture(o.TEXTURE0+u),o.bindTexture(o.TEXTURE_2D,p.texture),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE),o.texImage2D(o.TEXTURE_2D,0,o.RGBA,1,1,0,o.RGBA,o.UNSIGNED_BYTE,new Uint8Array([255,0,0,255])),this._atlasTextures[u]=p}o.enable(o.BLEND),o.blendFunc(o.SRC_ALPHA,o.ONE_MINUS_SRC_ALPHA),this.handleResize()}beginFrame(){return this._atlas?this._atlas.beginFrame():!0}updateCell(e,t,r,n,o,_,s,h,a){this._updateCell(this._vertices.attributes,e,t,r,n,o,_,s,h,a)}_updateCell(e,t,r,n,o,_,s,h,a,d){if(he=(r*this._terminal.cols+t)*rt,n===0||n===void 0){e.fill(0,he,he+rt-1-va);return}this._atlas&&(h&&h.length>1?se=this._atlas.getRasterizedGlyphCombinedChar(h,o,_,s,!1,this._terminal.element):se=this._atlas.getRasterizedGlyph(n,o,_,s,!1,this._terminal.element),Vi=Math.floor((this._dimensions.device.cell.width-this._dimensions.device.char.width)/2),o!==d&&se.offset.x>Vi?(Ht=se.offset.x-Vi,e[he]=-(se.offset.x-Ht)+this._dimensions.device.char.left,e[he+1]=-se.offset.y+this._dimensions.device.char.top,e[he+2]=(se.size.x-Ht)/this._dimensions.device.canvas.width,e[he+3]=se.size.y/this._dimensions.device.canvas.height,e[he+4]=se.texturePage,e[he+5]=se.texturePositionClipSpace.x+Ht/this._atlas.pages[se.texturePage].canvas.width,e[he+6]=se.texturePositionClipSpace.y,e[he+7]=se.sizeClipSpace.x-Ht/this._atlas.pages[se.texturePage].canvas.width,e[he+8]=se.sizeClipSpace.y):(e[he]=-se.offset.x+this._dimensions.device.char.left,e[he+1]=-se.offset.y+this._dimensions.device.char.top,e[he+2]=se.size.x/this._dimensions.device.canvas.width,e[he+3]=se.size.y/this._dimensions.device.canvas.height,e[he+4]=se.texturePage,e[he+5]=se.texturePositionClipSpace.x,e[he+6]=se.texturePositionClipSpace.y,e[he+7]=se.sizeClipSpace.x,e[he+8]=se.sizeClipSpace.y),this._optionsService.rawOptions.rescaleOverlappingGlyphs&&Io(n,a,se.size.x,this._dimensions.device.cell.width)&&(e[he+2]=(this._dimensions.device.cell.width-1)/this._dimensions.device.canvas.width))}clear(){let e=this._terminal,t=e.cols*e.rows*rt;this._vertices.count!==t?this._vertices.attributes=new Float32Array(t):this._vertices.attributes.fill(0);let r=0;for(;r<this._vertices.attributesBuffers.length;r++)this._vertices.count!==t?this._vertices.attributesBuffers[r]=new Float32Array(t):this._vertices.attributesBuffers[r].fill(0);this Line truncated
layout (location = 0) in vec2 a_position;
layout (location = 1) in vec2 a_size;
layout (location = 2) in vec4 a_color;
@@ -114,7 +114,7 @@ void main() {
vec2 zeroToOne = a_position + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_color = a_color;
}\`,sn=\`#version 300 es
}\`,xa=\`#version 300 es
precision lowp float;
in vec4 v_color;
@@ -123,13 +123,21 @@ out vec4 outColor;
void main() {
outColor = v_color;
}\`,Me=8,Tt=Me*Float32Array.BYTES_PER_ELEMENT,rn=20*Me,Ei=class{constructor(){this.attributes=new Float32Array(rn),this.count=0}},Ae=0,Li=0,ki=0,Ri=0,Ai=0,Mi=0,Bi=0,nn=class extends we{constructor(r,e,_,C){super(),this._terminal=r,this._gl=e,this._dimensions=_,this._themeService=C,this._vertices=new Ei,this._verticesCursor=new Ei;let g=this._gl;this._program=ne(Bs(g,tn,sn)),this._register(_e(()=>g.deleteProgram(this._program))),this._projectionLocation=ne(g.getUniformLocation(this._program,"u_projection")),this._vertexArrayObject=g.createVertexArray(),g.bindVertexArray(this._vertexArrayObject);let k=new Float32Array([0,0,1,0,0,1,1,1]),w=g.createBuffer();this._register(_e(()=>g.deleteBuffer(w))),g.bindBuffer(g.ARRAY_BUFFER,w),g.bufferData(g.ARRAY_BUFFER,k,g.STATIC_DRAW),g.enableVertexAttribArray(3),g.vertexAttribPointer(3,2,this._gl.FLOAT,!1,0,0);let s=new Uint8Array([0,1,2,3]),a=g.createBuffer();this._register(_e(()=>g.deleteBuffer(a))),g.bindBuffer(g.ELEMENT_ARRAY_BUFFER,a),g.bufferData(g.ELEMENT_ARRAY_BUFFER,s,g.STATIC_DRAW),this._attributesBuffer=ne(g.createBuffer()),this._register(_e(()=>g.deleteBuffer(this._attributesBuffer))),g.bindBuffer(g.ARRAY_BUFFER,this._attributesBuffer),g.enableVertexAttribArray(0),g.vertexAttribPointer(0,2,g.FLOAT,!1,Tt,0),g.vertexAttribDivisor(0,1),g.enableVertexAttribArray(1),g.vertexAttribPointer(1,2,g.FLOAT,!1,Tt,2*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(1,1),g.enableVertexAttribArray(2),g.vertexAttribPointer(2,4,g.FLOAT,!1,Tt,4*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(2,1),this._updateCachedColors(C.colors),this._register(this._themeService.onChangeColors(l=>{this._updateCachedColors(l),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(r){let e=this._gl;e.useProgram(this._program),e.bindVertexArray(this._vertexArrayObject),e.uniformMatrix4fv(this._projectionLocation,!1,Ms),e.bindBuffer(e.ARRAY_BUFFER,this._attributesBuffer),e.bufferData(e.ARRAY_BUFFER,r.attributes,e.DYNAMIC_DRAW),e.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,e.UNSIGNED_BYTE,0,r.count)}handleResize(){this._updateViewportRectangle()}setDimensions(r){this._dimensions=r}_updateCachedColors(r){this._bgFloat=this._colorToFloat32Array(r.background),this._cursorFloat=this._colorToFloat32Array(r.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(r){let e=this._terminal,_=this._vertices,C=1,g,k,w,s,a,l,d,h,u,v,b;for(g=0;g<e.rows;g++){for(w=-1,s=0,a=0,l=!1,k=0;k<e.cols;k++)d=(g*e.cols+k)*mt,h=r.cells[d+pt],u=r.cells[d+vt],v=!!(u&67108864),(h!==s||u!==a&&(l||v))&&((s!==0||l&&a!==0)&&(b=C++*Me,this._updateRectangle(_,b,a,s,w,k,g)),w=k,s=h,a=u,l=v);(s!==0||l&&a!==0)&&(b=C++*Me,this._updateRectangle(_,b,a,s,w,e.cols,g))}_.count=C}updateCursor(r){let e=this._verticesCursor,_=r.cursor;if(!_||_.style==="block"){e.count=0;return}let C,g=0;(_.style==="bar"||_.style==="outline")&&(C=g++*Me,this._addRectangleFloat(e.attributes,C,_.x*this._dimensions.device.cell.width,_.y*this._dimensions.device.cell.height,_.style==="bar"?_.dpr*_.cursorWidth:_.dpr,this._dimensions.device.cell.height,this._cursorFloat)),(_.style==="underline"||_.style==="outline")&&(C=g++*Me,this._addRectangleFloat(e.attributes,C,_.x*this._dimensions.device.cell.width,(_.y+1)*this._dimensions.device.cell.height-_.dpr,_.width*this._dimensions.device.cell.width,_.dpr,this._cursorFloat)),_.style==="outline"&&(C=g++*Me,this._addRectangleFloat(e.attributes,C,_.x*this._dimensions.device.cell.width,_.y*this._dimensions.device.cell.height,_.width*this._dimensions.device.cell.width,_.dpr,this._cursorFloat),C=g++*Me,this._addRectangleFloat(e.attributes,C,(_.x+_.width)*this._dimensions.device.cell.width-_.dpr,_.y*this._dimensions.device.cell.height,_.dpr,this._dimensions.device.cell.height,this._cursorFloat)),e.count=g}_updateRectangle(r,e,_,C,g,k,w){if(_&67108864)switch(_&50331648){case 16777216:case 33554432:Ae=this._themeService.colors.ansi[_&255].rgba;break;case 50331648:Ae=(_&16777215)<<8;break;case 0:default:Ae=this._themeService.colors.foreground.rgba}else switch(C&50331648){case 16777216:case 33554432:Ae=this._themeService.colors.ansi[C&255].rgba;break;case 50331648:Ae=(C&16777215)<<8;break;case 0:default:Ae=this._themeService.colors.background.rgba}r.attributes.length<e+4&&(r.attributes=Zr(r.attributes,this._terminal.rows*this._terminal.cols*Me)),Li=g*this._dimensions.device.cell.width,ki=w*this._dimensions.device.cell.height,Ri=(Ae>>24&255)/255,Ai=(Ae>>16&255)/255,Mi=(Ae>>8&255)/255,Bi=1,this._addRectangle(r.attributes,e,Li,ki,(k-g)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,Ri,Ai,Mi,Bi)}_addRectangle(r,e,_,C,g,k,w,s,a,l){r[e]=_/this._dimensions.device.canvas.width,r[e+1]=C/this._dimensions.device.canvas.heig Line truncated
\`+C+" ("+g+" bytes)")){_.skip();return}const w=[];_.on("input",s=>{w.push(s)}),_.accept().then(()=>{Yt.Browser.save_to_disk(w,C)})}),e.start()}handleSend(e){const _=document.createElement("input");_.type="file",_.multiple=!0,_.style.display="none",_.onchange=C=>{const g=C.target.files;g&&g.length>0?Yt.Browser.send_files(e,g,{on_offer_response:(k,w)=>{w||console.warn("Server rejected file:",k.name)},on_progress:(k,w,s)=>{},on_file_complete:k=>{console.log("Finished uploading:",k.name)}}).then(()=>{e.close()}):e.close()},document.body.appendChild(_),_.click(),document.body.removeChild(_)}}const ts={cyberpunk:{background:"#0a0a0a",foreground:"#4af626",cursor:"#14d1ff",cursorAccent:"#0a0a0a",selectionBackground:"#273747"},glacier:{background:"#0a192f",foreground:"#64ffda",cursor:"#e6f1ff",cursorAccent:"#0a192f",selectionBackground:"#112240"},gruvbox:{background:"#282828",foreground:"#ebdbb2",cursor:"#d3869b",cursorAccent:"#282828",selectionBackground:"#504945"}};class Wn{constructor(e){Y(this,"terminal");Y(this,"fitAddon");Y(this,"webglAddon");Y(this,"ws",null);Y(this,"container");Y(this,"disposables",[]);Y(this,"heartbeatInterval",null);Y(this,"reconnectAttempts",0);Y(this,"maxReconnectAttempts",5);Y(this,"reconnectTimeout",null);Y(this,"lastConfig",null);this.container=document.getElementById(e),this.terminal=new zs.Terminal({cursorBlink:!0,cursorStyle:"block",fontSize:14,fontFamily:'"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',theme:ts.cyberpunk,allowProposedApi:!0,scrollback:1e4}),this.fitAddon=new Zs.FitAddon,this.terminal.loadAddon(this.fitAddon),this.terminal.loadAddon(new Ks.WebLinksAddon),window.addEventListener("resize",()=>this.fit()),this.container.addEventListener("contextmenu",async _=>{var C;_.preventDefault();try{const g=await navigator.clipboard.readText();g&&((C=this.ws)==null?void 0:C.readyState)===WebSocket.OPEN&&this.ws.send(g)}catch(g){console.error("Failed to read clipboard",g)}})}setTheme(e){this.terminal.options.theme=ts[e]}mount(){this.terminal.open(this.container);try{this.webglAddon=new An,this.webglAddon.onContextLoss(e=>{console.warn("WebGL context lost",e),this.webglAddon.dispose()}),this.terminal.loadAddon(this.webglAddon)}catch(e){console.warn("WebGL addon failed to load, falling back to canvas/dom",e)}this.fit(),this.terminal.writeln("\\x1B[1;33m╔══════════════════════════════════╗\\x1B[0m"),this.terminal.writeln("\\x1B[1;33m║ Connecting to CloudSSH ║\\x1B[0m"),this.terminal.writeln("\\x1B[1;33m╚══════════════════════════════════╝\\x1B[0m"),this.terminal.writeln("")}async connect(e){this.lastConfig=e;const _=new URL(window.location.href);return _.protocol=_.protocol==="https:"?"wss:":"ws:",_.pathname="/api/ssh",new Promise((C,g)=>{this.ws=new WebSocket(_.toString()),this.ws.onopen=()=>{var k;this.terminal.writeln("\\x1B[32m[+] WebSocket connected, sending credentials...\\x1B[0m"),(k=this.ws)==null||k.send(JSON.stringify({host:e.host,port:e.port,username:e.username,password:e.password,authMethod:e.authMethod,privateKey:e.privateKey})),this.reconnectAttempts=0,this.startHeartbeat(),C()},this.ws.onerror=()=>{g(new Error("WebSocket connection failed"))},this.ws.onclose=()=>{this.terminal.writeln("\\x1B[31m[-] 连接已关闭\\x1B[0m");const k=document.getElementById("status-text");k&&(k.innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE')},this.setupWebSocketHandlers(g)})}connectWithWebSocket(e){this.lastConfig=null,this.ws=e,e.onopen=()=>{this.terminal.writeln("\\x1B[32m[+] WebSocket connected, authenticating...\\x1B[0m"),this.reconnectAttempts=0,this.startHeartbeat()},this.setupWebSocketHandlers()}setupWebSocketHandlers(e){if(!this.ws)return;const _=new Nn(C=>this.terminal.write(C),C=>{var g;((g=this.ws)==null?void 0:g.readyState)===WebSocket.OPEN&&this.ws.send(C)});this.ws.onmessage=C=>{if(typeof C.data=="string")try{const g=JSON.parse(C.data);switch(g.type){case"status":if(this.terminal.writeln(\`\\x1B[32m[*] \${g.message}\\x1B[0m\`),g.message==="认证成功"){const k=document.getElementById("status-text");k&&(k.innerHTML='<span class="w-2 h-2 bg-[#4af626] inline-block animate-pulse"></span> STATUS: ONLINE')}break;case"error":this.terminal.writeln(\`\\x1B[31m[!] \${g.message}\\x1B[0m\`);break}}catch{this.terminal.write(C.data)}else{const g=new FileReader;g.onload=()=>{_.consume(g.result)},g.readAsArrayBuffer(C.data)}},this.ws.onclose=C=>{this.stopHeartbeat(),this.terminal.writeln(\`\\x1B[33m[*] Connection closed (code=\${C.code})\\x1B[0m\`);const g=document.getElementById("term-status");g&&(g.innerHTML='<div class="w-2 h-2 bg-red-500"></div> Disconnected');const k=document.getElementById("status-text");k&&(k.innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'),C.code!==1e3&&this.lastConfig&&this.reconnectAttempts<this.maxReconnectAttempts&&thi Line truncated
}\`,Ge=8,Zi=Ge*Float32Array.BYTES_PER_ELEMENT,Ea=20*Ge,Js=class{constructor(){this.attributes=new Float32Array(Ea),this.count=0}},Ke=0,Qs=0,er=0,tr=0,ir=0,sr=0,rr=0,La=class extends Pe{constructor(e,t,r,n){super(),this._terminal=e,this._gl=t,this._dimensions=r,this._themeService=n,this._vertices=new Js,this._verticesCursor=new Js;let o=this._gl;this._program=ce(En(o,Ca,xa)),this._register(Se(()=>o.deleteProgram(this._program))),this._projectionLocation=ce(o.getUniformLocation(this._program,"u_projection")),this._vertexArrayObject=o.createVertexArray(),o.bindVertexArray(this._vertexArrayObject);let _=new Float32Array([0,0,1,0,0,1,1,1]),s=o.createBuffer();this._register(Se(()=>o.deleteBuffer(s))),o.bindBuffer(o.ARRAY_BUFFER,s),o.bufferData(o.ARRAY_BUFFER,_,o.STATIC_DRAW),o.enableVertexAttribArray(3),o.vertexAttribPointer(3,2,this._gl.FLOAT,!1,0,0);let h=new Uint8Array([0,1,2,3]),a=o.createBuffer();this._register(Se(()=>o.deleteBuffer(a))),o.bindBuffer(o.ELEMENT_ARRAY_BUFFER,a),o.bufferData(o.ELEMENT_ARRAY_BUFFER,h,o.STATIC_DRAW),this._attributesBuffer=ce(o.createBuffer()),this._register(Se(()=>o.deleteBuffer(this._attributesBuffer))),o.bindBuffer(o.ARRAY_BUFFER,this._attributesBuffer),o.enableVertexAttribArray(0),o.vertexAttribPointer(0,2,o.FLOAT,!1,Zi,0),o.vertexAttribDivisor(0,1),o.enableVertexAttribArray(1),o.vertexAttribPointer(1,2,o.FLOAT,!1,Zi,2*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(1,1),o.enableVertexAttribArray(2),o.vertexAttribPointer(2,4,o.FLOAT,!1,Zi,4*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(2,1),this._updateCachedColors(n.colors),this._register(this._themeService.onChangeColors(d=>{this._updateCachedColors(d),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(e){let t=this._gl;t.useProgram(this._program),t.bindVertexArray(this._vertexArrayObject),t.uniformMatrix4fv(this._projectionLocation,!1,xn),t.bindBuffer(t.ARRAY_BUFFER,this._attributesBuffer),t.bufferData(t.ARRAY_BUFFER,e.attributes,t.DYNAMIC_DRAW),t.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,t.UNSIGNED_BYTE,0,e.count)}handleResize(){this._updateViewportRectangle()}setDimensions(e){this._dimensions=e}_updateCachedColors(e){this._bgFloat=this._colorToFloat32Array(e.background),this._cursorFloat=this._colorToFloat32Array(e.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(e){let t=this._terminal,r=this._vertices,n=1,o,_,s,h,a,d,u,p,m,C,w;for(o=0;o<t.rows;o++){for(s=-1,h=0,a=0,d=!1,_=0;_<t.cols;_++)u=(o*t.cols+_)*Ai,p=e.cells[u+xi],m=e.cells[u+Ei],C=!!(m&67108864),(p!==h||m!==a&&(d||C))&&((h!==0||d&&a!==0)&&(w=n++*Ge,this._updateRectangle(r,w,a,h,s,_,o)),s=_,h=p,a=m,d=C);(h!==0||d&&a!==0)&&(w=n++*Ge,this._updateRectangle(r,w,a,h,s,t.cols,o))}r.count=n}updateCursor(e){let t=this._verticesCursor,r=e.cursor;if(!r||r.style==="block"){t.count=0;return}let n,o=0;(r.style==="bar"||r.style==="outline")&&(n=o++*Ge,this._addRectangleFloat(t.attributes,n,r.x*this._dimensions.device.cell.width,r.y*this._dimensions.device.cell.height,r.style==="bar"?r.dpr*r.cursorWidth:r.dpr,this._dimensions.device.cell.height,this._cursorFloat)),(r.style==="underline"||r.style==="outline")&&(n=o++*Ge,this._addRectangleFloat(t.attributes,n,r.x*this._dimensions.device.cell.width,(r.y+1)*this._dimensions.device.cell.height-r.dpr,r.width*this._dimensions.device.cell.width,r.dpr,this._cursorFloat)),r.style==="outline"&&(n=o++*Ge,this._addRectangleFloat(t.attributes,n,r.x*this._dimensions.device.cell.width,r.y*this._dimensions.device.cell.height,r.width*this._dimensions.device.cell.width,r.dpr,this._cursorFloat),n=o++*Ge,this._addRectangleFloat(t.attributes,n,(r.x+r.width)*this._dimensions.device.cell.width-r.dpr,r.y*this._dimensions.device.cell.height,r.dpr,this._dimensions.device.cell.height,this._cursorFloat)),t.count=o}_updateRectangle(e,t,r,n,o,_,s){if(r&67108864)switch(r&50331648){case 16777216:case 33554432:Ke=this._themeService.colors.ansi[r&255].rgba;break;case 50331648:Ke=(r&16777215)<<8;break;case 0:default:Ke=this._themeService.colors.foreground.rgba}else switch(n&50331648){case 16777216:case 33554432:Ke=this._themeService.colors.ansi[n&255].rgba;break;case 50331648:Ke=(n&16777215)<<8;break;case 0:default:Ke=this._themeService.colors.background.rgba}e.attributes.length<t+4&&(e.attributes=fa(e.attributes,this._terminal.rows*this._terminal.cols*Ge)),Qs=o*this._dimensions.device.cell.width,er=s*this._dimensions.device.cell.height,tr=(Ke>>24&255)/255,ir=(Ke>>16&255)/255,sr=(Ke>>8&255)/255,rr=1,this._addRectangle(e.attributes,t,Qs,er,(_-o)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,tr,ir,sr,rr)}_addRectangle(e,t,r,n,o,_,s,h,a,d){e[t]=r/this._dimensions.device.canvas.width,e[t+1]=n/this._dimensions.device.canvas.heig Line truncated
- \`)}const te=eo("fs"),Je=eo("path");function eo(i){try{return require(i)}catch{return{}}}(function(i,e){for(const t of e)i[t+"Async"]=(...r)=>new Promise(n=>{i[t](...r,o=>n(!o))})})(te,["rm","rmdir","unlink","access"]),(function(i,e){for(const t of e)i[t+"Async"]=(...r)=>new Promise((n,o)=>{i[t](...r,(_,s)=>{_?o(_):n(s)})})})(te,["stat","mkdir","readdir","close","open","realpath","write"]);class $r{constructor(e,t,r,n,o){this.closed=!1,this.fd=null,this.pathId=e,this.absPath=t,this.relPath=r,this.dir=n,this.size=o}getPathId(){return this.pathId}getRelPath(){return this.relPath}isDir(){return this.dir}getSize(){return this.size}readFile(e){return q(this,void 0,void 0,function*(){if(this.closed)throw new X(\`File closed: \${this.absPath}\`,null,!0);this.fd===null&&(this.fd=yield te.openAsync(this.absPath,"r"));const t=new Uint8Array(e);return(function(r,n,o,_,s){return q(this,void 0,void 0,function*(){return new Promise((h,a)=>te.read(r,n,o,_,s,(d,u,p)=>{d?a(d):h(p.subarray(0,u))}))})})(this.fd,t,0,t.length,null)})}closeFile(){return q(this,void 0,void 0,function*(){this.closed||(this.closed=!0,this.fd!==null&&(yield te.closeAsync(this.fd),this.fd=null))})}}function to(i,e,t,r,n,o){return q(this,void 0,void 0,function*(){if(!t.isDirectory()){if(!t.isFile())throw new X(\`Not a regular file: \${e}\`);if(!(yield te.accessAsync(e,te.constants.R_OK)))throw new X(\`No permission to read: \${e}\`);return void r.push(new $r(i,e,n,!1,t.size))}const _=yield te.realpathAsync(e);if(o.has(_))throw new X(\`Duplicate link: \${e}\`);o.add(_),r.push(new $r(i,e,n,!0,0));const s=yield te.readdirAsync(e);for(const h of s){const a=Je.join(e,h),d=yield te.statAsync(a);yield to(i,a,d,r,[...n,h],o)}})}function Hr(i){return q(this,arguments,void 0,function*(e,t=!1){if(!e||!e.length)return;const r=[],n=e.entries();for(const[o,_]of n){const s=Je.resolve(_);if(!(yield te.accessAsync(s)))throw new X(\`No such file: \${s}\`);const h=yield te.statAsync(s);if(!t&&h.isDirectory())throw new X(\`Is a directory: \${s}\`);const a=new Set;yield to(o,s,h,r,[Je.basename(s)],a)}return r})}class ps{constructor(e,t,r,n,o=!1){this.closed=!1,this.absPath=e,this.fileName=t,this.localName=r,this.fd=n,this.dir=o}getFileName(){return this.fileName}getLocalName(){return this.localName}isDir(){return this.dir}writeFile(e){return q(this,void 0,void 0,function*(){yield te.writeAsync(this.fd,e)})}closeFile(){return q(this,void 0,void 0,function*(){this.closed||(this.closed=!0,this.fd!==null&&(yield te.closeAsync(this.fd),this.fd=null))})}deleteFile(){return q(this,void 0,void 0,function*(){if(!this.absPath||!(yield te.accessAsync(this.absPath)))return"";try{if(yield this.closeFile(),typeof te.rm=="function"){if(yield te.rmAsync(this.absPath,{recursive:!0}))return this.absPath}else if(this.isDir){if(yield te.rmdirAsync(this.absPath,{recursive:!0}))return this.absPath}else if(yield te.unlinkAsync(this.absPath))return this.absPath}catch(e){console.log(\`delete [\${this.absPath}] failed\`,e)}return""})}}function zr(i,e){return q(this,void 0,void 0,function*(){if(!(yield te.accessAsync(Je.join(i,e))))return e;for(let t=0;t<1e3;t++){const r=\`\${e}.\${t}\`;if(!(yield te.accessAsync(Je.join(i,r))))return r}throw new X("Fail to assign new file name")})}function Wr(i){return q(this,void 0,void 0,function*(){try{return yield te.openAsync(i,"w")}catch(e){throw e.errno===-13||e.errno===-4048?new X(\`No permission to write: \${i}\`):e.errno===-21||e.errno===-4068?new X(\`Is a directory: \${i}\`):e}})}function Ur(i){return q(this,void 0,void 0,function*(){if(!(yield te.accessAsync(i)))return yield te.mkdirAsync(i,{recursive:!0,mode:493}),!0;if(!(yield te.statAsync(i)).isDirectory())throw new X(\`Not a directory: \${i}\`);return!1})}function el(i,e,t,r){return q(this,void 0,void 0,function*(){if(!t)return(function(h,a,d){return q(this,void 0,void 0,function*(){const u=d?a:yield zr(h,a),p=Je.join(h,u),m=yield Wr(p);return new ps(p,a,u,m)})})(i.path,e,r);const n=JSON.parse(e);if(!n.hasOwnProperty("path_name")||!n.hasOwnProperty("path_id")||!n.hasOwnProperty("is_dir")||n.path_name.length<1)throw new X(\`Invalid name: \${e}\`);let o,_;if(e=n.path_name[n.path_name.length-1],r?o=n.path_name[0]:i.maps.has(n.path_id)?o=i.maps.get(n.path_id):(o=yield zr(i.path,n.path_name[0]),i.maps.set(n.path_id,o)),n.path_name.length>1){const h=Je.join(i.path,o,...n.path_name.slice(1,n.path_name.length-1));yield Ur(h),_=Je.join(h,e)}else _=Je.join(i.path,o);if(n.is_dir===!0){let h="";return(yield Ur(_))&&(h=_),new ps(h,e,o,null,!0)}const s=yield Wr(_);return new ps(_,e,o,s)})}class ai{constructor(e,t,r,n){this.closed=!1,this.pos=0,this.pathId=e,this.relPath=t,this.file=r,this.dir=n}getPathId(){return this.pathId}getRelPath(){return this.relPath}isDir(){return this.dir}getSize(){return this.file.size}readFile(e){return q(this,void 0,void 0,function*(){if(this.pos>=this.file.size)return new Uint8Array(0);try{const t=Math.min(e.byteLength,this.file.size-this.pos),r=this.file.slice(this.pos,this.pos+t Line truncated
\`,this.writer=e,this.isWindowsShell=t,this.maxDataChunkSize=r}cleanup(){for(const e of this.openedFiles)e.closeFile()}addReceivedData(e){this.stopped||this.buffer.addBuffer(e),this.lastInputTime=Date.now()}stopTransferring(){return q(this,void 0,void 0,function*(){this.cleanTimeoutInMilliseconds=Math.max(2*this.maxChunkTimeInMilliseconds,500),this.stopped=!0,this.buffer.stopBuffer()})}cleanInput(e){return q(this,void 0,void 0,function*(){for(this.stopped=!0,this.buffer.drainBuffer(),this.lastInputTime=Date.now();;){const t=e-(Date.now()-this.lastInputTime);if(t<=0)return;yield new Promise(r=>setTimeout(r,t))}})}sendLine(e,t){return q(this,void 0,void 0,function*(){this.writer(\`#\${e}:\${t}\${this.protocolNewline}\`)})}recvLine(e){return q(this,arguments,void 0,function*(t,r=!1){if(this.stopped)throw new X("Stopped");if(this.isWindowsShell||this.remoteIsWindows){let o=yield this.buffer.readLineOnWindows();const _=o.lastIndexOf("#"+t+":");if(_>=0)o=o.substring(_);else{const s=o.lastIndexOf("#");s>0&&(o=o.substring(s))}return o}let n=yield this.buffer.readLine();if(this.tmuxOutputJunk||r){if(n.length>0)for(;n[n.length-1]==="\\r";)n=n.substring(0,n.length-1)+(yield this.buffer.readLine());const o=n.lastIndexOf("#"+t+":");if(o>=0)n=n.substring(o);else{const _=n.lastIndexOf("#");_>0&&(n=n.substring(_))}n=(function(_){for(;;){const s=_.indexOf("\\x1BP=");if(s<0)return _;let h=s+3;const a=_.substring(h).indexOf("\\x1BP=");if(a<0)return _.substring(0,s);h+=a+3;const d=_.substring(h).indexOf("\\x1B\\\\");if(d<0)return _.substring(0,s);h+=d+2,_=_.substring(0,s)+_.substring(h)}})(n)}return n})}recvCheck(e){return q(this,arguments,void 0,function*(t,r=!1){const n=yield this.recvLine(t,r),o=n.indexOf(":");if(o<1)throw new X(gs(n),"colon",!0);const _=n.substring(1,o),s=n.substring(o+1);if(_!=t)throw new X(s,_,!0);return s})}sendInteger(e,t){return q(this,void 0,void 0,function*(){yield this.sendLine(e,t.toString())})}recvInteger(e){return q(this,arguments,void 0,function*(t,r=!1){const n=yield this.recvCheck(t,r);return Number(n)})}checkInteger(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("SUCC");if(t!==e)throw new X(\`Integer check [\${t}] <> [\${e}]\`,null,!0)})}sendString(e,t){return q(this,void 0,void 0,function*(){yield this.sendLine(e,gs(t))})}recvString(e){return q(this,arguments,void 0,function*(t,r=!1){return Di(As(yield this.recvCheck(t,r)),"utf8")})}checkString(e){return q(this,void 0,void 0,function*(){const t=yield this.recvString("SUCC");if(t!==e)throw new X(\`String check [\${t}] <> [\${e}]\`,null,!0)})}sendBinary(e,t){return q(this,void 0,void 0,function*(){yield this.sendLine(e,gs(t))})}recvBinary(e){return q(this,arguments,void 0,function*(t,r=!1){return As(yield this.recvCheck(t,r))})}checkBinary(e){return q(this,void 0,void 0,function*(){const t=yield this.recvBinary("SUCC");if(t.length!=e.length)throw new X(\`Binary length check [\${t.length}] <> [\${e.length}]\`,null,!0);for(let r=0;r<t.length;r++)if(t[r]!=e[r])throw new X(\`Binary check [\${t[r]}] <> [\${e[r]}]\`,null,!0)})}sendData(e,t,r){return q(this,void 0,void 0,function*(){if(!t)return void(yield this.sendBinary("DATA",e));const n=(function(o,_){if(!_.length)return o;const s=new Uint8Array(2*o.length);let h=0;for(let a=0;a<o.length;a++){let d=-1;for(let u=0;u<_.length;u++)if(o[a]==_[u][0]){d=u;break}d<0?s[h++]=o[a]:(s[h++]=_[d][1],s[h++]=_[d][2])}return s.subarray(0,h)})(e,r);this.writer(\`#DATA:\${n.length}
\`),this.writer(n)})}recvData(e,t,r){return q(this,void 0,void 0,function*(){return yield Promise.race([new Promise((n,o)=>setTimeout(()=>{this.cleanTimeoutInMilliseconds=3e3,o(new X("Receive data timeout"))},r)),q(this,void 0,void 0,function*(){if(!e)return yield this.recvBinary("DATA");const n=yield this.recvInteger("DATA");return(function(o,_){if(!_.length)return o;const s=new Uint8Array(o.length);let h=0;for(let a=0;a<o.length;a++){let d=-1;if(a<o.length-1){for(let u=0;u<_.length;u++)if(o[a]==_[u][1]&&o[a+1]==_[u][2]){d=u;break}}d<0?s[h++]=o[a]:(s[h++]=_[d][0],a++)}return s.subarray(0,h)})(yield this.buffer.readBinary(n),t)})])})}sendAction(e,t){return q(this,void 0,void 0,function*(){const r={lang:"js",confirm:e,version:"1.1.6",support_dir:!0};(this.isWindowsShell||t)&&(r.binary=!1,r.newline=\`!
\`),t&&(this.remoteIsWindows=!0,this.protocolNewline=\`!
\`),yield this.sendString("ACT",JSON.stringify(r))})}recvAction(){return q(this,void 0,void 0,function*(){const e=yield this.recvString("ACT",!0),t=JSON.parse(e);return t.newline&&(this.protocolNewline=t.newline),t})}sendConfig(e,t,r,n){return q(this,void 0,void 0,function*(){const o={lang:"js"};e.quiet&&(o.quiet=!0),e.binary&&(o.binary=!0,o.escape_chars=t),e.directory&&(o.directory=!0),e.bufsize&&(o.bufsize=e.bufsize),e.timeout&&(o.timeout=e.timeout),e.overwrite&&(o.overwrite=!0),r==Qh&&(o.tmux_output_junk=!0),n>0&&(o.tmux_pane_width=n);let _=JSON.stringify(o);_=_.replace(/[\\u007F-\\uFFFF]/g,function(s){return"\\\\u"+("0000"+s.charCodeAt(0).toString(16)).slice(-4)}),this.transferConfig=o,yield this.sendString("CFG",_)})}recvConfig(){return q(this,void 0,void 0,function*(){const e=yield this.recvString("CFG",!0);return this.transferConfig=JSON.parse(e),this.tmuxOutputJunk=this.transferConfig.tmux_output_junk===!0,this.transferConfig})}clientExit(e){return q(this,void 0,void 0,function*(){yield this.sendString("EXIT",e)})}recvExit(){return q(this,void 0,void 0,function*(){return this.recvString("EXIT")})}serverExit(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(500),yield(function(){return q(this,void 0,void 0,function*(){})})(),this.isWindowsShell?(e=e.replace(/\\n/g,\`\\r
\`),process.stdout.write("\\x1B[H\\x1B[2J\\x1B[?1049l")):process.stdout.write("\\x1B8\\x1B[0J"),process.stdout.write(e),process.stdout.write(\`\\r
\`),process.stdout.write("\\x1B[?25h"),this.transferConfig.tmux_output_junk&&(yield(function(){return q(this,void 0,void 0,function*(){yield require("util").promisify(require("child_process").exec)("tmux refresh-client")})})())})}deleteCreatedFiles(){return q(this,void 0,void 0,function*(){const e=[];for(const t of this.createdFiles){const r=yield t.deleteFile();r&&e.push(r)}return e})}clientError(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(this.cleanTimeoutInMilliseconds);const t=X.getErrorMessage(e);let r=!0;if(e instanceof X){if(r=e.isTraceBack(),e.isRemoteExit())return;if(e.isRemoteFail())return void(r&&console.log(t))}yield this.sendString(r?"FAIL":"fail",t),r&&console.log(t)})}serverError(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(this.cleanTimeoutInMilliseconds);const t=X.getErrorMessage(e);let r=!0;if(e instanceof X){if(e.isStopAndDelete()){const n=yield this.deleteCreatedFiles();if(n&&n.length)return void(yield this.serverExit([e.message+":"].concat(n).join(\`\\r
- \`)))}if(r=e.isTraceBack(),e.isRemoteExit()||e.isRemoteFail())return void(yield this.serverExit(t))}yield this.sendString(r?"FAIL":"fail",t),yield this.serverExit(t)})}sendFileNum(e,t){return q(this,void 0,void 0,function*(){yield this.sendInteger("NUM",e),yield this.checkInteger(e),t&&t.onNum(e)})}sendFileName(e,t,r){return q(this,void 0,void 0,function*(){const n=e.getRelPath(),o=n[n.length-1];if(t){const s={path_id:e.getPathId(),path_name:n,is_dir:e.isDir()};yield this.sendString("NAME",JSON.stringify(s))}else yield this.sendString("NAME",o);const _=yield this.recvString("SUCC");return r&&r.onName(o),_})}sendFileSize(e,t){return q(this,void 0,void 0,function*(){yield this.sendInteger("SIZE",e),yield this.checkInteger(e),t&&t.onSize(e)})}sendFileData(e,t,r,n,o,_){return q(this,void 0,void 0,function*(){let s=0;_&&_.onStep(s);let h=1024,a=new ArrayBuffer(h);const d=new hi;for(;s<t;){const u=Date.now(),p=yield e.readFile(a);yield this.sendData(p,r,n),d.appendByteArray(p),yield this.checkInteger(p.length),s+=p.length,_&&_.onStep(s);const m=Date.now()-u;p.length==h&&m<500&&h<o?(h=Math.min(2*h,o),a=new ArrayBuffer(h)):m>=2e3&&h>1024&&(h=1024,a=new ArrayBuffer(h)),m>this.maxChunkTimeInMilliseconds&&(this.maxChunkTimeInMilliseconds=m)}return new Uint8Array(d.end(!0).buffer)})}sendFileMD5(e,t){return q(this,void 0,void 0,function*(){yield this.sendBinary("MD5",e),yield this.checkBinary(e),t&&t.onDone()})}sendFiles(e,t){return q(this,void 0,void 0,function*(){this.openedFiles.push(...e);const r=this.transferConfig.binary===!0,n=this.transferConfig.directory===!0,o=this.transferConfig.bufsize?Math.min(this.transferConfig.bufsize,this.maxDataChunkSize):this.maxDataChunkSize,_=this.transferConfig.escape_chars?qr(this.transferConfig.escape_chars):[];yield this.sendFileNum(e.length,t);const s=[];for(const h of e){const a=yield this.sendFileName(h,n,t);if(s.includes(a)||s.push(a),h.isDir())continue;const d=h.getSize();yield this.sendFileSize(d,t);const u=yield this.sendFileData(h,d,r,_,o,t);h.closeFile(),yield this.sendFileMD5(u,t)}return s})}recvFileNum(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("NUM");return yield this.sendInteger("SUCC",t),e&&e.onNum(t),t})}recvFileName(e,t,r,n,o){return q(this,void 0,void 0,function*(){const _=yield this.recvString("NAME"),s=yield t(e,_,r,n);return this.createdFiles.push(s),yield this.sendString("SUCC",s.getLocalName()),o&&o.onName(s.getFileName()),s})}recvFileSize(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("SIZE");return yield this.sendInteger("SUCC",t),e&&e.onSize(t),t})}recvFileData(e,t,r,n,o,_){return q(this,void 0,void 0,function*(){let s=0;_&&_.onStep(s);const h=new hi;for(;s<t;){const a=Date.now(),d=yield this.recvData(r,n,o);yield e.writeFile(d),s+=d.length,_&&_.onStep(s),yield this.sendInteger("SUCC",d.length),h.appendByteArray(d);const u=Date.now()-a;u>this.maxChunkTimeInMilliseconds&&(this.maxChunkTimeInMilliseconds=u)}return new Uint8Array(h.end(!0).buffer)})}recvFileMD5(e,t){return q(this,void 0,void 0,function*(){const r=yield this.recvBinary("MD5");if(e.length!=r.length)throw new X("Check MD5 failed");for(let n=0;n<e.length;n++)if(e[n]!=r[n])throw new X("Check MD5 failed");yield this.sendBinary("SUCC",e),t&&t.onDone()})}recvFiles(e,t,r){return q(this,void 0,void 0,function*(){const n=this.transferConfig.binary===!0,o=this.transferConfig.directory===!0,_=this.transferConfig.overwrite===!0,s=this.transferConfig.timeout?1e3*this.transferConfig.timeout:1e5,h=this.transferConfig.escape_chars?qr(this.transferConfig.escape_chars):[],a=yield this.recvFileNum(r),d=[];for(let u=0;u<a;u++){const p=yield this.recvFileName(e,t,o,_,r);if(d.includes(p.getLocalName())||d.push(p.getLocalName()),p.isDir())continue;this.openedFiles.push(p);const m=yield this.recvFileSize(r),C=yield this.recvFileData(p,m,n,h,s,r);p.closeFile(),yield this.recvFileMD5(C,r)}return d})}}function Ci(i,e){e-=3;let t=0,r="";for(let n=0;n<i.length;n++){if(i.charCodeAt(n)>=19968&&i.charCodeAt(n)<=40869){if(t+2>e)return{sub:r+"...",len:t+3};t+=2}else{if(t+1>e)return{sub:r+"...",len:t+3};t+=1}r+=i[n]}return{sub:r+"...",len:t+3}}function Kr(i){let e="B";do{if(i<1024||(e="KB",(i/=1024)<1024)||(e="MB",(i/=1024)<1024)||(e="GB",(i/=1024)<1024))break;i/=1024,e="TB"}while(!1);return i>=100?\`\${i.toFixed(0)} \${e}\`:i>=10?\`\${i.toFixed(1)} \${e}\`:\`\${i.toFixed(2)} \${e}\`}Oi.MAX_DATA_CHUNK_SIZE=10485760;class ol{constructor(e,t,r=void 0){this.lastUpdateTime=0,this.firstWrite=!0,this.speedCnt=0,this.speedIdx=0,this.timeArray=new Array(30),this.stepArray=new Array(30),this.writer=e,this.tmuxPaneColumns=r||0,this.columns=this.tmuxPaneColumns>1?this.tmuxPaneColumns-1:t}setTerminalColumns(e){this.columns=e,this.tmuxPaneColumns>0&&(this.tmuxPaneColumns=0)}onNum(e){this.fileCount=e,this.fileIdx=0}onName(e){this.fileName=e,this.fileIdx+=1,this.startTime=Date.now(),this.timeArray[0]=this.startTime,this.stepArray[0]=0,this.speedCnt=1,this.speedIdx=1,this.fileStep=- Line truncated
\`)}setTimeout(()=>this.detectAndHandleTrzsz(e),10),this.writeToTerminal(e)}}processTerminalInput(e){this.isTransferringFiles()?e===""&&this.stopTransferringFiles():this.sendToServer(e)}processBinaryInput(e){this.isTransferringFiles()||this.sendToServer(Ti(e))}setTerminalColumns(e){this.terminalColumns=e,this.textProgressBar!=null&&this.textProgressBar.setTerminalColumns(e)}isTransferringFiles(){return this.trzszTransfer!=null}stopTransferringFiles(){this.isTransferringFiles()&&this.trzszTransfer.stopTransferring()}uploadFiles(e){return q(this,void 0,void 0,function*(){if(this.uploadFilesList||this.isTransferringFiles())throw new Error("The previous upload has not been completed yet");if(!Ut&&(function(r,n){if(!Array.isArray(r))return!1;for(const o of r)if(typeof o!==n)return!1;return!0})(e,"string"))this.uploadFilesList=yield Hr(e,!0);else{if(!(typeof DataTransferItemList<"u"&&e instanceof DataTransferItemList))throw new Error("The upload items type is not supported");this.uploadFilesList=yield(function(r){return q(this,void 0,void 0,function*(){const n=[],o=[];for(const _ of r)o.push(_.webkitGetAsEntry());for(let _=0;_<o.length;_++){const s=o[_];s&&(yield io(_,s,n,[s.name]))}return n})})(e)}if(!this.uploadFilesList||!this.uploadFilesList.length)throw this.uploadFilesList=null,new Error("No files to upload");let t=!1;for(const r of this.uploadFilesList)if(r.isDir()||r.getRelPath().length>1){t=!0;break}return this.uploadInterrupting=!0,this.sendToServer(""),yield new Promise(r=>setTimeout(r,200)),this.uploadInterrupting=!1,this.uploadSkipTrzCommand=!0,this.sendToServer(t?"trz -d\\r":"trz\\r"),setTimeout(()=>{this.uploadFilesList&&(this.uploadFilesList=null,this.uploadFilesResolve=null,this.uploadFilesReject&&(this.uploadFilesReject("Upload does not start"),this.uploadFilesReject=null))},this.dragInitTimeout),new Promise((r,n)=>{this.uploadFilesResolve=r,this.uploadFilesReject=n})})}uniqueIdExists(e){if(e.length<8||!this.isWindowsShell&&e.length==14&&e.endsWith("00"))return!1;if(this.uniqueIdMaps.has(e))return!0;if(this.uniqueIdMaps.size>=100){const t=new Map;for(const[r,n]of this.uniqueIdMaps)n>=50&&t.set(r,n-50);this.uniqueIdMaps=t}return this.uniqueIdMaps.set(e,this.uniqueIdMaps.size),!1}detectAndHandleTrzsz(e){return q(this,void 0,void 0,function*(){const t=yield(function(h){return q(this,void 0,void 0,function*(){if(typeof h=="string"){const p=h.lastIndexOf(so);return p<0?null:h.substring(p)}let a;if(h instanceof ArrayBuffer)a=new Uint8Array(h);else if(h instanceof Uint8Array)a=h;else{if(!(h instanceof Blob))return null;a=new Uint8Array(yield h.arrayBuffer())}if(a.length<26)return null;let d=-1,u=-1;for(;;){if(d=a.indexOf(58,d+1),d<0||a.length-d<26)return u>=0?Di(a.subarray(u)):null;const p=new Float64Array(a.buffer.slice(a.byteOffset+d,a.byteOffset+d+16));p[0]==Vr[0]&&p[1]==Vr[1]&&(u=d,d+=25)}})})(e);if(!t)return;const r=t.match(al);if(!r)return;const n=r.length>3?r[3]:"";if(this.uniqueIdExists(n))return;const o=r[1],_=r[2];let s=!1;(n==":1"||n.length==14&&n.endsWith("10"))&&(s=!0);try{this.trzszTransfer=new Oi(this.sendToServer,this.isWindowsShell,this.maxDataChunkSize),o==="S"?yield this.handleTrzszDownloadFiles(_,s):o==="R"?yield this.handleTrzszUploadFiles(_,!1,s):o==="D"&&(yield this.handleTrzszUploadFiles(_,!0,s)),this.uploadFilesResolve&&this.uploadFilesResolve()}catch(h){yield this.trzszTransfer.clientError(h),this.uploadFilesReject&&this.uploadFilesReject(h)}finally{this.uploadFilesResolve=null,this.uploadFilesReject=null,this.trzszTransfer.cleanup(),this.textProgressBar&&this.textProgressBar.showCursor(),this.textProgressBar=null,this.trzszTransfer=null}})}createProgressBar(e,t){e!==!0?(this.textProgressBar=new ol(this.writeToTerminal,this.terminalColumns,t),this.textProgressBar.hideCursor()):this.textProgressBar=null}handleTrzszDownloadFiles(e,t){return q(this,void 0,void 0,function*(){let r,n,o;if(Ut){const h=yield(function(){return q(this,void 0,void 0,function*(){if(typeof window.showDirectoryPicker!="function")throw Bs();try{return yield window.showDirectoryPicker({id:"trzsz_download",startIn:"downloads",mode:"readwrite"})}catch(a){if(a.name==="AbortError")return;throw a}})})();if(!h)return void(yield this.trzszTransfer.sendAction(!1,t));r=h.name,n={handle:h,maps:new Map},o=il}else{if(r=yield this.chooseSaveDirectory(),!r)return void(yield this.trzszTransfer.sendAction(!1,t));yield(function(h){return q(this,void 0,void 0,function*(){if(!h)return!1;if(!(yield te.accessAsync(h)))throw new X(\`No such directory: \${h}\`);if(!(yield te.statAsync(h)).isDirectory())throw new X(\`Not a directory: \${h}\`);if(!(yield te.accessAsync(h,te.constants.W_OK)))throw new X(\`No permission to write: \${h}\`);return!0})})(r),n={path:r,maps:new Map},o=el}yield this.trzszTransfer.sendAction(!0,t);const _=yield this.trzszTransfer.recvConfig();this.createProgressBar(_.quiet,_.tmux_pane_width);const s=yield this.trzszTransfer.recvFiles(n,o,this.textProgressBar);yield this.trzszTransfer.clientExit(Fr(s,r))}) Line truncated
<button type="button" id="github-login-btn" class="github-login-btn text-[11px] font-bold tracking-[0.1em] text-[#bbccb0] hover:text-[#4af626] transition-all cursor-pointer flex items-center gap-1.5 bg-transparent border border-[#3c4b36] px-3 py-1 hover:border-[#4af626]">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
LOGIN
</button>
\`,(_=document.getElementById("github-login-btn"))==null||_.addEventListener("click",()=>{window.location.href="/api/auth/github"}))}renderTurnstile(){const e=document.getElementById("turnstile-widget");if(!e||!window.turnstile)return;const _=document.getElementById("turnstile-container");_&&(_.style.display="block"),this.turnstileWidgetId=window.turnstile.render(e,{sitekey:this.turnstileSitekey,theme:"dark",callback:async C=>{try{if((await(await fetch("/api/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:C})})).json()).success){this.turnstileVerified=!0;const w=document.getElementById("turnstile-container");w&&(w.style.display="none")}}catch{this.turnstileVerified=!1}},"expired-callback":()=>{this.turnstileVerified=!1},"error-callback":()=>{this.turnstileVerified=!1}})}render(){const e=document.getElementById("connection-form-container");e.innerHTML=\`
\`,(t=document.getElementById("github-login-btn"))==null||t.addEventListener("click",()=>{window.location.href="/api/auth/github"}))}renderTurnstile(){const e=document.getElementById("turnstile-widget");if(!e||!window.turnstile)return;const t=document.getElementById("turnstile-container");t&&(t.style.display="block"),this.turnstileWidgetId=window.turnstile.render(e,{sitekey:this.turnstileSitekey,theme:"dark",callback:async r=>{try{if((await(await fetch("/api/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r})})).json()).success){this.turnstileVerified=!0;const _=document.getElementById("turnstile-container");_&&(_.style.display="none")}}catch{this.turnstileVerified=!1}},"expired-callback":()=>{this.turnstileVerified=!1},"error-callback":()=>{this.turnstileVerified=!1}})}render(){const e=document.getElementById("connection-form-container");e.innerHTML=\`
<form class="space-y-6" id="connection-form">
<div class="grid grid-cols-4 gap-4">
<div class="col-span-3">
@@ -190,10 +198,10 @@ void main() {
<span id="github-login-placeholder"></span>
</div>
</form>
\`,document.getElementById("connect-btn").addEventListener("click",()=>{this.handleConnect()}),document.getElementById("connection-form").addEventListener("keypress",_=>{_.key==="Enter"&&this.handleConnect()}),document.getElementById("auth-tab-password").addEventListener("click",()=>{this.setAuthMode("password")}),document.getElementById("auth-tab-key").addEventListener("click",()=>{this.setAuthMode("key")})}setAuthMode(e){this.authMode=e;const _=document.getElementById("auth-tab-password"),C=document.getElementById("auth-tab-key"),g=document.getElementById("auth-password-section"),k=document.getElementById("auth-key-section");e==="password"?(_.style.background="#4af626",_.style.color="#0a0a0a",_.style.borderColor="#4af626",C.style.background="transparent",C.style.color="#bbccb0",C.style.borderColor="#3c4b36",g.style.display="",k.style.display="none"):(C.style.background="#4af626",C.style.color="#0a0a0a",C.style.borderColor="#4af626",_.style.background="transparent",_.style.color="#bbccb0",_.style.borderColor="#3c4b36",k.style.display="",g.style.display="none")}async loadSavedCredentials(){const e=localStorage.getItem("cloudssh_cred");if(!e)return;const _=await Un(e);if(!_){localStorage.removeItem("cloudssh_cred");return}document.getElementById("host").value=_.host||"",document.getElementById("port").value=_.port||"22",document.getElementById("username").value=_.username||"",document.getElementById("password").value=_.password||"",document.getElementById("private-key").value=_.privateKey||"",document.getElementById("remember-me").checked=!0,_.authMethod==="key"?this.setAuthMode("key"):this.setAuthMode("password")}async handleConnect(){const _=document.getElementById("host").value.replace(/^\\[|\\]$/g,"").trim(),C=parseInt(document.getElementById("port").value||"22"),g=document.getElementById("username").value,k=document.getElementById("password").value,w=document.getElementById("private-key").value,s=document.getElementById("remember-me").checked;if(!_||!g){alert("请填写主机名和用户名");return}if(this.authMode==="password"&&!k){alert("请输入密码");return}if(this.authMode==="key"&&!w){alert("请粘贴私钥内容");return}if(this.turnstileEnabled&&!this.turnstileVerified){alert("请完成人机验证");return}if(s){const d=await $n({host:_,port:C.toString(),username:g,password:k,privateKey:w,authMethod:this.authMode==="key"?"publickey":"password"});localStorage.setItem("cloudssh_cred",d)}else localStorage.removeItem("cloudssh_cred");const a=document.getElementById("auth-section"),l=document.getElementById("terminal-section");a.classList.add("hidden"),l.classList.remove("hidden"),l.classList.add("flex"),document.getElementById("term-host").textContent="Host: "+_,document.getElementById("term-user").textContent="User: "+g,document.getElementById("term-port").textContent="Port: "+C,this.terminal.mount();try{await this.terminal.connect({host:_,port:C,username:g,password:k,authMethod:this.authMode==="key"?"publickey":"password",privateKey:w})}catch{l.classList.add("hidden"),l.classList.remove("flex"),a.classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'}}}class jn{constructor(e,_,C){Y(this,"user");Y(this,"servers",[]);Y(this,"onConnect");Y(this,"onLogout");Y(this,"editingServerId",null);Y(this,"modalAuthMode","password");this.user=e,this.onConnect=_,this.onLogout=C,this.init()}async init(){this.renderUserInfo(),this.bindEvents(),await this.fetchServers();const e=document.getElementById("user-copyright-year");e&&(e.textContent=new Date().getFullYear().toString())}renderUserInfo(){const e=document.getElementById("user-info");e&&(e.innerHTML=\`
\`,document.getElementById("connect-btn").addEventListener("click",()=>{this.handleConnect()}),document.getElementById("connection-form").addEventListener("keypress",t=>{t.key==="Enter"&&this.handleConnect()}),document.getElementById("auth-tab-password").addEventListener("click",()=>{this.setAuthMode("password")}),document.getElementById("auth-tab-key").addEventListener("click",()=>{this.setAuthMode("key")})}setAuthMode(e){this.authMode=e;const t=document.getElementById("auth-tab-password"),r=document.getElementById("auth-tab-key"),n=document.getElementById("auth-password-section"),o=document.getElementById("auth-key-section");e==="password"?(t.style.background="#4af626",t.style.color="#0a0a0a",t.style.borderColor="#4af626",r.style.background="transparent",r.style.color="#bbccb0",r.style.borderColor="#3c4b36",n.style.display="",o.style.display="none"):(r.style.background="#4af626",r.style.color="#0a0a0a",r.style.borderColor="#4af626",t.style.background="transparent",t.style.color="#bbccb0",t.style.borderColor="#3c4b36",o.style.display="",n.style.display="none")}async loadSavedCredentials(){const e=localStorage.getItem("cloudssh_cred");if(!e)return;const t=await dl(e);if(!t){localStorage.removeItem("cloudssh_cred");return}document.getElementById("host").value=t.host||"",document.getElementById("port").value=t.port||"22",document.getElementById("username").value=t.username||"",document.getElementById("password").value=t.password||"",document.getElementById("private-key").value=t.privateKey||"",document.getElementById("remember-me").checked=!0,t.authMethod==="key"?this.setAuthMode("key"):this.setAuthMode("password")}async handleConnect(){const t=document.getElementById("host").value.replace(/^\\[|\\]$/g,"").trim(),r=parseInt(document.getElementById("port").value||"22"),n=document.getElementById("username").value,o=document.getElementById("password").value,_=document.getElementById("private-key").value,s=document.getElementById("remember-me").checked;if(!t||!n){alert("请填写主机名和用户名");return}if(this.authMode==="password"&&!o){alert("请输入密码");return}if(this.authMode==="key"&&!_){alert("请粘贴私钥内容");return}if(this.turnstileEnabled&&!this.turnstileVerified){alert("请完成人机验证");return}if(s){const d=await cl({host:t,port:r.toString(),username:n,password:o,privateKey:_,authMethod:this.authMode==="key"?"publickey":"password"});localStorage.setItem("cloudssh_cred",d)}else localStorage.removeItem("cloudssh_cred");const h=document.getElementById("auth-section"),a=document.getElementById("terminal-section");h.classList.add("hidden"),a.classList.remove("hidden"),a.classList.add("flex"),document.getElementById("term-host").textContent="Host: "+t,document.getElementById("term-user").textContent="User: "+n,document.getElementById("term-port").textContent="Port: "+r,this.terminal.mount();try{await this.terminal.connect({host:t,port:r,username:n,password:o,authMethod:this.authMode==="key"?"publickey":"password",privateKey:_})}catch{a.classList.add("hidden"),a.classList.remove("flex"),h.classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'}}}class fl{constructor(e,t){Q(this,"user");Q(this,"servers",[]);Q(this,"onLogout");Q(this,"editingServerId",null);Q(this,"modalAuthMode","password");this.user=e,this.onLogout=t,this.init()}async init(){this.renderUserInfo(),this.bindEvents(),await this.fetchServers();const e=document.getElementById("user-copyright-year");e&&(e.textContent=new Date().getFullYear().toString())}renderUserInfo(){const e=document.getElementById("user-info");e&&(e.innerHTML=\`
<img src="\${this.user.avatar_url}" alt="\${this.user.username}" class="user-avatar w-8 h-8" />
<span class="text-xs font-bold tracking-[0.1em] text-[#bbccb0]">\${this.user.username}</span>
\`)}bindEvents(){var e,_,C,g,k,w,s,a,l;(e=document.getElementById("logout-btn"))==null||e.addEventListener("click",()=>this.logout()),(_=document.getElementById("add-server-btn"))==null||_.addEventListener("click",()=>this.showModal("add")),(C=document.getElementById("empty-add-btn"))==null||C.addEventListener("click",()=>this.showModal("add")),(g=document.getElementById("modal-close-btn"))==null||g.addEventListener("click",()=>this.hideModal()),(k=document.getElementById("modal-backdrop"))==null||k.addEventListener("click",()=>this.hideModal()),(w=document.getElementById("server-submit-btn"))==null||w.addEventListener("click",()=>this.handleSubmit()),(s=document.getElementById("modal-auth-tab-password"))==null||s.addEventListener("click",()=>this.setModalAuthMode("password")),(a=document.getElementById("modal-auth-tab-key"))==null||a.addEventListener("click",()=>this.setModalAuthMode("key")),(l=document.getElementById("server-form"))==null||l.addEventListener("keypress",d=>{d.key==="Enter"&&(d.preventDefault(),this.handleSubmit())})}async fetchServers(){try{const e=await fetch("/api/servers");if(!e.ok)throw new Error("Failed to fetch servers");this.servers=await e.json(),this.renderServerGrid()}catch(e){console.error("Failed to fetch servers:",e),this.servers=[],this.renderServerGrid()}}renderServerGrid(){const e=document.getElementById("server-grid"),_=document.getElementById("empty-state");if(!(!e||!_)){if(this.servers.length===0){e.innerHTML="",_.classList.remove("hidden"),_.classList.add("flex");return}_.classList.add("hidden"),_.classList.remove("flex"),e.innerHTML=this.servers.map(C=>this.renderServerCard(C)).join(""),this.servers.forEach(C=>{var g,k,w;(g=document.getElementById(\`connect-\${C.id}\`))==null||g.addEventListener("click",()=>this.connectServer(C.id)),(k=document.getElementById(\`edit-\${C.id}\`))==null||k.addEventListener("click",()=>this.showModal("edit",C)),(w=document.getElementById(\`delete-\${C.id}\`))==null||w.addEventListener("click",()=>this.deleteServer(C.id))})}}renderServerCard(e){const _=e.auth_method==="publickey"?"vpn_key":"password",C=e.auth_method==="publickey"?"KEY":"PWD";return\`
\`)}bindEvents(){var e,t,r,n,o,_,s,h,a;(e=document.getElementById("logout-btn"))==null||e.addEventListener("click",()=>this.logout()),(t=document.getElementById("add-server-btn"))==null||t.addEventListener("click",()=>this.showModal("add")),(r=document.getElementById("empty-add-btn"))==null||r.addEventListener("click",()=>this.showModal("add")),(n=document.getElementById("modal-close-btn"))==null||n.addEventListener("click",()=>this.hideModal()),(o=document.getElementById("modal-backdrop"))==null||o.addEventListener("click",()=>this.hideModal()),(_=document.getElementById("server-submit-btn"))==null||_.addEventListener("click",()=>this.handleSubmit()),(s=document.getElementById("modal-auth-tab-password"))==null||s.addEventListener("click",()=>this.setModalAuthMode("password")),(h=document.getElementById("modal-auth-tab-key"))==null||h.addEventListener("click",()=>this.setModalAuthMode("key")),(a=document.getElementById("server-form"))==null||a.addEventListener("keypress",d=>{d.key==="Enter"&&(d.preventDefault(),this.handleSubmit())})}async fetchServers(){try{const e=await fetch("/api/servers");if(!e.ok)throw new Error("Failed to fetch servers");this.servers=await e.json(),this.renderServerGrid()}catch(e){console.error("Failed to fetch servers:",e),this.servers=[],this.renderServerGrid()}}renderServerGrid(){const e=document.getElementById("server-grid"),t=document.getElementById("empty-state");if(!(!e||!t)){if(this.servers.length===0){e.innerHTML="",t.classList.remove("hidden"),t.classList.add("flex");return}t.classList.add("hidden"),t.classList.remove("flex"),e.innerHTML=this.servers.map(r=>this.renderServerCard(r)).join(""),this.servers.forEach(r=>{var n,o,_;(n=document.getElementById(\`connect-\${r.id}\`))==null||n.addEventListener("click",()=>this.connectServer(r.id)),(o=document.getElementById(\`edit-\${r.id}\`))==null||o.addEventListener("click",()=>this.showModal("edit",r)),(_=document.getElementById(\`delete-\${r.id}\`))==null||_.addEventListener("click",()=>this.deleteServer(r.id))})}}renderServerCard(e){const t=e.auth_method==="publickey"?"vpn_key":"password",r=e.auth_method==="publickey"?"KEY":"PWD";return\`
<div class="server-card p-5 relative group" id="card-\${e.id}">
<div class="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-[#3c4b36] to-transparent group-hover:via-[#4af626] transition-all duration-300"></div>
@@ -203,8 +211,8 @@ void main() {
<h3 class="text-sm font-bold text-[#4af626] tracking-[0.05em]">\${this.escapeHtml(e.name)}</h3>
</div>
<span class="text-[10px] font-bold tracking-[0.1em] text-[#bbccb0] border border-[#3c4b36] px-2 py-0.5 flex items-center gap-1">
<span class="material-symbols-outlined" style="font-size: 12px;">\${_}</span>
\${C}
<span class="material-symbols-outlined" style="font-size: 12px;">\${t}</span>
\${r}
</span>
</div>
@@ -232,25 +240,25 @@ void main() {
</button>
</div>
</div>
\`}async connectServer(e){const _=this.servers.find(g=>g.id===e);if(!_)return;const C=document.getElementById(\`connect-\${e}\`);C&&(C.innerHTML=\`
\`}async connectServer(e){const t=this.servers.find(n=>n.id===e);if(!t)return;const r=document.getElementById(\`connect-\${e}\`);r&&(r.innerHTML=\`
<span class="material-symbols-outlined animate-spin" style="font-size: 14px;">progress_activity</span>
CONNECTING...
\`,C.disabled=!0);try{const g=await fetch(\`/api/servers/\${e}/connect\`,{method:"POST"});if(!g.ok){const w=await g.json();throw new Error(w.error||"Connection failed")}const{wsUrl:k}=await g.json();this.onConnect(k,_.name),C&&(C.innerHTML=\`
\`,r.disabled=!0);try{const n=await fetch(\`/api/servers/\${e}/connect\`,{method:"POST"});if(!n.ok){const s=await n.json();throw new Error(s.error||"Connection failed")}const{wsUrl:o}=await n.json(),_=\`/?wsUrl=\${encodeURIComponent(o)}&name=\${encodeURIComponent(t.name)}\`;window.open(_,"_blank"),r&&(r.innerHTML=\`
<span class="material-symbols-outlined" style="font-size: 14px;">power_settings_new</span>
CONNECT
\`,C.disabled=!1)}catch(g){alert(\`连接失败: \${g instanceof Error?g.message:String(g)}\`),C&&(C.innerHTML=\`
\`,r.disabled=!1)}catch(n){alert(\`连接失败: \${n instanceof Error?n.message:String(n)}\`),r&&(r.innerHTML=\`
<span class="material-symbols-outlined" style="font-size: 14px;">power_settings_new</span>
CONNECT
\`,C.disabled=!1)}}async deleteServer(e){const _=this.servers.find(C=>C.id===e);if(_&&confirm(\`确认删除服务器 "\${_.name}" ?\`))try{const C=document.getElementById(\`card-\${e}\`);if(C&&C.classList.add("removing"),!(await fetch(\`/api/servers/\${e}\`,{method:"DELETE"})).ok)throw new Error("Delete failed");await new Promise(k=>setTimeout(k,300)),this.servers=this.servers.filter(k=>k.id!==e),this.renderServerGrid()}catch(C){alert(\`删除失败: \${C instanceof Error?C.message:String(C)}\`),await this.fetchServers()}}showModal(e,_){this.editingServerId=e==="edit"&&_?_.id:null;const C=document.getElementById("server-modal"),g=document.getElementById("modal-title"),k=document.getElementById("server-submit-btn");!C||!g||!k||(g.textContent=e==="add"?"ADD_SERVER":"EDIT_SERVER",k.innerHTML=\`
\`,r.disabled=!1)}}async deleteServer(e){const t=this.servers.find(r=>r.id===e);if(t&&confirm(\`确认删除服务器 "\${t.name}" ?\`))try{const r=document.getElementById(\`card-\${e}\`);if(r&&r.classList.add("removing"),!(await fetch(\`/api/servers/\${e}\`,{method:"DELETE"})).ok)throw new Error("Delete failed");await new Promise(o=>setTimeout(o,300)),this.servers=this.servers.filter(o=>o.id!==e),this.renderServerGrid()}catch(r){alert(\`删除失败: \${r instanceof Error?r.message:String(r)}\`),await this.fetchServers()}}showModal(e,t){this.editingServerId=e==="edit"&&t?t.id:null;const r=document.getElementById("server-modal"),n=document.getElementById("modal-title"),o=document.getElementById("server-submit-btn");!r||!n||!o||(n.textContent=e==="add"?"ADD_SERVER":"EDIT_SERVER",o.innerHTML=\`
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
\${e==="add"?"SAVE_SERVER":"UPDATE_SERVER"}
\`,e==="edit"&&_?(document.getElementById("server-name").value=_.name,document.getElementById("server-host").value=_.host,document.getElementById("server-port").value=_.port.toString(),document.getElementById("server-username").value=_.username,document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",_.auth_method==="publickey"?this.setModalAuthMode("key"):this.setModalAuthMode("password")):(document.getElementById("server-name").value="",document.getElementById("server-host").value="",document.getElementById("server-port").value="22",document.getElementById("server-username").value="",document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",this.setModalAuthMode("password")),C.classList.remove("hidden"),C.classList.add("flex"),setTimeout(()=>{var w;(w=document.getElementById("server-name"))==null||w.focus()},100))}hideModal(){const e=document.getElementById("server-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex")),this.editingServerId=null}setModalAuthMode(e){this.modalAuthMode=e;const _=document.getElementById("modal-auth-tab-password"),C=document.getElementById("modal-auth-tab-key"),g=document.getElementById("modal-password-section"),k=document.getElementById("modal-key-section");e==="password"?(_.style.background="#4af626",_.style.color="#0a0a0a",_.style.borderColor="#4af626",C.style.background="transparent",C.style.color="#bbccb0",C.style.borderColor="#3c4b36",g.style.display="",k.style.display="none"):(C.style.background="#4af626",C.style.color="#0a0a0a",C.style.borderColor="#4af626",_.style.background="transparent",_.style.color="#bbccb0",_.style.borderColor="#3c4b36",k.style.display="",g.style.display="none")}async handleSubmit(){const e=document.getElementById("server-name").value.trim(),_=document.getElementById("server-host").value.trim(),C=parseInt(document.getElementById("server-port").value||"22"),g=document.getElementById("server-username").value.trim(),k=document.getElementById("server-password").value,w=document.getElementById("server-private-key").value;if(!e||!_||!g){alert("请填写服务器名称、主机和用户名");return}const s=this.modalAuthMode==="key"?"publickey":"password",a=s==="publickey"?w:k;if(!this.editingServerId&&!a){alert(s==="publickey"?"请粘贴私钥内容":"请输入密码");return}const l=document.getElementById("server-submit-btn");l.disabled=!0,l.innerHTML=\`
\`,e==="edit"&&t?(document.getElementById("server-name").value=t.name,document.getElementById("server-host").value=t.host,document.getElementById("server-port").value=t.port.toString(),document.getElementById("server-username").value=t.username,document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",t.auth_method==="publickey"?this.setModalAuthMode("key"):this.setModalAuthMode("password")):(document.getElementById("server-name").value="",document.getElementById("server-host").value="",document.getElementById("server-port").value="22",document.getElementById("server-username").value="",document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",this.setModalAuthMode("password")),r.classList.remove("hidden"),r.classList.add("flex"),setTimeout(()=>{var _;(_=document.getElementById("server-name"))==null||_.focus()},100))}hideModal(){const e=document.getElementById("server-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex")),this.editingServerId=null}setModalAuthMode(e){this.modalAuthMode=e;const t=document.getElementById("modal-auth-tab-password"),r=document.getElementById("modal-auth-tab-key"),n=document.getElementById("modal-password-section"),o=document.getElementById("modal-key-section");e==="password"?(t.style.background="#4af626",t.style.color="#0a0a0a",t.style.borderColor="#4af626",r.style.background="transparent",r.style.color="#bbccb0",r.style.borderColor="#3c4b36",n.style.display="",o.style.display="none"):(r.style.background="#4af626",r.style.color="#0a0a0a",r.style.borderColor="#4af626",t.style.background="transparent",t.style.color="#bbccb0",t.style.borderColor="#3c4b36",o.style.display="",n.style.display="none")}async handleSubmit(){const e=document.getElementById("server-name").value.trim(),t=document.getElementById("server-host").value.trim(),r=parseInt(document.getElementById("server-port").value||"22"),n=document.getElementById("server-username").value.trim(),o=document.getElementById("server-password").value,_=document.getElementById("server-private-key").value;if(!e||!t||!n){alert("请填写服务器名称、主机和用户名");return}const s=this.modalAuthMode==="key"?"publickey":"password",h=s==="publickey"?_:o;if(!this.editingServerId&&!h){alert(s==="publickey"?"请粘贴私钥内容":"请输入密码");return}const a=document.getElementById("server-submit-btn");a.disabled=!0,a.innerHTML=\`
<span class="material-symbols-outlined animate-spin" style="font-size: 18px;">progress_activity</span>
SAVING...
\`;try{const d={name:e,host:_,port:C,username:g,auth_method:s};a&&(d.credential=a);let h;if(this.editingServerId?h=await fetch(\`/api/servers/\${this.editingServerId}\`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}):h=await fetch("/api/servers",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}),!h.ok){const u=await h.json();throw new Error(u.error||"Save failed")}this.hideModal(),await this.fetchServers()}catch(d){alert(\`保存失败: \${d instanceof Error?d.message:String(d)}\`)}finally{l.disabled=!1,l.innerHTML=\`
\`;try{const d={name:e,host:t,port:r,username:n,auth_method:s};h&&(d.credential=h);let u;if(this.editingServerId?u=await fetch(\`/api/servers/\${this.editingServerId}\`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}):u=await fetch("/api/servers",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}),!u.ok){const p=await u.json();throw new Error(p.error||"Save failed")}this.hideModal(),await this.fetchServers()}catch(d){alert(\`保存失败: \${d instanceof Error?d.message:String(d)}\`)}finally{a.disabled=!1,a.innerHTML=\`
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
\${this.editingServerId?"UPDATE_SERVER":"SAVE_SERVER"}
\`}}async logout(){try{await fetch("/api/auth/logout",{method:"POST"})}catch{}this.onLogout()}escapeHtml(e){const _=document.createElement("div");return _.textContent=e,_.innerHTML}}const ot=new Wn("terminal-container");let is=null,ni=!1;function Hs(){document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),document.getElementById("server-modal").classList.add("hidden"),document.getElementById("server-modal").classList.remove("flex"),is||(is=new zn(ot))}function Zn(r){ni=!0,document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),new jn(r,(e,_)=>{qn(e,_)},()=>{ni=!1,Hs()})}function qn(r,e){document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.remove("hidden"),document.getElementById("terminal-section").classList.add("flex"),document.getElementById("term-host").textContent=\`Server: \${e}\`,document.getElementById("term-user").textContent="",document.getElementById("term-port").textContent="",ot.mount();const _=new WebSocket(r);ot.connectWithWebSocket(_)}var os;(os=document.getElementById("disconnect-btn"))==null||os.addEventListener("click",()=>{ot.disconnect();const r=document.getElementById("terminal-section");r.classList.add("hidden"),r.classList.remove("flex"),ni?(document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex")):document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'});var as;(as=document.getElementById("theme-selector"))==null||as.addEventListener("change",r=>{const e=r.target.value;ot.setTheme(e)});async function Kn(){const r=document.getElementById("copyright-year");r&&(r.textContent=new Date().getFullYear().toString());try{const e=await fetch("/api/auth/me");if(e.ok){const _=await e.json();Zn(_);return}}catch{}Hs()}Kn();
\`}}async logout(){try{await fetch("/api/auth/logout",{method:"POST"})}catch{}this.onLogout()}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}}const Rt=new ll("terminal-container");let Zr=null,Ms=!1;function _l(){return new URLSearchParams(window.location.search).has("wsUrl")}function gl(){var n;const i=new URLSearchParams(window.location.search),e=i.get("wsUrl"),t=i.get("name")||"Server";document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.remove("hidden"),document.getElementById("terminal-section").classList.add("flex"),document.getElementById("term-host").textContent=\`Server: \${t}\`,document.getElementById("term-user").textContent="",document.getElementById("term-port").textContent="",Rt.mount();const r=new WebSocket(e);r.binaryType="arraybuffer",Rt.connectWithWebSocket(r),(n=document.getElementById("disconnect-btn"))==null||n.addEventListener("click",()=>{Rt.disconnect(),window.close()})}function no(){document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),document.getElementById("server-modal").classList.add("hidden"),document.getElementById("server-modal").classList.remove("flex"),Zr||(Zr=new ul(Rt))}function pl(i){Ms=!0,document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),new fl(i,()=>{Ms=!1,no()})}var Qr;(Qr=document.getElementById("disconnect-btn"))==null||Qr.addEventListener("click",()=>{Rt.disconnect();const i=document.getElementById("terminal-section");i.classList.add("hidden"),i.classList.remove("flex"),Ms?(document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex")):document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'});var en;(en=document.getElementById("theme-selector"))==null||en.addEventListener("change",i=>{const e=i.target.value;Rt.setTheme(e)});async function vl(){const i=document.getElementById("copyright-year");if(i&&(i.textContent=new Date().getFullYear().toString()),_l()){gl();return}try{const e=await fetch("/api/auth/me");if(e.ok){const t=await e.json();pl(t);return}}catch{}no()}vl();
</script>
<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:JetBrains Mono,monospace;background-color:#0a0a0a;color:#4af626;min-height:100vh;overflow:hidden}.scanlines{position:fixed;top:0;left:0;width:100vw;height:100vh;background:linear-gradient(to bottom,#fff0,#fff0 50%,#0000001a 50%,#0000001a);background-size:100% 4px;pointer-events:none;z-index:50}.flicker{animation:flicker .15s infinite;pointer-events:none;position:fixed;top:0;left:0;width:100%;height:100%;background:#4af62605;z-index:49}@keyframes flicker{0%{opacity:.8}50%{opacity:1}to{opacity:.9}}.terminal-input{background:transparent;border:none;border-bottom:1px solid #3c4b36;color:#4af626;font-family:JetBrains Mono,monospace;padding:8px 0;width:100%;outline:none;transition:border-color .2s}.terminal-input:focus{border-bottom:1px solid #4af626;box-shadow:none}.terminal-input::placeholder{color:#3c4b36}.blinking-cursor:after{content:"█";color:#14d1ff;animation:blink 1s step-end infinite;margin-left:4px;font-size:.9em}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.cyber-box{background-color:#121212;border:1px solid #1f1f1f}.cyber-button{border:1px solid #4af626;color:#4af626;transition:all .2s ease;position:relative;overflow:hidden;cursor:pointer}.cyber-button:hover{background-color:#4af626;color:#0a0a0a}.cursor-blink{animation:blink 1s step-end infinite}.scanline{background:linear-gradient(to bottom,#fff0,#fff0 50%,#0000001a 50%,#0000001a);background-size:100% 4px}.custom-scrollbar::-webkit-scrollbar{width:8px}.custom-scrollbar::-webkit-scrollbar-track{background:#1c1b1b80}.custom-scrollbar::-webkit-scrollbar-thumb{background:#3c4b36cc;border-radius:4px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#86957dcc}.server-card{background-color:#121212;border:1px solid #1f1f1f;transition:all .3s ease;position:relative}.server-card:hover{border-color:#4af626;transform:translateY(-2px);box-shadow:0 4px 20px #4af62614}.server-card.removing{animation:fadeOut .3s ease forwards}@keyframes fadeOut{to{opacity:0;transform:translateY(-10px)}}.modal-overlay{background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.user-avatar{border:2px solid #4af626;border-radius:50%;transition:border-color .2s}.user-avatar:hover{border-color:#14d1ff}.github-login-btn{transition:all .2s ease}.github-login-btn:hover{border-color:#4af626;color:#4af626}.animate-spin{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
@@ -285,207 +293,207 @@ void main() {
* other features.
*/.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:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.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}
</style>
</head>
<body class="min-h-screen font-body text-sm overflow-hidden">
<div id="app">
<!-- Login Page (匿名连接) -->
<div id="auth-section" class="min-h-screen flex items-center justify-center p-6 relative">
<div class="scanlines"></div>
<div class="flicker"></div>
<main class="w-full max-w-md relative z-10">
<div class="mb-8 text-center">
<div class="text-3xl font-bold text-[#4af626] tracking-tighter mb-2">CloudSSH<span class="blinking-cursor"></span></div>
</div>
<div class="cyber-box p-6 shadow-2xl relative">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#4af626] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-8 pb-4 border-b border-[#3c4b36]">
<span class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">CONNECTION_PARAMETERS</span>
<span class="material-symbols-outlined text-[#14d1ff]" style="font-variation-settings: 'FILL' 0;">terminal</span>
</div>
<div id="connection-form-container"></div>
</div>
<div class="mt-8 text-center text-[13px] text-[#bbccb0] opacity-60">SYSTEM READY. WAITING FOR INPUT.</div>
<div class="mt-4 text-center">
<a href="https://github.com/newbietan/CloudSSH" class="text-[13px] text-[#4af626] opacity-60 hover:opacity-100 transition-colors tracking-widest uppercase">[ GitHub Open Source ]</a>
</div>
</main>
</div>
<!-- User Space Page (登录后) -->
<div id="user-space-section" class="hidden min-h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer" id="user-space-logo">CloudSSH</h1>
</div>
<div class="flex items-center gap-4">
<div id="user-info" class="flex items-center gap-3">
<!-- 动态渲染:头像 + 用户名 -->
</div>
<button id="logout-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center text-[#ffb4ab]" title="Logout">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">logout</span>
</button>
</div>
</header>
<!-- Server List Area -->
<main class="flex-1 p-6 relative overflow-auto custom-scrollbar">
<div class="scanlines pointer-events-none"></div>
<div class="max-w-5xl mx-auto">
<!-- 顶部操作栏 -->
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-lg font-bold text-[#4af626] tracking-[0.1em]">SERVER_LIST</h2>
<p class="text-xs text-[#bbccb0] opacity-60 mt-1">Manage your SSH connections</p>
</div>
<button id="add-server-btn" class="cyber-button py-2 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_SERVER
</button>
</div>
<!-- 服务器卡片网格 -->
<div id="server-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 由 JS 动态渲染 -->
</div>
<!-- 空状态 -->
<div id="empty-state" class="hidden flex-col items-center justify-center py-20">
<span class="material-symbols-outlined text-[#3c4b36] mb-4" style="font-size: 64px; font-variation-settings: 'FILL' 0;">dns</span>
<p class="text-[#bbccb0] text-sm tracking-[0.1em] mb-2">NO_SERVERS_CONFIGURED</p>
<p class="text-[#3c4b36] text-xs mb-6">Add a server to get started</p>
<button id="empty-add-btn" class="cyber-button py-2 px-6 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_FIRST_SERVER
</button>
</div>
</div>
</main>
<!-- Footer -->
<footer class="px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="user-copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
<!-- Server Modal (添加/编辑) -->
<div id="server-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="modal-overlay absolute inset-0" id="modal-backdrop"></div>
<div class="cyber-box p-6 shadow-2xl relative z-10 w-full max-w-md mx-4">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#14d1ff] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-6 pb-4 border-b border-[#3c4b36]">
<span id="modal-title" class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">ADD_SERVER</span>
<button id="modal-close-btn" class="text-[#bbccb0] hover:text-[#4af626] transition-colors cursor-pointer">
<span class="material-symbols-outlined" style="font-size: 20px;">close</span>
</button>
</div>
<form id="server-form" class="space-y-4">
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">SERVER_NAME</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">label</span>
<input id="server-name" class="terminal-input text-[13px]" placeholder="My Server" type="text" required>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="col-span-3">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">HOST_ADDRESS</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">&gt;</span>
<input id="server-host" class="terminal-input text-[13px]" placeholder="192.168.1.1" type="text" required>
</div>
</div>
<div class="col-span-1">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">PORT</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">:</span>
<input id="server-port" class="terminal-input text-[13px]" placeholder="22" type="text" value="22">
</div>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_USER</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">person</span>
<input id="server-username" class="terminal-input text-[13px]" placeholder="root" type="text" required>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_METHOD</label>
<div class="flex gap-2 mb-3">
<button type="button" id="modal-auth-tab-password" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#4af626] text-[#4af626] bg-transparent cursor-pointer transition-all" style="background:#4af626;color:#0a0a0a;">PASSWORD</button>
<button type="button" id="modal-auth-tab-key" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#3c4b36] text-[#bbccb0] bg-transparent cursor-pointer transition-all">PRIVATE_KEY</button>
</div>
<div id="modal-password-section">
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">key</span>
<input id="server-password" class="terminal-input text-[13px]" placeholder="••••••••" type="password">
</div>
</div>
<div id="modal-key-section" style="display:none;">
<textarea id="server-private-key" class="terminal-input text-[11px] w-full" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...粘贴私钥内容...&#10;-----END OPENSSH PRIVATE KEY-----" style="resize:vertical;border:1px solid #3c4b36;padding:8px;"></textarea>
</div>
</div>
<div class="pt-2">
<button id="server-submit-btn" class="cyber-button w-full py-3 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center justify-center gap-2 bg-[#4af626] text-[#022100]" type="button">
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
SAVE_SERVER
</button>
</div>
</form>
</div>
</div>
<!-- Terminal Page -->
<div id="terminal-section" class="hidden h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer">CloudSSH</h1>
</div>
<div class="flex items-center gap-4 text-primary-container">
<select id="theme-selector" class="bg-surface-variant text-on-surface-variant text-xs py-1 px-2 border-none outline-none cursor-pointer rounded">
<option value="cyberpunk">Cyberpunk</option>
<option value="glacier">Glacier</option>
<option value="gruvbox">Gruvbox</option>
</select>
<button id="disconnect-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center" title="Disconnect">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">power_settings_new</span>
</button>
</div>
</header>
<!-- Main Terminal -->
<main class="flex-1 p-6 pb-14 relative bg-surface-container-lowest overflow-hidden">
<div class="absolute inset-0 scanline pointer-events-none z-10 opacity-30"></div>
<div id="terminal-wrapper" class="h-full w-full border border-outline-variant bg-[#0a0a0a] p-4 overflow-hidden font-code text-[13px] text-primary-container relative z-0 flex flex-col">
<!-- Status Bar -->
<div class="flex justify-between items-center border-b border-surface-variant pb-2 mb-4 text-on-surface-variant opacity-80 text-[11px]">
<div class="flex gap-6">
<span id="term-status" class="flex items-center gap-1"><div class="w-2 h-2 bg-primary-container"></div> Connected</span>
<span id="term-host">Host: -</span>
<span id="term-user">User: -</span>
<span id="term-port">Port: -</span>
</div>
<div>
<span id="term-info"></span>
</div>
</div>
<!-- Terminal Output -->
<div id="terminal-container" class="flex-1 overflow-hidden"></div>
</div>
</main>
<!-- Footer -->
<footer class="fixed bottom-0 left-0 w-full px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
</div>
</body>
</html>
</head>
<body class="min-h-screen font-body text-sm overflow-hidden">
<div id="app">
<!-- Login Page (匿名连接) -->
<div id="auth-section" class="min-h-screen flex items-center justify-center p-6 relative">
<div class="scanlines"></div>
<div class="flicker"></div>
<main class="w-full max-w-md relative z-10">
<div class="mb-8 text-center">
<div class="text-3xl font-bold text-[#4af626] tracking-tighter mb-2">CloudSSH<span class="blinking-cursor"></span></div>
</div>
<div class="cyber-box p-6 shadow-2xl relative">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#4af626] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-8 pb-4 border-b border-[#3c4b36]">
<span class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">CONNECTION_PARAMETERS</span>
<span class="material-symbols-outlined text-[#14d1ff]" style="font-variation-settings: 'FILL' 0;">terminal</span>
</div>
<div id="connection-form-container"></div>
</div>
<div class="mt-8 text-center text-[13px] text-[#bbccb0] opacity-60">SYSTEM READY. WAITING FOR INPUT.</div>
<div class="mt-4 text-center">
<a href="https://github.com/newbietan/CloudSSH" class="text-[13px] text-[#4af626] opacity-60 hover:opacity-100 transition-colors tracking-widest uppercase">[ GitHub Open Source ]</a>
</div>
</main>
</div>
<!-- User Space Page (登录后) -->
<div id="user-space-section" class="hidden min-h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer" id="user-space-logo">CloudSSH</h1>
</div>
<div class="flex items-center gap-4">
<div id="user-info" class="flex items-center gap-3">
<!-- 动态渲染:头像 + 用户名 -->
</div>
<button id="logout-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center text-[#ffb4ab]" title="Logout">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">logout</span>
</button>
</div>
</header>
<!-- Server List Area -->
<main class="flex-1 p-6 relative overflow-auto custom-scrollbar">
<div class="scanlines pointer-events-none"></div>
<div class="max-w-5xl mx-auto">
<!-- 顶部操作栏 -->
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-lg font-bold text-[#4af626] tracking-[0.1em]">SERVER_LIST</h2>
<p class="text-xs text-[#bbccb0] opacity-60 mt-1">Manage your SSH connections</p>
</div>
<button id="add-server-btn" class="cyber-button py-2 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_SERVER
</button>
</div>
<!-- 服务器卡片网格 -->
<div id="server-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 由 JS 动态渲染 -->
</div>
<!-- 空状态 -->
<div id="empty-state" class="hidden flex-col items-center justify-center py-20">
<span class="material-symbols-outlined text-[#3c4b36] mb-4" style="font-size: 64px; font-variation-settings: 'FILL' 0;">dns</span>
<p class="text-[#bbccb0] text-sm tracking-[0.1em] mb-2">NO_SERVERS_CONFIGURED</p>
<p class="text-[#3c4b36] text-xs mb-6">Add a server to get started</p>
<button id="empty-add-btn" class="cyber-button py-2 px-6 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_FIRST_SERVER
</button>
</div>
</div>
</main>
<!-- Footer -->
<footer class="px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="user-copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
<!-- Server Modal (添加/编辑) -->
<div id="server-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="modal-overlay absolute inset-0" id="modal-backdrop"></div>
<div class="cyber-box p-6 shadow-2xl relative z-10 w-full max-w-md mx-4">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#14d1ff] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-6 pb-4 border-b border-[#3c4b36]">
<span id="modal-title" class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">ADD_SERVER</span>
<button id="modal-close-btn" class="text-[#bbccb0] hover:text-[#4af626] transition-colors cursor-pointer">
<span class="material-symbols-outlined" style="font-size: 20px;">close</span>
</button>
</div>
<form id="server-form" class="space-y-4">
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">SERVER_NAME</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">label</span>
<input id="server-name" class="terminal-input text-[13px]" placeholder="My Server" type="text" required>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="col-span-3">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">HOST_ADDRESS</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">&gt;</span>
<input id="server-host" class="terminal-input text-[13px]" placeholder="192.168.1.1" type="text" required>
</div>
</div>
<div class="col-span-1">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">PORT</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">:</span>
<input id="server-port" class="terminal-input text-[13px]" placeholder="22" type="text" value="22">
</div>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_USER</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">person</span>
<input id="server-username" class="terminal-input text-[13px]" placeholder="root" type="text" required>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_METHOD</label>
<div class="flex gap-2 mb-3">
<button type="button" id="modal-auth-tab-password" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#4af626] text-[#4af626] bg-transparent cursor-pointer transition-all" style="background:#4af626;color:#0a0a0a;">PASSWORD</button>
<button type="button" id="modal-auth-tab-key" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#3c4b36] text-[#bbccb0] bg-transparent cursor-pointer transition-all">PRIVATE_KEY</button>
</div>
<div id="modal-password-section">
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">key</span>
<input id="server-password" class="terminal-input text-[13px]" placeholder="••••••••" type="password">
</div>
</div>
<div id="modal-key-section" style="display:none;">
<textarea id="server-private-key" class="terminal-input text-[11px] w-full" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...粘贴私钥内容...&#10;-----END OPENSSH PRIVATE KEY-----" style="resize:vertical;border:1px solid #3c4b36;padding:8px;"></textarea>
</div>
</div>
<div class="pt-2">
<button id="server-submit-btn" class="cyber-button w-full py-3 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center justify-center gap-2 bg-[#4af626] text-[#022100]" type="button">
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
SAVE_SERVER
</button>
</div>
</form>
</div>
</div>
<!-- Terminal Page -->
<div id="terminal-section" class="hidden h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer">CloudSSH</h1>
</div>
<div class="flex items-center gap-4 text-primary-container">
<select id="theme-selector" class="bg-surface-variant text-on-surface-variant text-xs py-1 px-2 border-none outline-none cursor-pointer rounded">
<option value="cyberpunk">Cyberpunk</option>
<option value="glacier">Glacier</option>
<option value="gruvbox">Gruvbox</option>
</select>
<button id="disconnect-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center" title="Disconnect">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">power_settings_new</span>
</button>
</div>
</header>
<!-- Main Terminal -->
<main class="flex-1 p-6 pb-14 relative bg-surface-container-lowest overflow-hidden">
<div class="absolute inset-0 scanline pointer-events-none z-10 opacity-30"></div>
<div id="terminal-wrapper" class="h-full w-full border border-outline-variant bg-[#0a0a0a] p-4 overflow-hidden font-code text-[13px] text-primary-container relative z-0 flex flex-col">
<!-- Status Bar -->
<div class="flex justify-between items-center border-b border-surface-variant pb-2 mb-4 text-on-surface-variant opacity-80 text-[11px]">
<div class="flex gap-6">
<span id="term-status" class="flex items-center gap-1"><div class="w-2 h-2 bg-primary-container"></div> Connected</span>
<span id="term-host">Host: -</span>
<span id="term-user">User: -</span>
<span id="term-port">Port: -</span>
</div>
<div>
<span id="term-info"></span>
</div>
</div>
<!-- Terminal Output -->
<div id="terminal-container" class="flex-1 overflow-hidden"></div>
</div>
</main>
<!-- Footer -->
<footer class="fixed bottom-0 left-0 w-full px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
</div>
</body>
</html>
`;
+204 -76
View File
@@ -25,10 +25,22 @@ import {
} from '../types';
import { SSHTransport } from '../ssh/transport';
import { SSHPacketParser, SSHPacketBuilder } from '../ssh/packet';
import { KEXInitBuilder, parseKEXInit, negotiate } from '../ssh/kex';
import {
KEXInitBuilder,
parseKEXInit,
negotiate
} from '../ssh/kex';
import {
getCipherSpec,
getMacAlgorithmsForCipher,
getMacSpec,
KEX_ALGORITHM_ECDH_NISTP256,
isCurve25519KEXAlgorithm
} from '../ssh/algorithms';
import { ECDHKeyExchange } from '../ssh/kex-ecdh';
import { Curve25519KeyExchange, Curve25519KeyPair } from '../ssh/kex-curve25519';
import { KeyDerivation } from '../ssh/keys';
import { SSHAESGCMCipher } from '../ssh/crypto';
import { SSHAESCTRCipher, SSHAESGCMCipher, SSHHMAC } from '../ssh/crypto';
import { SSHAuth } from '../ssh/auth';
import { SSHChannel } from '../ssh/channel';
@@ -41,8 +53,10 @@ export class SSHSession {
private transport: SSHTransport;
private packetParser: SSHPacketParser;
private channel: SSHChannel;
private encryptCipher: SSHAESGCMCipher | null = null;
private decryptCipher: SSHAESGCMCipher | null = null;
private encryptCipher: SSHAESGCMCipher | SSHAESCTRCipher | null = null;
private decryptCipher: SSHAESGCMCipher | SSHAESCTRCipher | null = null;
private encryptMac: SSHHMAC | null = null;
private decryptMac: SSHHMAC | null = null;
private derivedKeys: SessionKeys | null = null;
private seqNumSend: number = 0;
@@ -52,18 +66,23 @@ export class SSHSession {
private kexInitLocal: Uint8Array | null = null;
private kexInitRemote: Uint8Array | null = null;
private ecdhKeyPair!: CryptoKeyPair;
private ecdhRawPublicKey!: Uint8Array;
private negotiatedKexAlgorithm: string | null = null;
private ecdhKeyPair: CryptoKeyPair | null = null;
private curve25519KeyPair: Curve25519KeyPair | null = null;
private kexRawPublicKey: Uint8Array | null = null;
private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'ready'
private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'shell-requested' | 'ready'
= 'connecting';
private hostKeyFingerprint: string = '';
private versionRawBuffer: Uint8Array = new Uint8Array(0);
private negotiatedCipherC2S: string = 'aes256-gcm@openssh.com';
private negotiatedCipherS2C: string = 'aes256-gcm@openssh.com';
private negotiatedMacC2S: string = 'none';
private negotiatedMacS2C: string = 'none';
private keepaliveInterval: ReturnType<typeof setInterval> | null = null;
private shellReadyTimeout: ReturnType<typeof setTimeout> | null = null;
constructor(
ws: WebSocket,
@@ -196,16 +215,33 @@ export class SSHSession {
);
await this.writeSocket(packet);
console.log('[KEX] KEXINIT sent');
}
this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair();
this.ecdhRawPublicKey = await ECDHKeyExchange.exportRawPublicKey(this.ecdhKeyPair);
private async sendKEXECDHInit(): Promise<void> {
if (!this.negotiatedKexAlgorithm) {
throw new Error('KEX algorithm not negotiated');
}
let kexInit: Uint8Array;
if (isCurve25519KEXAlgorithm(this.negotiatedKexAlgorithm)) {
this.curve25519KeyPair = await Curve25519KeyExchange.generateKeyPair();
this.ecdhKeyPair = null;
this.kexRawPublicKey = await Curve25519KeyExchange.exportRawPublicKey(this.curve25519KeyPair);
kexInit = Curve25519KeyExchange.buildInit(this.kexRawPublicKey);
} else if (this.negotiatedKexAlgorithm === KEX_ALGORITHM_ECDH_NISTP256) {
this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair();
this.curve25519KeyPair = null;
this.kexRawPublicKey = await ECDHKeyExchange.exportRawPublicKey(this.ecdhKeyPair);
kexInit = ECDHKeyExchange.buildInit(this.kexRawPublicKey);
} else {
throw new Error(`Unsupported KEX algorithm: ${this.negotiatedKexAlgorithm}`);
}
const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhRawPublicKey);
const ecdhPacket = await SSHPacketBuilder.build(
ecdhInit, 8, null, this.seqNumSend++
kexInit, 8, null, this.seqNumSend++
);
await this.writeSocket(ecdhPacket);
console.log('[KEX] ECDH_INIT sent, waiting for server reply');
console.log(`[KEX] ECDH_INIT sent using ${this.negotiatedKexAlgorithm}, waiting for server reply`);
}
private async writeSocket(data: Uint8Array): Promise<void> {
@@ -214,8 +250,29 @@ export class SSHSession {
writer.releaseLock();
}
private async buildEncryptedPacket(payload: Uint8Array): Promise<Uint8Array> {
if (!this.encryptCipher) {
throw new Error('Encryption not initialized');
}
const cipher = getCipherSpec(this.negotiatedCipherC2S);
return SSHPacketBuilder.build(
payload,
cipher.blockSize,
(data, seq, aad) => this.encryptCipher!.encrypt(data, seq, aad),
this.seqNumSend++,
cipher.aead,
this.encryptMac
? (packetData, seq) => this.encryptMac!.sign(packetData, seq)
: undefined
);
}
private async processPackets(): Promise<void> {
const blockSize = this.decryptCipher ? 16 : 8;
const cipher = this.decryptCipher ? getCipherSpec(this.negotiatedCipherS2C) : null;
const blockSize = cipher ? cipher.blockSize : 8;
const hasAuthTag = !!cipher?.aead;
const macLength = this.decryptCipher && !hasAuthTag ? getMacSpec(this.negotiatedMacS2C).length : 0;
const hasDecrypt = !!this.decryptCipher;
this.sendDebug(`processPackets: blockSize=${blockSize}, hasDecrypt=${hasDecrypt}, bufferLen=${this.packetParser.getBufferLength()}`);
@@ -224,9 +281,13 @@ export class SSHSession {
const packet = await this.packetParser.nextPacket(
blockSize,
this.decryptCipher
? (data, seq, aad) => this.decryptCipher!.decrypt(data, seq, aad)
? (data, seq, aad, commit) => this.decryptCipher!.decrypt(data, seq, aad, commit)
: (data) => data,
!!this.decryptCipher
hasAuthTag,
macLength,
this.decryptMac
? (packet, mac, seq) => this.decryptMac!.verify(packet, seq, mac)
: undefined
);
if (!packet) {
@@ -273,6 +334,7 @@ export class SSHSession {
break;
case 'shell':
case 'shell-requested':
case 'ready':
await this.handleSessionPacket(msgType, packet.payload);
break;
@@ -329,9 +391,17 @@ export class SSHSession {
try {
const serverKex = parseKEXInit(payload);
const clientKex = parseKEXInit(this.kexInitLocal!);
this.negotiatedCipherC2S = negotiate(clientKex.encryptionC2S, serverKex.encryptionC2S);
this.negotiatedCipherS2C = negotiate(clientKex.encryptionS2C, serverKex.encryptionS2C);
this.sendDebug(`Negotiated C2S: ${this.negotiatedCipherC2S}, S2C: ${this.negotiatedCipherS2C}`);
this.negotiatedKexAlgorithm = negotiate(clientKex.kexAlgorithms, serverKex.kexAlgorithms, 'KEX algorithm');
this.negotiatedCipherC2S = negotiate(clientKex.encryptionC2S, serverKex.encryptionC2S, 'C2S cipher');
this.negotiatedCipherS2C = negotiate(clientKex.encryptionS2C, serverKex.encryptionS2C, 'S2C cipher');
this.negotiatedMacC2S = getCipherSpec(this.negotiatedCipherC2S).aead
? 'none'
: negotiate(getMacAlgorithmsForCipher(this.negotiatedCipherC2S), serverKex.macC2S, 'C2S MAC');
this.negotiatedMacS2C = getCipherSpec(this.negotiatedCipherS2C).aead
? 'none'
: negotiate(getMacAlgorithmsForCipher(this.negotiatedCipherS2C), serverKex.macS2C, 'S2C MAC');
this.sendDebug(`Negotiated KEX: ${this.negotiatedKexAlgorithm}, C2S: ${this.negotiatedCipherC2S}/${this.negotiatedMacC2S}, S2C: ${this.negotiatedCipherS2C}/${this.negotiatedMacS2C}`);
await this.sendKEXECDHInit();
} catch (e) {
const errMsg = e instanceof Error ? e.message : String(e);
this.sendError('算法协商失败: ' + errMsg);
@@ -386,22 +456,53 @@ export class SSHSession {
ECDHKeyExchange.parseReply(payload);
this.sendDebug(`ECDH_REPLY parsed: hostKey=${hostKey.length}, serverPubKey=${serverRawPublicKey.length}, sig=${signature.length}`);
const sharedSecret = await ECDHKeyExchange.computeSharedSecret(
this.ecdhKeyPair.privateKey,
serverRawPublicKey
);
if (!this.negotiatedKexAlgorithm || !this.kexRawPublicKey) {
throw new Error('KEX reply received before KEX init was sent');
}
let sharedSecret: Uint8Array;
if (isCurve25519KEXAlgorithm(this.negotiatedKexAlgorithm)) {
if (!this.curve25519KeyPair) {
throw new Error('Curve25519 key pair not initialized');
}
sharedSecret = await Curve25519KeyExchange.computeSharedSecret(
this.curve25519KeyPair.privateKey,
serverRawPublicKey
);
} else if (this.negotiatedKexAlgorithm === KEX_ALGORITHM_ECDH_NISTP256) {
if (!this.ecdhKeyPair) {
throw new Error('ECDH key pair not initialized');
}
sharedSecret = await ECDHKeyExchange.computeSharedSecret(
this.ecdhKeyPair.privateKey,
serverRawPublicKey
);
} else {
throw new Error(`Unsupported KEX algorithm: ${this.negotiatedKexAlgorithm}`);
}
this.sendDebug(`Shared secret: ${sharedSecret.length} bytes`);
const H = await ECDHKeyExchange.computeExchangeHash(
this.transport.getLocalVersion(),
this.transport.getRemoteVersion(),
this.kexInitLocal!,
this.kexInitRemote!,
hostKey,
this.ecdhRawPublicKey,
serverRawPublicKey,
sharedSecret
);
const H = isCurve25519KEXAlgorithm(this.negotiatedKexAlgorithm)
? await Curve25519KeyExchange.computeExchangeHash(
this.transport.getLocalVersion(),
this.transport.getRemoteVersion(),
this.kexInitLocal!,
this.kexInitRemote!,
hostKey,
this.kexRawPublicKey,
serverRawPublicKey,
sharedSecret
)
: await ECDHKeyExchange.computeExchangeHash(
this.transport.getLocalVersion(),
this.transport.getRemoteVersion(),
this.kexInitLocal!,
this.kexInitRemote!,
hostKey,
this.kexRawPublicKey,
serverRawPublicKey,
sharedSecret
);
const hHex = Array.from(H).map(b => b.toString(16).padStart(2, '0')).join('');
this.sendDebug(`Exchange hash H=${hHex}`);
@@ -449,7 +550,20 @@ export class SSHSession {
this.sendDebug('Session ID set');
}
this.derivedKeys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!);
const cipherC2S = getCipherSpec(this.negotiatedCipherC2S);
const cipherS2C = getCipherSpec(this.negotiatedCipherS2C);
const macC2S = getMacSpec(this.negotiatedMacC2S);
const macS2C = getMacSpec(this.negotiatedMacS2C);
this.derivedKeys = await KeyDerivation.deriveKeys(
sharedSecret,
H,
this.sessionID!,
cipherC2S.ivLength,
cipherS2C.ivLength,
macC2S.keyLength,
macS2C.keyLength
);
this.sendDebug('Keys derived, waiting for NEWKEYS');
}
@@ -622,29 +736,36 @@ export class SSHSession {
private async enableEncryption(): Promise<void> {
const keys = this.derivedKeys!;
let encKeyC2S = keys.encKeyClientToServer;
let encKeyS2C = keys.encKeyServerToClient;
if (this.negotiatedCipherC2S === 'aes128-gcm@openssh.com') {
encKeyC2S = encKeyC2S.slice(0, 16);
}
if (this.negotiatedCipherS2C === 'aes128-gcm@openssh.com') {
encKeyS2C = encKeyS2C.slice(0, 16);
}
const cipherC2S = getCipherSpec(this.negotiatedCipherC2S);
const cipherS2C = getCipherSpec(this.negotiatedCipherS2C);
const encKeyC2S = keys.encKeyClientToServer.slice(0, cipherC2S.keyLength);
const encKeyS2C = keys.encKeyServerToClient.slice(0, cipherS2C.keyLength);
this.sendDebug('Initializing ciphers');
this.encryptCipher = new SSHAESGCMCipher(
encKeyC2S,
keys.ivClientToServer
);
if (cipherC2S.mode === 'gcm') {
this.encryptCipher = new SSHAESGCMCipher(encKeyC2S, keys.ivClientToServer);
this.encryptMac = null;
} else {
this.encryptCipher = new SSHAESCTRCipher(encKeyC2S, keys.ivClientToServer);
this.encryptMac = this.negotiatedMacC2S === 'none'
? null
: new SSHHMAC(this.negotiatedMacC2S, keys.integrityKeyC2S);
}
await this.encryptCipher.init();
if (this.encryptMac) await this.encryptMac.init();
this.decryptCipher = new SSHAESGCMCipher(
encKeyS2C,
keys.ivServerToClient
);
if (cipherS2C.mode === 'gcm') {
this.decryptCipher = new SSHAESGCMCipher(encKeyS2C, keys.ivServerToClient);
this.decryptMac = null;
} else {
this.decryptCipher = new SSHAESCTRCipher(encKeyS2C, keys.ivServerToClient);
this.decryptMac = this.negotiatedMacS2C === 'none'
? null
: new SSHHMAC(this.negotiatedMacS2C, keys.integrityKeyS2C);
}
await this.decryptCipher.init();
if (this.decryptMac) await this.decryptMac.init();
this.sendDebug('Ciphers initialized');
}
@@ -660,15 +781,7 @@ export class SSHSession {
console.log('[AUTH] SERVICE_REQUEST payload len=' + serviceRequest.length + ', seqNum=' + this.seqNumSend);
console.log('[AUTH] encryptCipher exists=' + !!this.encryptCipher);
const packet = await SSHPacketBuilder.build(
serviceRequest, 16,
(data, seq, aad) => {
console.log('[AUTH] Encrypting: dataLen=' + data.length + ', seq=' + seq + ', aadLen=' + aad?.length);
return this.encryptCipher!.encrypt(data, seq, aad);
},
this.seqNumSend++,
true
);
const packet = await this.buildEncryptedPacket(serviceRequest);
console.log('[AUTH] Encrypted packet len=' + packet.length + ', first16=' + Array.from(packet.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(''));
await this.writeSocket(packet);
console.log('[AUTH] SERVICE_REQUEST sent to socket');
@@ -691,12 +804,7 @@ export class SSHSession {
);
}
const packet = await SSHPacketBuilder.build(
authRequest, 16,
(data, seq, aad) => this.encryptCipher!.encrypt(data, seq, aad),
this.seqNumSend++,
true
);
const packet = await this.buildEncryptedPacket(authRequest);
await this.writeSocket(packet);
}
@@ -745,21 +853,46 @@ export class SSHSession {
case SSH_MSG_CHANNEL_SUCCESS:
if (this.state === 'shell') {
// PTY 请求确认,发送 shell 请求
const shellReq = this.channel.buildShellRequest();
await this.sendEncrypted(shellReq);
this.state = 'shell-requested';
// 设置超时兜底:如果服务器不发送 shell 确认,3秒后自动进入 ready
this.shellReadyTimeout = setTimeout(() => {
if (this.state === 'shell-requested') {
this.state = 'ready';
this.sendStatus('Shell 已就绪');
}
}, 3000);
} else if (this.state === 'shell-requested') {
// Shell 请求确认,进入 ready 状态
if (this.shellReadyTimeout) {
clearTimeout(this.shellReadyTimeout);
this.shellReadyTimeout = null;
}
this.state = 'ready';
this.sendStatus('Shell 已就绪');
}
break;
case SSH_MSG_CHANNEL_FAILURE:
if (this.state === 'shell') {
if (this.state === 'shell' || this.state === 'shell-requested') {
this.sendError('PTY 或 Shell 请求被拒绝');
this.close();
}
break;
case SSH_MSG_CHANNEL_DATA: {
// 某些 SSH 服务器(如 Dropbear)不会为 shell 请求发送 CHANNEL_SUCCESS,
// 而是直接发送 shell 输出。收到 CHANNEL_DATA 说明 shell 已就绪。
if (this.state === 'shell-requested') {
if (this.shellReadyTimeout) {
clearTimeout(this.shellReadyTimeout);
this.shellReadyTimeout = null;
}
this.state = 'ready';
this.sendStatus('Shell 已就绪');
}
const outputData = this.channel.handleChannelData(payload);
this.ws.send(outputData.slice().buffer as ArrayBuffer);
const adjustMsg = this.channel.buildWindowAdjust(outputData.length);
@@ -823,16 +956,7 @@ export class SSHSession {
private async sendEncrypted(payload: Uint8Array): Promise<void> {
const operation = this.sendMutex.then(async () => {
if (!this.encryptCipher) {
throw new Error('Encryption not initialized');
}
const encrypted = await SSHPacketBuilder.build(
payload, 16,
(data, seq, aad) => this.encryptCipher!.encrypt(data, seq, aad),
this.seqNumSend++,
true
);
const encrypted = await this.buildEncryptedPacket(payload);
await this.writeSocket(encrypted);
});
@@ -864,6 +988,10 @@ export class SSHSession {
clearInterval(this.keepaliveInterval);
this.keepaliveInterval = null;
}
if (this.shellReadyTimeout) {
clearTimeout(this.shellReadyTimeout);
this.shellReadyTimeout = null;
}
try { this.socket.close(); } catch {}
try { this.ws.close(normal ? 1000 : 1011); } catch {}
}
+9 -6
View File
@@ -3,12 +3,15 @@ main = "src/worker/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
# no_bundle = true
workers_dev = false
workers_dev = true
# 构建监视路径:仅源代码变动时触发重新构建
[build]
watch_dir = "."
# 下述注释的配置推荐采用环境变量的形式在worker页面直接手动添加
# GitHub OAuth(可选,不配置则登录功能自动隐藏)
# 创建 GitHub OAuth App 后,取消下方注释并填入你的 Client ID
# Secret 通过 CLI 设置:npx wrangler secret set GITHUB_CLIENT_SECRET
# npx wrangler secret set SESSION_SECRET
# [vars]
# GITHUB_CLIENT_ID = "your-github-oauth-client-id"
# BASE_URL = "https://your-domain.com"
@@ -19,9 +22,9 @@ workers_dev = false
# TURNSTILE_SECRET = "your-turnstile-secret-key"
# TURNSTILE_SITEKEY = "your-turnstile-site-key"
[[routes]]
pattern = "ssh.newbietan.cn"
custom_domain = true
#[[routes]]
#pattern = "自定义域名"
#custom_domain = true
[[durable_objects.bindings]]
name = "SSH_SESSION"