node-command 2.0.13 → 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.
- package/lib/setup.mjs +96 -23
- package/package.json +1 -1
package/lib/setup.mjs
CHANGED
|
@@ -707,6 +707,16 @@ 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
|
+
// 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'],
|
|
717
|
+
['modules', 'directory'],
|
|
718
|
+
['release', 'directory']
|
|
719
|
+
]);
|
|
710
720
|
const INCOMPLETE_HELPER_DAEMON = INCOMPLETE_HELPER_APP + '/privileged-helper-linux-daemon.mjs';
|
|
711
721
|
const INCOMPLETE_HELPER_PROFILE = INCOMPLETE_HELPER_APP + '/privileged-helper-profile.json';
|
|
712
722
|
const INCOMPLETE_HELPER_UNIT = '/etc/systemd/system/nc-privileged-helper.service';
|
|
@@ -823,12 +833,48 @@ function systemValidateHelperArtifacts(io, runtime) {
|
|
|
823
833
|
return { service: !!service, tmpfiles: !!tmpfiles };
|
|
824
834
|
}
|
|
825
835
|
|
|
836
|
+
/**
|
|
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.
|
|
841
|
+
*/
|
|
842
|
+
function systemValidateStagedHelperBootstrapTree(io) {
|
|
843
|
+
const stage = systemLstatOrNull(io, INCOMPLETE_HELPER_BOOTSTRAP);
|
|
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');
|
|
847
|
+
const staged = systemReaddir(io)(INCOMPLETE_HELPER_BOOTSTRAP);
|
|
848
|
+
if (staged.some((name) => !INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES.has(name)) ||
|
|
849
|
+
required.some(([name]) => !staged.includes(name))) return false;
|
|
850
|
+
for (const [name, type] of INCOMPLETE_HELPER_BOOTSTRAP_ENTRIES) {
|
|
851
|
+
if (!staged.includes(name)) continue;
|
|
852
|
+
const info = systemLstatOrNull(io, path.join(INCOMPLETE_HELPER_BOOTSTRAP, name));
|
|
853
|
+
if (!info || info.isSymbolicLink() || info.uid !== 0 || (info.mode & 0o022)) return false;
|
|
854
|
+
if (type === 'directory' ? !info.isDirectory() : (!info.isFile() || info.nlink !== 1)) return false;
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
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
|
+
|
|
826
870
|
/**
|
|
827
871
|
* Recognize the two root-recoverable system-install leftovers this CLI may
|
|
828
872
|
* remove without the packaged maintenance entry: a helper provisioned but
|
|
829
873
|
* never bound (`pending`/`not_initialized`, no profile, no lifecycle custody)
|
|
830
874
|
* and a failed install whose app payload never landed but whose stale unit
|
|
831
|
-
* remains.
|
|
875
|
+
* remains. A failed install may also leave the exact root-owned protected
|
|
876
|
+
* helper bootstrap staged but never provisioned. Anything else returns null so
|
|
877
|
+
* callers keep failing closed.
|
|
832
878
|
* @param {string} root
|
|
833
879
|
* @param {{ platform?: string, io?: typeof fs }} [options]
|
|
834
880
|
* @returns {null | { kind: 'pending-helper'|'failed-install', runtime: string, payloadUid: number, helperArtifacts: { service: boolean, tmpfiles: boolean } }}
|
|
@@ -861,6 +907,11 @@ export function incompleteSystemInstallationShape(root, { platform = os.platform
|
|
|
861
907
|
// ordinary root all require their own trusted maintenance.
|
|
862
908
|
if (systemLstatOrNull(io, INCOMPLETE_HELPER_PROFILE) || systemLstatOrNull(io, INCOMPLETE_SYSTEM_LIFECYCLE) ||
|
|
863
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;
|
|
864
915
|
const bootstrap = JSON.parse(readSystemFile(io, INCOMPLETE_HELPER_APP + '/helper-bootstrap.json', { mode: 0o600 }));
|
|
865
916
|
const controlPlaneId = (value) => typeof value === 'string' && value.length > 0 && value.length <= 256 &&
|
|
866
917
|
value.trim() === value && !/[\x00-\x1f\x7f]/.test(value);
|
|
@@ -875,20 +926,37 @@ export function incompleteSystemInstallationShape(root, { platform = os.platform
|
|
|
875
926
|
bootstrap.ordinary_service_scope !== 'system' ||
|
|
876
927
|
bootstrap.ack_private_key_file !== INCOMPLETE_HELPER_APP + '/privileged-helper-ack-key.pem' ||
|
|
877
928
|
!systemCanonical(bootstrap.node_bin)) return null;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (
|
|
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
|
+
}
|
|
885
954
|
readSystemFile(io, INCOMPLETE_SYSTEM_LAUNCHER, { uids: new Set([0, payloadUid]) });
|
|
886
955
|
const hostEnv = readSystemFile(io, INCOMPLETE_SYSTEM_HOST_ENV, { uids: new Set([0, payloadUid]) });
|
|
887
956
|
const ids = hostEnv.split(/\r?\n/).filter((line) => /^\s*NC_CLIENT_ID\s*=/.test(line));
|
|
888
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*$/);
|
|
889
958
|
if (ids.length !== 1 || !clientId || (clientId[1] || clientId[2] || clientId[3]) !== bootstrap.client_id) return null;
|
|
890
959
|
systemValidateClientUnit(io, bootstrap.node_bin);
|
|
891
|
-
const helperArtifacts = systemValidateHelperArtifacts(io, bootstrap.node_bin);
|
|
892
960
|
return Object.freeze({ kind: 'pending-helper', runtime: bootstrap.node_bin, payloadUid, helperArtifacts });
|
|
893
961
|
}
|
|
894
962
|
// Failed-install shape: no helper payload, no packaged uninstaller, and an
|
|
@@ -900,7 +968,7 @@ export function incompleteSystemInstallationShape(root, { platform = os.platform
|
|
|
900
968
|
if (systemLstatOrNull(io, INCOMPLETE_SYSTEM_LIFECYCLE) || systemLstatOrNull(io, INCOMPLETE_SYSTEM_PAIRED_ROOT)) return null;
|
|
901
969
|
const libexec = systemLstatOrNull(io, path.join(root, 'libexec'));
|
|
902
970
|
if (libexec) {
|
|
903
|
-
if (libexec.isSymbolicLink() || !libexec.isDirectory() ||
|
|
971
|
+
if (libexec.isSymbolicLink() || !libexec.isDirectory() || !systemValidateStagedHelperBootstrap(io, path.join(root, 'libexec'))) return null;
|
|
904
972
|
}
|
|
905
973
|
const launcher = systemLstatOrNull(io, INCOMPLETE_SYSTEM_LAUNCHER);
|
|
906
974
|
if (launcher && (launcher.isSymbolicLink() || !launcher.isFile() || launcher.size !== 0 ||
|
|
@@ -1066,19 +1134,24 @@ export function findRemovalTargets({ io = {}, ...rest } = {}) {
|
|
|
1066
1134
|
let pair = null;
|
|
1067
1135
|
let incompleteRoot = null;
|
|
1068
1136
|
if ((rest.platform || os.platform()) === 'linux') {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
pairedError =
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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;
|
|
1082
1155
|
}
|
|
1083
1156
|
}
|
|
1084
1157
|
const env = rest.env || process.env;
|