node-command 2.0.11 → 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 +87 -11
- package/package.json +1 -1
package/lib/setup.mjs
CHANGED
|
@@ -479,6 +479,42 @@ const PARTIAL_SYSTEM_FORBIDDEN_PATHS = [
|
|
|
479
479
|
'/var/lib/nodecommand/nc'
|
|
480
480
|
];
|
|
481
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
|
+
|
|
482
518
|
/**
|
|
483
519
|
* True when `root` is the canonical per-user tree for the current account and
|
|
484
520
|
* is not a protected system root. This is the cheap path/ownership gate; the
|
|
@@ -522,8 +558,43 @@ export function isPartialSystemRemovalCandidate(root, { platform = os.platform()
|
|
|
522
558
|
return path.resolve(root) === PARTIAL_SYSTEM_ROOT;
|
|
523
559
|
}
|
|
524
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
|
+
|
|
525
596
|
/** Shared bounded removal of an already-authorized root. */
|
|
526
|
-
function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner }) {
|
|
597
|
+
function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner, verifyConfig = requireHostEnvironment }) {
|
|
527
598
|
const resolved = path.resolve(root);
|
|
528
599
|
if (io.realpathSync(resolved) !== resolved) throw refused('a symlinked ancestor is not removable');
|
|
529
600
|
const rootInfo = io.lstatSync(resolved);
|
|
@@ -536,14 +607,7 @@ function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner }) {
|
|
|
536
607
|
if (info.isSymbolicLink()) throw refused(`top-level entry "${name}" is a symlink`);
|
|
537
608
|
if (info.uid !== uid) throw refused(`top-level entry "${name}" is not owned by ${owner}`);
|
|
538
609
|
}
|
|
539
|
-
|
|
540
|
-
const info = io.lstatSync(path.join(resolved, file));
|
|
541
|
-
if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile())) {
|
|
542
|
-
throw refused(`the ${file} marker is missing or not a regular ${directory ? 'directory' : 'file'}`);
|
|
543
|
-
}
|
|
544
|
-
if (info.uid !== uid) throw refused(`the ${file} marker is not owned by ${owner}`);
|
|
545
|
-
}
|
|
546
|
-
io.accessSync(path.join(resolved, 'config', 'nc-client.env'), fs.constants.R_OK);
|
|
610
|
+
verifyConfig(resolved, { uid, io, refused, owner });
|
|
547
611
|
for (const name of entries) {
|
|
548
612
|
io.rmSync(path.join(resolved, name), { recursive: true, force: true });
|
|
549
613
|
}
|
|
@@ -609,12 +673,22 @@ export function removePartialSystemRoot(root, { platform = os.platform(), getuid
|
|
|
609
673
|
}
|
|
610
674
|
throw refused(`${file} exists; a real installation requires its packaged maintenance`);
|
|
611
675
|
}
|
|
612
|
-
|
|
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, {
|
|
613
686
|
uid: 0,
|
|
614
687
|
allowedEntries: PARTIAL_SYSTEM_ENTRIES,
|
|
615
688
|
io,
|
|
616
689
|
refused,
|
|
617
|
-
owner: 'root'
|
|
690
|
+
owner: 'root',
|
|
691
|
+
verifyConfig
|
|
618
692
|
});
|
|
619
693
|
} catch (err) {
|
|
620
694
|
if (err?.code === 'PARTIAL_SYSTEM_REMOVAL_REFUSED') throw err;
|
|
@@ -719,6 +793,8 @@ export function findRemovalTargets({ io = {}, ...rest } = {}) {
|
|
|
719
793
|
const status = probeRemovalRoot(root, io);
|
|
720
794
|
if (status === 'install' && !install.includes(root)) install.push(root);
|
|
721
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);
|
|
722
798
|
}
|
|
723
799
|
// The paired relation is only actionable when its root is itself readable.
|
|
724
800
|
// An unverifiable/foreign paired root is reported through `unreadable`.
|