node-command 2.0.10 → 2.0.12
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/lib/setup.mjs +97 -12
- package/package.json +5 -12
package/lib/setup.mjs
CHANGED
|
@@ -296,7 +296,16 @@ function linuxPairedInstallation(io = fs) {
|
|
|
296
296
|
} finally { io.closeSync(fd); }
|
|
297
297
|
};
|
|
298
298
|
check(app, true);
|
|
299
|
-
|
|
299
|
+
const libexec = helperRoot + '/libexec';
|
|
300
|
+
const requiredLibexecEntries = ['nc-linux-peercred', 'privileged-helper'];
|
|
301
|
+
const managedSealedSnapshot = name => {
|
|
302
|
+
if (!/^privileged-helper-sealed-[0-9a-f]{64}$/.test(name)) return false;
|
|
303
|
+
const st = lstat(libexec + '/' + name);
|
|
304
|
+
return st.isDirectory() && !st.isSymbolicLink() && st.uid === 0 && st.gid === 0 && (st.mode & 0o7777) === 0o700;
|
|
305
|
+
};
|
|
306
|
+
const libexecEntries = io.readdirSync(libexec).sort();
|
|
307
|
+
if (!requiredLibexecEntries.every(name => libexecEntries.includes(name)) ||
|
|
308
|
+
!libexecEntries.every(name => requiredLibexecEntries.includes(name) || managedSealedSnapshot(name))) fail();
|
|
300
309
|
check(helperRoot + '/libexec/nc-linux-peercred');
|
|
301
310
|
const bootstrap = JSON.parse(read(app + '/helper-bootstrap.json'));
|
|
302
311
|
const selection = JSON.parse(read('/var/lib/nc-lifecycle/selected.json'));
|
|
@@ -470,6 +479,42 @@ const PARTIAL_SYSTEM_FORBIDDEN_PATHS = [
|
|
|
470
479
|
'/var/lib/nodecommand/nc'
|
|
471
480
|
];
|
|
472
481
|
|
|
482
|
+
/** Files a rejected/expired enrollment may leave in the system config tree. */
|
|
483
|
+
const PARTIAL_ENROLLMENT_CONFIG_ENTRIES = new Set(['.nc-cli.lock', 'nc-device-key.pem', 'nc-enroll-state.json']);
|
|
484
|
+
|
|
485
|
+
/** The exact residue that identifies the narrow recoverable enrollment shape. */
|
|
486
|
+
const PARTIAL_ENROLLMENT_REQUIRED = ['nc-device-key.pem', 'nc-enroll-state.json'];
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* True only for the exact canonical system root carrying the residue of a
|
|
490
|
+
* rejected/expired enrollment: a root-owned config directory with the device
|
|
491
|
+
* key and enrollment state, no host environment, and no application payload.
|
|
492
|
+
* Discovery uses this so the abandoned tree is a removal target; removal
|
|
493
|
+
* revalidates every entry before deleting anything.
|
|
494
|
+
* @param {string} root
|
|
495
|
+
* @param {typeof fs} [io]
|
|
496
|
+
*/
|
|
497
|
+
function partialEnrollmentSystemRoot(root, io = fs) {
|
|
498
|
+
if (path.resolve(root) !== PARTIAL_SYSTEM_ROOT) return false;
|
|
499
|
+
const lstat = io.lstatSync || fs.lstatSync;
|
|
500
|
+
const readdir = io.readdirSync || fs.readdirSync;
|
|
501
|
+
try {
|
|
502
|
+
if (!readdir(PARTIAL_SYSTEM_ROOT).every((name) => PARTIAL_SYSTEM_ENTRIES.has(name))) return false;
|
|
503
|
+
const config = path.join(PARTIAL_SYSTEM_ROOT, 'config');
|
|
504
|
+
const configInfo = lstat(config);
|
|
505
|
+
if (configInfo.isSymbolicLink() || !configInfo.isDirectory()) return false;
|
|
506
|
+
const names = readdir(config);
|
|
507
|
+
if (!names.every((name) => PARTIAL_ENROLLMENT_CONFIG_ENTRIES.has(name))) return false;
|
|
508
|
+
if (!PARTIAL_ENROLLMENT_REQUIRED.every((name) => names.includes(name))) return false;
|
|
509
|
+
return PARTIAL_ENROLLMENT_REQUIRED.every((name) => {
|
|
510
|
+
const entry = lstat(path.join(config, name));
|
|
511
|
+
return !entry.isSymbolicLink() && entry.isFile();
|
|
512
|
+
});
|
|
513
|
+
} catch {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
473
518
|
/**
|
|
474
519
|
* True when `root` is the canonical per-user tree for the current account and
|
|
475
520
|
* is not a protected system root. This is the cheap path/ownership gate; the
|
|
@@ -513,8 +558,43 @@ export function isPartialSystemRemovalCandidate(root, { platform = os.platform()
|
|
|
513
558
|
return path.resolve(root) === PARTIAL_SYSTEM_ROOT;
|
|
514
559
|
}
|
|
515
560
|
|
|
561
|
+
/** A real installation is proven by a readable, owner-matching host env. */
|
|
562
|
+
function requireHostEnvironment(root, { uid, io, refused, owner }) {
|
|
563
|
+
for (const [file, directory] of [['config', true], [path.join('config', 'nc-client.env'), false]]) {
|
|
564
|
+
const info = io.lstatSync(path.join(root, file));
|
|
565
|
+
if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile())) {
|
|
566
|
+
throw refused(`the ${file} marker is missing or not a regular ${directory ? 'directory' : 'file'}`);
|
|
567
|
+
}
|
|
568
|
+
if (info.uid !== uid) throw refused(`the ${file} marker is not owned by ${owner}`);
|
|
569
|
+
}
|
|
570
|
+
io.accessSync(path.join(root, 'config', 'nc-client.env'), fs.constants.R_OK);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* A rejected/expired enrollment leaves only the device key and enrollment
|
|
575
|
+
* state (plus this process's own CLI lock) in config/. A host environment, an
|
|
576
|
+
* app payload or any unknown file means this is not the narrow recoverable
|
|
577
|
+
* shape, so removal fails closed.
|
|
578
|
+
*/
|
|
579
|
+
function requirePartialEnrollment(root, { uid, io, refused, owner }) {
|
|
580
|
+
const config = path.join(root, 'config');
|
|
581
|
+
const info = io.lstatSync(config);
|
|
582
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw refused('the config marker is missing or not a regular directory');
|
|
583
|
+
if (info.uid !== uid) throw refused(`the config marker is not owned by ${owner}`);
|
|
584
|
+
const names = io.readdirSync(config);
|
|
585
|
+
for (const name of names) {
|
|
586
|
+
if (!PARTIAL_ENROLLMENT_CONFIG_ENTRIES.has(name)) throw refused(`unexpected config entry "${name}"`);
|
|
587
|
+
const entry = io.lstatSync(path.join(config, name));
|
|
588
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw refused(`config entry "${name}" is not a regular file`);
|
|
589
|
+
if (entry.uid !== uid) throw refused(`config entry "${name}" is not owned by ${owner}`);
|
|
590
|
+
}
|
|
591
|
+
for (const name of PARTIAL_ENROLLMENT_REQUIRED) {
|
|
592
|
+
if (!names.includes(name)) throw refused(`the partial-enrollment marker "${name}" is missing`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
516
596
|
/** Shared bounded removal of an already-authorized root. */
|
|
517
|
-
function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner }) {
|
|
597
|
+
function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner, verifyConfig = requireHostEnvironment }) {
|
|
518
598
|
const resolved = path.resolve(root);
|
|
519
599
|
if (io.realpathSync(resolved) !== resolved) throw refused('a symlinked ancestor is not removable');
|
|
520
600
|
const rootInfo = io.lstatSync(resolved);
|
|
@@ -527,14 +607,7 @@ function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner }) {
|
|
|
527
607
|
if (info.isSymbolicLink()) throw refused(`top-level entry "${name}" is a symlink`);
|
|
528
608
|
if (info.uid !== uid) throw refused(`top-level entry "${name}" is not owned by ${owner}`);
|
|
529
609
|
}
|
|
530
|
-
|
|
531
|
-
const info = io.lstatSync(path.join(resolved, file));
|
|
532
|
-
if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile())) {
|
|
533
|
-
throw refused(`the ${file} marker is missing or not a regular ${directory ? 'directory' : 'file'}`);
|
|
534
|
-
}
|
|
535
|
-
if (info.uid !== uid) throw refused(`the ${file} marker is not owned by ${owner}`);
|
|
536
|
-
}
|
|
537
|
-
io.accessSync(path.join(resolved, 'config', 'nc-client.env'), fs.constants.R_OK);
|
|
610
|
+
verifyConfig(resolved, { uid, io, refused, owner });
|
|
538
611
|
for (const name of entries) {
|
|
539
612
|
io.rmSync(path.join(resolved, name), { recursive: true, force: true });
|
|
540
613
|
}
|
|
@@ -600,12 +673,22 @@ export function removePartialSystemRoot(root, { platform = os.platform(), getuid
|
|
|
600
673
|
}
|
|
601
674
|
throw refused(`${file} exists; a real installation requires its packaged maintenance`);
|
|
602
675
|
}
|
|
603
|
-
|
|
676
|
+
const resolved = path.resolve(root);
|
|
677
|
+
const hostEnvMarker = path.join(resolved, 'config', 'nc-client.env');
|
|
678
|
+
let verifyConfig = requireHostEnvironment;
|
|
679
|
+
try {
|
|
680
|
+
io.lstatSync(hostEnvMarker);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
if (err?.code !== 'ENOENT') throw refused(`the host environment marker at ${hostEnvMarker} cannot be verified`);
|
|
683
|
+
verifyConfig = requirePartialEnrollment;
|
|
684
|
+
}
|
|
685
|
+
removeBoundedRoot(resolved, {
|
|
604
686
|
uid: 0,
|
|
605
687
|
allowedEntries: PARTIAL_SYSTEM_ENTRIES,
|
|
606
688
|
io,
|
|
607
689
|
refused,
|
|
608
|
-
owner: 'root'
|
|
690
|
+
owner: 'root',
|
|
691
|
+
verifyConfig
|
|
609
692
|
});
|
|
610
693
|
} catch (err) {
|
|
611
694
|
if (err?.code === 'PARTIAL_SYSTEM_REMOVAL_REFUSED') throw err;
|
|
@@ -710,6 +793,8 @@ export function findRemovalTargets({ io = {}, ...rest } = {}) {
|
|
|
710
793
|
const status = probeRemovalRoot(root, io);
|
|
711
794
|
if (status === 'install' && !install.includes(root)) install.push(root);
|
|
712
795
|
else if (status === 'unreadable' && !unreadable.includes(root)) unreadable.push(root);
|
|
796
|
+
else if (status === 'foreign' && !install.includes(root) && isPartialSystemRemovalCandidate(root, rest) &&
|
|
797
|
+
partialEnrollmentSystemRoot(root, io)) install.push(root);
|
|
713
798
|
}
|
|
714
799
|
// The paired relation is only actionable when its root is itself readable.
|
|
715
800
|
// An unverifiable/foreign paired root is reported through `unreadable`.
|
package/package.json
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-command",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Node Command CLI installer for the Node Command agent",
|
|
3
|
+
"version": "2.0.12",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"license": "Apache-2.0",
|
|
7
6
|
"engines": {
|
|
8
7
|
"node": ">=24"
|
|
9
8
|
},
|
|
10
9
|
"bin": {
|
|
11
|
-
"nc": "bin/nc.mjs",
|
|
12
|
-
"
|
|
13
|
-
"nodecommand": "bin/nc.mjs",
|
|
14
|
-
"node-command": "bin/nc.mjs"
|
|
10
|
+
"nc": "./bin/nc.mjs",
|
|
11
|
+
"nodecommand": "./bin/nc.mjs"
|
|
15
12
|
},
|
|
16
13
|
"files": [
|
|
17
14
|
"bin/",
|
|
18
|
-
"lib/"
|
|
19
|
-
|
|
20
|
-
],
|
|
21
|
-
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
}
|
|
15
|
+
"lib/"
|
|
16
|
+
]
|
|
24
17
|
}
|