viberoom 0.5.9 → 0.6.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/dist/ws.js ADDED
@@ -0,0 +1,175 @@
1
+ // viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
2
+ import { createHash } from "node:crypto";
3
+ const HANDSHAKE_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4
+ export function acceptKey(key) {
5
+ return createHash("sha1").update(key + HANDSHAKE_GUID).digest("base64");
6
+ }
7
+ export const OPCODE = { continuation: 0, text: 1, binary: 2, close: 8, ping: 9, pong: 10 };
8
+ export function encodeFrame(opcode, payload = Buffer.alloc(0)) {
9
+ const length = payload.length;
10
+ let header;
11
+ if (length < 126) {
12
+ header = Buffer.alloc(2);
13
+ header[1] = length;
14
+ }
15
+ else if (length < 65536) {
16
+ header = Buffer.alloc(4);
17
+ header[1] = 126;
18
+ header.writeUInt16BE(length, 2);
19
+ }
20
+ else {
21
+ header = Buffer.alloc(10);
22
+ header[1] = 127;
23
+ header.writeBigUInt64BE(BigInt(length), 2);
24
+ }
25
+ header[0] = 0x80 | (opcode & 0x0f);
26
+ return Buffer.concat([header, payload]);
27
+ }
28
+ export function encodeText(text) {
29
+ return encodeFrame(OPCODE.text, Buffer.from(text, "utf8"));
30
+ }
31
+ export function encodeClose(code = 1000, reason = "") {
32
+ const text = Buffer.from(reason, "utf8");
33
+ const payload = Buffer.alloc(2 + text.length);
34
+ payload.writeUInt16BE(code, 0);
35
+ text.copy(payload, 2);
36
+ return encodeFrame(OPCODE.close, payload);
37
+ }
38
+ export const MAX_FRAME_BYTES = 1 << 20;
39
+ export function decodeFrames(buffer) {
40
+ const frames = [];
41
+ let offset = 0;
42
+ while (buffer.length - offset >= 2) {
43
+ const first = buffer[offset];
44
+ const second = buffer[offset + 1];
45
+ const fin = (first & 0x80) !== 0;
46
+ const opcode = first & 0x0f;
47
+ const masked = (second & 0x80) !== 0;
48
+ let length = second & 0x7f;
49
+ let position = offset + 2;
50
+ if (length === 126) {
51
+ if (buffer.length < position + 2)
52
+ break;
53
+ length = buffer.readUInt16BE(position);
54
+ position += 2;
55
+ }
56
+ else if (length === 127) {
57
+ if (buffer.length < position + 8)
58
+ break;
59
+ const big = buffer.readBigUInt64BE(position);
60
+ if (big > BigInt(MAX_FRAME_BYTES))
61
+ throw new Error(`frame of ${big} bytes refused`);
62
+ length = Number(big);
63
+ position += 8;
64
+ }
65
+ if (length > MAX_FRAME_BYTES)
66
+ throw new Error(`frame of ${length} bytes refused`);
67
+ let mask = null;
68
+ if (masked) {
69
+ if (buffer.length < position + 4)
70
+ break;
71
+ mask = buffer.subarray(position, position + 4);
72
+ position += 4;
73
+ }
74
+ if (buffer.length < position + length)
75
+ break;
76
+ const payload = Buffer.from(buffer.subarray(position, position + length));
77
+ if (mask)
78
+ for (let i = 0; i < payload.length; i++)
79
+ payload[i] ^= mask[i & 3];
80
+ frames.push({ fin, opcode, payload });
81
+ offset = position + length;
82
+ }
83
+ return { frames, rest: buffer.subarray(offset) };
84
+ }
85
+ export class WebSocketPeer {
86
+ socket;
87
+ buffer;
88
+ closed = false;
89
+ closeListeners = [];
90
+ alive = true;
91
+ constructor(socket, head = Buffer.alloc(0)) {
92
+ this.socket = socket;
93
+ this.buffer = Buffer.alloc(0);
94
+ socket.on("data", (chunk) => this.onData(chunk));
95
+ socket.on("close", () => this.finish());
96
+ socket.on("end", () => this.finish());
97
+ socket.on("error", () => this.finish());
98
+ if (head.length)
99
+ this.onData(head);
100
+ }
101
+ onClose(listener) {
102
+ this.closeListeners.push(listener);
103
+ }
104
+ send(text) {
105
+ if (this.closed)
106
+ return;
107
+ this.socket.write(encodeText(text));
108
+ }
109
+ ping() {
110
+ if (this.closed)
111
+ return;
112
+ this.alive = false;
113
+ this.socket.write(encodeFrame(OPCODE.ping));
114
+ }
115
+ close(code = 1000, reason = "") {
116
+ if (this.closed)
117
+ return;
118
+ try {
119
+ this.socket.write(encodeClose(code, reason));
120
+ }
121
+ catch {
122
+ }
123
+ this.socket.end();
124
+ this.finish();
125
+ }
126
+ onData(chunk) {
127
+ if (this.closed)
128
+ return;
129
+ this.buffer = Buffer.concat([this.buffer, chunk]);
130
+ let decoded;
131
+ try {
132
+ decoded = decodeFrames(this.buffer);
133
+ }
134
+ catch {
135
+ this.close(1009, "frame too large");
136
+ return;
137
+ }
138
+ this.buffer = Buffer.from(decoded.rest);
139
+ for (const frame of decoded.frames) {
140
+ if (frame.opcode === OPCODE.ping)
141
+ this.socket.write(encodeFrame(OPCODE.pong, frame.payload));
142
+ else if (frame.opcode === OPCODE.pong)
143
+ this.alive = true;
144
+ else if (frame.opcode === OPCODE.close) {
145
+ try {
146
+ this.socket.write(encodeFrame(OPCODE.close, frame.payload.subarray(0, 2)));
147
+ }
148
+ catch {
149
+ }
150
+ this.socket.end();
151
+ this.finish();
152
+ return;
153
+ }
154
+ }
155
+ }
156
+ finish() {
157
+ if (this.closed)
158
+ return;
159
+ this.closed = true;
160
+ for (const listener of this.closeListeners)
161
+ listener();
162
+ }
163
+ }
164
+ export function acceptUpgrade(req, socket, head = Buffer.alloc(0)) {
165
+ const key = req.headers["sec-websocket-key"];
166
+ const upgrade = String(req.headers.upgrade ?? "").toLowerCase();
167
+ const version = req.headers["sec-websocket-version"];
168
+ if (upgrade !== "websocket" || typeof key !== "string" || version !== "13") {
169
+ socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
170
+ socket.destroy();
171
+ return null;
172
+ }
173
+ socket.write(["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${acceptKey(key)}`, "", ""].join("\r\n"));
174
+ return new WebSocketPeer(socket, head);
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberoom",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "viberoom: group chat rooms for a human and several coding agents over the Agent Client Protocol",
5
5
  "type": "module",
6
6
  "engines": {
@@ -33,6 +33,7 @@
33
33
  "@agentclientprotocol/sdk": "1.4.0",
34
34
  "marked": "^16.4.2",
35
35
  "mermaid": "^11.17.2",
36
+ "prismjs": "^1.30.0",
36
37
  "zod": "^4.0.0"
37
38
  },
38
39
  "devDependencies": {