@neotales/is-elevated 0.0.0-alpha.0 → 0.0.0-alpha.2
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/LICENSE.md +1 -1
- package/README.md +18 -9
- package/esm/ffi_bun.js +20 -23
- package/esm/ffi_deno.js +4 -10
- package/esm/ffi_koffi.js +3 -5
- package/esm/ffi_node.js +36 -29
- package/esm/index.js +37 -22
- package/esm/index.test.js +39 -0
- package/package.json +5 -6
- package/types/index.d.ts +7 -0
- package/types/index.test.d.ts +1 -0
package/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MIT License
|
|
2
2
|
|
|
3
|
-
Copyright 2024-2026
|
|
3
|
+
Copyright 2024-2026 Neotales
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
6
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ On Unix-like systems this typically means root. On Windows it checks whether the
|
|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
[](https://jsr.io/@neotales/is-elevated)
|
|
11
|
-
[](https://badge.fury.io/js/@neotales%2Fis-elevated)
|
|
12
|
+
[](https://badge.fury.io/gh/neotales%2Fjs)
|
|
13
13
|
|
|
14
14
|
## Documentation
|
|
15
15
|
|
|
@@ -64,15 +64,24 @@ On Windows, the package opens the current process token and calls `GetTokenInfor
|
|
|
64
64
|
|
|
65
65
|
This intentionally does not use `Shell32.IsUserAnAdmin`. That API checks administrator-group membership rather than the current process token, so it can disagree under User Account Control when an administrator account is running with a filtered, non-elevated token. `TokenElevation` reports the process state directly.
|
|
66
66
|
|
|
67
|
-
## Runtime
|
|
67
|
+
## Runtime Support
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
This npm package supports Node, Bun, and Deno. Deno users can import it with
|
|
70
|
+
`npm:@neotales/is-elevated`. Call `isElevatedAvailable()` before relying on
|
|
71
|
+
Windows elevation detection when the runtime's FFI configuration may be unknown.
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
- **Deno**: run with `--allow-ffi`.
|
|
74
|
+
- **Bun**: the native `bun:ffi` backend is selected automatically.
|
|
75
|
+
- **Node.js**: see [Node.js FFI](#nodejs-ffi).
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
## Node.js FFI
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
On Windows, Node.js requires a native FFI backend. Run Node >= 26 with
|
|
80
|
+
`--experimental-ffi` to enable `node:ffi`, or install koffi with
|
|
81
|
+
`npm install koffi` when optional dependencies were omitted or unavailable.
|
|
82
|
+
When neither is available, `isElevatedAvailable()` returns `false` and
|
|
83
|
+
`isElevated()` throws an error that links to this section.
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
[MIT License](./LICENSE.md)
|
package/esm/ffi_bun.js
CHANGED
|
@@ -17,13 +17,6 @@ export function evalIsProcessElevated(cache = true) {
|
|
|
17
17
|
if (cache && elevated !== undefined) {
|
|
18
18
|
return elevated;
|
|
19
19
|
}
|
|
20
|
-
elevated = globalThis.process?.getuid?.() === 0;
|
|
21
|
-
if (!globalThis.Bun) {
|
|
22
|
-
return elevated;
|
|
23
|
-
}
|
|
24
|
-
if (globalThis.process?.platform !== "win32") {
|
|
25
|
-
return elevated;
|
|
26
|
-
}
|
|
27
20
|
const advapi32 = dlopen("Advapi32.dll", {
|
|
28
21
|
OpenProcessToken: { args: [FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.bool },
|
|
29
22
|
GetTokenInformation: {
|
|
@@ -36,24 +29,28 @@ export function evalIsProcessElevated(cache = true) {
|
|
|
36
29
|
CloseHandle: { args: [FFIType.ptr], returns: FFIType.bool },
|
|
37
30
|
GetLastError: { args: [], returns: FFIType.i32 },
|
|
38
31
|
});
|
|
39
|
-
const TOKEN_QUERY = 0x0008;
|
|
40
|
-
const TOKEN_ELEVATION = 20;
|
|
41
|
-
const processHandle = kernel32.symbols.GetCurrentProcess();
|
|
42
|
-
const tokenHandle = new BigUint64Array(1);
|
|
43
|
-
const tokenHandlePtr = ptr(tokenHandle);
|
|
44
|
-
const success = advapi32.symbols.OpenProcessToken(processHandle, TOKEN_QUERY, tokenHandlePtr);
|
|
45
|
-
if (!success)
|
|
46
|
-
throw new Error("Failed to open process token");
|
|
47
32
|
try {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
const TOKEN_QUERY = 0x0008;
|
|
34
|
+
const TOKEN_ELEVATION = 20;
|
|
35
|
+
const tokenHandle = new BigUint64Array(1);
|
|
36
|
+
const success = advapi32.symbols.OpenProcessToken(kernel32.symbols.GetCurrentProcess(), TOKEN_QUERY, ptr(tokenHandle));
|
|
37
|
+
if (!success)
|
|
38
|
+
throw new Error("Failed to open process token");
|
|
39
|
+
try {
|
|
40
|
+
const tokenInfo = new Uint8Array(4);
|
|
41
|
+
const returnLength = new Uint32Array(1);
|
|
42
|
+
const result = advapi32.symbols.GetTokenInformation(tokenHandle[0], TOKEN_ELEVATION, ptr(tokenInfo), 4, ptr(returnLength));
|
|
43
|
+
if (!result)
|
|
44
|
+
throw new Error(`Failed to get token information ${kernel32.symbols.GetLastError()}`);
|
|
45
|
+
elevated = tokenInfo[0] !== 0;
|
|
46
|
+
return elevated;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
kernel32.symbols.CloseHandle(tokenHandle[0]);
|
|
50
|
+
}
|
|
55
51
|
}
|
|
56
52
|
finally {
|
|
57
|
-
|
|
53
|
+
advapi32.close();
|
|
54
|
+
kernel32.close();
|
|
58
55
|
}
|
|
59
56
|
}
|
package/esm/ffi_deno.js
CHANGED
|
@@ -18,12 +18,8 @@ const deno = globalThis.Deno;
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export function evalIsProcessElevated(cache = true) {
|
|
21
|
-
if (
|
|
22
|
-
elevated = deno.uid() === 0;
|
|
23
|
-
}
|
|
24
|
-
if (deno.build.os !== "windows") {
|
|
21
|
+
if (cache && elevated !== undefined)
|
|
25
22
|
return elevated;
|
|
26
|
-
}
|
|
27
23
|
const advapi32 = deno.dlopen("Advapi32.dll", {
|
|
28
24
|
OpenProcessToken: { parameters: ["pointer", "u32", "pointer"], result: "bool" },
|
|
29
25
|
GetTokenInformation: {
|
|
@@ -43,21 +39,19 @@ export function evalIsProcessElevated(cache = true) {
|
|
|
43
39
|
const tokenHandle = new BigUint64Array(1);
|
|
44
40
|
const tokenHandlePtr = deno.UnsafePointer.of(tokenHandle);
|
|
45
41
|
const success = advapi32.symbols.OpenProcessToken(processHandle, TOKEN_QUERY, tokenHandlePtr);
|
|
46
|
-
if (!success)
|
|
42
|
+
if (!success)
|
|
47
43
|
throw new Error("Failed to open process token");
|
|
48
|
-
}
|
|
49
44
|
try {
|
|
50
45
|
const tokenInfo = new Uint8Array(4);
|
|
51
46
|
const returnLength = new Uint32Array(1);
|
|
52
47
|
const result = advapi32.symbols.GetTokenInformation(tokenHandle[0], TOKEN_ELEVATION, deno.UnsafePointer.of(tokenInfo), 4, deno.UnsafePointer.of(returnLength));
|
|
53
|
-
if (!result)
|
|
48
|
+
if (!result)
|
|
54
49
|
throw new Error("Failed to get token information " + kernel32.symbols.GetLastError());
|
|
55
|
-
}
|
|
56
50
|
elevated = tokenInfo[0] !== 0;
|
|
57
51
|
return elevated;
|
|
58
52
|
}
|
|
59
53
|
finally {
|
|
60
|
-
kernel32.symbols.CloseHandle(
|
|
54
|
+
kernel32.symbols.CloseHandle(deno.UnsafePointer.create(tokenHandle[0]));
|
|
61
55
|
}
|
|
62
56
|
}
|
|
63
57
|
finally {
|
package/esm/ffi_koffi.js
CHANGED
|
@@ -31,16 +31,14 @@ export function evalIsProcessElevated(cache = true) {
|
|
|
31
31
|
const processHandle = GetCurrentProcess();
|
|
32
32
|
const tokenOut = [null];
|
|
33
33
|
const opened = OpenProcessToken(processHandle, TOKEN_QUERY, tokenOut);
|
|
34
|
-
if (!opened)
|
|
34
|
+
if (!opened)
|
|
35
35
|
throw new Error(`Failed to open process token (${GetLastError()})`);
|
|
36
|
-
}
|
|
37
36
|
try {
|
|
38
37
|
const tokenInfo = new Uint8Array(4);
|
|
39
38
|
const returnLength = [0];
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
39
|
+
const result = GetTokenInformation(tokenOut[0], TOKEN_ELEVATION, tokenInfo, 4, returnLength);
|
|
40
|
+
if (!result)
|
|
42
41
|
throw new Error(`Failed to get token information (${GetLastError()})`);
|
|
43
|
-
}
|
|
44
42
|
elevated = tokenInfo[0] !== 0;
|
|
45
43
|
return elevated;
|
|
46
44
|
}
|
package/esm/ffi_node.js
CHANGED
|
@@ -2,18 +2,9 @@ import process from "node:process";
|
|
|
2
2
|
const { createRequire } = process.getBuiltinModule("node:module");
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
const ffi = require("node:ffi");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
arguments: ["pointer", "u32", "pointer", "u32", "pointer"],
|
|
9
|
-
return: "u8",
|
|
10
|
-
},
|
|
11
|
-
});
|
|
12
|
-
const kernel32 = ffi.dlopen("Kernel32.dll", {
|
|
13
|
-
GetCurrentProcess: { arguments: [], return: "pointer" },
|
|
14
|
-
CloseHandle: { arguments: ["pointer"], return: "u8" },
|
|
15
|
-
GetLastError: { arguments: [], return: "u32" },
|
|
16
|
-
});
|
|
5
|
+
if (!ffi) {
|
|
6
|
+
throw new Error();
|
|
7
|
+
}
|
|
17
8
|
let elevated;
|
|
18
9
|
/**
|
|
19
10
|
* Reports whether the current process is running with elevated privileges.
|
|
@@ -31,26 +22,42 @@ export function evalIsProcessElevated(cache = true) {
|
|
|
31
22
|
if (cache && elevated !== undefined) {
|
|
32
23
|
return elevated;
|
|
33
24
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
25
|
+
const advapi32 = ffi.dlopen("Advapi32.dll", {
|
|
26
|
+
OpenProcessToken: { arguments: ["pointer", "u32", "pointer"], return: "u8" },
|
|
27
|
+
GetTokenInformation: {
|
|
28
|
+
arguments: ["pointer", "u32", "pointer", "u32", "pointer"],
|
|
29
|
+
return: "u8",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const kernel32 = ffi.dlopen("Kernel32.dll", {
|
|
33
|
+
GetCurrentProcess: { arguments: [], return: "pointer" },
|
|
34
|
+
CloseHandle: { arguments: ["pointer"], return: "u8" },
|
|
35
|
+
GetLastError: { arguments: [], return: "u32" },
|
|
36
|
+
});
|
|
43
37
|
try {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
const TOKEN_QUERY = 0x0008;
|
|
39
|
+
const TOKEN_ELEVATION = 20;
|
|
40
|
+
const processHandle = kernel32.functions.GetCurrentProcess();
|
|
41
|
+
const tokenHandleBuf = new Uint8Array(8);
|
|
42
|
+
const success = advapi32.functions.OpenProcessToken(processHandle, TOKEN_QUERY, tokenHandleBuf);
|
|
43
|
+
if (!success)
|
|
44
|
+
throw new Error(`Failed to open process token (${kernel32.functions.GetLastError()})`);
|
|
45
|
+
const tokenHandle = new DataView(tokenHandleBuf.buffer).getBigUint64(0, true);
|
|
46
|
+
try {
|
|
47
|
+
const tokenInfo = new Uint8Array(4);
|
|
48
|
+
const returnLength = new Uint8Array(4);
|
|
49
|
+
const result = advapi32.functions.GetTokenInformation(tokenHandle, TOKEN_ELEVATION, tokenInfo, 4, returnLength);
|
|
50
|
+
if (!result)
|
|
51
|
+
throw new Error(`Failed to get token information (${kernel32.functions.GetLastError()})`);
|
|
52
|
+
elevated = tokenInfo[0] !== 0;
|
|
53
|
+
return elevated;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
kernel32.functions.CloseHandle(tokenHandle);
|
|
49
57
|
}
|
|
50
|
-
elevated = tokenInfo[0] !== 0;
|
|
51
|
-
return elevated;
|
|
52
58
|
}
|
|
53
59
|
finally {
|
|
54
|
-
|
|
60
|
+
advapi32.lib.close();
|
|
61
|
+
kernel32.lib.close();
|
|
55
62
|
}
|
|
56
63
|
}
|
package/esm/index.js
CHANGED
|
@@ -8,42 +8,57 @@ function evalUnixElevation(cache = true) {
|
|
|
8
8
|
elevated = uid === 0;
|
|
9
9
|
return elevated;
|
|
10
10
|
}
|
|
11
|
-
let impl = evalUnixElevation;
|
|
12
11
|
const runtime = globalThis;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
12
|
+
const isWindows = process.platform === "win32";
|
|
13
|
+
function unsupportedNodeFfi() {
|
|
14
|
+
throw new Error("Unable to determine Windows process elevation because neither node:ffi nor koffi is available. Run Node.js >= 26 with --experimental-ffi, or install koffi with npm install koffi. See https://github.com/neotales/js-os/blob/dev/npm/is-elevated/README.md#nodejs-ffi");
|
|
15
|
+
}
|
|
16
|
+
function unsupportedRuntimeFfi() {
|
|
17
|
+
throw new Error("Unable to determine Windows process elevation because the runtime FFI backend is unavailable. Run Deno with --allow-ffi. See https://github.com/neotales/js-os/blob/dev/npm/is-elevated/README.md#runtime-support");
|
|
18
|
+
}
|
|
19
|
+
let impl = evalUnixElevation;
|
|
20
|
+
let isSupported = !isWindows;
|
|
21
|
+
if (isWindows) {
|
|
22
|
+
try {
|
|
23
|
+
if (runtime.Deno) {
|
|
24
|
+
impl = (await import("./ffi_deno.js")).evalIsProcessElevated;
|
|
25
|
+
}
|
|
26
|
+
else if (runtime.Bun) {
|
|
27
|
+
impl = (await import("./ffi_bun.js")).evalIsProcessElevated;
|
|
30
28
|
}
|
|
31
|
-
|
|
29
|
+
else {
|
|
32
30
|
try {
|
|
33
|
-
impl =
|
|
31
|
+
impl = (await import("./ffi_node.js")).evalIsProcessElevated;
|
|
34
32
|
}
|
|
35
33
|
catch {
|
|
36
|
-
|
|
37
|
-
console.debug(error);
|
|
34
|
+
impl = (await import("./ffi_koffi.js")).evalIsProcessElevated;
|
|
38
35
|
}
|
|
39
36
|
}
|
|
37
|
+
// A backend import can succeed while FFI permissions or DLL loading fail.
|
|
38
|
+
// Probe it once before exposing a supported evaluator.
|
|
39
|
+
impl();
|
|
40
|
+
isSupported = true;
|
|
40
41
|
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (process.env.DEBUG === "true")
|
|
44
|
+
console.debug(error);
|
|
45
|
+
impl = runtime.Deno || runtime.Bun ? unsupportedRuntimeFfi : unsupportedNodeFfi;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns whether elevation detection is available in the current runtime.
|
|
50
|
+
*
|
|
51
|
+
* @returns `true` when {@linkcode isElevated} can evaluate the current process.
|
|
52
|
+
*/
|
|
53
|
+
export function isElevatedAvailable() {
|
|
54
|
+
return isSupported;
|
|
41
55
|
}
|
|
42
56
|
/**
|
|
43
57
|
* Reports whether the current process is running with elevated privileges.
|
|
44
58
|
*
|
|
45
59
|
* @param cache - Whether to reuse the result from the first evaluation.
|
|
46
60
|
* @returns Whether the process has elevated privileges.
|
|
61
|
+
* @throws {Error} On Windows when the runtime has no usable FFI backend.
|
|
47
62
|
* @example
|
|
48
63
|
* import { isElevated } from "@neotales/is-elevated";
|
|
49
64
|
*
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { strictEqual, throws } from "node:assert/strict";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { isElevated, isElevatedAvailable } from "./index.js";
|
|
5
|
+
import { evalIsProcessElevated as evalNodeIsProcessElevated } from "./node.js";
|
|
6
|
+
test("isElevated returns a boolean", () => {
|
|
7
|
+
if (process.platform === "win32" && !isElevatedAvailable())
|
|
8
|
+
return;
|
|
9
|
+
strictEqual(typeof isElevated(), "boolean");
|
|
10
|
+
strictEqual(typeof isElevated(false), "boolean");
|
|
11
|
+
});
|
|
12
|
+
test("isElevatedAvailable returns a boolean", () => {
|
|
13
|
+
strictEqual(typeof isElevatedAvailable(), "boolean");
|
|
14
|
+
});
|
|
15
|
+
test("isElevated caches by default", () => {
|
|
16
|
+
if (process.platform === "win32" && !isElevatedAvailable())
|
|
17
|
+
return;
|
|
18
|
+
strictEqual(isElevated(), isElevated());
|
|
19
|
+
});
|
|
20
|
+
test("node evaluator returns a boolean", () => {
|
|
21
|
+
strictEqual(typeof evalNodeIsProcessElevated(), "boolean");
|
|
22
|
+
strictEqual(typeof evalNodeIsProcessElevated(false), "boolean");
|
|
23
|
+
});
|
|
24
|
+
test("node evaluator matches effective uid semantics on unix-like runtimes", { skip: process.platform === "win32" || (!process.geteuid && !process.getuid) }, () => {
|
|
25
|
+
strictEqual(evalNodeIsProcessElevated(false), (process.geteuid?.() ?? process.getuid?.()) === 0);
|
|
26
|
+
});
|
|
27
|
+
test("isElevated matches effective uid semantics on unix-like runtimes", { skip: process.platform === "win32" || (!process.geteuid && !process.getuid) }, () => {
|
|
28
|
+
strictEqual(isElevated(false), (process.geteuid?.() ?? process.getuid?.()) === 0);
|
|
29
|
+
});
|
|
30
|
+
test("node evaluator falls back to false when uid is unavailable", { skip: process.platform !== "win32" }, () => {
|
|
31
|
+
strictEqual(evalNodeIsProcessElevated(false), false);
|
|
32
|
+
});
|
|
33
|
+
test("root helper returns a boolean on Windows-specific runtimes", { skip: process.platform !== "win32" }, () => {
|
|
34
|
+
if (!isElevatedAvailable()) {
|
|
35
|
+
throws(() => isElevated(), /node:ffi nor koffi.*#nodejs-ffi/);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
strictEqual(typeof isElevated(false), "boolean");
|
|
39
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neotales/is-elevated",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.2",
|
|
4
4
|
"description": "Detect whether the current process is elevated.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"admin",
|
|
@@ -47,10 +47,9 @@
|
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc -p tsconfig.json",
|
|
50
|
-
"
|
|
51
|
-
"test": "
|
|
52
|
-
"test:
|
|
53
|
-
"test:
|
|
54
|
-
"test:deno": "pnpm run build:test && deno test -A .test/tests"
|
|
50
|
+
"test": "node --test esm/*.test.js",
|
|
51
|
+
"test:ffi": "node --experimental-ffi --test esm/*.test.js",
|
|
52
|
+
"test:bun": "bun test ./esm",
|
|
53
|
+
"test:deno": "deno test -A esm/*.test.js"
|
|
55
54
|
}
|
|
56
55
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether elevation detection is available in the current runtime.
|
|
3
|
+
*
|
|
4
|
+
* @returns `true` when {@linkcode isElevated} can evaluate the current process.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isElevatedAvailable(): boolean;
|
|
1
7
|
/**
|
|
2
8
|
* Reports whether the current process is running with elevated privileges.
|
|
3
9
|
*
|
|
4
10
|
* @param cache - Whether to reuse the result from the first evaluation.
|
|
5
11
|
* @returns Whether the process has elevated privileges.
|
|
12
|
+
* @throws {Error} On Windows when the runtime has no usable FFI backend.
|
|
6
13
|
* @example
|
|
7
14
|
* import { isElevated } from "@neotales/is-elevated";
|
|
8
15
|
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|