node-command 2.0.5 → 2.0.7
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/bin/nc.mjs +15 -9
- package/lib/setup.mjs +69 -9
- package/package.json +1 -1
package/bin/nc.mjs
CHANGED
|
@@ -2867,31 +2867,37 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
|
|
|
2867
2867
|
const pair = paired.find(selected => selected.ncRoot === root);
|
|
2868
2868
|
return pair ? pair.helperRoot === root ? `${root} (co-located protected helper)` : `${root} (paired protected helper: ${pair.helperRoot})` : root;
|
|
2869
2869
|
}).join(', ')}`);
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2870
|
+
const pairedRoots = new Set(paired.map(selected => selected.ncRoot));
|
|
2871
|
+
const canRemovePaired = os.platform() === 'linux' && process.getuid?.() === 0 && process.geteuid?.() === 0;
|
|
2872
|
+
let failed = 0;
|
|
2873
|
+
// A non-root caller can never execute the paired protected root. Report it
|
|
2874
|
+
// for explicit root maintenance, but keep going so the caller's own readable
|
|
2875
|
+
// tree is still removed in this invocation. No implicit sudo is attempted.
|
|
2876
|
+
const skippedPaired = canRemovePaired ? [] : [...pairedRoots];
|
|
2877
|
+
for (const root of skippedPaired) {
|
|
2878
|
+
console.error(`error: [TRUSTED_ROOT_MAINTENANCE_REQUIRED] The paired system installation at ${root} requires explicitly invoked real/effective Root maintenance; it was not modified.`);
|
|
2879
|
+
console.error('suggested_action: Run `sudo npx --yes node-command@latest uninstall --yes` to remove this trusted root installation explicitly.');
|
|
2880
|
+
failed += 1;
|
|
2875
2881
|
}
|
|
2882
|
+
const removable = install.filter(root => !skippedPaired.includes(root));
|
|
2876
2883
|
if (unreadable.length > 0) {
|
|
2877
2884
|
console.log(
|
|
2878
2885
|
`Explicit root maintenance required for system install(s): ${unreadable.join(', ')} (not readable as the current user).`
|
|
2879
2886
|
);
|
|
2880
2887
|
}
|
|
2881
|
-
if (!yes) {
|
|
2888
|
+
if (removable.length > 0 && !yes) {
|
|
2882
2889
|
if (!process.stdin.isTTY) {
|
|
2883
2890
|
console.error(`error: [CONFIRMATION_REQUIRED] ${command} needs confirmation in non-interactive shells.`);
|
|
2884
2891
|
console.error(`suggested_action: Re-run as \`nodecommand ${command} --yes\`.`);
|
|
2885
2892
|
process.exit(1);
|
|
2886
2893
|
}
|
|
2887
|
-
const confirmed = await confirmPrompt(`${command === 'uninstall' ? 'Uninstall' : 'Remove'} ${
|
|
2894
|
+
const confirmed = await confirmPrompt(`${command === 'uninstall' ? 'Uninstall' : 'Remove'} ${removable.length === 1 ? 'this installation' : 'these installations'}? [y/N] `);
|
|
2888
2895
|
if (!confirmed) {
|
|
2889
2896
|
console.log('Aborted (nothing removed).');
|
|
2890
2897
|
return;
|
|
2891
2898
|
}
|
|
2892
2899
|
}
|
|
2893
|
-
|
|
2894
|
-
for (const root of install) {
|
|
2900
|
+
for (const root of removable) {
|
|
2895
2901
|
const pair = paired.find(selected => selected.ncRoot === root);
|
|
2896
2902
|
const script = pair ? LINUX_PAIRED_UNINSTALL_ENTRY : uninstallerForRoot(root);
|
|
2897
2903
|
if (!pair && !fs.existsSync(script)) {
|
package/lib/setup.mjs
CHANGED
|
@@ -445,32 +445,92 @@ export function unreadableSystemInstallations({ platform = os.platform(), env =
|
|
|
445
445
|
return probeInstallRoot(root, io) === 'unreadable' ? [root] : [];
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Every fixed path `linuxPairedInstallation()` must stat/read. A root-owned
|
|
450
|
+
* 0600 metadata file is lstat-able for an ordinary user but unreadable, so
|
|
451
|
+
* both the stat and the read permission are probed.
|
|
452
|
+
*/
|
|
453
|
+
const PAIRED_PROTECTED_PATHS = [
|
|
454
|
+
'/opt/nc',
|
|
455
|
+
'/opt/nc/libexec',
|
|
456
|
+
'/opt/nc/libexec/privileged-helper',
|
|
457
|
+
'/opt/nc/libexec/privileged-helper/helper-bootstrap.json',
|
|
458
|
+
'/opt/nc/libexec/nc-linux-peercred',
|
|
459
|
+
'/opt/nc/app',
|
|
460
|
+
'/opt/nc/app/launcher.mjs',
|
|
461
|
+
'/opt/nc/config',
|
|
462
|
+
'/opt/nc/config/nc-client.env',
|
|
463
|
+
'/var/lib/nodecommand',
|
|
464
|
+
'/var/lib/nodecommand/nc',
|
|
465
|
+
'/var/lib/nodecommand/nc/config',
|
|
466
|
+
'/var/lib/nodecommand/nc/config/nc-client.env',
|
|
467
|
+
'/var/lib/nodecommand/nc/app/launcher.mjs',
|
|
468
|
+
'/var/lib/nc-lifecycle',
|
|
469
|
+
'/var/lib/nc-lifecycle/selected.json',
|
|
470
|
+
'/etc/systemd/system/nc-client.service',
|
|
471
|
+
'/etc/systemd/user/nc-client.service',
|
|
472
|
+
'/etc/systemd/system/nc-privileged-helper.service',
|
|
473
|
+
'/etc/systemd/system/nc-privileged-helper.socket',
|
|
474
|
+
'/etc/tmpfiles.d/nc-privileged-helper.conf'
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
/** True when the fixed protected helper paths deny this account access. */
|
|
478
|
+
function pairedMetadataUnreadable(io) {
|
|
479
|
+
const lstat = io.lstatSync || fs.lstatSync;
|
|
480
|
+
const access = io.accessSync || fs.accessSync;
|
|
481
|
+
for (const file of PAIRED_PROTECTED_PATHS) {
|
|
482
|
+
try {
|
|
483
|
+
lstat(file);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (err?.code === 'EACCES' || err?.code === 'EPERM') return true;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
access(file, fs.constants.R_OK);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
if (err?.code === 'EACCES' || err?.code === 'EPERM') return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
448
497
|
/**
|
|
449
498
|
* Removal targets split by accessibility. `install` roots are removed by the
|
|
450
499
|
* local flow; `unreadable` roots (system installs seen from a non-root shell)
|
|
451
500
|
* report explicit trusted-maintenance requirements instead of being
|
|
452
501
|
* silently reported as "nothing to remove".
|
|
453
502
|
* @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string, io?: object }} [options]
|
|
454
|
-
* @returns {{ install: string[], unreadable: string[], paired
|
|
503
|
+
* @returns {{ install: string[], unreadable: string[], paired: { ncRoot: string, helperRoot: string, clientId: string }[] }}
|
|
455
504
|
*/
|
|
456
505
|
export function findRemovalTargets({ io = {}, ...rest } = {}) {
|
|
457
506
|
const install = [];
|
|
458
507
|
const unreadable = [];
|
|
459
|
-
|
|
508
|
+
let pair = null;
|
|
509
|
+
if ((rest.platform || os.platform()) === 'linux') {
|
|
510
|
+
try {
|
|
511
|
+
pair = linuxPairedInstallation(io);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
// Protected metadata this account cannot stat or read is not a local
|
|
514
|
+
// removal target. Inventory discovery continues so an ordinary user can
|
|
515
|
+
// still remove their own readable tree; nothing here executes a root.
|
|
516
|
+
// Any other validation failure remains a hard stop.
|
|
517
|
+
if (err?.code !== 'TRUSTED_ROOT_MAINTENANCE_REQUIRED' || !pairedMetadataUnreadable(io)) throw err;
|
|
518
|
+
pair = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
460
521
|
const env = rest.env || process.env;
|
|
461
522
|
if (pair && env.NC_ROOT && ![pair.ncRoot, pair.helperRoot].includes(env.NC_ROOT)) throw new Error('TRUSTED_ROOT_MAINTENANCE_REQUIRED: NC_ROOT conflicts with the protected paired installation.');
|
|
462
523
|
const roots = candidateInstallRoots(pair ? { ...rest, env: { ...env, NC_ROOT: undefined } } : rest);
|
|
463
524
|
for (const root of pair ? [...new Set([...roots, pair.ncRoot])] : roots) {
|
|
464
525
|
if (root === pair?.helperRoot && root !== pair.ncRoot) continue;
|
|
465
526
|
const status = probeRemovalRoot(root, io);
|
|
466
|
-
if (status === 'install') install.push(root);
|
|
467
|
-
else if (status === 'unreadable') unreadable.push(root);
|
|
468
|
-
}
|
|
469
|
-
if (pair) {
|
|
470
|
-
if (install.length !== 1 || install[0] !== pair.ncRoot || unreadable.length) throw new Error('TRUSTED_ROOT_MAINTENANCE_REQUIRED: conflicting paired installation inventory; preserve all roots.');
|
|
471
|
-
return { install, unreadable, paired: [pair] };
|
|
527
|
+
if (status === 'install' && !install.includes(root)) install.push(root);
|
|
528
|
+
else if (status === 'unreadable' && !unreadable.includes(root)) unreadable.push(root);
|
|
472
529
|
}
|
|
473
|
-
|
|
530
|
+
// The paired relation is only actionable when its root is itself readable.
|
|
531
|
+
// An unverifiable/foreign paired root is reported through `unreadable`.
|
|
532
|
+
const paired = pair && install.includes(pair.ncRoot) ? [pair] : [];
|
|
533
|
+
return { install, unreadable, paired };
|
|
474
534
|
}
|
|
475
535
|
|
|
476
536
|
/**
|