node-command 2.0.14 → 2.0.15

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 +73 -32
  2. package/package.json +1 -1
package/lib/setup.mjs CHANGED
@@ -711,6 +711,9 @@ const INCOMPLETE_HELPER_BOOTSTRAP = '/opt/nc/libexec/protected-helper-bootstrap'
711
711
  const INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES = new Map([
712
712
  ['archive.tar.gz', 'file'],
713
713
  ['install-request.json', 'file'],
714
+ // Written by the provisioner from the signed archive before the helper tree
715
+ // is published; installations that failed earlier may not have it yet.
716
+ ['control-plane-public-key.pem', 'optional-file'],
714
717
  ['modules', 'directory'],
715
718
  ['release', 'directory']
716
719
  ]);
@@ -831,21 +834,21 @@ function systemValidateHelperArtifacts(io, runtime) {
831
834
  }
832
835
 
833
836
  /**
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.
837
+ * The exact staging tree a protected helper bootstrap writes before
838
+ * provisioning: root-owned regular files/directories, no symlinks, no
839
+ * group/other write bits, and no unknown entry. An absent stage is not a
840
+ * failure by itself; callers that require it check separately.
838
841
  */
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;
842
+ function systemValidateStagedHelperBootstrapTree(io) {
843
843
  const stage = systemLstatOrNull(io, INCOMPLETE_HELPER_BOOTSTRAP);
844
- if (!stage || stage.isSymbolicLink() || !stage.isDirectory() || stage.uid !== 0 || (stage.mode & 0o022)) return false;
844
+ if (!stage) return true;
845
+ if (stage.isSymbolicLink() || !stage.isDirectory() || stage.uid !== 0 || (stage.mode & 0o022)) return false;
846
+ const required = [...INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES].filter(([, type]) => type !== 'optional-file');
845
847
  const staged = systemReaddir(io)(INCOMPLETE_HELPER_BOOTSTRAP);
846
- if (staged.length !== INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES.size) return false;
848
+ if (staged.some((name) => !INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES.has(name)) ||
849
+ required.some(([name]) => !staged.includes(name))) return false;
847
850
  for (const [name, type] of INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES) {
848
- if (!staged.includes(name)) return false;
851
+ if (!staged.includes(name)) continue;
849
852
  const info = systemLstatOrNull(io, path.join(INCOMPLETE_HELPER_BOOTSTRAP, name));
850
853
  if (!info || info.isSymbolicLink() || info.uid !== 0 || (info.mode & 0o022)) return false;
851
854
  if (type === 'directory' ? !info.isDirectory() : (!info.isFile() || info.nlink !== 1)) return false;
@@ -853,6 +856,17 @@ function systemValidateStagedHelperBootstrap(io, libexec) {
853
856
  return true;
854
857
  }
855
858
 
859
+ /**
860
+ * `libexec` is removable only while empty or holding that exact staging tree.
861
+ * A provisioned helper, peercred shim or any other content fails closed.
862
+ */
863
+ function systemValidateStagedHelperBootstrap(io, libexec) {
864
+ const entries = systemReaddir(io)(libexec);
865
+ if (entries.length === 0) return true;
866
+ if (entries.length !== 1 || entries[0] !== 'protected-helper-bootstrap') return false;
867
+ return systemValidateStagedHelperBootstrapTree(io);
868
+ }
869
+
856
870
  /**
857
871
  * Recognize the two root-recoverable system-install leftovers this CLI may
858
872
  * remove without the packaged maintenance entry: a helper provisioned but
@@ -893,6 +907,11 @@ export function incompleteSystemInstallationShape(root, { platform = os.platform
893
907
  // ordinary root all require their own trusted maintenance.
894
908
  if (systemLstatOrNull(io, INCOMPLETE_HELPER_PROFILE) || systemLstatOrNull(io, INCOMPLETE_SYSTEM_LIFECYCLE) ||
895
909
  systemLstatOrNull(io, INCOMPLETE_SYSTEM_PAIRED_ROOT)) return null;
910
+ // libexec may hold only the helper tree, its peercred shim and the exact
911
+ // pre-provision protected bootstrap stage. Any other entry fails closed.
912
+ const libexecEntries = systemReaddir(io)(path.join(root, 'libexec'));
913
+ if (!libexecEntries.every((name) => ['nc-linux-peercred', 'privileged-helper', 'protected-helper-bootstrap'].includes(name)) ||
914
+ !systemValidateStagedHelperBootstrapTree(io)) return null;
896
915
  const bootstrap = JSON.parse(readSystemFile(io, INCOMPLETE_HELPER_APP + '/helper-bootstrap.json', { mode: 0o600 }));
897
916
  const controlPlaneId = (value) => typeof value === 'string' && value.length > 0 && value.length <= 256 &&
898
917
  value.trim() === value && !/[\x00-\x1f\x7f]/.test(value);
@@ -907,20 +926,37 @@ export function incompleteSystemInstallationShape(root, { platform = os.platform
907
926
  bootstrap.ordinary_service_scope !== 'system' ||
908
927
  bootstrap.ack_private_key_file !== INCOMPLETE_HELPER_APP + '/privileged-helper-ack-key.pem' ||
909
928
  !systemCanonical(bootstrap.node_bin)) return null;
910
- readSystemFile(io, INCOMPLETE_HELPER_APP + '/privileged-helper.json', { mode: 0o600 });
911
- readSystemFile(io, INCOMPLETE_HELPER_APP + '/privileged-helper-ack-key.pem', { mode: 0o600 });
912
- readSystemFile(io, INCOMPLETE_HELPER_APP + '/privileged-helper-public.pem', { mode: 0o644 });
913
- const peercred = systemLstatOrNull(io, '/opt/nc/libexec/nc-linux-peercred');
914
- if (!peercred || peercred.isSymbolicLink() || !peercred.isFile() || peercred.uid !== 0 || peercred.nlink !== 1) return null;
915
- const daemon = systemLstatOrNull(io, INCOMPLETE_HELPER_DAEMON);
916
- if (!daemon || daemon.isSymbolicLink() || !daemon.isFile() || daemon.uid !== 0 || !(daemon.mode & 0o111)) return null;
929
+ // The helper tree is either fully prepared but unbound, or only a
930
+ // bootstrap record whose provisioning never completed. The latter is
931
+ // removable only while the exact protected staging tree still exists.
932
+ const helperEntries = systemReaddir(io)(INCOMPLETE_HELPER_APP);
933
+ const bootstrapOnly = helperEntries.length === 1 && helperEntries[0] === 'helper-bootstrap.json';
934
+ let helperArtifacts;
935
+ if (bootstrapOnly) {
936
+ if (!systemLstatOrNull(io, INCOMPLETE_HELPER_BOOTSTRAP) ||
937
+ systemLstatOrNull(io, '/opt/nc/libexec/nc-linux-peercred') ||
938
+ systemLstatOrNull(io, INCOMPLETE_HELPER_UNIT) || systemLstatOrNull(io, INCOMPLETE_HELPER_SOCKET) ||
939
+ systemLstatOrNull(io, INCOMPLETE_HELPER_TMPFILES) || systemLstatOrNull(io, INCOMPLETE_HELPER_UNIT_DROPIN) ||
940
+ systemLstatOrNull(io, INCOMPLETE_HELPER_SOCKET_DROPIN)) return null;
941
+ helperArtifacts = Object.freeze({ service: false, tmpfiles: false });
942
+ } else {
943
+ readSystemFile(io, INCOMPLETE_HELPER_APP + '/privileged-helper.json', { mode: 0o600 });
944
+ readSystemFile(io, INCOMPLETE_HELPER_APP + '/privileged-helper-ack-key.pem', { mode: 0o600 });
945
+ readSystemFile(io, INCOMPLETE_HELPER_APP + '/privileged-helper-public.pem', { mode: 0o644 });
946
+ const peercred = systemLstatOrNull(io, '/opt/nc/libexec/nc-linux-peercred');
947
+ if (!peercred || peercred.isSymbolicLink() || !peercred.isFile() || peercred.uid !== 0 || peercred.nlink !== 1) return null;
948
+ // Node executes the .mjs daemon regardless of its executable bit; the
949
+ // released module is 0644. Require the protected root-owned metadata.
950
+ const daemon = systemLstatOrNull(io, INCOMPLETE_HELPER_DAEMON);
951
+ if (!daemon || daemon.isSymbolicLink() || !daemon.isFile() || daemon.uid !== 0 || (daemon.mode & 0o022)) return null;
952
+ helperArtifacts = systemValidateHelperArtifacts(io, bootstrap.node_bin);
953
+ }
917
954
  readSystemFile(io, INCOMPLETE_SYSTEM_LAUNCHER, { uids: new Set([0, payloadUid]) });
918
955
  const hostEnv = readSystemFile(io, INCOMPLETE_SYSTEM_HOST_ENV, { uids: new Set([0, payloadUid]) });
919
956
  const ids = hostEnv.split(/\r?\n/).filter((line) => /^\s*NC_CLIENT_ID\s*=/.test(line));
920
957
  const clientId = ids[0]?.match(/^\s*NC_CLIENT_ID\s*=\s*(?:"(nc_[0-9a-f]{64})"|'(nc_[0-9a-f]{64})'|(nc_[0-9a-f]{64}))\s*$/);
921
958
  if (ids.length !== 1 || !clientId || (clientId[1] || clientId[2] || clientId[3]) !== bootstrap.client_id) return null;
922
959
  systemValidateClientUnit(io, bootstrap.node_bin);
923
- const helperArtifacts = systemValidateHelperArtifacts(io, bootstrap.node_bin);
924
960
  return Object.freeze({ kind: 'pending-helper', runtime: bootstrap.node_bin, payloadUid, helperArtifacts });
925
961
  }
926
962
  // Failed-install shape: no helper payload, no packaged uninstaller, and an
@@ -1098,19 +1134,24 @@ export function findRemovalTargets({ io = {}, ...rest } = {}) {
1098
1134
  let pair = null;
1099
1135
  let incompleteRoot = null;
1100
1136
  if ((rest.platform || os.platform()) === 'linux') {
1101
- let pairedError = null;
1102
- try {
1103
- pair = linuxPairedInstallation(io);
1104
- } catch (err) {
1105
- // Protected metadata this account cannot stat or read is not a local
1106
- // removal target. Any other validation failure remains a hard stop
1107
- // unless the tree is the exact recoverable pending/failed shape.
1108
- if (err?.code !== 'TRUSTED_ROOT_MAINTENANCE_REQUIRED') throw err;
1109
- pairedError = err;
1110
- }
1111
- if (!pair) {
1112
- if (incompleteSystemInstallationShape(PARTIAL_SYSTEM_ROOT, { platform: 'linux', io })) incompleteRoot = PARTIAL_SYSTEM_ROOT;
1113
- if (pairedError && !incompleteRoot && !pairedMetadataUnreadable(io)) throw pairedError;
1137
+ // A validated recoverable pending/failed shape takes precedence over the
1138
+ // paired classification: the unprovisioned staging tree makes the paired
1139
+ // metadata unverifiable, but the tree is not a paired installation. A
1140
+ // genuinely prepared or bound pair never matches this shape (it requires
1141
+ // lifecycle custody and a profile), so it keeps using the packaged path.
1142
+ if (incompleteSystemInstallationShape(PARTIAL_SYSTEM_ROOT, { platform: 'linux', io })) {
1143
+ incompleteRoot = PARTIAL_SYSTEM_ROOT;
1144
+ } else {
1145
+ let pairedError = null;
1146
+ try {
1147
+ pair = linuxPairedInstallation(io);
1148
+ } catch (err) {
1149
+ // Protected metadata this account cannot stat or read is not a local
1150
+ // removal target. Any other validation failure remains a hard stop.
1151
+ if (err?.code !== 'TRUSTED_ROOT_MAINTENANCE_REQUIRED') throw err;
1152
+ pairedError = err;
1153
+ }
1154
+ if (!pair && pairedError && !pairedMetadataUnreadable(io)) throw pairedError;
1114
1155
  }
1115
1156
  }
1116
1157
  const env = rest.env || process.env;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-command",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {