instbyte 1.10.0 → 1.11.1
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 +8 -22
- package/bin/instbyte.js +14 -6
- package/client/js/app.js +19 -14
- package/package.json +1 -1
- package/server/config.js +15 -6
- package/server/server.js +74 -27
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
npx instbyte
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
You're three feet from your teammate. *A quick share* still leaves your building before it reaches them.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Your chat app is for conversation. Your cloud storage is for files. Your email is for async.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
None of them are for *right now, on this network, gone tomorrow.*
|
|
29
|
+
|
|
30
|
+
That's what **Instbyte** is for.
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
@@ -191,27 +193,11 @@ The difference between *a tool you use* and *a tool you own.*
|
|
|
191
193
|
|
|
192
194
|
## Broadcasting
|
|
193
195
|
|
|
194
|
-
One person shares their screen — everyone else on the network watches live in their browser. No plugins, no accounts, no external services. Built on WebRTC
|
|
195
|
-
|
|
196
|
-
### How to broadcast
|
|
197
|
-
|
|
198
|
-
Click **📡 Broadcast** in the composer. Your browser will ask you to choose a screen, window, or tab to share. Once you pick one, a live bar appears at the top for all connected devices — teammates click **Join** to watch.
|
|
199
|
-
|
|
200
|
-
While broadcasting you can still use Instbyte normally — send text, drop files, switch channels. The broadcast runs in the background.
|
|
201
|
-
|
|
202
|
-
To stop, click **⏹ Stop** in the composer or use the browser's built-in "Stop sharing" bar.
|
|
203
|
-
|
|
204
|
-
### As a viewer
|
|
205
|
-
|
|
206
|
-
When a broadcast is live, a bar appears at the top of the page. Click **Join** to open the viewer panel. The panel is draggable and resizable — move it anywhere on your screen.
|
|
196
|
+
One person shares their screen — everyone else on the network watches live in their browser. No plugins, no accounts, no external services. Built on WebRTC.
|
|
207
197
|
|
|
208
|
-
|
|
209
|
-
- **✋ Raise hand** — notifies the broadcaster with a sound and toast
|
|
210
|
-
- **🔇 / 🔊** — mute and unmute audio
|
|
211
|
-
- **─** — minimize the panel without leaving the broadcast
|
|
212
|
-
- **✕** — leave the broadcast entirely
|
|
198
|
+
Viewers can save the current frame to a channel, raise a hand to notify the broadcaster, and toggle audio from the panel.
|
|
213
199
|
|
|
214
|
-
For HTTPS setup
|
|
200
|
+
For HTTPS setup and advanced network configuration, see the [Deployment Guide](docs/deployment.md#broadcasting).
|
|
215
201
|
|
|
216
202
|
---
|
|
217
203
|
|
package/bin/instbyte.js
CHANGED
|
@@ -11,17 +11,25 @@ const fs = require("fs");
|
|
|
11
11
|
// When run via npx or global install, we want data to live
|
|
12
12
|
// in the user's current working directory, not inside the
|
|
13
13
|
// npm cache or global node_modules.
|
|
14
|
+
//
|
|
15
|
+
// When run via Docker, INSTBYTE_DATA and INSTBYTE_UPLOADS may
|
|
16
|
+
// already be set via environment variables — respect those and
|
|
17
|
+
// don't override them.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
if (!process.env.INSTBYTE_DATA) {
|
|
20
|
+
process.env.INSTBYTE_DATA = path.join(process.cwd(), "instbyte-data");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!process.env.INSTBYTE_UPLOADS) {
|
|
24
|
+
process.env.INSTBYTE_UPLOADS = path.join(process.env.INSTBYTE_DATA, "uploads");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dataDir = process.env.INSTBYTE_DATA;
|
|
28
|
+
const uploadsDir = process.env.INSTBYTE_UPLOADS;
|
|
17
29
|
|
|
18
30
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
19
31
|
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
|
|
20
32
|
|
|
21
|
-
// Pass locations to the rest of the app via env vars
|
|
22
|
-
process.env.INSTBYTE_DATA = dataDir;
|
|
23
|
-
process.env.INSTBYTE_UPLOADS = uploadsDir;
|
|
24
|
-
|
|
25
33
|
// ========================
|
|
26
34
|
// BOOT
|
|
27
35
|
// ========================
|
package/client/js/app.js
CHANGED
|
@@ -425,6 +425,17 @@ async function togglePreview(id, filename) {
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
function safeCreateIcons(container) {
|
|
429
|
+
if (typeof lucide === "undefined") return;
|
|
430
|
+
requestAnimationFrame(() => {
|
|
431
|
+
if (container) {
|
|
432
|
+
lucide.createIcons({ nodes: [container] });
|
|
433
|
+
} else {
|
|
434
|
+
lucide.createIcons();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
428
439
|
function handleRowClick(el, type, value) {
|
|
429
440
|
if (type === "text") {
|
|
430
441
|
navigator.clipboard.writeText(value).then(() => {
|
|
@@ -720,11 +731,6 @@ function buildItemEl(i) {
|
|
|
720
731
|
<div class="preview-panel" id="preview-${i.id}"></div>`;
|
|
721
732
|
|
|
722
733
|
seenObserver.observe(div);
|
|
723
|
-
if (typeof lucide !== "undefined") {
|
|
724
|
-
lucide.createIcons({ nodes: [div] });
|
|
725
|
-
} else {
|
|
726
|
-
console.warn("Lucide not yet defined when building item", i.id);
|
|
727
|
-
}
|
|
728
734
|
return div;
|
|
729
735
|
}
|
|
730
736
|
|
|
@@ -896,18 +902,18 @@ function editContent(id) {
|
|
|
896
902
|
function render(data) {
|
|
897
903
|
const el = document.getElementById("items");
|
|
898
904
|
el.innerHTML = "";
|
|
899
|
-
|
|
900
905
|
if (!data.length) {
|
|
901
906
|
el.innerHTML = `<div class="empty-state">Nothing here yet — paste, type, or drop a file to share</div>`;
|
|
902
907
|
return;
|
|
903
908
|
}
|
|
904
|
-
|
|
905
909
|
data.forEach(i => el.appendChild(buildItemEl(i)));
|
|
910
|
+
safeCreateIcons(el);
|
|
906
911
|
}
|
|
907
912
|
|
|
908
913
|
function renderMore(data) {
|
|
909
914
|
const el = document.getElementById("items");
|
|
910
915
|
data.forEach(i => el.appendChild(buildItemEl(i)));
|
|
916
|
+
safeCreateIcons(el);
|
|
911
917
|
}
|
|
912
918
|
|
|
913
919
|
function renderGrouped(data) {
|
|
@@ -929,12 +935,13 @@ function renderGrouped(data) {
|
|
|
929
935
|
const section = document.createElement("div");
|
|
930
936
|
section.style.marginTop = "20px";
|
|
931
937
|
section.innerHTML = `
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
938
|
+
<div style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:8px;">
|
|
939
|
+
${ch.toUpperCase()}
|
|
940
|
+
</div>`;
|
|
935
941
|
grouped[ch].forEach(i => section.appendChild(buildItemEl(i)));
|
|
936
942
|
el.appendChild(section);
|
|
937
943
|
});
|
|
944
|
+
safeCreateIcons(el); // once after all sections appended, not inside the loop
|
|
938
945
|
}
|
|
939
946
|
|
|
940
947
|
async function sendText() {
|
|
@@ -1111,7 +1118,7 @@ socket.on("new-item", item => {
|
|
|
1111
1118
|
}
|
|
1112
1119
|
}
|
|
1113
1120
|
|
|
1114
|
-
|
|
1121
|
+
safeCreateIcons(el);
|
|
1115
1122
|
if (item.uploader !== uploader) playChime();
|
|
1116
1123
|
|
|
1117
1124
|
} else if (item.uploader !== uploader) {
|
|
@@ -1378,9 +1385,7 @@ async function uploadFiles(files, overrideChannel) {
|
|
|
1378
1385
|
|
|
1379
1386
|
status.style.display = "none";
|
|
1380
1387
|
fileInput.value = "";
|
|
1381
|
-
//
|
|
1382
|
-
// but this catches any case where the socket path didn't fire (e.g. network hiccup).
|
|
1383
|
-
setTimeout(() => load(), 500);
|
|
1388
|
+
// socket new-item handles DOM update in real time — no reload needed
|
|
1384
1389
|
}
|
|
1385
1390
|
|
|
1386
1391
|
|
package/package.json
CHANGED
package/server/config.js
CHANGED
|
@@ -43,12 +43,21 @@ function loadConfig() {
|
|
|
43
43
|
let userConfig = {};
|
|
44
44
|
|
|
45
45
|
if (fs.existsSync(configPath)) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
if (fs.statSync(configPath).isDirectory()) {
|
|
47
|
+
console.error(
|
|
48
|
+
"Error: instbyte.config.json is a directory, not a file.\n" +
|
|
49
|
+
"This usually happens in Docker when the config file doesn't exist on the host before the container starts.\n" +
|
|
50
|
+
"Fix: stop the container, run `rm -rf instbyte.config.json && touch instbyte.config.json`, then start again.\n" +
|
|
51
|
+
"Using defaults for now."
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
56
|
+
userConfig = JSON.parse(raw);
|
|
57
|
+
console.log("Config loaded from instbyte.config.json");
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn("Warning: instbyte.config.json is invalid JSON, using defaults.");
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
|
package/server/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const db = require("./db");
|
|
|
19
19
|
|
|
20
20
|
const config = require("./config");
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
const UPLOADS_DIR = process.env.INSTBYTE_UPLOADS
|
|
23
24
|
|| path.join(__dirname, "../uploads");
|
|
24
25
|
|
|
@@ -63,8 +64,16 @@ app.use(helmet({
|
|
|
63
64
|
|
|
64
65
|
app.use((req, res, next) => {
|
|
65
66
|
if (req.path === '/upload') return next();
|
|
67
|
+
|
|
68
|
+
const ct = req.headers['content-type'] || '';
|
|
69
|
+
|
|
70
|
+
if (ct.startsWith('text/plain')) {
|
|
71
|
+
return express.text({ limit: '10mb' })(req, res, next);
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
express.json()(req, res, next);
|
|
67
75
|
});
|
|
76
|
+
|
|
68
77
|
app.use(cookieParser());
|
|
69
78
|
app.use(requireAuth);
|
|
70
79
|
app.use("/uploads", (req, res, next) => {
|
|
@@ -225,19 +234,28 @@ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
|
225
234
|
const sessions = new Map();
|
|
226
235
|
|
|
227
236
|
function requireAuth(req, res, next) {
|
|
228
|
-
if (!config.auth.passphrase) return next();
|
|
237
|
+
if (!config.auth.passphrase) return next();
|
|
229
238
|
|
|
230
|
-
// Allow the login route itself through
|
|
231
239
|
if (req.path === "/login" || req.path === "/info" || req.path === "/health") return next();
|
|
232
240
|
|
|
241
|
+
// Terminal / API clients — accept passphrase via header
|
|
242
|
+
const headerPass = req.headers["x-passphrase"];
|
|
243
|
+
if (headerPass && headerPass === config.auth.passphrase) return next();
|
|
233
244
|
|
|
234
|
-
//
|
|
245
|
+
// Browser clients — check session cookie
|
|
235
246
|
const cookie = req.cookies[COOKIE_NAME];
|
|
236
247
|
if (cookie && sessions.has(cookie)) return next();
|
|
237
248
|
|
|
238
|
-
// Not authenticated
|
|
239
249
|
if (req.path.startsWith("/socket.io")) return next();
|
|
240
|
-
|
|
250
|
+
|
|
251
|
+
// Return JSON 401 for any non-browser request
|
|
252
|
+
const ct = req.headers["content-type"] || "";
|
|
253
|
+
const isApiRequest = ct.startsWith("application/json") ||
|
|
254
|
+
ct.startsWith("text/plain") ||
|
|
255
|
+
req.xhr ||
|
|
256
|
+
req.headers["x-passphrase"] !== undefined;
|
|
257
|
+
|
|
258
|
+
if (isApiRequest) {
|
|
241
259
|
return res.status(401).json({ error: "Unauthorized" });
|
|
242
260
|
}
|
|
243
261
|
|
|
@@ -453,9 +471,27 @@ app.use((err, req, res, next) => {
|
|
|
453
471
|
next(err);
|
|
454
472
|
});
|
|
455
473
|
|
|
456
|
-
/* TEXT/LINK */
|
|
457
|
-
|
|
458
|
-
|
|
474
|
+
/* TEXT/LINK — accepts application/json and text/plain */
|
|
475
|
+
function handleTextPost(req, res) {
|
|
476
|
+
let content, channel, uploader;
|
|
477
|
+
|
|
478
|
+
if (req.is("text/plain")) {
|
|
479
|
+
// Terminal path — body is raw string, metadata comes from headers
|
|
480
|
+
content = typeof req.body === "string" ? req.body : String(req.body);
|
|
481
|
+
channel = (req.headers["x-channel"] || "general").trim();
|
|
482
|
+
uploader = (req.headers["x-uploader"] || "terminal").trim();
|
|
483
|
+
} else {
|
|
484
|
+
// Browser / JSON path — existing behaviour unchanged
|
|
485
|
+
({ content, channel, uploader } = req.body || {});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!content || !content.trim()) {
|
|
489
|
+
return res.status(400).json({ error: "Content is required" });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!channel) {
|
|
493
|
+
return res.status(400).json({ error: "Channel is required" });
|
|
494
|
+
}
|
|
459
495
|
|
|
460
496
|
const item = {
|
|
461
497
|
type: "text",
|
|
@@ -476,7 +512,11 @@ app.post("/text", dropLimiter, (req, res) => {
|
|
|
476
512
|
res.json(item);
|
|
477
513
|
}
|
|
478
514
|
);
|
|
479
|
-
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
app.post("/text", dropLimiter, handleTextPost);
|
|
518
|
+
app.post("/push", dropLimiter, handleTextPost); // terminal-friendly alias
|
|
519
|
+
|
|
480
520
|
|
|
481
521
|
/* DELETE ITEM */
|
|
482
522
|
app.delete("/item/:id", (req, res) => {
|
|
@@ -1037,14 +1077,18 @@ function getLocalIP() {
|
|
|
1037
1077
|
return preferred ? preferred.address : "localhost";
|
|
1038
1078
|
}
|
|
1039
1079
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1080
|
+
|
|
1081
|
+
function listenOnFreePort(server, preferredPort) {
|
|
1082
|
+
return new Promise((resolve, reject) => {
|
|
1083
|
+
server.listen(preferredPort, () => resolve(server.address().port));
|
|
1084
|
+
server.on('error', (err) => {
|
|
1085
|
+
if (err.code === 'EADDRINUSE') {
|
|
1086
|
+
server.removeAllListeners('error');
|
|
1087
|
+
listenOnFreePort(server, preferredPort + 1).then(resolve).catch(reject);
|
|
1088
|
+
} else {
|
|
1089
|
+
reject(err);
|
|
1090
|
+
}
|
|
1046
1091
|
});
|
|
1047
|
-
srv.on("error", () => resolve(findFreePort(start + 1)));
|
|
1048
1092
|
});
|
|
1049
1093
|
}
|
|
1050
1094
|
|
|
@@ -1053,19 +1097,22 @@ const PREFERRED = parseInt(process.env.PORT) || config.server.port;
|
|
|
1053
1097
|
const localIP = getLocalIP();
|
|
1054
1098
|
|
|
1055
1099
|
let PORT;
|
|
1100
|
+
|
|
1056
1101
|
if (process.env.INSTBYTE_BOOT === '1') {
|
|
1057
|
-
|
|
1102
|
+
listenOnFreePort(server, PREFERRED).then(async p => {
|
|
1058
1103
|
PORT = p;
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1104
|
+
console.log("\nInstbyte running");
|
|
1105
|
+
console.log("Local: http://localhost:" + PORT);
|
|
1106
|
+
console.log("Network: http://" + localIP + ":" + PORT);
|
|
1107
|
+
if (PORT !== PREFERRED) {
|
|
1108
|
+
console.log(`(port ${PREFERRED} was busy, switched to ${PORT})`);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
console.log("");
|
|
1112
|
+
scanOrphans();
|
|
1113
|
+
}).catch(err => {
|
|
1114
|
+
console.error("Failed to start server:", err.message);
|
|
1115
|
+
process.exit(1);
|
|
1069
1116
|
});
|
|
1070
1117
|
}
|
|
1071
1118
|
|