node-command 2.0.7 → 2.0.9
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 +94 -5
- package/lib/setup.mjs +184 -0
- package/package.json +1 -1
package/bin/nc.mjs
CHANGED
|
@@ -21,8 +21,12 @@ import {
|
|
|
21
21
|
getNcRootForMode,
|
|
22
22
|
installerEnvForMode,
|
|
23
23
|
isElevated,
|
|
24
|
+
isPartialSystemRemovalCandidate,
|
|
25
|
+
isPartialUserRemovalCandidate,
|
|
24
26
|
isUserInstallRoot,
|
|
25
27
|
probeInstallRoot,
|
|
28
|
+
removePartialSystemRoot,
|
|
29
|
+
removePartialUserRoot,
|
|
26
30
|
resolveExecutable,
|
|
27
31
|
setupTarget,
|
|
28
32
|
unreadableSystemInstallations,
|
|
@@ -708,6 +712,37 @@ export function runInstallerScript(scriptPath, ncRoot, extraEnv = {}) {
|
|
|
708
712
|
});
|
|
709
713
|
}
|
|
710
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Best-effort evidence when the agent installer reports a Node.js runtime
|
|
717
|
+
* failure. The Linux installer suppresses the portable runtime's loader error
|
|
718
|
+
* and prints only "Node.js 24+ required (portable runtime failed to run)";
|
|
719
|
+
* replay the binary to surface the real reason (e.g. a missing `libatomic`
|
|
720
|
+
* dependency of Node.js 25+ official Linux builds). Diagnostic only: it never
|
|
721
|
+
* changes the installer's runtime requirement or substitutes a downgrade.
|
|
722
|
+
* @param {string} ncRoot
|
|
723
|
+
* @returns {string|null}
|
|
724
|
+
*/
|
|
725
|
+
function portableRuntimeFailureDetail(ncRoot) {
|
|
726
|
+
const bin = path.join(ncRoot, 'tools', 'node', 'bin', 'node');
|
|
727
|
+
try {
|
|
728
|
+
const info = fs.lstatSync(bin);
|
|
729
|
+
if (!info.isFile() || info.isSymbolicLink()) return null;
|
|
730
|
+
// Probe only a runtime this account owns: never execute a planted binary
|
|
731
|
+
// from an arbitrary/attacker-writable install tree.
|
|
732
|
+
if (typeof process.getuid === 'function' && info.uid !== process.getuid()) return null;
|
|
733
|
+
} catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const probe = child_process.spawnSync(bin, ['-p', 'process.versions.node'], {
|
|
737
|
+
encoding: 'utf8',
|
|
738
|
+
timeout: 5000,
|
|
739
|
+
env: { PATH: '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C' }
|
|
740
|
+
});
|
|
741
|
+
const detail = String(probe.stderr || '').trim() || (probe.error?.message || '');
|
|
742
|
+
if (!detail) return null;
|
|
743
|
+
return sanitizeForDisplay(detail.split(/\r?\n/).filter(Boolean).slice(-3).join(' '), 300);
|
|
744
|
+
}
|
|
745
|
+
|
|
711
746
|
/**
|
|
712
747
|
* Zero-touch device pairing library.
|
|
713
748
|
*
|
|
@@ -1740,7 +1775,11 @@ export async function downloadAndInstallAgent({ ncRoot = undefined, updatesUrl =
|
|
|
1740
1775
|
invokeInstaller(scriptPath, root);
|
|
1741
1776
|
} catch (err) {
|
|
1742
1777
|
if (err instanceof PairingError) throw err;
|
|
1743
|
-
|
|
1778
|
+
const runtimeDetail = installMode === 'user' ? portableRuntimeFailureDetail(root) : null;
|
|
1779
|
+
const hint = runtimeDetail
|
|
1780
|
+
? ` The bundled Node.js runtime at ${path.join(root, 'tools', 'node', 'bin', 'node')} also fails to run here: ${runtimeDetail}. Install the distribution runtime dependency (Node.js 25+ official Linux builds require libatomic) or a working Node.js 24+ system runtime, then re-run.`
|
|
1781
|
+
: '';
|
|
1782
|
+
throw new PairingError('INSTALL_FAILED', `Agent installer failed: ${sanitizeForDisplay(err.message, 300)}.${hint} Re-running resumes from the stored host credentials.`);
|
|
1744
1783
|
}
|
|
1745
1784
|
} finally {
|
|
1746
1785
|
console.log('🧹 Cleaning up temporary directory...');
|
|
@@ -2440,7 +2479,12 @@ export async function runSetup(args = [], { allowHostIdOnly = false, promptToken
|
|
|
2440
2479
|
// existing device identity. The normal verified resume flow is kept.
|
|
2441
2480
|
const keyPath = getDeviceKeyPath(root);
|
|
2442
2481
|
if (!protectedContext) {
|
|
2443
|
-
|
|
2482
|
+
let keyInfo;
|
|
2483
|
+
try {
|
|
2484
|
+
keyInfo = fs.lstatSync(keyPath);
|
|
2485
|
+
} catch (err) {
|
|
2486
|
+
throw new Error(`Existing device key at ${root} is missing or cannot be verified; preserve it for selected-installation maintenance.`, { cause: err });
|
|
2487
|
+
}
|
|
2444
2488
|
if (!keyInfo.isFile() || keyInfo.isSymbolicLink() || keyInfo.size > 65536 ||
|
|
2445
2489
|
(os.platform() !== 'win32' && installMode === 'user' && keyInfo.uid !== process.getuid())) {
|
|
2446
2490
|
throw new Error(`Existing device key at ${root} has an unverifiable type/owner; refusing identity replacement.`);
|
|
@@ -2452,8 +2496,23 @@ export async function runSetup(args = [], { allowHostIdOnly = false, promptToken
|
|
|
2452
2496
|
throw new Error(`Existing device key at ${root} cannot be verified; preserve it for selected-installation maintenance.`);
|
|
2453
2497
|
}
|
|
2454
2498
|
existingEnv = localEnv;
|
|
2455
|
-
|
|
2456
|
-
|
|
2499
|
+
// A failed per-user install can leave config + device key while the
|
|
2500
|
+
// application directory was never staged. That is repairable from the
|
|
2501
|
+
// stored identity exactly like a missing launcher; it must not surface as
|
|
2502
|
+
// a raw ENOENT and block every retry.
|
|
2503
|
+
let appInfo = null;
|
|
2504
|
+
try {
|
|
2505
|
+
appInfo = fs.lstatSync(path.join(root, 'app'));
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
if (err?.code !== 'ENOENT') throw new Error(`Existing application at ${root} cannot be verified; use selected-installation maintenance.`, { cause: err });
|
|
2508
|
+
}
|
|
2509
|
+
if (!appInfo || !appInfo.isDirectory() || appInfo.isSymbolicLink()) {
|
|
2510
|
+
if (repairUserApplication && canRepairUserApplication({ mode: installMode, protectedContext })) {
|
|
2511
|
+
needsUserAppRepair = true;
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
throw new Error(`Existing application at ${root} is incomplete: the app/ directory is missing or unverifiable. Repair or reinstall the application from the existing root (config/nc-client.env and config/nc-device-key.pem are preserved); no new enrollment was attempted.`);
|
|
2515
|
+
}
|
|
2457
2516
|
const launcherPath = path.join(root, 'app', 'launcher.mjs');
|
|
2458
2517
|
let launcherInfo;
|
|
2459
2518
|
try {
|
|
@@ -2900,7 +2959,20 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
|
|
|
2900
2959
|
for (const root of removable) {
|
|
2901
2960
|
const pair = paired.find(selected => selected.ncRoot === root);
|
|
2902
2961
|
const script = pair ? LINUX_PAIRED_UNINSTALL_ENTRY : uninstallerForRoot(root);
|
|
2903
|
-
|
|
2962
|
+
// A failed per-user install can leave its own tree (config plus partial
|
|
2963
|
+
// payload) without the packaged uninstaller. Only the canonical user root
|
|
2964
|
+
// is eligible, and the bounded removal revalidates ancestry, ownership and
|
|
2965
|
+
// every top-level entry. Real system installs never use this fallback;
|
|
2966
|
+
// the exact /opt/nc partial tree has its own narrower root-only exception.
|
|
2967
|
+
const scriptExists = pair || fs.existsSync(script);
|
|
2968
|
+
const explicitRemoval = command === 'uninstall' || command === 'remove';
|
|
2969
|
+
const partialUserTree = !scriptExists && explicitRemoval && isPartialUserRemovalCandidate(root);
|
|
2970
|
+
// The narrow system exception is /opt/nc only, requires real+effective
|
|
2971
|
+
// root, and refuses app/, uninstall.sh, units, libexec/ and lifecycle
|
|
2972
|
+
// metadata inside removePartialSystemRoot.
|
|
2973
|
+
const partialSystemTree = !scriptExists && explicitRemoval && isPartialSystemRemovalCandidate(root);
|
|
2974
|
+
const boundedTree = partialUserTree || partialSystemTree;
|
|
2975
|
+
if (!scriptExists && !boundedTree) {
|
|
2904
2976
|
console.error(`error: [UNINSTALLER_MISSING] uninstaller not found at ${script}; skipping ${root}.`);
|
|
2905
2977
|
console.error(uninstallFailureSuggestion());
|
|
2906
2978
|
failed += 1;
|
|
@@ -2915,6 +2987,23 @@ async function runRemoval(args, { command, emptyMessage, elevateOnWindows = fals
|
|
|
2915
2987
|
continue;
|
|
2916
2988
|
}
|
|
2917
2989
|
try {
|
|
2990
|
+
if (boundedTree) {
|
|
2991
|
+
try {
|
|
2992
|
+
if (partialSystemTree) removePartialSystemRoot(root);
|
|
2993
|
+
else removePartialUserRoot(root);
|
|
2994
|
+
console.log(partialSystemTree
|
|
2995
|
+
? `Removed partial system Node Command installation at ${root} (no packaged uninstaller, unit or application payload).`
|
|
2996
|
+
: `Removed partial per-user Node Command installation at ${root} (no packaged uninstaller was present).`);
|
|
2997
|
+
} catch (err) {
|
|
2998
|
+
const code = typeof err?.code === 'string' && /_REFUSED$/.test(err.code) ? err.code : 'PARTIAL_ROOT_REMOVAL_REFUSED';
|
|
2999
|
+
console.error(`error: [${code}] ${sanitizeForDisplay(err instanceof Error ? err.message : String(err), 300)}`);
|
|
3000
|
+
console.error(partialSystemTree
|
|
3001
|
+
? 'suggested_action: The system tree was left untouched. Inspect /opt/nc and complete or repair the failed system installation through explicit root maintenance; credentials/config were preserved.'
|
|
3002
|
+
: 'suggested_action: The tree was left untouched. Repair it with `nodecommand --user`, or remove it yourself after confirming it is your own Node Command directory.');
|
|
3003
|
+
failed += 1;
|
|
3004
|
+
}
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
2918
3007
|
runUninstallerScript(script);
|
|
2919
3008
|
console.log(`Removed ${root}${pair && pair.helperRoot !== root ? ` and paired protected helper ${pair.helperRoot}` : ''} (exit 0).`);
|
|
2920
3009
|
} catch (err) {
|
package/lib/setup.mjs
CHANGED
|
@@ -429,6 +429,190 @@ export function probeRemovalRoot(root, io = {}) {
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/** Top-level entries a per-user Node Command tree may contain. A removal never
|
|
433
|
+
* touches anything else, so a user-owned foreign directory cannot be wiped by
|
|
434
|
+
* a misdetected install. */
|
|
435
|
+
const USER_INSTALL_ENTRIES = new Set([
|
|
436
|
+
'app',
|
|
437
|
+
'backups',
|
|
438
|
+
'bin',
|
|
439
|
+
'config',
|
|
440
|
+
'data',
|
|
441
|
+
'install-darwin.sh',
|
|
442
|
+
'install-linux.sh',
|
|
443
|
+
'install-macos.sh',
|
|
444
|
+
'lib',
|
|
445
|
+
'libexec',
|
|
446
|
+
'logs',
|
|
447
|
+
'releases',
|
|
448
|
+
'tmp',
|
|
449
|
+
'tools',
|
|
450
|
+
'uninstall.sh'
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
/** Canonical system root eligible only for a narrow, root-only cleanup. */
|
|
454
|
+
export const PARTIAL_SYSTEM_ROOT = '/opt/nc';
|
|
455
|
+
|
|
456
|
+
/** A failed system install may leave only these payload/config directories. */
|
|
457
|
+
const PARTIAL_SYSTEM_ENTRIES = new Set(['config', 'tools', 'logs', 'tmp', 'releases', 'backups']);
|
|
458
|
+
|
|
459
|
+
/** Artifacts that prove a real system installation exists; never bypassed. */
|
|
460
|
+
const PARTIAL_SYSTEM_FORBIDDEN_PATHS = [
|
|
461
|
+
'/etc/systemd/system/nc-client.service',
|
|
462
|
+
'/etc/systemd/system/nc-client.service.d',
|
|
463
|
+
'/etc/systemd/user/nc-client.service',
|
|
464
|
+
'/etc/systemd/user/nc-client.service.d',
|
|
465
|
+
'/etc/systemd/system/multi-user.target.wants/nc-client.service',
|
|
466
|
+
'/etc/systemd/system/nc-privileged-helper.service',
|
|
467
|
+
'/etc/systemd/system/nc-privileged-helper.socket',
|
|
468
|
+
'/etc/tmpfiles.d/nc-privileged-helper.conf',
|
|
469
|
+
'/var/lib/nc-lifecycle',
|
|
470
|
+
'/var/lib/nodecommand/nc'
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* True when `root` is the canonical per-user tree for the current account and
|
|
475
|
+
* is not a protected system root. This is the cheap path/ownership gate; the
|
|
476
|
+
* tree itself is validated again inside `removePartialUserRoot`.
|
|
477
|
+
* @param {string} root
|
|
478
|
+
* @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string, uid?: number }} [options]
|
|
479
|
+
*/
|
|
480
|
+
export function isPartialUserRemovalCandidate(root, { platform = os.platform(), env = process.env, homedir = os.homedir(), uid = process.getuid?.() } = {}) {
|
|
481
|
+
// `uninstall.sh` is a POSIX entry; Windows keeps its own uninstaller contract.
|
|
482
|
+
if (platform === 'win32') return false;
|
|
483
|
+
if (!Number.isSafeInteger(uid) || uid <= 0) return false;
|
|
484
|
+
const canonical = { ...env };
|
|
485
|
+
delete canonical.NC_ROOT;
|
|
486
|
+
const resolved = path.resolve(root);
|
|
487
|
+
const userRoot = path.resolve(getNcRootForMode('user', { platform, env: canonical, homedir }));
|
|
488
|
+
if (resolved !== userRoot) return false;
|
|
489
|
+
const systemRoots = [
|
|
490
|
+
getNcRootForMode('system', { platform, env: canonical, homedir }),
|
|
491
|
+
...(platform === 'linux' ? ['/opt/nc', '/var/lib/nodecommand/nc'] : platform === 'darwin' ? ['/opt/nc'] : [])
|
|
492
|
+
].map((candidate) => path.resolve(candidate));
|
|
493
|
+
return !systemRoots.some(
|
|
494
|
+
(candidate) => resolved === candidate || resolved.startsWith(candidate + path.sep) || candidate.startsWith(resolved + path.sep)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* True only for the exact Linux system root when the process holds real AND
|
|
500
|
+
* effective root. The tree contents and every real-install artifact are
|
|
501
|
+
* checked again inside `removePartialSystemRoot`.
|
|
502
|
+
* @param {string} root
|
|
503
|
+
* @param {{ platform?: string, getuid?: (() => number)|null, geteuid?: (() => number)|null }} [options]
|
|
504
|
+
*/
|
|
505
|
+
export function isPartialSystemRemovalCandidate(root, { platform = os.platform(), getuid = process.getuid, geteuid = process.geteuid } = {}) {
|
|
506
|
+
if (platform !== 'linux') return false;
|
|
507
|
+
if (typeof getuid !== 'function' || typeof geteuid !== 'function') return false;
|
|
508
|
+
try {
|
|
509
|
+
if (getuid() !== 0 || geteuid() !== 0) return false;
|
|
510
|
+
} catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
return path.resolve(root) === PARTIAL_SYSTEM_ROOT;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Shared bounded removal of an already-authorized root. */
|
|
517
|
+
function removeBoundedRoot(root, { uid, allowedEntries, io, refused, owner }) {
|
|
518
|
+
const resolved = path.resolve(root);
|
|
519
|
+
if (io.realpathSync(resolved) !== resolved) throw refused('a symlinked ancestor is not removable');
|
|
520
|
+
const rootInfo = io.lstatSync(resolved);
|
|
521
|
+
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw refused('the root is not a regular directory');
|
|
522
|
+
if (rootInfo.uid !== uid) throw refused(`the root is not owned by ${owner}`);
|
|
523
|
+
const entries = io.readdirSync(resolved);
|
|
524
|
+
for (const name of entries) {
|
|
525
|
+
if (!allowedEntries.has(name)) throw refused(`unexpected top-level entry "${name}"`);
|
|
526
|
+
const info = io.lstatSync(path.join(resolved, name));
|
|
527
|
+
if (info.isSymbolicLink()) throw refused(`top-level entry "${name}" is a symlink`);
|
|
528
|
+
if (info.uid !== uid) throw refused(`top-level entry "${name}" is not owned by ${owner}`);
|
|
529
|
+
}
|
|
530
|
+
for (const [file, directory] of [['config', true], [path.join('config', 'nc-client.env'), false]]) {
|
|
531
|
+
const info = io.lstatSync(path.join(resolved, file));
|
|
532
|
+
if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile())) {
|
|
533
|
+
throw refused(`the ${file} marker is missing or not a regular ${directory ? 'directory' : 'file'}`);
|
|
534
|
+
}
|
|
535
|
+
if (info.uid !== uid) throw refused(`the ${file} marker is not owned by ${owner}`);
|
|
536
|
+
}
|
|
537
|
+
io.accessSync(path.join(resolved, 'config', 'nc-client.env'), fs.constants.R_OK);
|
|
538
|
+
for (const name of entries) {
|
|
539
|
+
io.rmSync(path.join(resolved, name), { recursive: true, force: true });
|
|
540
|
+
}
|
|
541
|
+
io.rmdirSync(resolved);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Bounded removal of a per-user Node Command tree whose packaged `uninstall.sh`
|
|
546
|
+
* is missing (a partial/failed install). Only the canonical per-user root is
|
|
547
|
+
* eligible; system/protected roots always require their packaged uninstaller.
|
|
548
|
+
* Refuses symlinked ancestors or entries, foreign ownership, unexpected
|
|
549
|
+
* top-level entries, and a missing/unreadable `config/nc-client.env` marker.
|
|
550
|
+
* The caller must hold the install lock. Credentials are removed only because
|
|
551
|
+
* this function is reached from an explicit uninstall/remove command.
|
|
552
|
+
* @param {string} root
|
|
553
|
+
* @param {{ platform?: string, env?: NodeJS.ProcessEnv, homedir?: string, uid?: number, io?: typeof fs }} [options]
|
|
554
|
+
*/
|
|
555
|
+
export function removePartialUserRoot(root, { platform = os.platform(), env = process.env, homedir = os.homedir(), uid = process.getuid?.(), io = fs } = {}) {
|
|
556
|
+
const refused = (reason) => Object.assign(
|
|
557
|
+
new Error(`Refusing bounded removal of ${root}: ${reason}.`),
|
|
558
|
+
{ code: 'USER_TREE_REMOVAL_REFUSED' }
|
|
559
|
+
);
|
|
560
|
+
if (!isPartialUserRemovalCandidate(root, { platform, env, homedir, uid })) {
|
|
561
|
+
throw refused('it is not the canonical per-user Node Command root');
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
removeBoundedRoot(root, {
|
|
565
|
+
uid,
|
|
566
|
+
allowedEntries: USER_INSTALL_ENTRIES,
|
|
567
|
+
io,
|
|
568
|
+
refused,
|
|
569
|
+
owner: 'the current user'
|
|
570
|
+
});
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (err?.code === 'USER_TREE_REMOVAL_REFUSED') throw err;
|
|
573
|
+
throw refused(err instanceof Error ? err.message : 'the tree could not be verified');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Root-only bounded removal of a failed SYSTEM install at /opt/nc. This is not
|
|
579
|
+
* a general system-root fallback: it requires real+effective root, the exact
|
|
580
|
+
* canonical root, no unit/helper/lifecycle artifact, and only the known partial
|
|
581
|
+
* payload directories. A real or unverifiable installation stays fail-closed.
|
|
582
|
+
* @param {string} root
|
|
583
|
+
* @param {{ platform?: string, getuid?: (() => number)|null, geteuid?: (() => number)|null, io?: typeof fs }} [options]
|
|
584
|
+
*/
|
|
585
|
+
export function removePartialSystemRoot(root, { platform = os.platform(), getuid = process.getuid, geteuid = process.geteuid, io = fs } = {}) {
|
|
586
|
+
const refused = (reason) => Object.assign(
|
|
587
|
+
new Error(`Refusing bounded removal of ${root}: ${reason}.`),
|
|
588
|
+
{ code: 'PARTIAL_SYSTEM_REMOVAL_REFUSED' }
|
|
589
|
+
);
|
|
590
|
+
if (!isPartialSystemRemovalCandidate(root, { platform, getuid, geteuid })) {
|
|
591
|
+
throw refused('it is not the canonical partial system root');
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
for (const file of PARTIAL_SYSTEM_FORBIDDEN_PATHS) {
|
|
595
|
+
try {
|
|
596
|
+
io.lstatSync(file);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
if (err?.code === 'ENOENT') continue;
|
|
599
|
+
throw refused(`existing installation state at ${file} cannot be verified`);
|
|
600
|
+
}
|
|
601
|
+
throw refused(`${file} exists; a real installation requires its packaged maintenance`);
|
|
602
|
+
}
|
|
603
|
+
removeBoundedRoot(path.resolve(root), {
|
|
604
|
+
uid: 0,
|
|
605
|
+
allowedEntries: PARTIAL_SYSTEM_ENTRIES,
|
|
606
|
+
io,
|
|
607
|
+
refused,
|
|
608
|
+
owner: 'root'
|
|
609
|
+
});
|
|
610
|
+
} catch (err) {
|
|
611
|
+
if (err?.code === 'PARTIAL_SYSTEM_REMOVAL_REFUSED') throw err;
|
|
612
|
+
throw refused(err instanceof Error ? err.message : 'the tree could not be verified');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
432
616
|
/**
|
|
433
617
|
* Canonical machine-wide roots that exist but cannot be inspected by the
|
|
434
618
|
* current unprivileged caller. Setup must escalate before deciding anything
|