instbyte 1.10.0 → 1.11.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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
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": {
@@ -43,6 +43,7 @@
43
43
  "express-rate-limit": "^7.1.5",
44
44
  "helmet": "^8.1.0",
45
45
  "multer": "^2.0.2",
46
+ "multicast-dns": "^7.2.5",
46
47
  "sharp": "^0.33.2",
47
48
  "socket.io": "^4.6.1",
48
49
  "sqlite3": "^5.1.6"
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,9 @@ const db = require("./db");
19
19
 
20
20
  const config = require("./config");
21
21
 
22
+ let mdns = null;
23
+ try { mdns = require('multicast-dns'); } catch (e) { }
24
+
22
25
  const UPLOADS_DIR = process.env.INSTBYTE_UPLOADS
23
26
  || path.join(__dirname, "../uploads");
24
27
 
@@ -63,8 +66,16 @@ app.use(helmet({
63
66
 
64
67
  app.use((req, res, next) => {
65
68
  if (req.path === '/upload') return next();
69
+
70
+ const ct = req.headers['content-type'] || '';
71
+
72
+ if (ct.startsWith('text/plain')) {
73
+ return express.text({ limit: '10mb' })(req, res, next);
74
+ }
75
+
66
76
  express.json()(req, res, next);
67
77
  });
78
+
68
79
  app.use(cookieParser());
69
80
  app.use(requireAuth);
70
81
  app.use("/uploads", (req, res, next) => {
@@ -225,19 +236,28 @@ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
225
236
  const sessions = new Map();
226
237
 
227
238
  function requireAuth(req, res, next) {
228
- if (!config.auth.passphrase) return next(); // no passphrase set, skip
239
+ if (!config.auth.passphrase) return next();
229
240
 
230
- // Allow the login route itself through
231
241
  if (req.path === "/login" || req.path === "/info" || req.path === "/health") return next();
232
242
 
243
+ // Terminal / API clients — accept passphrase via header
244
+ const headerPass = req.headers["x-passphrase"];
245
+ if (headerPass && headerPass === config.auth.passphrase) return next();
233
246
 
234
- // Check cookie holds a valid session token
247
+ // Browser clients check session cookie
235
248
  const cookie = req.cookies[COOKIE_NAME];
236
249
  if (cookie && sessions.has(cookie)) return next();
237
250
 
238
- // Not authenticated
239
251
  if (req.path.startsWith("/socket.io")) return next();
240
- if (req.headers["content-type"] === "application/json" || req.xhr) {
252
+
253
+ // Return JSON 401 for any non-browser request
254
+ const ct = req.headers["content-type"] || "";
255
+ const isApiRequest = ct.startsWith("application/json") ||
256
+ ct.startsWith("text/plain") ||
257
+ req.xhr ||
258
+ req.headers["x-passphrase"] !== undefined;
259
+
260
+ if (isApiRequest) {
241
261
  return res.status(401).json({ error: "Unauthorized" });
242
262
  }
243
263
 
@@ -453,9 +473,27 @@ app.use((err, req, res, next) => {
453
473
  next(err);
454
474
  });
455
475
 
456
- /* TEXT/LINK */
457
- app.post("/text", dropLimiter, (req, res) => {
458
- const { content, channel, uploader } = req.body;
476
+ /* TEXT/LINK — accepts application/json and text/plain */
477
+ function handleTextPost(req, res) {
478
+ let content, channel, uploader;
479
+
480
+ if (req.is("text/plain")) {
481
+ // Terminal path — body is raw string, metadata comes from headers
482
+ content = typeof req.body === "string" ? req.body : String(req.body);
483
+ channel = (req.headers["x-channel"] || "general").trim();
484
+ uploader = (req.headers["x-uploader"] || "terminal").trim();
485
+ } else {
486
+ // Browser / JSON path — existing behaviour unchanged
487
+ ({ content, channel, uploader } = req.body || {});
488
+ }
489
+
490
+ if (!content || !content.trim()) {
491
+ return res.status(400).json({ error: "Content is required" });
492
+ }
493
+
494
+ if (!channel) {
495
+ return res.status(400).json({ error: "Channel is required" });
496
+ }
459
497
 
460
498
  const item = {
461
499
  type: "text",
@@ -476,7 +514,11 @@ app.post("/text", dropLimiter, (req, res) => {
476
514
  res.json(item);
477
515
  }
478
516
  );
479
- });
517
+ }
518
+
519
+ app.post("/text", dropLimiter, handleTextPost);
520
+ app.post("/push", dropLimiter, handleTextPost); // terminal-friendly alias
521
+
480
522
 
481
523
  /* DELETE ITEM */
482
524
  app.delete("/item/:id", (req, res) => {
@@ -1037,14 +1079,74 @@ function getLocalIP() {
1037
1079
  return preferred ? preferred.address : "localhost";
1038
1080
  }
1039
1081
 
1040
- function findFreePort(start) {
1082
+
1083
+ function getMdnsName() {
1084
+ const appName = config.branding && config.branding.appName;
1085
+ if (appName && appName.toLowerCase() !== 'instbyte') {
1086
+ return appName.toLowerCase().replace(/\s+/g, '-') + '.local';
1087
+ }
1088
+ return 'instbyte.local';
1089
+ }
1090
+
1091
+ function getHostnameFallback() {
1092
+ return os.hostname().toLowerCase() + '.local';
1093
+ }
1094
+
1095
+ function probeMdns(name) {
1041
1096
  return new Promise((resolve) => {
1042
- const srv = net.createServer();
1043
- srv.listen(start, () => {
1044
- const port = srv.address().port;
1045
- srv.close(() => resolve(port));
1097
+ if (!mdns) return resolve(false);
1098
+ const m = mdns();
1099
+ const timer = setTimeout(() => {
1100
+ m.destroy();
1101
+ resolve(false); // no response = name is free
1102
+ }, 2000);
1103
+
1104
+ m.on('response', (response) => {
1105
+ const taken = response.answers.some(a =>
1106
+ a.name === name && a.type === 'A'
1107
+ );
1108
+ if (taken) {
1109
+ clearTimeout(timer);
1110
+ m.destroy();
1111
+ resolve(true); // name is taken
1112
+ }
1113
+ });
1114
+
1115
+ m.query({ questions: [{ name, type: 'A' }] });
1116
+ });
1117
+ }
1118
+
1119
+ function startMdnsAdvertise(name, ip) {
1120
+ if (!mdns) return null;
1121
+ try {
1122
+ const m = mdns();
1123
+ m.on('query', (query) => {
1124
+ query.questions.forEach(q => {
1125
+ if (q.name === name && q.type === 'A') {
1126
+ m.respond({
1127
+ answers: [{ name, type: 'A', ttl: 300, data: ip }]
1128
+ });
1129
+ }
1130
+ });
1131
+ });
1132
+ return m;
1133
+ } catch (e) {
1134
+ return null;
1135
+ }
1136
+ }
1137
+
1138
+
1139
+ function listenOnFreePort(server, preferredPort) {
1140
+ return new Promise((resolve, reject) => {
1141
+ server.listen(preferredPort, () => resolve(server.address().port));
1142
+ server.on('error', (err) => {
1143
+ if (err.code === 'EADDRINUSE') {
1144
+ server.removeAllListeners('error');
1145
+ listenOnFreePort(server, preferredPort + 1).then(resolve).catch(reject);
1146
+ } else {
1147
+ reject(err);
1148
+ }
1046
1149
  });
1047
- srv.on("error", () => resolve(findFreePort(start + 1)));
1048
1150
  });
1049
1151
  }
1050
1152
 
@@ -1053,19 +1155,50 @@ const PREFERRED = parseInt(process.env.PORT) || config.server.port;
1053
1155
  const localIP = getLocalIP();
1054
1156
 
1055
1157
  let PORT;
1158
+ let mdnsAdvertiser = null;
1159
+
1056
1160
  if (process.env.INSTBYTE_BOOT === '1') {
1057
- findFreePort(PREFERRED).then(p => {
1161
+ listenOnFreePort(server, PREFERRED).then(async p => {
1058
1162
  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})`);
1163
+ console.log("\nInstbyte running");
1164
+ console.log("Local: http://localhost:" + PORT);
1165
+ console.log("Network: http://" + localIP + ":" + PORT);
1166
+ if (PORT !== PREFERRED) {
1167
+ console.log(`(port ${PREFERRED} was busy, switched to ${PORT})`);
1168
+ }
1169
+
1170
+ // mDNS
1171
+ if (mdns) {
1172
+ try {
1173
+ const preferredName = getMdnsName();
1174
+ const isTaken = await probeMdns(preferredName);
1175
+
1176
+ let mdnsName;
1177
+ if (isTaken) {
1178
+ mdnsName = getHostnameFallback();
1179
+ if (os.platform() !== 'win32') {
1180
+ console.log(`mDNS: http://${preferredName} already in use on this network`);
1181
+ console.log(`mDNS: http://${mdnsName}`);
1182
+ }
1183
+ } else {
1184
+ mdnsName = preferredName;
1185
+ if (os.platform() !== 'win32') {
1186
+ console.log(`mDNS: http://${mdnsName}`);
1187
+ }
1188
+ }
1189
+
1190
+ mdnsAdvertiser = startMdnsAdvertise(mdnsName, localIP);
1191
+
1192
+ } catch (e) {
1193
+ console.log("mDNS: unavailable on this network — use IP or QR code to join");
1065
1194
  }
1066
- console.log("");
1067
- scanOrphans();
1068
- });
1195
+ }
1196
+
1197
+ console.log("");
1198
+ scanOrphans();
1199
+ }).catch(err => {
1200
+ console.error("Failed to start server:", err.message);
1201
+ process.exit(1);
1069
1202
  });
1070
1203
  }
1071
1204
 
@@ -1078,6 +1211,11 @@ module.exports = { app, server, sessions };
1078
1211
  function shutdown(signal) {
1079
1212
  console.log(`\n${signal} received — shutting down gracefully...`);
1080
1213
 
1214
+ // destroy mDNS advertiser if running
1215
+ if (typeof mdnsAdvertiser !== 'undefined' && mdnsAdvertiser) {
1216
+ try { mdnsAdvertiser.destroy(); } catch (e) { }
1217
+ }
1218
+
1081
1219
  // stop accepting new connections
1082
1220
  server.close(() => {
1083
1221
  console.log("HTTP server closed");