@decentnetwork/lan 0.1.277 → 0.1.279
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/dist/carrier/peer-manager.d.ts +5 -2
- package/dist/carrier/peer-manager.js +6 -2
- package/dist/cli/commands.d.ts +14 -0
- package/dist/cli/commands.js +48 -0
- package/dist/cli/index.js +6 -1
- package/dist/daemon/server.js +41 -4
- package/dist/ui/desktop/app.js +1 -1
- package/dist/utils/config-owner.d.ts +42 -0
- package/dist/utils/config-owner.js +103 -0
- package/package.json +2 -2
|
@@ -95,8 +95,11 @@ export declare class PeerManager extends EventEmitter {
|
|
|
95
95
|
sendAppPacket(userid: string, id: number, data: Uint8Array): Promise<void>;
|
|
96
96
|
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
97
97
|
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
98
|
-
* receive online. Use for native friends; JS friends keep sendFile().
|
|
99
|
-
|
|
98
|
+
* receive online. Use for native friends; JS friends keep sendFile().
|
|
99
|
+
* Returns the SDK's delivery verdict: "acked" is the only outcome that
|
|
100
|
+
* justifies a "sent" checkmark. Older SDKs (< 0.1.140) resolve void —
|
|
101
|
+
* treat that as "accepted" (bytes out, delivery unproven). */
|
|
102
|
+
sendInlineFile(userid: string, data: Uint8Array, name: string): Promise<"acked" | "accepted" | "offline">;
|
|
100
103
|
/**
|
|
101
104
|
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
102
105
|
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
@@ -186,11 +186,15 @@ export class PeerManager extends EventEmitter {
|
|
|
186
186
|
}
|
|
187
187
|
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
188
188
|
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
189
|
-
* receive online. Use for native friends; JS friends keep sendFile().
|
|
189
|
+
* receive online. Use for native friends; JS friends keep sendFile().
|
|
190
|
+
* Returns the SDK's delivery verdict: "acked" is the only outcome that
|
|
191
|
+
* justifies a "sent" checkmark. Older SDKs (< 0.1.140) resolve void —
|
|
192
|
+
* treat that as "accepted" (bytes out, delivery unproven). */
|
|
190
193
|
async sendInlineFile(userid, data, name) {
|
|
191
194
|
if (!this.peer)
|
|
192
195
|
throw new Error("Peer not created. Call create() first.");
|
|
193
|
-
await this.peer.sendInlineFile(userid, { name, data });
|
|
196
|
+
const r = (await this.peer.sendInlineFile(userid, { name, data }));
|
|
197
|
+
return r?.delivery ?? "accepted";
|
|
194
198
|
}
|
|
195
199
|
/**
|
|
196
200
|
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -533,6 +533,20 @@ export declare function cmdServiceRestart(args: {
|
|
|
533
533
|
* the new code without a sudo prompt — the daemon already has the
|
|
534
534
|
* privileges it needs to rebind the TUN.
|
|
535
535
|
*/
|
|
536
|
+
/**
|
|
537
|
+
* One-shot repair for the classic root-owned-config-dir mess.
|
|
538
|
+
*
|
|
539
|
+
* The daemon runs as root for the TUN device; the Beagle chat UI runs as you.
|
|
540
|
+
* Every file the daemon created under ~/.agentnet before this was fixed came
|
|
541
|
+
* out root-owned, so the UI's writes into downloads/ and outbox/ fail with
|
|
542
|
+
* EACCES and file sends die. New daemons repair this themselves at startup —
|
|
543
|
+
* this is the command for an install that predates that, or one whose daemon
|
|
544
|
+
* cannot be restarted right now.
|
|
545
|
+
*/
|
|
546
|
+
export declare function cmdFixPerms(args: {
|
|
547
|
+
configDir?: string;
|
|
548
|
+
user?: string;
|
|
549
|
+
}): Promise<void>;
|
|
536
550
|
export declare function cmdRestart(args: {
|
|
537
551
|
configDir?: string;
|
|
538
552
|
}): Promise<void>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -2665,6 +2665,54 @@ export async function cmdServiceRestart(args) {
|
|
|
2665
2665
|
* the new code without a sudo prompt — the daemon already has the
|
|
2666
2666
|
* privileges it needs to rebind the TUN.
|
|
2667
2667
|
*/
|
|
2668
|
+
/**
|
|
2669
|
+
* One-shot repair for the classic root-owned-config-dir mess.
|
|
2670
|
+
*
|
|
2671
|
+
* The daemon runs as root for the TUN device; the Beagle chat UI runs as you.
|
|
2672
|
+
* Every file the daemon created under ~/.agentnet before this was fixed came
|
|
2673
|
+
* out root-owned, so the UI's writes into downloads/ and outbox/ fail with
|
|
2674
|
+
* EACCES and file sends die. New daemons repair this themselves at startup —
|
|
2675
|
+
* this is the command for an install that predates that, or one whose daemon
|
|
2676
|
+
* cannot be restarted right now.
|
|
2677
|
+
*/
|
|
2678
|
+
export async function cmdFixPerms(args) {
|
|
2679
|
+
const { configOwner, restoreOwnership } = await import("../utils/config-owner.js");
|
|
2680
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
2681
|
+
if (!existsSync(dir)) {
|
|
2682
|
+
throw new Error(`No config dir at ${dir}. Run 'agentnet init' first, or pass --config-dir.`);
|
|
2683
|
+
}
|
|
2684
|
+
let owner = configOwner(dir);
|
|
2685
|
+
if (args.user) {
|
|
2686
|
+
// Explicit target: `sudo agentnet fix-perms --user alice`, for a dir whose
|
|
2687
|
+
// own ownership is already wrong (a whole tree accidentally chowned root).
|
|
2688
|
+
const { execFileSync } = await import("child_process");
|
|
2689
|
+
const uid = parseInt(execFileSync("id", ["-u", args.user], { encoding: "utf-8" }).trim(), 10);
|
|
2690
|
+
const gid = parseInt(execFileSync("id", ["-g", args.user], { encoding: "utf-8" }).trim(), 10);
|
|
2691
|
+
if (!Number.isInteger(uid) || !Number.isInteger(gid))
|
|
2692
|
+
throw new Error(`Unknown user: ${args.user}`);
|
|
2693
|
+
owner = { uid, gid };
|
|
2694
|
+
}
|
|
2695
|
+
if (!owner) {
|
|
2696
|
+
// Not root, or the dir genuinely belongs to root. Either way there is
|
|
2697
|
+
// nothing this process can chown — say which, precisely.
|
|
2698
|
+
if (process.getuid?.() !== 0) {
|
|
2699
|
+
console.log(`Nothing to do: not running as root, so every file this user creates is already theirs.`);
|
|
2700
|
+
console.log(`If a file send failed with EACCES, the root-owned files need root to repair:`);
|
|
2701
|
+
console.log(` sudo agentnet fix-perms`);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
console.log(`Nothing to do: ${dir} belongs to root and no invoking user was found.`);
|
|
2705
|
+
console.log(`If this node should belong to a normal user, name them: sudo agentnet fix-perms --user <name>`);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
const changed = await restoreOwnership(dir, owner);
|
|
2709
|
+
if (changed === 0) {
|
|
2710
|
+
console.log(`Already correct: everything under ${dir} belongs to uid ${owner.uid}.`);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
console.log(`Fixed ${changed} path(s) under ${dir} → uid ${owner.uid}:${owner.gid}.`);
|
|
2714
|
+
console.log(`File sends from the Beagle UI should work now — no restart needed.`);
|
|
2715
|
+
}
|
|
2668
2716
|
export async function cmdRestart(args) {
|
|
2669
2717
|
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
2670
2718
|
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
10
10
|
// Belt-and-braces — also raise it here in case the CLI is run directly
|
|
11
11
|
// (e.g. `node dist/cli/index.js` rather than via dist/index.js).
|
|
12
12
|
EventEmitter.defaultMaxListeners = 100;
|
|
13
|
-
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyFileWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdProxyTrustCa, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdBootstrapShow, cmdBootstrapUpdate, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdHardenSsh, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyFileWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdProxyTrustCa, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdBootstrapShow, cmdBootstrapUpdate, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdHardenSsh, cmdServiceInstall, cmdFixPerms, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -203,6 +203,11 @@ async function main() {
|
|
|
203
203
|
throw new Error("path is required: agentnet file send <userid> <path>");
|
|
204
204
|
await cmdFileSend({ to: argv.userid, path: argv.path, configDir: argv["config-dir"] });
|
|
205
205
|
}
|
|
206
|
+
})
|
|
207
|
+
.command("fix-perms", "Repair root-owned files under ~/.agentnet (run with sudo) — fixes 'file send failed' EACCES", (y) => y
|
|
208
|
+
.option("config-dir", { type: "string" })
|
|
209
|
+
.option("user", { type: "string", describe: "Owner to restore to (default: the config dir's own user)" }), async (argv) => {
|
|
210
|
+
await cmdFixPerms({ configDir: argv["config-dir"], user: argv.user });
|
|
206
211
|
})
|
|
207
212
|
// Tell the running daemon to re-exec itself with its original argv.
|
|
208
213
|
// The daemon inherits its own uid (root if it was launched as root)
|
package/dist/daemon/server.js
CHANGED
|
@@ -289,6 +289,20 @@ export class DaemonServer {
|
|
|
289
289
|
catch (err) {
|
|
290
290
|
this.logger.warn(`Could not write pidfile ${this.pidFile}: ${err}`);
|
|
291
291
|
}
|
|
292
|
+
// We are probably root (TUN needs it) while the config dir belongs to a
|
|
293
|
+
// normal user, and unprivileged apps — the Beagle chat UI above all — write
|
|
294
|
+
// into downloads/ and outbox/. Anything root left behind there would fail
|
|
295
|
+
// their writes with EACCES, so hand it back before we create anything new.
|
|
296
|
+
try {
|
|
297
|
+
const { restoreConfigDirOwnership } = await import("../utils/config-owner.js");
|
|
298
|
+
const fixed = await restoreConfigDirOwnership(this.configDir);
|
|
299
|
+
if (fixed > 0) {
|
|
300
|
+
this.logger.info(`Restored ownership of ${fixed} path(s) under ${this.configDir} to the config dir's user`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
this.logger.warn(`Could not restore config dir ownership: ${err.message}`);
|
|
305
|
+
}
|
|
292
306
|
this.logger.info(`Starting daemon (node: ${this.config.node.name})`);
|
|
293
307
|
this.startedAt = Date.now();
|
|
294
308
|
try {
|
|
@@ -564,12 +578,35 @@ export class DaemonServer {
|
|
|
564
578
|
throw new Error(`This friend is a native (iOS/Android) client — files are sent inline and limited to ` +
|
|
565
579
|
`${(INLINE_MAX / 1024 / 1024).toFixed(1)} MB (this is ${(data.length / 1024 / 1024).toFixed(1)} MB).`);
|
|
566
580
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
581
|
+
// Append the chip as "sending" FIRST, then let the SDK's delivery
|
|
582
|
+
// verdict decide what it becomes. Marking "sent" the moment
|
|
583
|
+
// sendInlineFile resolved was a lie — that only meant the bytes
|
|
584
|
+
// left this machine; a half-open session swallowed them silently
|
|
585
|
+
// while the sender stared at a checkmark (INBOX 2026-08-21 A).
|
|
586
|
+
const safeName = sanitizeFileName(name);
|
|
587
|
+
const chip = this.messageStore?.appendFile(userid, "out", { name: safeName, size: data.length, status: "sending", sent: 0 });
|
|
570
588
|
this.friendMeta?.ensure(userid);
|
|
571
589
|
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
572
|
-
|
|
590
|
+
let delivery;
|
|
591
|
+
try {
|
|
592
|
+
delivery = await this.peerManager.sendInlineFile(userid, new Uint8Array(data), name);
|
|
593
|
+
}
|
|
594
|
+
catch (e) {
|
|
595
|
+
if (chip)
|
|
596
|
+
this.messageStore?.patchFile(userid, chip.id, { status: "failed" });
|
|
597
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
598
|
+
throw e;
|
|
599
|
+
}
|
|
600
|
+
const status = delivery === "acked" ? "sent" : delivery === "offline" ? "queued" : "failed";
|
|
601
|
+
if (chip)
|
|
602
|
+
this.messageStore?.patchFile(userid, chip.id, { status, sent: delivery === "acked" ? data.length : 0 });
|
|
603
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
604
|
+
this.logger.info(`Inline file "${name}" (${data.length}B) to native ${userid.slice(0, 8)}: delivery=${delivery} → ${status}`);
|
|
605
|
+
if (delivery === "accepted") {
|
|
606
|
+
throw new Error(`The peer never confirmed receiving "${name}" — it likely did not arrive. ` +
|
|
607
|
+
`Check that their app is in the foreground and try again.`);
|
|
608
|
+
}
|
|
609
|
+
return { inline: true, name, size: data.length, delivery };
|
|
573
610
|
}
|
|
574
611
|
// Add the "out" chip immediately as STATUS=sending (not "sent" — the
|
|
575
612
|
// transfer is reliable+acked, so it only flips to "sent" once the
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.279";
|
|
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"/>',
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership of the config dir when the daemon runs as root.
|
|
3
|
+
*
|
|
4
|
+
* The daemon needs root for the TUN device, but `~/.agentnet` belongs to a
|
|
5
|
+
* normal user — and so do the other programs that read and write it. The Beagle
|
|
6
|
+
* chat UI runs unprivileged and writes uploads into `downloads/`, queues into
|
|
7
|
+
* `outbox/`, and appends to the message store; every file root creates there
|
|
8
|
+
* comes out root-owned, so those writes fail with EACCES and a file send dies
|
|
9
|
+
* with nothing but a permission error in a log the user never sees.
|
|
10
|
+
*
|
|
11
|
+
* So: hand ownership of everything under the config dir back to the user who
|
|
12
|
+
* owns the dir itself. Best-effort throughout — a failure here must never stop
|
|
13
|
+
* the daemon from starting, it just leaves the user where they were.
|
|
14
|
+
*/
|
|
15
|
+
export interface ConfigOwner {
|
|
16
|
+
uid: number;
|
|
17
|
+
gid: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The uid/gid that SHOULD own the config dir, or null when there is nothing to
|
|
21
|
+
* do (not root, or the dir is already root's own — a genuinely root-run node).
|
|
22
|
+
*
|
|
23
|
+
* `SUDO_UID` covers `sudo agentnet up`; the config dir's own owner covers a
|
|
24
|
+
* daemon started by launchd/systemd, where sudo never set anything.
|
|
25
|
+
*/
|
|
26
|
+
export declare function configOwner(dir: string): ConfigOwner | null;
|
|
27
|
+
/**
|
|
28
|
+
* Recursively hand `dir` to `owner`, touching only the paths that are actually
|
|
29
|
+
* wrong. A full walk of the config dir costs a readdir per directory — cheap
|
|
30
|
+
* enough to run on every daemon start, and the steady state changes nothing.
|
|
31
|
+
*
|
|
32
|
+
* Note we cannot skip a correctly-owned directory: the daemon writing into a
|
|
33
|
+
* user-created `downloads/` produces exactly that shape — right dir, wrong
|
|
34
|
+
* files inside.
|
|
35
|
+
*/
|
|
36
|
+
export declare function restoreOwnership(dir: string, owner: ConfigOwner): Promise<number>;
|
|
37
|
+
/**
|
|
38
|
+
* Startup sweep: if we are root and the config dir belongs to someone else,
|
|
39
|
+
* give everything under it back to them. Returns how many paths changed (0 when
|
|
40
|
+
* there was nothing to do, which is the steady state).
|
|
41
|
+
*/
|
|
42
|
+
export declare function restoreConfigDirOwnership(dir: string): Promise<number>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership of the config dir when the daemon runs as root.
|
|
3
|
+
*
|
|
4
|
+
* The daemon needs root for the TUN device, but `~/.agentnet` belongs to a
|
|
5
|
+
* normal user — and so do the other programs that read and write it. The Beagle
|
|
6
|
+
* chat UI runs unprivileged and writes uploads into `downloads/`, queues into
|
|
7
|
+
* `outbox/`, and appends to the message store; every file root creates there
|
|
8
|
+
* comes out root-owned, so those writes fail with EACCES and a file send dies
|
|
9
|
+
* with nothing but a permission error in a log the user never sees.
|
|
10
|
+
*
|
|
11
|
+
* So: hand ownership of everything under the config dir back to the user who
|
|
12
|
+
* owns the dir itself. Best-effort throughout — a failure here must never stop
|
|
13
|
+
* the daemon from starting, it just leaves the user where they were.
|
|
14
|
+
*/
|
|
15
|
+
import { lchown, lstat, readdir } from "fs/promises";
|
|
16
|
+
import { statSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
/**
|
|
19
|
+
* The uid/gid that SHOULD own the config dir, or null when there is nothing to
|
|
20
|
+
* do (not root, or the dir is already root's own — a genuinely root-run node).
|
|
21
|
+
*
|
|
22
|
+
* `SUDO_UID` covers `sudo agentnet up`; the config dir's own owner covers a
|
|
23
|
+
* daemon started by launchd/systemd, where sudo never set anything.
|
|
24
|
+
*/
|
|
25
|
+
export function configOwner(dir) {
|
|
26
|
+
if (process.getuid?.() !== 0)
|
|
27
|
+
return null; // not root: whatever we create is already ours
|
|
28
|
+
const sudoUid = Number(process.env.SUDO_UID);
|
|
29
|
+
const sudoGid = Number(process.env.SUDO_GID);
|
|
30
|
+
if (Number.isInteger(sudoUid) && sudoUid > 0) {
|
|
31
|
+
return { uid: sudoUid, gid: Number.isInteger(sudoGid) && sudoGid >= 0 ? sudoGid : sudoUid };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const st = statSync(dir);
|
|
35
|
+
if (st.uid > 0)
|
|
36
|
+
return { uid: st.uid, gid: st.gid };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// No dir yet — nothing to own.
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** Give one path to `owner` if it isn't already. Returns true when changed. */
|
|
44
|
+
async function chownOne(path, owner) {
|
|
45
|
+
try {
|
|
46
|
+
const st = await lstat(path);
|
|
47
|
+
if (st.uid === owner.uid && st.gid === owner.gid)
|
|
48
|
+
return false;
|
|
49
|
+
// lchown, not chown: a symlink inside the config dir must not let us
|
|
50
|
+
// retarget ownership of whatever it points at.
|
|
51
|
+
await lchown(path, owner.uid, owner.gid);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Recursively hand `dir` to `owner`, touching only the paths that are actually
|
|
60
|
+
* wrong. A full walk of the config dir costs a readdir per directory — cheap
|
|
61
|
+
* enough to run on every daemon start, and the steady state changes nothing.
|
|
62
|
+
*
|
|
63
|
+
* Note we cannot skip a correctly-owned directory: the daemon writing into a
|
|
64
|
+
* user-created `downloads/` produces exactly that shape — right dir, wrong
|
|
65
|
+
* files inside.
|
|
66
|
+
*/
|
|
67
|
+
export async function restoreOwnership(dir, owner) {
|
|
68
|
+
let changed = 0;
|
|
69
|
+
const walk = async (path, depth) => {
|
|
70
|
+
if (depth > 12)
|
|
71
|
+
return; // pathological nesting / symlink loop guard
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await readdir(path, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return; // unreadable or vanished — nothing we can fix here
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const child = join(path, entry.name);
|
|
81
|
+
if (await chownOne(child, owner))
|
|
82
|
+
changed++;
|
|
83
|
+
// Directories only: never follow a symlink out of the config dir.
|
|
84
|
+
if (entry.isDirectory())
|
|
85
|
+
await walk(child, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (await chownOne(dir, owner))
|
|
89
|
+
changed++;
|
|
90
|
+
await walk(dir, 0);
|
|
91
|
+
return changed;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Startup sweep: if we are root and the config dir belongs to someone else,
|
|
95
|
+
* give everything under it back to them. Returns how many paths changed (0 when
|
|
96
|
+
* there was nothing to do, which is the steady state).
|
|
97
|
+
*/
|
|
98
|
+
export async function restoreConfigDirOwnership(dir) {
|
|
99
|
+
const owner = configOwner(dir);
|
|
100
|
+
if (!owner)
|
|
101
|
+
return 0;
|
|
102
|
+
return restoreOwnership(dir, owner);
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.279",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@decentnetwork/dora": "^0.1.14",
|
|
87
|
-
"@decentnetwork/peer": "^0.1.
|
|
87
|
+
"@decentnetwork/peer": "^0.1.140",
|
|
88
88
|
"@decentnetwork/peer-webrtc": "^0.2.10",
|
|
89
89
|
"ink": "^5.2.1",
|
|
90
90
|
"js-yaml": "^4.1.0",
|