Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
236 lines (213 loc) · 28.6 KB
/
Copy pathserver.js
File metadata and controls
236 lines (213 loc) · 28.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// ===========================================================================
// Covert Chat — SINGLE FILE version.
// Everything (page, styles, logic, server) is in this one file.
// No other files or folders are needed.
//
// Run: node server.js
// Then open the URL it prints (usually http://localhost:3000).
// ===========================================================================
const http = require("http");
const crypto = require("crypto");
// ===========================================================================
// CONFIG — edit this section to set up your two users, reveal code, etc.
// ===========================================================================
const config = {
users: {
"alpha": { name: "Alex", role: "A" }, // role A = sees SCRAMBLED text
"bravo": { name: "Sam", role: "B" }, // role B = sees everything normally
},
revealCode: "2468", // code person A types to reveal real messages
revealSeconds: 15, // how long real text stays visible
scramble: {
mode: "local", // "local" | "ollama" | "openai"
ollama: { url: "http://localhost:11434/api/generate", model: "llama3.2:1b" },
openai: { url: "https://api.openai.com/v1/chat/completions", model: "gpt-4o-mini", apiKey: process.env.OPENAI_API_KEY || "" },
},
port: process.env.PORT || 3000,
};
// ===========================================================================
// SCRAMBLE — turns a real message into innocuous "cover" text.
// ===========================================================================
const LINES = [
"yeah that works for me", "sounds good, let me know", "haha yeah totally",
"ok i'll check and get back to you", "not sure yet, still thinking about it",
"we should grab coffee sometime", "i'm running a little late honestly",
"did you see the game last night", "the weather's been so weird lately",
"i'll probably just stay in tonight", "lol that's hilarious",
"no worries, take your time", "let's figure it out tomorrow",
"i totally forgot about that", "sure, whatever's easiest for you",
"my phone's been dying all day", "traffic was insane this morning",
"i think i left it at home", "we can talk about it later",
"that new place is pretty good", "i've been so busy this week",
"remind me again what time", "yeah i'll send it over in a bit",
"honestly i could go either way", "cool, see you then",
"i need more coffee before this", "the dog would not stop barking",
"i'll ask and see what they say", "maybe next weekend instead",
"that makes sense i guess", "still waiting to hear back",
"long day, i'm pretty wiped", "let me double check real quick",
"sounds like a plan to me", "i'll take care of it, don't worry",
];
function hash(str) {
let h = 2166136261;
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return h >>> 0;
}
function localScramble(text) {
const words = text.trim().split(/\s+/).filter(Boolean).length || 1;
const count = Math.max(1, Math.min(4, Math.round(words / 7)));
let seed = hash(text);
const picked = [], used = new Set();
for (let i = 0; i < count; i++) {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
let idx = seed % LINES.length, guard = 0;
while (used.has(idx) && guard++ < LINES.length) idx = (idx + 1) % LINES.length;
used.add(idx); picked.push(LINES[idx]);
}
return picked.join(". ").replace(/\.\s*\./g, ".");
}
async function ollamaScramble(text, cfg) {
const prompt = "Rewrite the following message as a completely different, mundane, everyday text message of similar length. Keep it casual and innocuous. Do not reference the original meaning. Output ONLY the rewritten message, nothing else.\n\nMessage: " + text;
const res = await fetch(cfg.url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: cfg.model, prompt, stream: false }) });
const data = await res.json();
return (data.response || "").trim();
}
async function openaiScramble(text, cfg) {
const res = await fetch(cfg.url, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + cfg.apiKey },
body: JSON.stringify({ model: cfg.model, messages: [
{ role: "system", content: "You rewrite messages as unrelated, mundane, innocuous everyday chat text of similar length. Output only the rewritten message." },
{ role: "user", content: text },
] }),
});
const data = await res.json();
return (data.choices?.[0]?.message?.content || "").trim();
}
async function scramble(text, c) {
try {
if (c.mode === "ollama") return (await ollamaScramble(text, c.ollama)) || localScramble(text);
if (c.mode === "openai") return (await openaiScramble(text, c.openai)) || localScramble(text);
} catch (err) { console.error("[scramble] AI mode failed, using local:", err.message); }
return localScramble(text);
}
// ===========================================================================
// PAGE — the whole front-end, encoded so it needs no separate files.
// ===========================================================================
const PAGE = Buffer.from("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgPG1ldGEgY2hhcnNldD0iVVRGLTgiIC8+CiAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+CiAgPHRpdGxlPkNoYXRHUFQ8L3RpdGxlPgogIDxzdHlsZT4KOnJvb3QgewogIC0tYmc6ICMyMTIxMjE7CiAgLS1zaWRlYmFyOiAjMTcxNzE3OwogIC0tcGFuZWw6ICMyZjJmMmY7CiAgLS1idWJibGUtdXNlcjogIzJmMmYyZjsKICAtLXRleHQ6ICNlY2VjZWM7CiAgLS10ZXh0LWRpbTogI2I0YjRiNDsKICAtLWJvcmRlcjogIzNhM2EzYTsKICAtLWdyZWVuOiAjMTBhMzdmOwogIC0tYWNjZW50OiAjYWI2OGZmOwp9CgoqIHsgYm94LXNpemluZzogYm9yZGVyLWJveDsgbWFyZ2luOiAwOyBwYWRkaW5nOiAwOyB9CgpodG1sLCBib2R5IHsKICBoZWlnaHQ6IDEwMCU7CiAgYmFja2dyb3VuZDogdmFyKC0tYmcpOwogIGNvbG9yOiB2YXIoLS10ZXh0KTsKICBmb250LWZhbWlseTogIlPDtmhuZSIsIHVpLXNhbnMtc2VyaWYsIHN5c3RlbS11aSwgLWFwcGxlLXN5c3RlbSwgIlNlZ29lIFVJIiwgSGVsdmV0aWNhLCBBcmlhbCwgc2Fucy1zZXJpZjsKICBmb250LXNpemU6IDE1cHg7CiAgLXdlYmtpdC1mb250LXNtb290aGluZzogYW50aWFsaWFzZWQ7Cn0KCi5oaWRkZW4geyBkaXNwbGF5OiBub25lICFpbXBvcnRhbnQ7IH0KCi8qIC0tLS0tLS0tLS0gYnJhbmQgZG90IC0tLS0tLS0tLS0gKi8KLmJyYW5kLWRvdCB7CiAgd2lkdGg6IDMwcHg7IGhlaWdodDogMzBweDsgYm9yZGVyLXJhZGl1czogNTAlOwogIGJhY2tncm91bmQ6IGNvbmljLWdyYWRpZW50KGZyb20gMTgwZGVnLCB2YXIoLS1ncmVlbiksICMwZThmNmYsIHZhcigtLWdyZWVuKSk7CiAgbWFyZ2luOiAwIGF1dG8gMThweDsKfQouYnJhbmQtZG90LmJpZyB7IHdpZHRoOiA0NnB4OyBoZWlnaHQ6IDQ2cHg7IG1hcmdpbi1ib3R0b206IDIycHg7IH0KCi8qIC0tLS0tLS0tLS0gbG9naW4gLS0tLS0tLS0tLSAqLwoubG9naW4gewogIGhlaWdodDogMTAwJTsKICBkaXNwbGF5OiBmbGV4OyBhbGlnbi1pdGVtczogY2VudGVyOyBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjsKfQoubG9naW4tYm94IHsKICB3aWR0aDogMzQwcHg7IG1heC13aWR0aDogOTB2dzsKICB0ZXh0LWFsaWduOiBjZW50ZXI7CiAgcGFkZGluZzogNDBweCAzMnB4Owp9Ci5sb2dpbi1ib3ggaDEgeyBmb250LXNpemU6IDI2cHg7IGZvbnQtd2VpZ2h0OiA2MDA7IG1hcmdpbi1ib3R0b206IDRweDsgfQoubG9naW4tYm94IC5zdWIgeyBjb2xvcjogdmFyKC0tdGV4dC1kaW0pOyBtYXJnaW4tYm90dG9tOiAyNnB4OyBmb250LXNpemU6IDE0cHg7IH0KLmxvZ2luLWJveCBpbnB1dCB7CiAgd2lkdGg6IDEwMCU7IHBhZGRpbmc6IDEzcHggMTVweDsgbWFyZ2luLWJvdHRvbTogMTJweDsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbCk7IGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWJvcmRlcik7CiAgYm9yZGVyLXJhZGl1czogMTBweDsgY29sb3I6IHZhcigtLXRleHQpOyBmb250LXNpemU6IDE1cHg7IG91dGxpbmU6IG5vbmU7Cn0KLmxvZ2luLWJveCBpbnB1dDpmb2N1cyB7IGJvcmRlci1jb2xvcjogdmFyKC0tZ3JlZW4pOyB9Ci5sb2dpbi1ib3ggYnV0dG9uIHsKICB3aWR0aDogMTAwJTsgcGFkZGluZzogMTNweDsgYm9yZGVyOiBub25lOyBib3JkZXItcmFkaXVzOiAxMHB4OwogIGJhY2tncm91bmQ6IHZhcigtLXRleHQpOyBjb2xvcjogIzExMTsgZm9udC13ZWlnaHQ6IDYwMDsgZm9udC1zaXplOiAxNXB4OyBjdXJzb3I6IHBvaW50ZXI7Cn0KLmxvZ2luLWJveCBidXR0b246aG92ZXIgeyBvcGFjaXR5OiAuOTsgfQouZXJyIHsgY29sb3I6ICNmZjZiNmI7IGZvbnQtc2l6ZTogMTNweDsgbWFyZ2luLXRvcDogMTJweDsgbWluLWhlaWdodDogMTZweDsgfQoKLyogLS0tLS0tLS0tLSBhcHAgbGF5b3V0IC0tLS0tLS0tLS0gKi8KLmFwcCB7IGhlaWdodDogMTAwJTsgZGlzcGxheTogZmxleDsgfQoKLyogc2lkZWJhciAqLwouc2lkZWJhciB7CiAgd2lkdGg6IDI2MHB4OyBmbGV4LXNocmluazogMDsKICBiYWNrZ3JvdW5kOiB2YXIoLS1zaWRlYmFyKTsKICBkaXNwbGF5OiBmbGV4OyBmbGV4LWRpcmVjdGlvbjogY29sdW1uOwogIHBhZGRpbmc6IDhweDsKfQoubmV3LWNoYXQgewogIGRpc3BsYXk6IGZsZXg7IGFsaWduLWl0ZW1zOiBjZW50ZXI7IGdhcDogMTBweDsKICB3aWR0aDogMTAwJTsgcGFkZGluZzogMTFweCAxMnB4OyBtYXJnaW4tYm90dG9tOiA2cHg7CiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7IGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWJvcmRlcik7CiAgYm9yZGVyLXJhZGl1czogMTBweDsgY29sb3I6IHZhcigtLXRleHQpOyBmb250LXNpemU6IDE0cHg7IGN1cnNvcjogcG9pbnRlcjsKfQoubmV3LWNoYXQ6aG92ZXIgeyBiYWNrZ3JvdW5kOiAjMmEyYTJhOyB9Ci5wbHVzIHsgZm9udC1zaXplOiAxOHB4OyBsaW5lLWhlaWdodDogMTsgfQouaGlzdG9yeSB7IGZsZXg6IDE7IG92ZXJmbG93LXk6IGF1dG87IHBhZGRpbmctdG9wOiA2cHg7IH0KLmhpc3RvcnktaXRlbSB7CiAgcGFkZGluZzogMTBweCAxMnB4OyBib3JkZXItcmFkaXVzOiA4cHg7IGZvbnQtc2l6ZTogMTRweDsKICBjb2xvcjogdmFyKC0tdGV4dC1kaW0pOyBjdXJzb3I6IHBvaW50ZXI7IHdoaXRlLXNwYWNlOiBub3dyYXA7IG92ZXJmbG93OiBoaWRkZW47IHRleHQtb3ZlcmZsb3c6IGVsbGlwc2lzOwp9Ci5oaXN0b3J5LWl0ZW0uYWN0aXZlLCAuaGlzdG9yeS1pdGVtOmhvdmVyIHsgYmFja2dyb3VuZDogIzJhMmEyYTsgY29sb3I6IHZhcigtLXRleHQpOyB9Ci5zaWRlYmFyLWZvb3RlciB7CiAgZGlzcGxheTogZmxleDsgYWxpZ24taXRlbXM6IGNlbnRlcjsgZ2FwOiAxMHB4OwogIHBhZGRpbmc6IDEwcHggOHB4OyBib3JkZXItdG9wOiAxcHggc29saWQgdmFyKC0tYm9yZGVyKTsgZm9udC1zaXplOiAxNHB4Owp9Ci5hdmF0YXIgewogIHdpZHRoOiAyOHB4OyBoZWlnaHQ6IDI4cHg7IGJvcmRlci1yYWRpdXM6IDUwJTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1hY2NlbnQpOyBjb2xvcjogI2ZmZjsKICBkaXNwbGF5OiBmbGV4OyBhbGlnbi1pdGVtczogY2VudGVyOyBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjsKICBmb250LXNpemU6IDEzcHg7IGZvbnQtd2VpZ2h0OiA2MDA7Cn0KCi8qIG1haW4gKi8KLm1haW4geyBmbGV4OiAxOyBkaXNwbGF5OiBmbGV4OyBmbGV4LWRpcmVjdGlvbjogY29sdW1uOyBtaW4td2lkdGg6IDA7IH0KCi50b3BiYXIgewogIGhlaWdodDogNTRweDsgZmxleC1zaHJpbms6IDA7CiAgZGlzcGxheTogZmxleDsgYWxpZ24taXRlbXM6IGNlbnRlcjsgZ2FwOiAxMnB4OwogIHBhZGRpbmc6IDAgMTZweDsgcG9zaXRpb246IHJlbGF0aXZlOwp9Ci5tb2RlbC1zZWxlY3QgewogIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50OyBib3JkZXI6IG5vbmU7IGNvbG9yOiB2YXIoLS10ZXh0KTsKICBmb250LXNpemU6IDE2cHg7IGZvbnQtd2VpZ2h0OiA2MDA7IGN1cnNvcjogcG9pbnRlcjsKICBwYWRkaW5nOiA2cHggMTBweDsgYm9yZGVyLXJhZGl1czogOHB4Owp9Ci5tb2RlbC1zZWxlY3Q6aG92ZXIgeyBiYWNrZ3JvdW5kOiAjMmEyYTJhOyB9Ci5jYXJldCB7IGNvbG9yOiB2YXIoLS10ZXh0LWRpbSk7IGZvbnQtd2VpZ2h0OiA0MDA7IH0KCi5yZXZlYWwtcG9wIHsKICBwb3NpdGlvbjogYWJzb2x1dGU7IHRvcDogNDhweDsgbGVmdDogMTZweDsgei1pbmRleDogMjA7CiAgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwpOyBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ib3JkZXIpOwogIGJvcmRlci1yYWRpdXM6IDEwcHg7IHBhZGRpbmc6IDhweDsKICBib3gtc2hhZG93OiAwIDhweCAyNHB4IHJnYmEoMCwwLDAsLjQpOwp9Ci5yZXZlYWwtcG9wIGlucHV0IHsKICBiYWNrZ3JvdW5kOiAjMWYxZjFmOyBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ib3JkZXIpOyBib3JkZXItcmFkaXVzOiA4cHg7CiAgcGFkZGluZzogOXB4IDExcHg7IGNvbG9yOiB2YXIoLS10ZXh0KTsgZm9udC1zaXplOiAxNHB4OyBvdXRsaW5lOiBub25lOyB3aWR0aDogMTcwcHg7Cn0KLnJldmVhbC1wb3AgaW5wdXQ6Zm9jdXMgeyBib3JkZXItY29sb3I6IHZhcigtLWdyZWVuKTsgfQoKLnJldmVhbC10aW1lciB7CiAgZm9udC1zaXplOiAxMnB4OyBjb2xvcjogdmFyKC0tZ3JlZW4pOyBmb250LXdlaWdodDogNjAwOwogIGJhY2tncm91bmQ6IHJnYmEoMTYsMTYzLDEyNywuMTIpOyBwYWRkaW5nOiA0cHggMTBweDsgYm9yZGVyLXJhZGl1czogMjBweDsKfQoKLyogbWVzc2FnZXMgKi8KLm1lc3NhZ2VzIHsgZmxleDogMTsgb3ZlcmZsb3cteTogYXV0bzsgfQoubWVzc2FnZXMgLmVtcHR5IHsKICBoZWlnaHQ6IDEwMCU7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47CiAgYWxpZ24taXRlbXM6IGNlbnRlcjsganVzdGlmeS1jb250ZW50OiBjZW50ZXI7IGNvbG9yOiB2YXIoLS10ZXh0LWRpbSk7Cn0KLm1lc3NhZ2VzIC5lbXB0eSBwIHsgZm9udC1zaXplOiAyMHB4OyBjb2xvcjogdmFyKC0tdGV4dCk7IH0KCi5yb3cgeyB3aWR0aDogMTAwJTsgcGFkZGluZzogNnB4IDA7IGRpc3BsYXk6IGZsZXg7IH0KLnJvdy1pbm5lciB7CiAgbWF4LXdpZHRoOiA3NjBweDsgd2lkdGg6IDEwMCU7IG1hcmdpbjogMCBhdXRvOyBwYWRkaW5nOiAwIDI0cHg7CiAgZGlzcGxheTogZmxleDsgZ2FwOiAxMnB4Owp9Ci5yb3cudGhlbSAucm93LWlubmVyIHsganVzdGlmeS1jb250ZW50OiBmbGV4LXN0YXJ0OyB9Ci5yb3cubWUgLnJvdy1pbm5lciB7IGp1c3RpZnktY29udGVudDogZmxleC1lbmQ7IH0KCi5idWJibGUgewogIG1heC13aWR0aDogNzUlOyBwYWRkaW5nOiAxMXB4IDE1cHg7IGJvcmRlci1yYWRpdXM6IDE4cHg7CiAgbGluZS1oZWlnaHQ6IDEuNTsgd2hpdGUtc3BhY2U6IHByZS13cmFwOyB3b3JkLXdyYXA6IGJyZWFrLXdvcmQ7CiAgdHJhbnNpdGlvbjogb3BhY2l0eSAuMTVzIGVhc2U7Cn0KLnJvdy5tZSAuYnViYmxlIHsgYmFja2dyb3VuZDogdmFyKC0tYnViYmxlLXVzZXIpOyBib3JkZXItYm90dG9tLXJpZ2h0LXJhZGl1czogNnB4OyB9Ci5yb3cudGhlbSAuYnViYmxlIHsgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7IHBhZGRpbmctbGVmdDogMDsgfQoKLnJvdy50aGVtIC5yb3ctaW5uZXI6OmJlZm9yZSB7CiAgY29udGVudDogIiI7IHdpZHRoOiAzMHB4OyBoZWlnaHQ6IDMwcHg7IGJvcmRlci1yYWRpdXM6IDUwJTsgZmxleC1zaHJpbms6IDA7CiAgYmFja2dyb3VuZDogY29uaWMtZ3JhZGllbnQoZnJvbSAxODBkZWcsIHZhcigtLWdyZWVuKSwgIzBlOGY2ZiwgdmFyKC0tZ3JlZW4pKTsKfQoKLmJ1YmJsZS5yZXZlYWxlZCB7IG91dGxpbmU6IDFweCBzb2xpZCByZ2JhKDE2LDE2MywxMjcsLjUpOyB9CgovKiBjb21wb3NlciAqLwouY29tcG9zZXIgeyBmbGV4LXNocmluazogMDsgcGFkZGluZzogMCAxNnB4IDE0cHg7IH0KLmNvbXBvc2VyLWlubmVyIHsKICBtYXgtd2lkdGg6IDc2MHB4OyBtYXJnaW46IDAgYXV0bzsKICBkaXNwbGF5OiBmbGV4OyBhbGlnbi1pdGVtczogZmxleC1lbmQ7IGdhcDogOHB4OwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tYm9yZGVyKTsKICBib3JkZXItcmFkaXVzOiAyNnB4OyBwYWRkaW5nOiA4cHggOHB4IDhweCAxOHB4Owp9CiNpbnB1dCB7CiAgZmxleDogMTsgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7IGJvcmRlcjogbm9uZTsgb3V0bGluZTogbm9uZTsgcmVzaXplOiBub25lOwogIGNvbG9yOiB2YXIoLS10ZXh0KTsgZm9udC1zaXplOiAxNXB4OyBsaW5lLWhlaWdodDogMS41OyBtYXgtaGVpZ2h0OiAxODBweDsKICBmb250LWZhbWlseTogaW5oZXJpdDsgcGFkZGluZzogNnB4IDA7Cn0KLnNlbmQtYnRuIHsKICB3aWR0aDogMzRweDsgaGVpZ2h0OiAzNHB4OyBib3JkZXItcmFkaXVzOiA1MCU7IGJvcmRlcjogbm9uZTsKICBiYWNrZ3JvdW5kOiB2YXIoLS10ZXh0KTsgY29sb3I6ICMxMTE7IGZvbnQtc2l6ZTogMTdweDsgY3Vyc29yOiBwb2ludGVyOyBmbGV4LXNocmluazogMDsKfQouc2VuZC1idG46ZGlzYWJsZWQgeyBiYWNrZ3JvdW5kOiAjNTU1OyBjb2xvcjogIzk5OTsgY3Vyc29yOiBkZWZhdWx0OyB9Ci5kaXNjbGFpbWVyIHsgdGV4dC1hbGlnbjogY2VudGVyOyBjb2xvcjogdmFyKC0tdGV4dC1kaW0pOyBmb250LXNpemU6IDEycHg7IG1hcmdpbi10b3A6IDEwcHg7IH0KCi8qIG1vYmlsZSAqLwpAbWVkaWEgKG1heC13aWR0aDogNzIwcHgpIHsKICAuc2lkZWJhciB7IGRpc3BsYXk6IG5vbmU7IH0KICAucm93LWlubmVyIHsgcGFkZGluZzogMCAxNHB4OyB9Cn0KCjwvc3R5bGU+CiAgPGxpbmsgcmVsPSJpY29uIiBocmVmPSJkYXRhOmltYWdlL3N2Zyt4bWwsPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTAnIGZpbGw9JyUyMzEwYTM3ZicvPjwvc3ZnPiIgLz4KPC9oZWFkPgo8Ym9keT4KCiAgPCEtLSA9PT09PSBMb2dpbiBzY3JlZW4gPT09PT0gLS0+CiAgPGRpdiBpZD0ibG9naW4iIGNsYXNzPSJsb2dpbiI+CiAgICA8ZGl2IGNsYXNzPSJsb2dpbi1ib3giPgogICAgICA8ZGl2IGNsYXNzPSJicmFuZC1kb3QiPjwvZGl2PgogICAgICA8aDE+Q2hhdEdQVDwvaDE+CiAgICAgIDxwIGNsYXNzPSJzdWIiPkVudGVyIHlvdXIgSUQgdG8gY29udGludWU8L3A+CiAgICAgIDxpbnB1dCBpZD0ibG9naW4taWQiIHR5cGU9InRleHQiIHBsYWNlaG9sZGVyPSJJRCIgYXV0b2NvbXBsZXRlPSJvZmYiIC8+CiAgICAgIDxidXR0b24gaWQ9ImxvZ2luLWJ0biI+Q29udGludWU8L2J1dHRvbj4KICAgICAgPGRpdiBpZD0ibG9naW4tZXJyIiBjbGFzcz0iZXJyIj48L2Rpdj4KICAgIDwvZGl2PgogIDwvZGl2PgoKICA8IS0tID09PT09IENoYXQgYXBwID09PT09IC0tPgogIDxkaXYgaWQ9ImFwcCIgY2xhc3M9ImFwcCBoaWRkZW4iPgogICAgPCEtLSBzaWRlYmFyIC0tPgogICAgPGFzaWRlIGNsYXNzPSJzaWRlYmFyIj4KICAgICAgPGJ1dHRvbiBjbGFzcz0ibmV3LWNoYXQiPgogICAgICAgIDxzcGFuIGNsYXNzPSJwbHVzIj4rPC9zcGFuPiBOZXcgY2hhdAogICAgICA8L2J1dHRvbj4KICAgICAgPGRpdiBjbGFzcz0iaGlzdG9yeSI+CiAgICAgICAgPGRpdiBjbGFzcz0iaGlzdG9yeS1pdGVtIGFjdGl2ZSI+TmV3IGNvbnZlcnNhdGlvbjwvZGl2PgogICAgICA8L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0ic2lkZWJhci1mb290ZXIiPgogICAgICAgIDxkaXYgY2xhc3M9ImF2YXRhciIgaWQ9InNpZGUtYXZhdGFyIj5VPC9kaXY+CiAgICAgICAgPHNwYW4gaWQ9InNpZGUtbmFtZSI+VXNlcjwvc3Bhbj4KICAgICAgPC9kaXY+CiAgICA8L2FzaWRlPgoKICAgIDwhLS0gbWFpbiAtLT4KICAgIDxtYWluIGNsYXNzPSJtYWluIj4KICAgICAgPGhlYWRlciBjbGFzcz0idG9wYmFyIj4KICAgICAgICA8IS0tIFRoZSBtb2RlbCBuYW1lIGlzIHRoZSBoaWRkZW4gcmV2ZWFsIHRyaWdnZXIuIENsaWNraW5nIGl0IG9wZW5zCiAgICAgICAgICAgICBhIHNtYWxsIGNvZGUgYm94OyBhIGNvcnJlY3QgY29kZSBicmllZmx5IHNob3dzIHJlYWwgbWVzc2FnZXMuIC0tPgogICAgICAgIDxidXR0b24gY2xhc3M9Im1vZGVsLXNlbGVjdCIgaWQ9Im1vZGVsLXNlbGVjdCI+CiAgICAgICAgICBDaGF0R1BUIDxzcGFuIGNsYXNzPSJjYXJldCI+NG8gJiM5NjYyOzwvc3Bhbj4KICAgICAgICA8L2J1dHRvbj4KICAgICAgICA8ZGl2IGlkPSJyZXZlYWwtdGltZXIiIGNsYXNzPSJyZXZlYWwtdGltZXIgaGlkZGVuIj48L2Rpdj4KCiAgICAgICAgPCEtLSBoaWRkZW4gY29kZSBlbnRyeSwgc3R5bGVkIGxpa2UgYSBzbWFsbCBkcm9wZG93biAtLT4KICAgICAgICA8ZGl2IGlkPSJyZXZlYWwtcG9wIiBjbGFzcz0icmV2ZWFsLXBvcCBoaWRkZW4iPgogICAgICAgICAgPGlucHV0IGlkPSJyZXZlYWwtY29kZSIgdHlwZT0icGFzc3dvcmQiIGlucHV0bW9kZT0ibnVtZXJpYyIgcGxhY2Vob2xkZXI9Ik1vZGVsIGFjY2VzcyIgYXV0b2NvbXBsZXRlPSJvZmYiIC8+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvaGVhZGVyPgoKICAgICAgPGRpdiBpZD0ibWVzc2FnZXMiIGNsYXNzPSJtZXNzYWdlcyI+CiAgICAgICAgPGRpdiBjbGFzcz0iZW1wdHkiPgogICAgICAgICAgPGRpdiBjbGFzcz0iYnJhbmQtZG90IGJpZyI+PC9kaXY+CiAgICAgICAgICA8cD5Ib3cgY2FuIEkgaGVscCB5b3UgdG9kYXk/PC9wPgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KCiAgICAgIDxmb290ZXIgY2xhc3M9ImNvbXBvc2VyIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb21wb3Nlci1pbm5lciI+CiAgICAgICAgICA8dGV4dGFyZWEgaWQ9ImlucHV0IiByb3dzPSIxIiBwbGFjZWhvbGRlcj0iTWVzc2FnZSBDaGF0R1BU4oCmIj48L3RleHRhcmVhPgogICAgICAgICAgPGJ1dHRvbiBpZD0ic2VuZCIgY2xhc3M9InNlbmQtYnRuIiBhcmlhLWxhYmVsPSJTZW5kIj4mIzg1OTM7PC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPHAgY2xhc3M9ImRpc2NsYWltZXIiPkNoYXRHUFQgY2FuIG1ha2UgbWlzdGFrZXMuIENoZWNrIGltcG9ydGFudCBpbmZvLjwvcD4KICAgICAgPC9mb290ZXI+CiAgICA8L21haW4+CiAgPC9kaXY+CgogIDxzY3JpcHQ+Ci8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQovLyAgYXBwLmpzICDigJQgY2xpZW50IGxvZ2ljCi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQoKbGV0IE1FID0gbnVsbDsgICAgICAgIC8vIHsgaWQsIG5hbWUsIHJvbGUgfQpsZXQgZXZ0U291cmNlID0gbnVsbDsKY29uc3QgbXNncyA9IFtdOyAgICAgIC8vIHsgaWQsIHNlbmRlck5hbWUsIHRleHQsIG1pbmUgfSAgKHRleHQgPSB3aGF0IFRISVMgdXNlciBzZWVzKQpsZXQgcmVhbHMgPSB7fTsgICAgICAgLy8gaWQgLT4gcmVhbCB0ZXh0IChvbmx5IHBvcHVsYXRlZCBkdXJpbmcgYSByZXZlYWwpCmxldCByZXZlYWxlZCA9IGZhbHNlOwpsZXQgcmV2ZWFsVGltZXIgPSBudWxsOwoKY29uc3QgJCA9IChzKSA9PiBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKHMpOwoKLy8gLS0tLS0tLS0tLSBsb2dpbiAtLS0tLS0tLS0tCmFzeW5jIGZ1bmN0aW9uIGxvZ2luKCkgewogIGNvbnN0IGlkID0gJCgiI2xvZ2luLWlkIikudmFsdWUudHJpbSgpOwogIGlmICghaWQpIHJldHVybjsKICBjb25zdCByID0gYXdhaXQgZmV0Y2goIi9sb2dpbiIsIHsKICAgIG1ldGhvZDogIlBPU1QiLAogICAgaGVhZGVyczogeyAiQ29udGVudC1UeXBlIjogImFwcGxpY2F0aW9uL2pzb24iIH0sCiAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IGlkIH0pLAogIH0pOwogIGNvbnN0IGRhdGEgPSBhd2FpdCByLmpzb24oKTsKICBpZiAoIWRhdGEub2spIHsKICAgICQoIiNsb2dpbi1lcnIiKS50ZXh0Q29udGVudCA9ICJJbnZhbGlkIElELiI7CiAgICByZXR1cm47CiAgfQogIE1FID0geyBpZCwgbmFtZTogZGF0YS5uYW1lLCByb2xlOiBkYXRhLnJvbGUgfTsKICBzdGFydEFwcCgpOwp9CgokKCIjbG9naW4tYnRuIikuYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLCBsb2dpbik7CiQoIiNsb2dpbi1pZCIpLmFkZEV2ZW50TGlzdGVuZXIoImtleWRvd24iLCAoZSkgPT4geyBpZiAoZS5rZXkgPT09ICJFbnRlciIpIGxvZ2luKCk7IH0pOwoKLy8gLS0tLS0tLS0tLSBzdGFydCAtLS0tLS0tLS0tCmZ1bmN0aW9uIHN0YXJ0QXBwKCkgewogICQoIiNsb2dpbiIpLmNsYXNzTGlzdC5hZGQoImhpZGRlbiIpOwogICQoIiNhcHAiKS5jbGFzc0xpc3QucmVtb3ZlKCJoaWRkZW4iKTsKICAkKCIjc2lkZS1uYW1lIikudGV4dENvbnRlbnQgPSBNRS5uYW1lOwogICQoIiNzaWRlLWF2YXRhciIpLnRleHRDb250ZW50ID0gTUUubmFtZVswXS50b1VwcGVyQ2FzZSgpOwoKICBldnRTb3VyY2UgPSBuZXcgRXZlbnRTb3VyY2UoIi9ldmVudHM/aWQ9IiArIGVuY29kZVVSSUNvbXBvbmVudChNRS5pZCkpOwogIGV2dFNvdXJjZS5hZGRFdmVudExpc3RlbmVyKCJtZXNzYWdlIiwgKGUpID0+IHsKICAgIGNvbnN0IG0gPSBKU09OLnBhcnNlKGUuZGF0YSk7CiAgICBtc2dzLnB1c2gobSk7CiAgICByZW5kZXIoKTsKICB9KTsKfQoKLy8gLS0tLS0tLS0tLSByZW5kZXIgLS0tLS0tLS0tLQpmdW5jdGlvbiByZW5kZXIoKSB7CiAgY29uc3QgYm94ID0gJCgiI21lc3NhZ2VzIik7CiAgaWYgKG1zZ3MubGVuZ3RoID09PSAwKSByZXR1cm47CiAgYm94LmlubmVySFRNTCA9ICIiOwogIGZvciAoY29uc3QgbSBvZiBtc2dzKSB7CiAgICBjb25zdCByb3cgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJkaXYiKTsKICAgIHJvdy5jbGFzc05hbWUgPSAicm93ICIgKyAobS5taW5lID8gIm1lIiA6ICJ0aGVtIik7CiAgICBjb25zdCBpbm5lciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImRpdiIpOwogICAgaW5uZXIuY2xhc3NOYW1lID0gInJvdy1pbm5lciI7CiAgICBjb25zdCBidWJibGUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJkaXYiKTsKICAgIGJ1YmJsZS5jbGFzc05hbWUgPSAiYnViYmxlIjsKCiAgICAvLyBEdXJpbmcgYSByZXZlYWwsIHNob3cgdGhlIHJlYWwgdGV4dCBpZiB3ZSBoYXZlIGl0LgogICAgaWYgKHJldmVhbGVkICYmIHJlYWxzW20uaWRdICE9PSB1bmRlZmluZWQpIHsKICAgICAgYnViYmxlLnRleHRDb250ZW50ID0gcmVhbHNbbS5pZF07CiAgICAgIGJ1YmJsZS5jbGFzc0xpc3QuYWRkKCJyZXZlYWxlZCIpOwogICAgfSBlbHNlIHsKICAgICAgYnViYmxlLnRleHRDb250ZW50ID0gbS50ZXh0OwogICAgfQoKICAgIGlubmVyLmFwcGVuZENoaWxkKGJ1YmJsZSk7CiAgICByb3cuYXBwZW5kQ2hpbGQoaW5uZXIpOwogICAgYm94LmFwcGVuZENoaWxkKHJvdyk7CiAgfQogIGJveC5zY3JvbGxUb3AgPSBib3guc2Nyb2xsSGVpZ2h0Owp9CgovLyAtLS0tLS0tLS0tIHNlbmQgLS0tLS0tLS0tLQpjb25zdCBpbnB1dCA9ICQoIiNpbnB1dCIpOwpjb25zdCBzZW5kQnRuID0gJCgiI3NlbmQiKTsKCmZ1bmN0aW9uIGF1dG9Hcm93KCkgewogIGlucHV0LnN0eWxlLmhlaWdodCA9ICJhdXRvIjsKICBpbnB1dC5zdHlsZS5oZWlnaHQgPSBNYXRoLm1pbihpbnB1dC5zY3JvbGxIZWlnaHQsIDE4MCkgKyAicHgiOwp9CmlucHV0LmFkZEV2ZW50TGlzdGVuZXIoImlucHV0IiwgYXV0b0dyb3cpOwoKYXN5bmMgZnVuY3Rpb24gc2VuZCgpIHsKICBjb25zdCB0ZXh0ID0gaW5wdXQudmFsdWUudHJpbSgpOwogIGlmICghdGV4dCkgcmV0dXJuOwogIGlucHV0LnZhbHVlID0gIiI7CiAgYXV0b0dyb3coKTsKICBhd2FpdCBmZXRjaCgiL3NlbmQiLCB7CiAgICBtZXRob2Q6ICJQT1NUIiwKICAgIGhlYWRlcnM6IHsgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiB9LAogICAgYm9keTogSlNPTi5zdHJpbmdpZnkoeyBpZDogTUUuaWQsIHRleHQgfSksCiAgfSk7CiAgLy8gV2UgaW50ZW50aW9uYWxseSBkbyBOT1QgZWNobyB0aGUgdHlwZWQgdGV4dCBsb2NhbGx5IOKAlCB3ZSB3YWl0IGZvciB0aGUKICAvLyBzZXJ2ZXIgdG8gcHVzaCBpdCBiYWNrLCBzbyBwZXJzb24gQSBzZWVzIHRoZSBzY3JhbWJsZWQgdmVyc2lvbiBldmVuIGZvcgogIC8vIHRoZWlyIG93biBtZXNzYWdlcy4KfQoKc2VuZEJ0bi5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsIHNlbmQpOwppbnB1dC5hZGRFdmVudExpc3RlbmVyKCJrZXlkb3duIiwgKGUpID0+IHsKICBpZiAoZS5rZXkgPT09ICJFbnRlciIgJiYgIWUuc2hpZnRLZXkpIHsgZS5wcmV2ZW50RGVmYXVsdCgpOyBzZW5kKCk7IH0KfSk7CgovLyAtLS0tLS0tLS0tIGhpZGRlbiByZXZlYWwgLS0tLS0tLS0tLQovLyBDbGlja2luZyB0aGUgIm1vZGVsIG5hbWUiIG9wZW5zIGEgc21hbGwgY29kZSBib3guIFRoaXMgaXMgZGVsaWJlcmF0ZWx5Ci8vIHVubGFiZWxlZCAvIGRpc2d1aXNlZCBhcyBhIG5vcm1hbCBwYXJ0IG9mIHRoZSBDaGF0R1BUIFVJLgpjb25zdCBtb2RlbEJ0biA9ICQoIiNtb2RlbC1zZWxlY3QiKTsKY29uc3QgcG9wID0gJCgiI3JldmVhbC1wb3AiKTsKY29uc3QgY29kZUlucHV0ID0gJCgiI3JldmVhbC1jb2RlIik7Cgptb2RlbEJ0bi5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsICgpID0+IHsKICAvLyBPbmx5IHBlcnNvbiBBIGhhcyBhbnl0aGluZyB0byByZXZlYWw7IGZvciBwZXJzb24gQiB0aGlzIGp1c3QgbG9va3MgaW5lcnQuCiAgcG9wLmNsYXNzTGlzdC50b2dnbGUoImhpZGRlbiIpOwogIGlmICghcG9wLmNsYXNzTGlzdC5jb250YWlucygiaGlkZGVuIikpIGNvZGVJbnB1dC5mb2N1cygpOwp9KTsKCmRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoImNsaWNrIiwgKGUpID0+IHsKICBpZiAoIXBvcC5jb250YWlucyhlLnRhcmdldCkgJiYgZS50YXJnZXQgIT09IG1vZGVsQnRuICYmICFtb2RlbEJ0bi5jb250YWlucyhlLnRhcmdldCkpIHsKICAgIHBvcC5jbGFzc0xpc3QuYWRkKCJoaWRkZW4iKTsKICB9Cn0pOwoKY29kZUlucHV0LmFkZEV2ZW50TGlzdGVuZXIoImtleWRvd24iLCBhc3luYyAoZSkgPT4gewogIGlmIChlLmtleSAhPT0gIkVudGVyIikgcmV0dXJuOwogIGNvbnN0IGNvZGUgPSBjb2RlSW5wdXQudmFsdWUudHJpbSgpOwogIGNvZGVJbnB1dC52YWx1ZSA9ICIiOwogIHBvcC5jbGFzc0xpc3QuYWRkKCJoaWRkZW4iKTsKCiAgY29uc3QgciA9IGF3YWl0IGZldGNoKCIvcmV2ZWFsIiwgewogICAgbWV0aG9kOiAiUE9TVCIsCiAgICBoZWFkZXJzOiB7ICJDb250ZW50LVR5cGUiOiAiYXBwbGljYXRpb24vanNvbiIgfSwKICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHsgaWQ6IE1FLmlkLCBjb2RlIH0pLAogIH0pOwogIGNvbnN0IGRhdGEgPSBhd2FpdCByLmpzb24oKTsKICBpZiAoIWRhdGEub2spIHJldHVybjsgLy8gd3JvbmcgY29kZTogc2lsZW50bHkgZG8gbm90aGluZwoKICByZWFscyA9IGRhdGEucmVhbHM7CiAgc3RhcnRSZXZlYWwoZGF0YS5zZWNvbmRzKTsKfSk7CgpmdW5jdGlvbiBzdGFydFJldmVhbChzZWNvbmRzKSB7CiAgcmV2ZWFsZWQgPSB0cnVlOwogIHJlbmRlcigpOwoKICBjb25zdCB0aW1lckVsID0gJCgiI3JldmVhbC10aW1lciIpOwogIHRpbWVyRWwuY2xhc3NMaXN0LnJlbW92ZSgiaGlkZGVuIik7CiAgbGV0IGxlZnQgPSBzZWNvbmRzOwogIHRpbWVyRWwudGV4dENvbnRlbnQgPSBsZWZ0ICsgInMiOwoKICBjbGVhckludGVydmFsKHJldmVhbFRpbWVyKTsKICByZXZlYWxUaW1lciA9IHNldEludGVydmFsKCgpID0+IHsKICAgIGxlZnQgLT0gMTsKICAgIGlmIChsZWZ0IDw9IDApIHsKICAgICAgY2xlYXJJbnRlcnZhbChyZXZlYWxUaW1lcik7CiAgICAgIHJldmVhbGVkID0gZmFsc2U7CiAgICAgIHJlYWxzID0ge307CiAgICAgIHRpbWVyRWwuY2xhc3NMaXN0LmFkZCgiaGlkZGVuIik7CiAgICAgIHJlbmRlcigpOwogICAgfSBlbHNlIHsKICAgICAgdGltZXJFbC50ZXh0Q29udGVudCA9IGxlZnQgKyAicyI7CiAgICB9CiAgfSwgMTAwMCk7Cn0KCjwvc2NyaXB0Pgo8L2JvZHk+CjwvaHRtbD4K", "base64").toString("utf8");
// ===========================================================================
// SERVER
// ===========================================================================
const messages = [];
const clients = [];
// ----- Saving messages (optional, via free Upstash Redis) ------------------
// Set these two environment variables on your host to turn on saving:
// UPSTASH_REDIS_REST_URL (from your Upstash database page)
// UPSTASH_REDIS_REST_TOKEN (from your Upstash database page)
// If they are not set, the app just keeps messages in memory (fine for
// running locally). Upstash's REST API is plain HTTP, so no extra installs.
const REDIS_URL = process.env.UPSTASH_REDIS_REST_URL || "";
const REDIS_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN || "";
const STORE_KEY = process.env.STORE_KEY || "covertchat:messages";
const persistence = Boolean(REDIS_URL && REDIS_TOKEN);
async function redisCommand(cmd) {
const res = await fetch(REDIS_URL, {
method: "POST",
headers: { Authorization: "Bearer " + REDIS_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(cmd),
});
if (!res.ok) throw new Error("Redis HTTP " + res.status);
return res.json();
}
async function loadMessages() {
if (!persistence) return;
try {
const out = await redisCommand(["GET", STORE_KEY]);
if (out && out.result) {
const arr = JSON.parse(out.result);
if (Array.isArray(arr)) { messages.length = 0; messages.push(...arr); }
}
console.log(" Loaded " + messages.length + " saved message(s).");
} catch (err) { console.error(" Could not load saved messages:", err.message); }
}
async function saveMessages() {
if (!persistence) return;
try { await redisCommand(["SET", STORE_KEY, JSON.stringify(messages)]); }
catch (err) { console.error(" Could not save messages:", err.message); }
}
function userFromId(id) { return config.users[(id || "").trim()] || null; }
function viewFor(role, m) {
return { id: m.id, senderName: m.senderName, ts: m.ts, mine: m.senderRole === role, text: role === "A" ? m.coverText : m.realText };
}
function sendEvent(client, event, payload) {
client.res.write("event: " + event + "\n");
client.res.write("data: " + JSON.stringify(payload) + "\n\n");
}
function broadcast(m) { for (const c of clients) sendEvent(c, "message", viewFor(c.role, m)); }
function readBody(req) {
return new Promise((resolve) => {
let data = ""; req.on("data", (c) => (data += c));
req.on("end", () => { try { resolve(JSON.parse(data || "{}")); } catch { resolve({}); } });
});
}
function json(res, code, obj) { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(obj)); }
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (req.method === "POST" && url.pathname === "/login") {
const body = await readBody(req);
const user = userFromId(body.id);
if (!user) return json(res, 401, { ok: false });
return json(res, 200, { ok: true, name: user.name, role: user.role });
}
if (req.method === "GET" && url.pathname === "/events") {
const user = userFromId(url.searchParams.get("id"));
if (!user) { res.writeHead(401); return res.end(); }
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
res.write("\n");
const client = { res, role: user.role };
clients.push(client);
for (const m of messages) sendEvent(client, "message", viewFor(user.role, m));
const ping = setInterval(() => res.write(": ping\n\n"), 25000);
req.on("close", () => { clearInterval(ping); const i = clients.indexOf(client); if (i >= 0) clients.splice(i, 1); });
return;
}
if (req.method === "POST" && url.pathname === "/send") {
const body = await readBody(req);
const user = userFromId(body.id);
const text = (body.text || "").trim();
if (!user || !text) return json(res, 400, { ok: false });
const coverText = await scramble(text, config.scramble);
const m = { id: crypto.randomUUID(), senderRole: user.role, senderName: user.name, realText: text, coverText, ts: Date.now() };
messages.push(m); broadcast(m);
await saveMessages();
return json(res, 200, { ok: true });
}
if (req.method === "POST" && url.pathname === "/reveal") {
const body = await readBody(req);
const user = userFromId(body.id);
if (!user || user.role !== "A") return json(res, 403, { ok: false });
if (String(body.code) !== String(config.revealCode)) return json(res, 401, { ok: false });
const reals = {};
for (const m of messages) reals[m.id] = m.realText;
return json(res, 200, { ok: true, reals, seconds: config.revealSeconds });
}
// Any other GET serves the app.
if (req.method === "GET") { res.writeHead(200, { "Content-Type": "text/html" }); return res.end(PAGE); }
res.writeHead(404); res.end("Not found");
});
let currentPort = Number(config.port) || 3000;
const basePort = currentPort;
server.on("listening", () => {
const port = server.address().port;
console.log("\n Chat is running. Open this in your browser:\n");
console.log(" http://localhost:" + port + "\n");
console.log(" Scramble mode: " + config.scramble.mode);
console.log(" Saving messages: " + (persistence ? "ON (Upstash)" : "off (in-memory only)"));
console.log(" Logins: " + Object.keys(config.users).map((k) => k + " (" + config.users[k].role + ")").join(", ") + "\n");
});
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
if (currentPort - basePort < 10) { console.log(" Port " + currentPort + " is busy — trying " + (currentPort + 1) + "..."); currentPort += 1; setTimeout(() => server.listen(currentPort), 150); }
else { console.error("\n Ports " + basePort + "-" + currentPort + " are all in use. Change \"port\" in the config at the top of this file.\n"); process.exit(1); }
} else { throw err; }
});
loadMessages().finally(() => server.listen(currentPort));