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 CHANGED
@@ -21,11 +21,13 @@
21
21
  npx instbyte
22
22
  ```
23
23
 
24
- **Instbyte** is a high-speed, real-time, short-lived LAN sharing utility built for teams and developers who need to move snippets, links, files, and structured notes across devices instantly — without cloud accounts, logins, or external services.
24
+ You're three feet from your teammate. *A quick share* still leaves your building before it reaches them.
25
25
 
26
- It operates entirely on your local network, acting as a lightweight "digital dead-drop" for frictionless collaboration.
26
+ Your chat app is for conversation. Your cloud storage is for files. Your email is for async.
27
27
 
28
- Instbyte can also be fully white-labelled the difference between ***a tool you use*** and ***a tool you own***.
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 for smooth, low-latency video.
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
- - **📸 Capture** — saves the current frame as an image to the active channel
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, enabling broadcast for all devices, and advanced network configuration, see the [Deployment Guide](docs/deployment.md#broadcasting).
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
- const dataDir = path.join(process.cwd(), "instbyte-data");
16
- const uploadsDir = path.join(dataDir, "uploads");
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
- <div style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:8px;">
933
- ${ch.toUpperCase()}
934
- </div>`;
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
- if (typeof lucide !== "undefined") lucide.createIcons({ nodes: [el] });
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
- // Delayed fallback socket event should have already updated the DOM by now,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "A self-hosted LAN sharing utility for fast, frictionless file, link, and snippet exchange across devices — no cloud required.",
5
5
  "main": "bin/instbyte.js",
6
6
  "bin": {
package/server/config.js CHANGED
@@ -43,12 +43,21 @@ function loadConfig() {
43
43
  let userConfig = {};
44
44
 
45
45
  if (fs.existsSync(configPath)) {
46
- try {
47
- const raw = fs.readFileSync(configPath, "utf-8");
48
- userConfig = JSON.parse(raw);
49
- console.log("Config loaded from instbyte.config.json");
50
- } catch (e) {
51
- console.warn("Warning: instbyte.config.json is invalid JSON, using defaults.");
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(); // no passphrase set, skip
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
- // Check cookie holds a valid session token
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
- if (req.headers["content-type"] === "application/json" || req.xhr) {
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
- app.post("/text", dropLimiter, (req, res) => {
458
- const { content, channel, uploader } = req.body;
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
- function findFreePort(start) {
1041
- return new Promise((resolve) => {
1042
- const srv = net.createServer();
1043
- srv.listen(start, () => {
1044
- const port = srv.address().port;
1045
- srv.close(() => resolve(port));
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
- findFreePort(PREFERRED).then(p => {
1102
+ listenOnFreePort(server, PREFERRED).then(async p => {
1058
1103
  PORT = p;
1059
- server.listen(PORT, () => {
1060
- console.log("\nInstbyte running");
1061
- console.log("Local: http://localhost:" + PORT);
1062
- console.log("Network: http://" + localIP + ":" + PORT);
1063
- if (PORT !== PREFERRED) {
1064
- console.log(`(port ${PREFERRED} was busy, switched to ${PORT})`);
1065
- }
1066
- console.log("");
1067
- scanOrphans();
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