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 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
- if (paired.length && (os.platform() !== 'linux' || process.getuid?.() !== 0 || process.geteuid?.() !== 0)) {
2871
- console.error('error: [TRUSTED_ROOT_MAINTENANCE_REQUIRED] Paired Linux removal requires explicitly invoked real/effective Root maintenance. No elevation or ordinary-script fallback was attempted; both roots and lifecycle records are preserved.');
2872
- console.error(uninstallFailureSuggestion());
2873
- process.exit(1);
2874
- return;
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'} ${targets.length === 1 ? 'this installation' : 'these installations'}? [y/N] `);
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
- let failed = 0;
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?: { ncRoot: string, helperRoot: string, clientId: string }[] }}
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
- const pair = (rest.platform || os.platform()) === 'linux' ? linuxPairedInstallation(io) : null;
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
- return { install, unreadable };
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-command",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {