node-command 2.0.13 → 2.0.14

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.
Files changed (2) hide show
  1. package/lib/setup.mjs +34 -2
  2. package/package.json +1 -1
package/lib/setup.mjs CHANGED
@@ -707,6 +707,13 @@ const INCOMPLETE_SYSTEM_HOST_ENV = '/opt/nc/config/nc-client.env';
707
707
  const INCOMPLETE_SYSTEM_UNIT = '/etc/systemd/system/nc-client.service';
708
708
  const INCOMPLETE_SYSTEM_UNIT_DROPIN = '/etc/systemd/system/nc-client.service.d';
709
709
  const INCOMPLETE_HELPER_APP = '/opt/nc/libexec/privileged-helper';
710
+ const INCOMPLETE_HELPER_BOOTSTRAP = '/opt/nc/libexec/protected-helper-bootstrap';
711
+ const INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES = new Map([
712
+ ['archive.tar.gz', 'file'],
713
+ ['install-request.json', 'file'],
714
+ ['modules', 'directory'],
715
+ ['release', 'directory']
716
+ ]);
710
717
  const INCOMPLETE_HELPER_DAEMON = INCOMPLETE_HELPER_APP + '/privileged-helper-linux-daemon.mjs';
711
718
  const INCOMPLETE_HELPER_PROFILE = INCOMPLETE_HELPER_APP + '/privileged-helper-profile.json';
712
719
  const INCOMPLETE_HELPER_UNIT = '/etc/systemd/system/nc-privileged-helper.service';
@@ -823,12 +830,37 @@ function systemValidateHelperArtifacts(io, runtime) {
823
830
  return { service: !!service, tmpfiles: !!tmpfiles };
824
831
  }
825
832
 
833
+ /**
834
+ * `libexec` is removable only while empty or holding the exact staging tree a
835
+ * protected helper bootstrap writes before provisioning: root-owned regular
836
+ * files/directories, no symlinks, no group/other write bits, and no unknown
837
+ * entry. A provisioned helper, peercred shim or any other content fails closed.
838
+ */
839
+ function systemValidateStagedHelperBootstrap(io, libexec) {
840
+ const entries = systemReaddir(io)(libexec);
841
+ if (entries.length === 0) return true;
842
+ if (entries.length !== 1 || entries[0] !== 'protected-helper-bootstrap') return false;
843
+ const stage = systemLstatOrNull(io, INCOMPLETE_HELPER_BOOTSTRAP);
844
+ if (!stage || stage.isSymbolicLink() || !stage.isDirectory() || stage.uid !== 0 || (stage.mode & 0o022)) return false;
845
+ const staged = systemReaddir(io)(INCOMPLETE_HELPER_BOOTSTRAP);
846
+ if (staged.length !== INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES.size) return false;
847
+ for (const [name, type] of INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES) {
848
+ if (!staged.includes(name)) return false;
849
+ const info = systemLstatOrNull(io, path.join(INCOMPLETE_HELPER_BOOTSTRAP, name));
850
+ if (!info || info.isSymbolicLink() || info.uid !== 0 || (info.mode & 0o022)) return false;
851
+ if (type === 'directory' ? !info.isDirectory() : (!info.isFile() || info.nlink !== 1)) return false;
852
+ }
853
+ return true;
854
+ }
855
+
826
856
  /**
827
857
  * Recognize the two root-recoverable system-install leftovers this CLI may
828
858
  * remove without the packaged maintenance entry: a helper provisioned but
829
859
  * never bound (`pending`/`not_initialized`, no profile, no lifecycle custody)
830
860
  * and a failed install whose app payload never landed but whose stale unit
831
- * remains. Anything else returns null so callers keep failing closed.
861
+ * remains. A failed install may also leave the exact root-owned protected
862
+ * helper bootstrap staged but never provisioned. Anything else returns null so
863
+ * callers keep failing closed.
832
864
  * @param {string} root
833
865
  * @param {{ platform?: string, io?: typeof fs }} [options]
834
866
  * @returns {null | { kind: 'pending-helper'|'failed-install', runtime: string, payloadUid: number, helperArtifacts: { service: boolean, tmpfiles: boolean } }}
@@ -900,7 +932,7 @@ export function incompleteSystemInstallationShape(root, { platform = os.platform
900
932
  if (systemLstatOrNull(io, INCOMPLETE_SYSTEM_LIFECYCLE) || systemLstatOrNull(io, INCOMPLETE_SYSTEM_PAIRED_ROOT)) return null;
901
933
  const libexec = systemLstatOrNull(io, path.join(root, 'libexec'));
902
934
  if (libexec) {
903
- if (libexec.isSymbolicLink() || !libexec.isDirectory() || systemReaddir(io)(path.join(root, 'libexec')).length !== 0) return null;
935
+ if (libexec.isSymbolicLink() || !libexec.isDirectory() || !systemValidateStagedHelperBootstrap(io, path.join(root, 'libexec'))) return null;
904
936
  }
905
937
  const launcher = systemLstatOrNull(io, INCOMPLETE_SYSTEM_LAUNCHER);
906
938
  if (launcher && (launcher.isSymbolicLink() || !launcher.isFile() || launcher.size !== 0 ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-command",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {