@decentnetwork/beagle 0.1.38 → 0.1.40

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.39";
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"/>',
@@ -1882,7 +1882,7 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
1882
1882
  return null;
1883
1883
  }), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
1884
1884
  }
1885
- function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onCall, onFormSubmit, answered, selMode, selected, onToggleSel, busy, isGroup }) {
1885
+ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onOpenFile, onCall, onFormSubmit, answered, selMode, selected, onToggleSel, busy, isGroup }) {
1886
1886
  const mine = m.from === "me";
1887
1887
  const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
1888
1888
  const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
@@ -2061,7 +2061,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
2061
2061
  {
2062
2062
  style: { ...cardStyle, cursor: "pointer", opacity: btnBusy ? 0.85 : 1 },
2063
2063
  title: T.open || "open",
2064
- onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
2064
+ onClick: () => onOpenFile ? onOpenFile(m.file.name) : window.open(dkFileUrl(m.file.name), "_blank", "noopener")
2065
2065
  },
2066
2066
  icon,
2067
2067
  meta,
@@ -2124,7 +2124,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
2124
2124
  {
2125
2125
  style: cardStyle,
2126
2126
  title: "open",
2127
- onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
2127
+ onClick: () => onOpenFile ? onOpenFile(m.file.name) : window.open(dkFileUrl(m.file.name), "_blank", "noopener")
2128
2128
  },
2129
2129
  icon,
2130
2130
  meta,
@@ -2309,10 +2309,21 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
2309
2309
  return;
2310
2310
  dkApi.revealFile(name).then((r) => {
2311
2311
  if (!r || r.ok === false) {
2312
- showFlash("err", (lang === "zh" ? "\u65E0\u6CD5\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A: " : "Could not reveal file: ") + (r && r.error || ""));
2312
+ const missing = /not found/i.test(r && r.error || "");
2313
+ showFlash("err", missing ? lang === "zh" ? "\u672C\u5730\u6CA1\u6709\u8FD9\u4E2A\u6587\u4EF6\u7684\u526F\u672C(\u5B83\u662F\u65E7\u7248\u672C\u53D1\u9001\u7684,\u5F53\u65F6\u53D1\u5B8C\u5373\u5220;\u65B0\u53D1\u9001\u7684\u6587\u4EF6\u90FD\u4F1A\u4FDD\u7559)" : "No local copy of this file \u2014 it was sent by an older version that did not keep sent copies. Files sent from now on are kept." : (lang === "zh" ? "\u65E0\u6CD5\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A: " : "Could not reveal file: ") + (r && r.error || ""));
2313
2314
  }
2314
2315
  });
2315
2316
  };
2317
+ const doOpenFile = (name) => {
2318
+ if (!name)
2319
+ return;
2320
+ fetch(dkFileDownloadUrl(name), { method: "GET", headers: { range: "bytes=0-0" } }).then((r) => {
2321
+ if (r.ok || r.status === 206)
2322
+ window.open(dkFileUrl(name), "_blank", "noopener");
2323
+ else
2324
+ showFlash("err", lang === "zh" ? "\u672C\u5730\u6CA1\u6709\u8FD9\u4E2A\u6587\u4EF6\u7684\u526F\u672C(\u65E7\u7248\u672C\u53D1\u9001\u7684\u6587\u4EF6\u672A\u4FDD\u7559;\u65B0\u53D1\u9001\u7684\u90FD\u4F1A\u4FDD\u7559)" : "No local copy \u2014 sent before local copies were kept.");
2325
+ }).catch(() => showFlash("err", lang === "zh" ? "\u6253\u5F00\u5931\u8D25" : "Could not open the file"));
2326
+ };
2316
2327
  const doCancel = (id) => {
2317
2328
  if (!id || fileBusy[id])
2318
2329
  return;
@@ -2525,6 +2536,7 @@ ${peer.address}`
2525
2536
  onCancel: doCancel,
2526
2537
  onRetry: doRetry,
2527
2538
  onReveal: doReveal,
2539
+ onOpenFile: doOpenFile,
2528
2540
  onCall,
2529
2541
  onFormSubmit: () => onReloadThread && onReloadThread(),
2530
2542
  answered,
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.40",
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",