green-screen-proxy 0.3.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.
Files changed (67) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +32 -0
  3. package/dist/hp6530/connection.d.ts +51 -0
  4. package/dist/hp6530/connection.js +258 -0
  5. package/dist/hp6530/constants.d.ts +64 -0
  6. package/dist/hp6530/constants.js +135 -0
  7. package/dist/hp6530/encoder.d.ts +37 -0
  8. package/dist/hp6530/encoder.js +89 -0
  9. package/dist/hp6530/parser.d.ts +45 -0
  10. package/dist/hp6530/parser.js +255 -0
  11. package/dist/hp6530/screen.d.ts +104 -0
  12. package/dist/hp6530/screen.js +252 -0
  13. package/dist/mock/mock-routes.d.ts +2 -0
  14. package/dist/mock/mock-routes.js +231 -0
  15. package/dist/protocols/hp6530-handler.d.ts +29 -0
  16. package/dist/protocols/hp6530-handler.js +64 -0
  17. package/dist/protocols/index.d.ts +11 -0
  18. package/dist/protocols/index.js +27 -0
  19. package/dist/protocols/tn3270-handler.d.ts +26 -0
  20. package/dist/protocols/tn3270-handler.js +61 -0
  21. package/dist/protocols/tn5250-handler.d.ts +26 -0
  22. package/dist/protocols/tn5250-handler.js +62 -0
  23. package/dist/protocols/types.d.ts +59 -0
  24. package/dist/protocols/types.js +7 -0
  25. package/dist/protocols/vt-handler.d.ts +30 -0
  26. package/dist/protocols/vt-handler.js +67 -0
  27. package/dist/routes.d.ts +2 -0
  28. package/dist/routes.js +141 -0
  29. package/dist/server.d.ts +1 -0
  30. package/dist/server.js +34 -0
  31. package/dist/session.d.ts +32 -0
  32. package/dist/session.js +88 -0
  33. package/dist/tn3270/connection.d.ts +31 -0
  34. package/dist/tn3270/connection.js +266 -0
  35. package/dist/tn3270/constants.d.ts +262 -0
  36. package/dist/tn3270/constants.js +261 -0
  37. package/dist/tn3270/encoder.d.ts +24 -0
  38. package/dist/tn3270/encoder.js +97 -0
  39. package/dist/tn3270/parser.d.ts +22 -0
  40. package/dist/tn3270/parser.js +284 -0
  41. package/dist/tn3270/screen.d.ts +89 -0
  42. package/dist/tn3270/screen.js +207 -0
  43. package/dist/tn5250/connection.d.ts +41 -0
  44. package/dist/tn5250/connection.js +254 -0
  45. package/dist/tn5250/constants.d.ts +128 -0
  46. package/dist/tn5250/constants.js +156 -0
  47. package/dist/tn5250/ebcdic.d.ts +10 -0
  48. package/dist/tn5250/ebcdic.js +89 -0
  49. package/dist/tn5250/encoder.d.ts +30 -0
  50. package/dist/tn5250/encoder.js +121 -0
  51. package/dist/tn5250/parser.d.ts +33 -0
  52. package/dist/tn5250/parser.js +412 -0
  53. package/dist/tn5250/screen.d.ts +80 -0
  54. package/dist/tn5250/screen.js +155 -0
  55. package/dist/vt/connection.d.ts +45 -0
  56. package/dist/vt/connection.js +229 -0
  57. package/dist/vt/constants.d.ts +97 -0
  58. package/dist/vt/constants.js +163 -0
  59. package/dist/vt/encoder.d.ts +30 -0
  60. package/dist/vt/encoder.js +55 -0
  61. package/dist/vt/parser.d.ts +36 -0
  62. package/dist/vt/parser.js +534 -0
  63. package/dist/vt/screen.d.ts +101 -0
  64. package/dist/vt/screen.js +424 -0
  65. package/dist/websocket.d.ts +6 -0
  66. package/dist/websocket.js +50 -0
  67. package/package.json +57 -0
