webssh 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # webssh
2
+
3
+ Web-based SSH terminal server and client with xterm.js integration. Access remote shells through your browser or CLI.
4
+
5
+ ## Features
6
+
7
+ - 🌐 **Browser-based terminal** – Full xterm.js integration with syntax highlighting and themes
8
+ - 🔌 **WebSocket transport** – Real-time bidirectional communication
9
+ - 🖥️ **CLI client** – Connect from command line with interactive prompt
10
+ - 📏 **Auto-resize** – Terminal dimensions sync between client and server
11
+ - 🔒 **Session management** – Unique session IDs with automatic cleanup
12
+ - ⚡ **Lightweight** – Minimal dependencies, fast startup
13
+ - 🚀 **Modern ES modules** – Native ESM with async/await support
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install webssh
19
+
20
+
21
+ #server mode
22
+
23
+ import webssh from 'webssh';
24
+
25
+ // Using async IIFE for top-level await
26
+ const { serve } = webssh;
27
+
28
+ const server = serve({
29
+ port: 3000,
30
+ host: 'localhost'
31
+ });
32
+
33
+ server.start(() => {
34
+ console.log('🚀 WebSSH server running at http://localhost:3000');
35
+ });
36
+
37
+
38
+ #client mode
39
+
40
+ import webssh from 'webssh';
41
+
42
+ const { connect } = webssh;
43
+
44
+ // Connect with defaults (localhost:3000)
45
+ await connect();
46
+
47
+ // Or specify custom host/port
48
+ await connect('remote.server.com', 8080);
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "webssh",
3
+ "version": "1.0.0",
4
+ "description": "Web-based SSH terminal server and client with xterm.js integration",
5
+ "type": "module",
6
+ "main": "webssh.js",
7
+ "exports": {
8
+ ".": "./webssh.js"
9
+ },
10
+ "files": [
11
+ "webssh.js",
12
+ "terminals.js",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "node webssh.js",
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "keywords": [
21
+ "ssh",
22
+ "terminal",
23
+ "web-terminal",
24
+ "xterm",
25
+ "websocket",
26
+ "remote-shell",
27
+ "browser-terminal",
28
+ "pty",
29
+ "web-ssh"
30
+ ],
31
+ "author": "littlejustnode",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/littlejustnode/webssh.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/littlejustnode/webssh/issues"
39
+ },
40
+ "homepage": "https://github.com/littlejustnode/webssh#readme",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "dependencies": {
45
+ "express": "^4.18.2",
46
+ "just-prompt": "^1.0.0",
47
+ "ws": "^8.14.2"
48
+ }
49
+ }
package/terminals.js ADDED
@@ -0,0 +1,63 @@
1
+ import * as os from 'node:os';
2
+ import * as pty from 'node-pty';
3
+
4
+ class TerminalManager {
5
+ constructor() {
6
+ this.sessions = new Map();
7
+ }
8
+
9
+ /**
10
+ * Creates a new terminal instance and stores it in the Map.
11
+ */
12
+ createSession(name, options = {}) {
13
+ const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
14
+
15
+ const ptyProcess = pty.spawn(shell, [], {
16
+ name: 'xterm-color',
17
+ cols: options.cols || process.stdout.columns || 80,
18
+ rows: options.rows || process.stdout.rows || 24,
19
+ cwd: options.cwd || process.env.HOME || process.cwd(),
20
+ env: options.env || process.env
21
+ });
22
+
23
+ // Define the session object with the methods you requested
24
+ const session = {
25
+ id: name,
26
+ process: ptyProcess,
27
+
28
+ // Listen for output
29
+ onOut: (callback) => {
30
+ ptyProcess.onData((data) => callback(data));
31
+ },
32
+
33
+ // Send commands (automatically adds newline if missing)
34
+ send: (data) => {
35
+ const command = data.endsWith('\r') || data.endsWith('\n') ? data : `${data}\r`;
36
+ ptyProcess.write(command);
37
+ },
38
+
39
+ // Kill this specific session and remove from memory
40
+ exit: () => {
41
+ ptyProcess.kill();
42
+ this.sessions.delete(name);
43
+ }
44
+ };
45
+
46
+ // Store it
47
+ this.sessions.set(name, session);
48
+ return session;
49
+ }
50
+
51
+ /**
52
+ * Terminates every session stored in memory.
53
+ */
54
+ close() {
55
+ this.sessions.forEach((session) => {
56
+ session.exit();
57
+ });
58
+ this.sessions.clear();
59
+ }
60
+ }
61
+
62
+ // Export a single instance to be used as 'terminals'
63
+ export const terminals = new TerminalManager();
package/webssh.js ADDED
@@ -0,0 +1,276 @@
1
+
2
+ import express from 'express';
3
+ import { createServer } from 'node:http';
4
+ import { WebSocketServer } from 'ws';
5
+ import { terminals } from './terminals.js';
6
+ import jp from 'just-prompt';
7
+
8
+
9
+ // ────────────────────────────────────────────────
10
+ // SERVER BUILDER – returns chainable object
11
+ // ────────────────────────────────────────────────
12
+
13
+ function serve(options = {}) {
14
+ const config = {
15
+ port: 3000,
16
+ host: 'localhost',
17
+ path: '/terminal',
18
+ cors: false,
19
+ sessionPrefix: 'ws-',
20
+ ...options,
21
+ };
22
+
23
+ const app = express();
24
+ const httpServer = createServer(app);
25
+ const wss = new WebSocketServer({ server: httpServer, path: config.path });
26
+
27
+ if (config.cors) {
28
+ app.use((req, res, next) => {
29
+ res.setHeader('Access-Control-Allow-Origin', '*');
30
+ res.setHeader('Access-Control-Allow-Headers', '*');
31
+ next();
32
+ });
33
+ }
34
+
35
+ // Serve browser terminal
36
+ app.get('/', (req, res) => {
37
+ res.type('html').send(getBrowserHTML(config.path));
38
+ });
39
+
40
+ const clientToSessionId = new WeakMap();
41
+
42
+ wss.on('connection', (ws) => {
43
+ let session = null;
44
+ let sessionId = null;
45
+
46
+ ws.on('message', (raw) => {
47
+ try {
48
+ const data = raw.toString();
49
+
50
+ if (data.startsWith('{')) {
51
+ try {
52
+ const msg = JSON.parse(data);
53
+ if (msg.type === 'resize' && session) {
54
+ session.process.resize(msg.cols, msg.rows);
55
+ }
56
+ } catch {}
57
+ return;
58
+ }
59
+
60
+ if (!session) {
61
+ sessionId = `${config.sessionPrefix}${Date.now()}-${Math.random().toString(36).slice(2,8)}`;
62
+ session = terminals.createSession(sessionId, {
63
+ cols: 80,
64
+ rows: 24,
65
+ });
66
+
67
+ clientToSessionId.set(ws, sessionId);
68
+
69
+ session.onOut((chunk) => {
70
+ if (ws.readyState === WebSocket.OPEN) ws.send(chunk);
71
+ });
72
+
73
+ ws.send('\r\nWeb terminal ready.\r\n');
74
+ }
75
+
76
+ session.send(data);
77
+ } catch (err) {
78
+ console.error('WS message error:', err);
79
+ }
80
+ });
81
+
82
+ ws.on('close', () => {
83
+ if (sessionId) {
84
+ const sess = terminals.sessions.get(sessionId);
85
+ if (sess) {
86
+ sess.exit();
87
+ terminals.sessions.delete(sessionId);
88
+ }
89
+ clientToSessionId.delete(ws);
90
+ }
91
+ });
92
+
93
+ ws.on('error', (err) => console.error('WS error:', err));
94
+ });
95
+
96
+ // Chainable start method
97
+ function start(callback) {
98
+ httpServer.listen(config.port, config.host, () => {
99
+ const displayHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
100
+ console.log(`Web terminal server listening at http://${displayHost}:${config.port}`);
101
+ console.log(` → Browser: http://${displayHost}:${config.port}/`);
102
+ console.log(` → WS: ws://${displayHost}:${config.port}${config.path}`);
103
+ if (callback) callback();
104
+ });
105
+ }
106
+
107
+ // Also expose close (optional)
108
+ function close() {
109
+ terminals.close();
110
+ httpServer.close();
111
+ wss.close();
112
+ }
113
+
114
+ return {
115
+ start,
116
+ close,
117
+ // for advanced usage if someone needs direct access
118
+ app,
119
+ httpServer,
120
+ wss,
121
+ };
122
+ }
123
+
124
+ // ────────────────────────────────────────────────
125
+ // CLIENT – single host string (unchanged)
126
+ // ────────────────────────────────────────────────
127
+
128
+ async function connect(host = 'localhost', port = 3000) {
129
+ const path = '/terminal';
130
+ const url = `ws://${host}:${port}${path}`;
131
+
132
+ const promptMessage = '→ ';
133
+ const exitCommand = 'exit';
134
+
135
+ const WebSocket = (await import('ws')).default;
136
+
137
+ return new Promise((resolve, reject) => {
138
+ console.log(`Connecting to ${url} ...`);
139
+
140
+ const socket = new WebSocket(url);
141
+
142
+ socket.on('open', () => {
143
+ console.clear();
144
+ console.log(`Connected to ${host}`);
145
+ console.log(`(type "${exitCommand}" to quit)\n`);
146
+
147
+ const prompt = jp.app(promptMessage, exitCommand, async (input) => {
148
+ if (socket.readyState === WebSocket.OPEN) {
149
+ socket.send(input + '\r');
150
+ }
151
+ });
152
+
153
+ prompt.onExit(() => {
154
+ socket.close();
155
+ resolve();
156
+ });
157
+
158
+ socket.on('message', (data) => {
159
+ process.stdout.write(data.toString());
160
+ });
161
+
162
+ socket.on('close', () => {
163
+ console.log(`\nDisconnected from ${host}.`);
164
+ prompt.stop();
165
+ resolve();
166
+ });
167
+
168
+ socket.on('error', (err) => {
169
+ console.error(`Connection error: ${err.message}`);
170
+ prompt.stop();
171
+ reject(err);
172
+ });
173
+
174
+ prompt.start().catch(reject);
175
+ });
176
+
177
+ socket.on('error', (err) => {
178
+ console.error(`Cannot connect to ${host}: ${err.message}`);
179
+ reject(err);
180
+ });
181
+ });
182
+ }
183
+
184
+ // ────────────────────────────────────────────────
185
+ // Browser HTML (unchanged)
186
+ // ────────────────────────────────────────────────
187
+
188
+ function getBrowserHTML(wsPath = '/terminal') {
189
+ return `
190
+ <!DOCTYPE html>
191
+ <html lang="en">
192
+ <head>
193
+ <meta charset="UTF-8"/>
194
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
195
+ <title>Web Terminal</title>
196
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"/>
197
+ <style>
198
+ body { margin:0; background:#000; height:100vh; overflow:hidden; }
199
+ #terminal { width:100%; height:100%; }
200
+ </style>
201
+ </head>
202
+ <body>
203
+ <div id="terminal"></div>
204
+
205
+ <script type="module">
206
+ import { Terminal } from 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm';
207
+ import { FitAddon } from 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/+esm';
208
+
209
+ const term = new Terminal({
210
+ cursorBlink: true,
211
+ theme: { background: '#1e1e1e', foreground: '#e0e0e0' }
212
+ });
213
+
214
+ const fitAddon = new FitAddon();
215
+ term.loadAddon(fitAddon);
216
+ term.open(document.getElementById('terminal'));
217
+ fitAddon.fit();
218
+
219
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
220
+ const ws = new WebSocket(protocol + '//' + location.host + '${wsPath}');
221
+
222
+ let inputBuffer = "";
223
+
224
+ term.onData(data => {
225
+ switch (data) {
226
+ case '\\r':
227
+ if (ws.readyState === WebSocket.OPEN) ws.send(inputBuffer + '\\r');
228
+ inputBuffer = "";
229
+ term.write('\\r\\n');
230
+ break;
231
+ case '\\u007F':
232
+ if (inputBuffer.length > 0) {
233
+ inputBuffer = inputBuffer.slice(0, -1);
234
+ term.write('\\b \\b');
235
+ }
236
+ break;
237
+ default:
238
+ if (data >= ' ' && data <= '~') {
239
+ inputBuffer += data;
240
+ term.write(data);
241
+ }
242
+ }
243
+ });
244
+
245
+ ws.onmessage = e => term.write(e.data);
246
+
247
+ function sendResize() {
248
+ fitAddon.fit();
249
+ if (ws.readyState === WebSocket.OPEN) {
250
+ ws.send(JSON.stringify({
251
+ type: 'resize',
252
+ cols: term.cols,
253
+ rows: term.rows
254
+ }));
255
+ }
256
+ }
257
+
258
+ window.addEventListener('resize', sendResize);
259
+ ws.onopen = () => {
260
+ sendResize();
261
+ term.writeln('Connecting...');
262
+ term.focus();
263
+ };
264
+ </script>
265
+ </body>
266
+ </html>`;
267
+ }
268
+
269
+ // ────────────────────────────────────────────────
270
+ // Public API
271
+ // ────────────────────────────────────────────────
272
+
273
+ export default {
274
+ serve, // ← new main server entry point
275
+ connect, // simple client
276
+ };