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 +8 -22
- package/bin/instbyte.js +14 -6
- package/package.json +2 -1
- package/server/config.js +15 -6
- package/server/server.js +163 -25
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instbyte",
|
|
3
|
-
"version": "1.
|
|
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
|
-
|
|
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,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();
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1161
|
+
listenOnFreePort(server, PREFERRED).then(async p => {
|
|
1058
1162
|
PORT = p;
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
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");
|