@@ -0,0 +1,101 @@
1
+ import type { ScreenData } from '../protocols/types.js';
2
+ /**
3
+ * Character attributes for a single cell in the VT screen buffer.
4
+ */
5
+ export interface CellAttrs {
6
+ bold: boolean;
7
+ dim: boolean;
8
+ italic: boolean;
9
+ underline: boolean;
10
+ blink: boolean;
11
+ reverse: boolean;
12
+ hidden: boolean;
13
+ strikethrough: boolean;
14
+ fg: number;
15
+ bg: number;
16
+ }
17
+ /** Create a default (reset) attribute set */
18
+ export declare function defaultAttrs(): CellAttrs;
19
+ /** Synthetic field detected from VT screen content */
20
+ export interface SyntheticField {
21
+ row: number;
22
+ col: number;
23
+ length: number;
24
+ is_input: boolean;
25
+ is_protected: boolean;
26
+ is_highlighted?: boolean;
27
+ is_reverse?: boolean;
28
+ }
29
+ /**
30
+ * VT terminal screen buffer.
31
+ *
32
+ * A simple character grid with per-cell attributes. No native field concept —
33
+ * fields are detected synthetically by scanning for common patterns like
34
+ * "Label: ____" or prompt strings.
35
+ */
36
+ export declare class VTScreenBuffer {
37
+ rows: number;
38
+ cols: number;
39
+ /** Character grid */
40
+ buffer: string[];
41
+ /** Per-cell attributes */
42
+ attrs: CellAttrs[];
43
+ cursorRow: number;
44
+ cursorCol: number;
45
+ /** Scroll region (top and bottom row, inclusive, 0-indexed) */
46
+ scrollTop: number;
47
+ scrollBottom: number;
48
+ /** Saved cursor state (DECSC / DECRC) */
49
+ private savedCursor;
50
+ /** Current drawing attributes (applied to new characters) */
51
+ currentAttrs: CellAttrs;
52
+ /** Line-wrapping mode (DECAWM) */
53
+ autoWrap: boolean;
54
+ /** Origin mode (DECOM) — cursor addressing relative to scroll region */
55
+ originMode: boolean;
56
+ /** Pending wrap — the next printable char wraps to next line */
57
+ pendingWrap: boolean;
58
+ constructor(rows?: number, cols?: number);
59
+ get size(): number;
60
+ offset(row: number, col: number): number;
61
+ /** Write a character at the current cursor position and advance cursor */
62
+ writeChar(ch: string): void;
63
+ /** Set a character at a specific position without moving cursor */
64
+ setChar(row: number, col: number, ch: string, cellAttrs?: CellAttrs): void;
65
+ getChar(row: number, col: number): string;
66
+ setCursor(row: number, col: number): void;
67
+ private clampRow;
68
+ private clampCol;
69
+ lineFeed(): void;
70
+ reverseLineFeed(): void;
71
+ /** Scroll the scroll region up by n lines (new blank lines at bottom) */
72
+ scrollUp(n: number): void;
73
+ /** Scroll the scroll region down by n lines (new blank lines at top) */
74
+ scrollDown(n: number): void;
75
+ /** ED — Erase in Display */
76
+ eraseInDisplay(mode: number): void;
77
+ /** EL — Erase in Line */
78
+ eraseInLine(mode: number): void;
79
+ /** ECH — Erase Characters */
80
+ eraseCharacters(n: number): void;
81
+ private eraseRange;
82
+ /** IL — Insert n blank lines at cursor row */
83
+ insertLines(n: number): void;
84
+ /** DL — Delete n lines at cursor row */
85
+ deleteLines(n: number): void;
86
+ /** ICH — Insert n blank characters at cursor */
87
+ insertCharacters(n: number): void;
88
+ /** DCH — Delete n characters at cursor */
89
+ deleteCharacters(n: number): void;
90
+ saveCursor(): void;
91
+ restoreCursor(): void;
92
+ reset(): void;
93
+ tabForward(): void;
94
+ /**
95
+ * Detect synthetic fields from the screen content. Best-effort heuristic —
96
+ * VT terminals have no native field concept.
97
+ */
98
+ detectFields(): SyntheticField[];
99
+ /** Convert to the protocol-agnostic ScreenData format */
100
+ toScreenData(): ScreenData;
101
+ }
@@ -0,0 +1,424 @@
1
+ import { createHash } from 'crypto';
2
+ import { DEFAULT_ROWS, DEFAULT_COLS } from './constants.js';
3
+ /** Create a default (reset) attribute set */
4
+ export function defaultAttrs() {
5
+ return {
6
+ bold: false,
7
+ dim: false,
8
+ italic: false,
9
+ underline: false,
10
+ blink: false,
11
+ reverse: false,
12
+ hidden: false,
13
+ strikethrough: false,
14
+ fg: 8, // default
15
+ bg: 8, // default
16
+ };
17
+ }
18
+ /**
19
+ * VT terminal screen buffer.
20
+ *
21
+ * A simple character grid with per-cell attributes. No native field concept —
22
+ * fields are detected synthetically by scanning for common patterns like
23
+ * "Label: ____" or prompt strings.
24
+ */
25
+ export class VTScreenBuffer {
26
+ rows;
27
+ cols;
28
+ /** Character grid */
29
+ buffer;
30
+ /** Per-cell attributes */
31
+ attrs;
32
+ cursorRow = 0;
33
+ cursorCol = 0;
34
+ /** Scroll region (top and bottom row, inclusive, 0-indexed) */
35
+ scrollTop = 0;
36
+ scrollBottom;
37
+ /** Saved cursor state (DECSC / DECRC) */
38
+ savedCursor = null;
39
+ /** Current drawing attributes (applied to new characters) */
40
+ currentAttrs;
41
+ /** Line-wrapping mode (DECAWM) */
42
+ autoWrap = true;
43
+ /** Origin mode (DECOM) — cursor addressing relative to scroll region */
44
+ originMode = false;
45
+ /** Pending wrap — the next printable char wraps to next line */
46
+ pendingWrap = false;
47
+ constructor(rows = DEFAULT_ROWS, cols = DEFAULT_COLS) {
48
+ this.rows = rows;
49
+ this.cols = cols;
50
+ this.scrollBottom = rows - 1;
51
+ const size = rows * cols;
52
+ this.buffer = new Array(size).fill(' ');
53
+ this.attrs = new Array(size);
54
+ for (let i = 0; i < size; i++) {
55
+ this.attrs[i] = defaultAttrs();
56
+ }
57
+ this.currentAttrs = defaultAttrs();
58
+ }
59
+ get size() {
60
+ return this.rows * this.cols;
61
+ }
62
+ offset(row, col) {
63
+ return row * this.cols + col;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Character operations
67
+ // ---------------------------------------------------------------------------
68
+ /** Write a character at the current cursor position and advance cursor */
69
+ writeChar(ch) {
70
+ if (this.pendingWrap) {
71
+ if (this.autoWrap) {
72
+ this.cursorCol = 0;
73
+ this.lineFeed();
74
+ }
75
+ this.pendingWrap = false;
76
+ }
77
+ const off = this.offset(this.cursorRow, this.cursorCol);
78
+ if (off >= 0 && off < this.size) {
79
+ this.buffer[off] = ch;
80
+ this.attrs[off] = { ...this.currentAttrs };
81
+ }
82
+ if (this.cursorCol < this.cols - 1) {
83
+ this.cursorCol++;
84
+ }
85
+ else {
86
+ // At last column — set pending wrap flag
87
+ this.pendingWrap = true;
88
+ }
89
+ }
90
+ /** Set a character at a specific position without moving cursor */
91
+ setChar(row, col, ch, cellAttrs) {
92
+ const off = this.offset(row, col);
93
+ if (off >= 0 && off < this.size) {
94
+ this.buffer[off] = ch;
95
+ if (cellAttrs)
96
+ this.attrs[off] = { ...cellAttrs };
97
+ }
98
+ }
99
+ getChar(row, col) {
100
+ const off = this.offset(row, col);
101
+ return off >= 0 && off < this.size ? this.buffer[off] : ' ';
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Cursor movement
105
+ // ---------------------------------------------------------------------------
106
+ setCursor(row, col) {
107
+ this.cursorRow = this.clampRow(row);
108
+ this.cursorCol = this.clampCol(col);
109
+ this.pendingWrap = false;
110
+ }
111
+ clampRow(row) {
112
+ return Math.max(0, Math.min(this.rows - 1, row));
113
+ }
114
+ clampCol(col) {
115
+ return Math.max(0, Math.min(this.cols - 1, col));
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Line feed / scrolling
119
+ // ---------------------------------------------------------------------------
120
+ lineFeed() {
121
+ if (this.cursorRow === this.scrollBottom) {
122
+ this.scrollUp(1);
123
+ }
124
+ else if (this.cursorRow < this.rows - 1) {
125
+ this.cursorRow++;
126
+ }
127
+ }
128
+ reverseLineFeed() {
129
+ if (this.cursorRow === this.scrollTop) {
130
+ this.scrollDown(1);
131
+ }
132
+ else if (this.cursorRow > 0) {
133
+ this.cursorRow--;
134
+ }
135
+ }
136
+ /** Scroll the scroll region up by n lines (new blank lines at bottom) */
137
+ scrollUp(n) {
138
+ for (let i = 0; i < n; i++) {
139
+ for (let r = this.scrollTop; r < this.scrollBottom; r++) {
140
+ const dstOff = r * this.cols;
141
+ const srcOff = (r + 1) * this.cols;
142
+ for (let c = 0; c < this.cols; c++) {
143
+ this.buffer[dstOff + c] = this.buffer[srcOff + c];
144
+ this.attrs[dstOff + c] = this.attrs[srcOff + c];
145
+ }
146
+ }
147
+ const bottomOff = this.scrollBottom * this.cols;
148
+ for (let c = 0; c < this.cols; c++) {
149
+ this.buffer[bottomOff + c] = ' ';
150
+ this.attrs[bottomOff + c] = defaultAttrs();
151
+ }
152
+ }
153
+ }
154
+ /** Scroll the scroll region down by n lines (new blank lines at top) */
155
+ scrollDown(n) {
156
+ for (let i = 0; i < n; i++) {
157
+ for (let r = this.scrollBottom; r > this.scrollTop; r--) {
158
+ const dstOff = r * this.cols;
159
+ const srcOff = (r - 1) * this.cols;
160
+ for (let c = 0; c < this.cols; c++) {
161
+ this.buffer[dstOff + c] = this.buffer[srcOff + c];
162
+ this.attrs[dstOff + c] = this.attrs[srcOff + c];
163
+ }
164
+ }
165
+ const topOff = this.scrollTop * this.cols;
166
+ for (let c = 0; c < this.cols; c++) {
167
+ this.buffer[topOff + c] = ' ';
168
+ this.attrs[topOff + c] = defaultAttrs();
169
+ }
170
+ }
171
+ }
172
+ // ---------------------------------------------------------------------------
173
+ // Erase operations
174
+ // ---------------------------------------------------------------------------
175
+ /** ED — Erase in Display */
176
+ eraseInDisplay(mode) {
177
+ switch (mode) {
178
+ case 0:
179
+ this.eraseRange(this.offset(this.cursorRow, this.cursorCol), this.size);
180
+ break;
181
+ case 1:
182
+ this.eraseRange(0, this.offset(this.cursorRow, this.cursorCol) + 1);
183
+ break;
184
+ case 2:
185
+ case 3:
186
+ this.eraseRange(0, this.size);
187
+ break;
188
+ }
189
+ }
190
+ /** EL — Erase in Line */
191
+ eraseInLine(mode) {
192
+ const rowStart = this.cursorRow * this.cols;
193
+ switch (mode) {
194
+ case 0:
195
+ this.eraseRange(this.offset(this.cursorRow, this.cursorCol), rowStart + this.cols);
196
+ break;
197
+ case 1:
198
+ this.eraseRange(rowStart, this.offset(this.cursorRow, this.cursorCol) + 1);
199
+ break;
200
+ case 2:
201
+ this.eraseRange(rowStart, rowStart + this.cols);
202
+ break;
203
+ }
204
+ }
205
+ /** ECH — Erase Characters */
206
+ eraseCharacters(n) {
207
+ const start = this.offset(this.cursorRow, this.cursorCol);
208
+ const end = Math.min(start + n, this.cursorRow * this.cols + this.cols);
209
+ this.eraseRange(start, end);
210
+ }
211
+ eraseRange(start, end) {
212
+ for (let i = start; i < end && i < this.size; i++) {
213
+ this.buffer[i] = ' ';
214
+ this.attrs[i] = defaultAttrs();
215
+ }
216
+ }
217
+ // ---------------------------------------------------------------------------
218
+ // Insert / Delete lines and characters
219
+ // ---------------------------------------------------------------------------
220
+ /** IL — Insert n blank lines at cursor row */
221
+ insertLines(n) {
222
+ if (this.cursorRow < this.scrollTop || this.cursorRow > this.scrollBottom)
223
+ return;
224
+ const oldTop = this.scrollTop;
225
+ this.scrollTop = this.cursorRow;
226
+ this.scrollDown(n);
227
+ this.scrollTop = oldTop;
228
+ }
229
+ /** DL — Delete n lines at cursor row */
230
+ deleteLines(n) {
231
+ if (this.cursorRow < this.scrollTop || this.cursorRow > this.scrollBottom)
232
+ return;
233
+ const oldTop = this.scrollTop;
234
+ this.scrollTop = this.cursorRow;
235
+ this.scrollUp(n);
236
+ this.scrollTop = oldTop;
237
+ }
238
+ /** ICH — Insert n blank characters at cursor */
239
+ insertCharacters(n) {
240
+ const rowOff = this.cursorRow * this.cols;
241
+ const curOff = rowOff + this.cursorCol;
242
+ const endOff = rowOff + this.cols;
243
+ for (let i = endOff - 1; i >= curOff + n; i--) {
244
+ this.buffer[i] = this.buffer[i - n];
245
+ this.attrs[i] = this.attrs[i - n];
246
+ }
247
+ for (let i = curOff; i < curOff + n && i < endOff; i++) {
248
+ this.buffer[i] = ' ';
249
+ this.attrs[i] = defaultAttrs();
250
+ }
251
+ }
252
+ /** DCH — Delete n characters at cursor */
253
+ deleteCharacters(n) {
254
+ const rowOff = this.cursorRow * this.cols;
255
+ const curOff = rowOff + this.cursorCol;
256
+ const endOff = rowOff + this.cols;
257
+ for (let i = curOff; i < endOff - n; i++) {
258
+ this.buffer[i] = this.buffer[i + n];
259
+ this.attrs[i] = this.attrs[i + n];
260
+ }
261
+ for (let i = endOff - n; i < endOff; i++) {
262
+ this.buffer[i] = ' ';
263
+ this.attrs[i] = defaultAttrs();
264
+ }
265
+ }
266
+ // ---------------------------------------------------------------------------
267
+ // Cursor save / restore (DECSC / DECRC)
268
+ // ---------------------------------------------------------------------------
269
+ saveCursor() {
270
+ this.savedCursor = {
271
+ row: this.cursorRow,
272
+ col: this.cursorCol,
273
+ attrs: { ...this.currentAttrs },
274
+ };
275
+ }
276
+ restoreCursor() {
277
+ if (this.savedCursor) {
278
+ this.cursorRow = this.savedCursor.row;
279
+ this.cursorCol = this.savedCursor.col;
280
+ this.currentAttrs = { ...this.savedCursor.attrs };
281
+ this.pendingWrap = false;
282
+ }
283
+ }
284
+ // ---------------------------------------------------------------------------
285
+ // Full reset
286
+ // ---------------------------------------------------------------------------
287
+ reset() {
288
+ this.buffer.fill(' ');
289
+ for (let i = 0; i < this.size; i++) {
290
+ this.attrs[i] = defaultAttrs();
291
+ }
292
+ this.cursorRow = 0;
293
+ this.cursorCol = 0;
294
+ this.scrollTop = 0;
295
+ this.scrollBottom = this.rows - 1;
296
+ this.currentAttrs = defaultAttrs();
297
+ this.autoWrap = true;
298
+ this.originMode = false;
299
+ this.pendingWrap = false;
300
+ this.savedCursor = null;
301
+ }
302
+ // ---------------------------------------------------------------------------
303
+ // Tab stops
304
+ // ---------------------------------------------------------------------------
305
+ tabForward() {
306
+ const nextTab = (Math.floor(this.cursorCol / 8) + 1) * 8;
307
+ this.cursorCol = Math.min(nextTab, this.cols - 1);
308
+ this.pendingWrap = false;
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // Synthetic field detection
312
+ // ---------------------------------------------------------------------------
313
+ /**
314
+ * Detect synthetic fields from the screen content. Best-effort heuristic —
315
+ * VT terminals have no native field concept.
316
+ */
317
+ detectFields() {
318
+ const fields = [];
319
+ for (let r = 0; r < this.rows; r++) {
320
+ const rowStart = r * this.cols;
321
+ const line = this.buffer.slice(rowStart, rowStart + this.cols).join('');
322
+ // Pattern 1: Underscore runs (3+ consecutive underscores)
323
+ const underscoreRe = /_{3,}/g;
324
+ let match;
325
+ while ((match = underscoreRe.exec(line)) !== null) {
326
+ fields.push({
327
+ row: r,
328
+ col: match.index,
329
+ length: match[0].length,
330
+ is_input: true,
331
+ is_protected: false,
332
+ });
333
+ }
334
+ // Pattern 2: Reverse-video runs (possible input fields)
335
+ let inReverse = false;
336
+ let reverseStart = 0;
337
+ for (let c = 0; c < this.cols; c++) {
338
+ const a = this.attrs[rowStart + c];
339
+ if (a.reverse && !inReverse) {
340
+ inReverse = true;
341
+ reverseStart = c;
342
+ }
343
+ else if (!a.reverse && inReverse) {
344
+ inReverse = false;
345
+ const len = c - reverseStart;
346
+ if (len >= 3) {
347
+ fields.push({
348
+ row: r,
349
+ col: reverseStart,
350
+ length: len,
351
+ is_input: true,
352
+ is_protected: false,
353
+ is_reverse: true,
354
+ });
355
+ }
356
+ }
357
+ }
358
+ if (inReverse) {
359
+ const len = this.cols - reverseStart;
360
+ if (len >= 3) {
361
+ fields.push({
362
+ row: r,
363
+ col: reverseStart,
364
+ length: len,
365
+ is_input: true,
366
+ is_protected: false,
367
+ is_reverse: true,
368
+ });
369
+ }
370
+ }
371
+ // Pattern 3: Prompt detection
372
+ const promptRe = /\b(Username|Login|Password|User|Passwd|Account|Host|Port)\s*:\s*/gi;
373
+ let pm;
374
+ while ((pm = promptRe.exec(line)) !== null) {
375
+ const afterPrompt = pm.index + pm[0].length;
376
+ const remaining = line.substring(afterPrompt);
377
+ const inputLen = remaining.length - remaining.trimEnd().length || remaining.length;
378
+ if (inputLen > 0) {
379
+ fields.push({
380
+ row: r,
381
+ col: afterPrompt,
382
+ length: Math.min(inputLen, this.cols - afterPrompt),
383
+ is_input: true,
384
+ is_protected: false,
385
+ });
386
+ }
387
+ }
388
+ }
389
+ // Deduplicate overlapping fields
390
+ const deduped = [];
391
+ for (const f of fields) {
392
+ const overlaps = deduped.some((d) => d.row === f.row &&
393
+ f.col < d.col + d.length &&
394
+ f.col + f.length > d.col);
395
+ if (!overlaps)
396
+ deduped.push(f);
397
+ }
398
+ return deduped;
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // Serialization
402
+ // ---------------------------------------------------------------------------
403
+ /** Convert to the protocol-agnostic ScreenData format */
404
+ toScreenData() {
405
+ const lines = [];
406
+ for (let r = 0; r < this.rows; r++) {
407
+ const start = r * this.cols;
408
+ lines.push(this.buffer.slice(start, start + this.cols).join(''));
409
+ }
410
+ const content = lines.join('\n');
411
+ const fields = this.detectFields();
412
+ const hash = createHash('md5').update(content).digest('hex').substring(0, 12);
413
+ return {
414
+ content,
415
+ cursor_row: this.cursorRow,
416
+ cursor_col: this.cursorCol,
417
+ rows: this.rows,
418
+ cols: this.cols,
419
+ fields,
420
+ screen_signature: hash,
421
+ timestamp: new Date().toISOString(),
422
+ };
423
+ }
424
+ }
@@ -0,0 +1,6 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { Server as HttpServer } from 'http';
3
+ import { Session } from './session.js';
4
+ export declare function setupWebSocket(server: HttpServer): WebSocketServer;
5
+ /** Subscribe to a session's events and push to connected WS clients */
6
+ export declare function bindSessionToWebSocket(session: Session): void;
@@ -0,0 +1,50 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import { URL } from 'url';
3
+ import { getSession, getDefaultSession } from './session.js';
4
+ const clients = new Set();
5
+ export function setupWebSocket(server) {
6
+ const wss = new WebSocketServer({ server, path: '/ws' });
7
+ wss.on('connection', (ws, req) => {
8
+ // Extract sessionId from query params
9
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
10
+ const sessionId = url.searchParams.get('sessionId');
11
+ const client = { ws, sessionId };
12
+ clients.add(client);
13
+ ws.on('close', () => {
14
+ clients.delete(client);
15
+ });
16
+ ws.on('error', () => {
17
+ clients.delete(client);
18
+ });
19
+ // Send current screen immediately if a session is available
20
+ const session = sessionId ? getSession(sessionId) : getDefaultSession();
21
+ if (session && session.status.connected) {
22
+ ws.send(JSON.stringify({ type: 'screen', data: session.getScreenData() }));
23
+ ws.send(JSON.stringify({ type: 'status', data: session.status }));
24
+ }
25
+ });
26
+ return wss;
27
+ }
28
+ /** Subscribe to a session's events and push to connected WS clients */
29
+ export function bindSessionToWebSocket(session) {
30
+ session.on('screenChange', (screenData) => {
31
+ const msg = JSON.stringify({ type: 'screen', data: screenData });
32
+ broadcastToSession(session.id, msg);
33
+ });
34
+ session.on('statusChange', (status) => {
35
+ const msg = JSON.stringify({ type: 'status', data: status });
36
+ broadcastToSession(session.id, msg);
37
+ });
38
+ }
39
+ function broadcastToSession(sessionId, message) {
40
+ for (const client of clients) {
41
+ if (client.ws.readyState !== WebSocket.OPEN)
42
+ continue;
43
+ // Send to clients that are either:
44
+ // 1. Explicitly bound to this session
45
+ // 2. Not bound to any session (will receive from default/single session)
46
+ if (client.sessionId === sessionId || client.sessionId === null) {
47
+ client.ws.send(message);
48
+ }
49
+ }
50
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "green-screen-proxy",
3
+ "version": "0.3.0",
4
+ "description": "WebSocket/REST proxy server for green-screen-react — connects browsers to TN5250, TN3270, VT220, and HP 6530 hosts over TCP",
5
+ "type": "module",
6
+ "bin": {
7
+ "green-screen-proxy": "dist/cli.js"
8
+ },
9
+ "main": "dist/server.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "dev": "tsx watch src/server.ts",
15
+ "build": "tsc",
16
+ "start": "node dist/server.js",
17
+ "lint": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "terminal",
22
+ "proxy",
23
+ "tn5250",
24
+ "tn3270",
25
+ "vt220",
26
+ "hp6530",
27
+ "ibm-i",
28
+ "as400",
29
+ "mainframe",
30
+ "green-screen",
31
+ "telnet",
32
+ "websocket"
33
+ ],
34
+ "author": "VisionBridge Solutions",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/visionbridge-solutions/green-screen-react.git",
39
+ "directory": "proxy"
40
+ },
41
+ "homepage": "https://github.com/visionbridge-solutions/green-screen-react#proxy-setup",
42
+ "bugs": {
43
+ "url": "https://github.com/visionbridge-solutions/green-screen-react/issues"
44
+ },
45
+ "dependencies": {
46
+ "cors": "^2.8.5",
47
+ "express": "^4.21.0",
48
+ "ws": "^8.18.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/cors": "^2.8.17",
52
+ "@types/express": "^4.17.21",
53
+ "@types/ws": "^8.5.12",
54
+ "tsx": "^4.19.0",
55
+ "typescript": "^5.7.0"
56
+ }
57
+ }