@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright 2024-2026 Mr. Neobeard
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
  ![logo](https://raw.githubusercontent.com/neotales/js-std/refs/heads/dev/eng/assets/logo.png)
9
9
 
10
10
  [![JSR](https://jsr.io/badges/@neotales/is-elevated)](https://jsr.io/@neotales/is-elevated)
11
- [![npm version](https://badge.fury.io/js/@neostd%2Fis-elevated.svg)](https://badge.fury.io/js/@neostd%2Fis-elevated)
12
- [![GitHub version](https://badge.fury.io/gh/neostd%2Fjs.svg)](https://badge.fury.io/gh/neostd%2Fjs)
11
+ [![npm version](https://badge.fury.io/js/@neotales%2Fis-elevated.svg)](https://badge.fury.io/js/@neotales%2Fis-elevated)
12
+ [![GitHub version](https://badge.fury.io/gh/neotales%2Fjs.svg)](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 Notes
67
+ ## Runtime Support
68
68
 
69
- Node.js uses either `node:ffi` or the optional `koffi` peer dependency when available. Bun and Deno use their native FFI support.
70
- Tests for OS-specific behavior are skipped when the current runtime does not support the required platform semantics.
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
- ## License
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
- [MIT License](./LICENSE.md)
77
+ ## Node.js FFI
75
78
 
76
- ## Runtime Support
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
- This npm package supports Node, Bun, and Deno. Deno users can import it with `npm:@neotales/is-elevated`; use the JSR package for the Deno-only implementation.
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 tokenInfo = new Uint8Array(4);
49
- const returnLength = new Uint32Array(1);
50
- const result = advapi32.symbols.GetTokenInformation(tokenHandle[0], TOKEN_ELEVATION, ptr(tokenInfo), 4, ptr(returnLength));
51
- if (!result)
52
- throw new Error(`Failed to get token information ${kernel32.symbols.GetLastError()}`);
53
- elevated = tokenInfo[0] !== 0;
54
- return elevated;
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
- kernel32.symbols.CloseHandle(tokenHandlePtr);
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 (!cache || elevated === undefined) {
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(tokenHandlePtr);
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 ok = GetTokenInformation(tokenOut[0], TOKEN_ELEVATION, tokenInfo, 4, returnLength);
41
- if (!ok) {
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
- const advapi32 = ffi.dlopen("Advapi32.dll", {
6
- OpenProcessToken: { arguments: ["pointer", "u32", "pointer"], return: "u8" },
7
- GetTokenInformation: {
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 TOKEN_QUERY = 0x0008;
35
- const TOKEN_ELEVATION = 20;
36
- const processHandle = kernel32.functions.GetCurrentProcess();
37
- const tokenHandleBuf = new Uint8Array(8);
38
- const opened = advapi32.functions.OpenProcessToken(processHandle, TOKEN_QUERY, tokenHandleBuf);
39
- if (!opened) {
40
- throw new Error(`Failed to open process token (${kernel32.functions.GetLastError()})`);
41
- }
42
- const tokenHandle = new DataView(tokenHandleBuf.buffer).getBigUint64(0, true);
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 tokenInfo = new Uint8Array(4);
45
- const returnLength = new Uint8Array(4);
46
- const ok = advapi32.functions.GetTokenInformation(tokenHandle, TOKEN_ELEVATION, tokenInfo, 4, returnLength);
47
- if (!ok) {
48
- throw new Error(`Failed to get token information (${kernel32.functions.GetLastError()})`);
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
- kernel32.functions.CloseHandle(tokenHandle);
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
- if (process.platform === "win32") {
14
- const { createRequire } = process.getBuiltinModule("node:module");
15
- const require = createRequire(import.meta.url);
16
- if (runtime.Deno) {
17
- impl = require("./ffi_deno.js").evalIsProcessElevated;
18
- }
19
- else if (runtime.Bun) {
20
- impl = require("./ffi_bun.js").evalIsProcessElevated;
21
- }
22
- else {
23
- try {
24
- if (process.getBuiltinModule("node:ffi")) {
25
- impl = require("./ffi_node.js").evalIsProcessElevated;
26
- }
27
- else {
28
- impl = require("./ffi_koffi.js").evalIsProcessElevated;
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
- catch (error) {
29
+ else {
32
30
  try {
33
- impl = require("./ffi_koffi.js").evalIsProcessElevated;
31
+ impl = (await import("./ffi_node.js")).evalIsProcessElevated;
34
32
  }
35
33
  catch {
36
- if (process.env.DEBUG === "true")
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.0",
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
- "build:test": "tsc -p tsconfig.test.json",
51
- "test": "pnpm run build:test && node --test .test/tests/*.test.js",
52
- "test:ffi": "pnpm run build:test && node --experimental-ffi --test .test/tests/*.test.js",
53
- "test:bun": "pnpm run build:test && bun test ./.test/tests",
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 {};