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.
- package/lib/setup.mjs +73 -32
- 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
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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))
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
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
|
+
}
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
pairedError =
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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;
|