@decentnetwork/beagle 0.1.38 → 0.1.39

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.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.37";
1
+ window.__DK_UI_VERSION="0.1.38";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
package/dist/server.js CHANGED
@@ -305,6 +305,39 @@ async function agentnetRestart() {
305
305
  }
306
306
  return { ok: false, error: lastErr };
307
307
  }
308
+ // ── saved-files locations ───────────────────────────────────────────────────
309
+ // The daemon (often running as root) owns downloads/ — on such machines this
310
+ // user-level process gets EACCES writing there. Files WE save (sent copies,
311
+ // browser-WebRTC receives) then go to a sibling `sent/` dir we own; readers
312
+ // (open / download / reveal) look in downloads first, then sent.
313
+ function sentDirFor(downloadsDir) {
314
+ return join(dirname(downloadsDir), "sent");
315
+ }
316
+ async function pickWritableSaveDir(downloadsDir) {
317
+ const fsp = await import("node:fs/promises");
318
+ try {
319
+ await fsp.mkdir(downloadsDir, { recursive: true });
320
+ await fsp.access(downloadsDir, (await import("node:fs")).constants.W_OK);
321
+ return downloadsDir;
322
+ }
323
+ catch {
324
+ const alt = sentDirFor(downloadsDir);
325
+ await fsp.mkdir(alt, { recursive: true });
326
+ return alt;
327
+ }
328
+ }
329
+ /** Resolve a saved file by name across downloads/ and sent/, with a
330
+ * path-traversal guard. Returns null when absent from both. */
331
+ function findSavedFile(downloadsDir, name) {
332
+ if (!name)
333
+ return null;
334
+ for (const dir of [downloadsDir, sentDirFor(downloadsDir)]) {
335
+ const f = join(dir, name);
336
+ if (f.startsWith(dir) && existsSync(f))
337
+ return f;
338
+ }
339
+ return null;
340
+ }
308
341
  /** True only when the request came from the local machine. Used to gate the
309
342
  * "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
310
343
  * identity signing to other hosts. The popup always runs in the local user's
@@ -977,8 +1010,7 @@ export function startBeagleServer(opts) {
977
1010
  const keep = !!opts.downloadsDir;
978
1011
  let destDir;
979
1012
  if (keep) {
980
- destDir = opts.downloadsDir;
981
- await fsp.mkdir(destDir, { recursive: true });
1013
+ destDir = await pickWritableSaveDir(opts.downloadsDir);
982
1014
  }
983
1015
  else {
984
1016
  destDir = await fsp.mkdtemp(join(os.tmpdir(), "agentnet-up-"));
@@ -1036,16 +1068,15 @@ export function startBeagleServer(opts) {
1036
1068
  return;
1037
1069
  }
1038
1070
  const fs = await import("node:fs");
1039
- const fsp = await import("node:fs/promises");
1040
- await fsp.mkdir(opts.downloadsDir, { recursive: true });
1071
+ const saveDir = await pickWritableSaveDir(opts.downloadsDir);
1041
1072
  let finalName = safe;
1042
- let finalPath = join(opts.downloadsDir, finalName);
1073
+ let finalPath = join(saveDir, finalName);
1043
1074
  const dot = safe.lastIndexOf(".");
1044
1075
  const base = dot > 0 ? safe.slice(0, dot) : safe;
1045
1076
  const ext = dot > 0 ? safe.slice(dot) : "";
1046
1077
  for (let i = 1; existsSync(finalPath); i++) {
1047
1078
  finalName = `${base}-${i}${ext}`;
1048
- finalPath = join(opts.downloadsDir, finalName);
1079
+ finalPath = join(saveDir, finalName);
1049
1080
  }
1050
1081
  try {
1051
1082
  await new Promise((resolve2, reject) => {
@@ -1088,8 +1119,8 @@ export function startBeagleServer(opts) {
1088
1119
  }
1089
1120
  const body = await readBody(req);
1090
1121
  const name = (typeof body.name === "string" ? body.name : "").replace(/[/\\]/g, "");
1091
- const file = join(opts.downloadsDir, name);
1092
- if (!name || !file.startsWith(opts.downloadsDir) || !existsSync(file)) {
1122
+ const file = findSavedFile(opts.downloadsDir, name);
1123
+ if (!file) {
1093
1124
  sendJson(res, 404, { ok: false, error: "not found" });
1094
1125
  return;
1095
1126
  }
@@ -1105,9 +1136,9 @@ export function startBeagleServer(opts) {
1105
1136
  }
1106
1137
  const q = new URL(req.url || "", "http://x").searchParams;
1107
1138
  const name = (q.get("name") || "").replace(/[/\\]/g, "");
1108
- const file = join(opts.downloadsDir, name);
1109
- // Path-traversal guard: the resolved path must stay under downloadsDir.
1110
- if (!name || !file.startsWith(opts.downloadsDir) || !existsSync(file)) {
1139
+ // Looks in downloads/ then sent/ (traversal-guarded in the helper).
1140
+ const file = findSavedFile(opts.downloadsDir, name);
1141
+ if (!file) {
1111
1142
  res.writeHead(404);
1112
1143
  res.end("not found");
1113
1144
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",