diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4fd12c1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.node binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98ac10b..44d129b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,20 @@ on: [push, pull_request] permissions: contents: read jobs: - macos: - runs-on: macos-latest + platforms: + strategy: + fail-fast: false + matrix: + os: + [ + macos-latest, + macos-15-intel, + ubuntu-latest, + ubuntu-24.04-arm, + windows-latest, + windows-11-arm, + ] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -16,6 +28,14 @@ jobs: - run: npm run typecheck - run: npm run build - run: npm test + - name: Hidden live processes fail closed + if: runner.os == 'Linux' + run: | + fixture=$(mktemp -d) + cp -R index.mjs lib test "$fixture/" + chmod -R a+rX "$fixture" + sudo unshare --mount --pid --fork --mount-proc sh -c 'mount -o remount,hidepid=2 /proc; exec "$@"' sh "$(command -v node)" "$fixture/test/hidepid.fixture.mjs" + - run: npm run test:package - uses: actions/setup-node@v4 with: node-version: 24 @@ -24,3 +44,33 @@ jobs: with: node-version: 26 - run: npm test + - uses: actions/setup-node@v4 + with: + node-version: 20.19.4 + - run: npm test + - uses: actions/upload-artifact@v4 + if: runner.os != 'Linux' + with: + name: prebuild-${{ runner.os }}-${{ runner.arch }} + path: prebuilds/ + package: + needs: platforms + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci --ignore-scripts + - uses: actions/download-artifact@v4 + with: + pattern: prebuild-* + path: prebuilds + merge-multiple: true + - run: npm run pack:release + - run: npm run test:package + - uses: actions/upload-artifact@v4 + with: + name: npm-package + path: unique-pid-*.tgz diff --git a/README.md b/README.md index d298e5c..a95966d 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,62 @@ # unique-pid -Persist a process identity. Later, check whether that PID still belongs to the -same process. +Save a process ID and identify the same process later. -**Early macOS prototype. Not published to npm or ready for production cleanup.** +Operating systems recycle PIDs. A PID saved in a file can eventually belong to +an unrelated process. `unique-pid` combines the PID with its exact OS-reported +start identity in a serializable token, so you can tell them apart. -Operating systems reuse process IDs. A saved PID alone can refer to an unrelated -process after the original exits. `unique-pid` pairs it with its exact -kernel-reported start time and boot-session identity. +Capture the token when a process starts, store it in a file or database, and +check it later, even from another invocation of your CLI. Supports macOS, +Linux, and Windows. No `ps` required. ```js -import { capture, check } from "./index.mjs"; - -const captured = capture(child.pid); -if (captured.status === "captured") { - // Persist captured.token in trusted application state. - const result = check(captured.token); - console.log(result.status); // same | different | gone | unknown +import { capture, check } from "unique-pid"; + +const captured = capture(process.pid); +if (!captured.ok) { + console.error(captured.error); +} else { + const checked = check(captured.value); + console.log(checked); // { ok: true, value: 'same' } } ``` -Both operations use the same native reader. There is no approximate timestamp, -`Date.now()`, shell command, `ps`, or running-process listing. - ## API -### `capture(pid)` - -Returns `{ status: 'captured', token }`, `{ status: 'gone', errno }`, or -`{ status: 'unknown', errno }`. Invalid PIDs throw. - -### `check(token)` - -Returns one of: - -| Status | Meaning | -| ----------- | ---------------------------------------------------------------------- | -| `same` | The observed PID, kernel start time, and boot session match. | -| `different` | The PID exists, but the recorded identity does not match. | -| `gone` | The OS explicitly reports that the process does not exist. | -| `unknown` | Permission denial or another inspection failure prevents verification. | - -Malformed tokens throw before querying the OS. Never treat `unknown` as `gone`. -`same` is a point-in-time identity observation, not an app-readiness check or -proof that the process is not a zombie. - -### `decode(token)` - -Returns the validated token fields for diagnostics. Tokens contain a format -version, platform, boot UUID, PID, and exact start seconds/microseconds stored as -decimal strings. Base64url is encoding, not encryption or authentication. +All functions return `{ ok: true, value }` or +`{ ok: false, error: { code, message } }`. Invalid input, unavailable native +binaries, and permission failures are results, not thrown exceptions. -## Implementation +- `capture(pid)` returns a serializable identity token. +- `check(token)` returns `same`, `different`, or `gone`. +- `decode(token)` returns the token's validated fields for inspection. -A small C addon uses the stable Node-API 8 ABI. The JavaScript layer owns token -encoding and validation, with TypeScript declarations. macOS uses -`proc_pidinfo(PROC_PIDTBSDINFO)` and `sysctlbyname("kern.bootsessionuuid")`. -It does not embed Python or compile a helper executable at runtime. +An inspection error is not proof that a process is gone. Tokens use exact +OS start values, not approximate dates. Linux also includes boot and PID +namespace identity; macOS includes boot identity. Windows uses the process's +64-bit creation time. -The initial prototype's same compiled addon was tested on Node 22, 24, and 26. -It worked under a sandbox that denied `/bin/ps`; a stricter sandbox denied native -inspection and correctly produced `unknown`. This is not a sandbox bypass or a -guarantee that every environment permits inspection. - -## Safety boundaries - -- Identity is not ownership. Only manage processes your application owns, and - keep tokens in trusted state. A forged token is not an authorization grant. -- Capture promptly after spawn and account for early exit. An arbitrary-PID - query cannot establish that the caller launched that process. A cooperating - child can send its self-captured token through IPC. -- Rechecking then signaling is not atomic on macOS. This prototype deliberately - exposes no `kill` or `terminate` API. Calling `process.kill()` after `check()` - still has a time-of-check/time-of-use race. -- Identifying a group leader does not establish ownership of all descendants. -- Tokens are local to the originating host/boot context. They are not portable - process references across machines or PID namespaces. -- Changes to command titles or an exec transition do not necessarily change - process identity; executable role and readiness need separate checks. - -The idea follows psutil's PID-plus-creation-time identity model, not a full port -of its monitoring API. See [psutil's PID reuse documentation](https://psutil.io/faq/#pid-reuse). +Tokens belong in trusted state on the originating machine. They are not +credentials. Identity checks do not prove ownership or make a later PID-based +signal atomic. This package does not terminate processes. ## Development -Requires macOS, Node 22+, Python, and Xcode Command Line Tools for the development -build. End-user prebuilt distribution is planned, not implemented yet. The -package remains private in package.json to prevent accidental npm publication. +Node 20.19.4+ (20.x) or 22.12+. Native development +builds require Python and Xcode Command Line Tools on macOS, or Visual Studio +C++ Build Tools on Windows. Linux reads procfs directly. ```sh npm ci --ignore-scripts npm run build -npm run format:check npm run typecheck +npm run format:check npm test ``` -Tests inspect only short-lived children they create and stop them through IPC. -They cover exact repeated reads, parent/child agreement, fresh-process token -verification, timestamp and boot mismatches, malformed tokens, invalid PIDs, -title changes, and actual process exit. Mismatches are simulated against a live -owned child; tests do not force PID recycling or change the system clock. - -## Next steps - -- Linux and Windows backends with exact native identity fields. -- Prebuilt binaries, package installation tests, and platform/architecture CI. -- Additional denial, restart, reboot, clock-change, and short-lived-child tests. -- A lifecycle API only after its ownership and race semantics are defined; - use retained OS handles where supported instead of promising atomic safety - from a serialized token alone. - -## License +CI builds and tests x64 and ARM64 binaries for macOS and Windows, then +assembles the release tarball with `npm run pack:release`. Installing that +package needs no compiler or install scripts. Linux reads procfs directly. MIT diff --git a/binding.gyp b/binding.gyp index a955888..9e8f2da 100644 --- a/binding.gyp +++ b/binding.gyp @@ -3,9 +3,15 @@ "target_name": "identity", "sources": ["src/identity.c"], "defines": ["NAPI_VERSION=8"], - "xcode_settings": { - "GCC_C_LANGUAGE_STANDARD": "c11", - "WARNING_CFLAGS": ["-Wall", "-Wextra", "-Werror"] - } + "conditions": [ + ["OS=='mac'", { + "sources": ["src/darwin.c"], + "xcode_settings": { + "GCC_C_LANGUAGE_STANDARD": "c11", + "WARNING_CFLAGS": ["-Wall", "-Wextra", "-Werror"] + } + }], + ["OS=='win'", { "sources": ["src/win32.c"] }] + ] }] } diff --git a/index.d.mts b/index.d.mts index 8e84e55..286b57b 100644 --- a/index.d.mts +++ b/index.d.mts @@ -1,20 +1,23 @@ +export type ErrorCode = + | "INVALID_ARGUMENT" + | "INVALID_TOKEN" + | "NOT_FOUND" + | "ACCESS_DENIED" + | "UNSUPPORTED_PLATFORM" + | "NATIVE_UNAVAILABLE" + | "INSPECTION_FAILED"; +export type Result = + | { ok: true; value: T } + | { ok: false; error: { code: ErrorCode; message: string } }; export interface ProcessIdentity { - v: 1; - platform: "darwin"; - bootId: string; - pid: number; - seconds: string; - micros: string; + readonly version: 1; + readonly platform: "darwin" | "linux" | "win32"; + readonly pid: number; + readonly bootId: string | null; + readonly startTime: string; } - -export type Unavailable = { status: "gone" | "unknown"; errno: number }; - -export declare function capture( - pid: number, -): { status: "captured"; token: string } | Unavailable; - +export declare function capture(pid: number): Result; export declare function check( token: string, -): { status: "same" | "different" } | Unavailable; - -export declare function decode(token: string): ProcessIdentity; +): Result<"same" | "different" | "gone">; +export declare function decode(token: string): Result; diff --git a/index.mjs b/index.mjs index f4d06e1..4c87c75 100644 --- a/index.mjs +++ b/index.mjs @@ -1,86 +1,105 @@ import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; +import { readLinux } from "./lib/linux.mjs"; +import { decodeIdentity, encodeIdentity } from "./lib/token.mjs"; -if (process.platform !== "darwin") - throw new Error("This prototype supports macOS only"); -const native = createRequire(import.meta.url)("node-gyp-build")( - fileURLToPath(new URL(".", import.meta.url)), -); -const prefix = "pi1."; -const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; -const decimal = /^(0|[1-9][0-9]{0,19})$/; +let native; +const failure = (code, message) => ({ ok: false, error: { code, message } }); -function validate(value) { - if ( - !value || - typeof value !== "object" || - Array.isArray(value) || - Object.keys(value).sort().join(",") !== - "bootId,micros,pid,platform,seconds,v" || - value.v !== 1 || - value.platform !== "darwin" || - !Number.isInteger(value.pid) || - value.pid < 1 || - value.pid > 2147483647 || - typeof value.bootId !== "string" || - !uuid.test(value.bootId) || - typeof value.seconds !== "string" || - !decimal.test(value.seconds) || - BigInt(value.seconds) > 18446744073709551615n || - typeof value.micros !== "string" || - !decimal.test(value.micros) || - BigInt(value.micros) > 999999n - ) { - throw new TypeError("Invalid process identity"); +function observe(pid) { + if (process.platform === "linux") return readLinux(pid); + if (process.platform !== "darwin" && process.platform !== "win32") { + return failure( + "UNSUPPORTED_PLATFORM", + "This operating system is not supported", + ); } - return value; -} - -export function decode(token) { - if ( - typeof token !== "string" || - token.length > 1024 || - !token.startsWith(prefix) - ) { - throw new TypeError("Invalid process identity token"); + if (!native) { + try { + native = createRequire(import.meta.url)("node-gyp-build")( + fileURLToPath(new URL(".", import.meta.url)), + ); + } catch { + return failure( + "NATIVE_UNAVAILABLE", + "The native addon could not be loaded", + ); + } } - const encoded = token.slice(prefix.length); - if (!/^[A-Za-z0-9_-]+$/.test(encoded)) - throw new TypeError("Invalid token encoding"); - const bytes = Buffer.from(encoded, "base64url"); - if (bytes.toString("base64url") !== encoded) - throw new TypeError("Noncanonical token encoding"); - const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes); - return validate(JSON.parse(json)); -} - -export function capture(pid) { const found = native.read(pid); - if (found.status !== "found") return found; - const identity = validate({ - v: 1, - platform: "darwin", - bootId: found.bootId, - pid: found.pid, - seconds: found.seconds, - micros: found.micros, - }); + if (found.status === "gone") + return failure("NOT_FOUND", "The process no longer exists"); + if (found.status === "denied") + return failure("ACCESS_DENIED", "Process inspection was denied"); + if (found.status !== "found") + return failure("INSPECTION_FAILED", "Process identity could not be read"); return { - status: "captured", - token: prefix + Buffer.from(JSON.stringify(identity)).toString("base64url"), + ok: true, + value: { + version: 1, + platform: process.platform, + pid, + bootId: found.bootId, + startTime: found.startTime, + }, }; } +export function capture(pid) { + try { + if (!Number.isInteger(pid) || pid < 1 || pid > 2147483647) { + return failure( + "INVALID_ARGUMENT", + "PID must be a positive 32-bit integer", + ); + } + const observed = observe(pid); + if (!observed.ok) return observed; + const token = encodeIdentity(observed.value); + if (!decodeIdentity(token).ok) + return failure( + "INSPECTION_FAILED", + "The OS returned an invalid identity", + ); + return { ok: true, value: token }; + } catch { + return failure("INSPECTION_FAILED", "Process identity could not be read"); + } +} + export function check(token) { - const expected = decode(token); - const found = native.read(expected.pid); - if (found.status !== "found") return found; - return { - status: - found.bootId === expected.bootId && - found.seconds === expected.seconds && - found.micros === expected.micros - ? "same" - : "different", - }; + try { + const decoded = decodeIdentity(token); + if (!decoded.ok) return decoded; + if (decoded.value.platform !== process.platform) + return { ok: true, value: "different" }; + const current = capture(decoded.value.pid); + if (!current.ok) + return current.error.code === "NOT_FOUND" + ? { ok: true, value: "gone" } + : current; + const observed = decodeIdentity(current.value); + if (!observed.ok) + return failure( + "INSPECTION_FAILED", + "The OS returned an invalid identity", + ); + return { + ok: true, + value: + observed.value.startTime === decoded.value.startTime && + observed.value.bootId === decoded.value.bootId + ? "same" + : "different", + }; + } catch { + return failure( + "INSPECTION_FAILED", + "Process identity could not be checked", + ); + } +} + +export function decode(token) { + return decodeIdentity(token); } diff --git a/lib/linux.mjs b/lib/linux.mjs new file mode 100644 index 0000000..9f5d1ab --- /dev/null +++ b/lib/linux.mjs @@ -0,0 +1,109 @@ +import { openSync, readSync, closeSync, statSync } from "node:fs"; + +function boundedRead(path, limit) { + const fd = openSync(path, "r"); + const bytes = Buffer.alloc(limit + 1); + let length = 0; + try { + while (length < bytes.length) { + const count = readSync(fd, bytes, length, bytes.length - length, null); + if (!count) break; + length += count; + } + } finally { + closeSync(fd); + } + if (length > limit) throw new Error("Oversized proc record"); + return new TextDecoder("utf-8", { fatal: true }).decode( + bytes.subarray(0, length), + ); +} + +export function parseStat(text, pid) { + if (!text.startsWith(`${pid} (`)) return null; + const end = text.lastIndexOf(")"); + if (end < 0 || text[end + 1] !== " ") return null; + const fields = text + .slice(end + 2) + .trim() + .split(/\s+/); + if ( + !/^[RSDZTtXxKWPI]$/.test(fields[0] ?? "") || + !/^(0|[1-9][0-9]{0,19})$/.test(fields[19] ?? "") || + BigInt(fields[19]) > 18446744073709551615n + ) + return null; + return { state: fields[0], startTime: fields[19] }; +} + +export function readLinux(pid) { + let raw; + try { + raw = boundedRead(`/proc/${pid}/stat`, 8192); + } catch (error) { + if (error.code === "ENOENT" || error.code === "ESRCH") { + try { + process.kill(pid, 0); + } catch (probeError) { + if (probeError.code === "ESRCH") { + return { + ok: false, + error: { + code: "NOT_FOUND", + message: "The process no longer exists", + }, + }; + } + error = probeError; + } + } + return { + ok: false, + error: { + code: + error.code === "EACCES" || error.code === "EPERM" + ? "ACCESS_DENIED" + : "INSPECTION_FAILED", + message: "Process identity could not be read", + }, + }; + } + const parsed = parseStat(raw, pid); + if (!parsed) + return { + ok: false, + error: { code: "INSPECTION_FAILED", message: "Invalid proc record" }, + }; + if (["Z", "X", "x"].includes(parsed.state)) + return { + ok: false, + error: { code: "NOT_FOUND", message: "The process has exited" }, + }; + try { + const bootId = boundedRead("/proc/sys/kernel/random/boot_id", 64) + .trim() + .toLowerCase(); + const namespace = statSync("/proc/self/ns/pid", { bigint: true }); + return { + ok: true, + value: { + version: 1, + platform: "linux", + pid, + bootId: `${bootId}:${namespace.dev}:${namespace.ino}`, + startTime: parsed.startTime, + }, + }; + } catch (error) { + return { + ok: false, + error: { + code: + error.code === "EACCES" || error.code === "EPERM" + ? "ACCESS_DENIED" + : "INSPECTION_FAILED", + message: "Process scope could not be read", + }, + }; + } +} diff --git a/lib/token.mjs b/lib/token.mjs new file mode 100644 index 0000000..10d91f1 --- /dev/null +++ b/lib/token.mjs @@ -0,0 +1,72 @@ +const prefix = "upid1."; +const uuid = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"; +const decimal = /^(0|[1-9][0-9]{0,19})$/; +const integer = (value) => + typeof value === "string" && + decimal.test(value) && + BigInt(value) <= 18446744073709551615n; + +function valid(value) { + if ( + !value || + typeof value !== "object" || + Array.isArray(value) || + Object.keys(value).sort().join(",") !== + "bootId,pid,platform,startTime,version" || + value.version !== 1 || + !Number.isInteger(value.pid) || + value.pid < 1 || + value.pid > 2147483647 + ) + return false; + if (value.platform === "win32") + return value.bootId === null && integer(value.startTime); + if (typeof value.bootId !== "string" || typeof value.startTime !== "string") + return false; + if (value.platform === "linux") + return ( + new RegExp(`^${uuid}:[0-9]{1,20}:[0-9]{1,20}$`).test(value.bootId) && + integer(value.startTime) + ); + if ( + value.platform !== "darwin" || + !new RegExp(`^${uuid}$`).test(value.bootId) + ) + return false; + const parts = value.startTime.split(":"); + return ( + parts.length === 2 && + integer(parts[0]) && + integer(parts[1]) && + BigInt(parts[1]) < 1000000n + ); +} + +export function encodeIdentity(identity) { + return prefix + Buffer.from(JSON.stringify(identity)).toString("base64url"); +} + +export function decodeIdentity(token) { + const invalid = { + ok: false, + error: { code: "INVALID_TOKEN", message: "Invalid process identity token" }, + }; + try { + if ( + typeof token !== "string" || + token.length > 1024 || + !token.startsWith(prefix) + ) + return invalid; + const encoded = token.slice(prefix.length); + if (!/^[A-Za-z0-9_-]+$/.test(encoded)) return invalid; + const bytes = Buffer.from(encoded, "base64url"); + if (bytes.toString("base64url") !== encoded) return invalid; + const value = JSON.parse( + new TextDecoder("utf-8", { fatal: true }).decode(bytes), + ); + return valid(value) ? { ok: true, value } : invalid; + } catch { + return invalid; + } +} diff --git a/package-lock.json b/package-lock.json index 80d5d63..5875e64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,45 +1,29 @@ { "name": "unique-pid", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "unique-pid", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "os": [ - "darwin" + "darwin", + "linux", + "win32" ], "dependencies": { "node-gyp-build": "4.8.4" }, "devDependencies": { "@types/node": "^22.0.0", - "node-gyp": "^11.0.0", + "node-gyp": "^12.4.0", "prettier": "^3.0.0", "typescript": "^5.0.0" }, "engines": { - "node": ">=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" + "node": "^20.19.4 || >=22.12.0" } }, "node_modules/@isaacs/fs-minipass": { @@ -55,47 +39,6 @@ "node": ">=18.0.0" } }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -107,90 +50,13 @@ } }, "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/chownr": { @@ -203,121 +69,6 @@ "node": ">=18" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -328,13 +79,6 @@ "node": ">=6" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -360,356 +104,33 @@ } } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=20" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -723,53 +144,29 @@ "node": ">= 18" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", - "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp-build": { @@ -784,66 +181,19 @@ } }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/p-map": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.7.tgz", - "integrity": "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/picomatch": { @@ -876,47 +226,15 @@ } }, "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -930,200 +248,6 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/tar": { "version": "7.5.22", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", @@ -1172,6 +296,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -1179,144 +313,20 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/yallist": { diff --git a/package.json b/package.json index 08db999..b42db70 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { "name": "unique-pid", - "version": "0.0.0", - "private": true, - "description": "Persist and verify OS-backed process identities without ps", + "version": "0.1.0", + "description": "Unique, serializable process IDs for Node.js", "type": "module", "gypfile": false, "engines": { - "node": ">=22" + "node": "^20.19.4 || >=22.12.0" }, "os": [ - "darwin" + "darwin", + "linux", + "win32" ], "exports": { ".": { @@ -20,6 +21,7 @@ "files": [ "index.mjs", "index.d.mts", + "lib", "prebuilds", "LICENSE", "README.md" @@ -30,8 +32,10 @@ }, "license": "MIT", "scripts": { - "build": "node-gyp rebuild", - "test": "node test/identity.mjs", + "build": "node scripts/build.mjs", + "test": "node --test test/identity.test.mjs test/linux.test.mjs test/loading.test.mjs", + "test:package": "node scripts/package-smoke.mjs", + "pack:release": "node scripts/pack-release.mjs", "typecheck": "tsc --noEmit", "format": "prettier --write .", "format:check": "prettier --check ." @@ -41,7 +45,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", - "node-gyp": "^11.0.0", + "node-gyp": "^12.4.0", "prettier": "^3.0.0", "typescript": "^5.0.0" } diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..8022efc --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,17 @@ +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { mkdirSync, copyFileSync } from "node:fs"; +if (process.platform !== "linux") { + const gyp = createRequire(import.meta.url).resolve( + "node-gyp/bin/node-gyp.js", + ); + const result = spawnSync(process.execPath, [gyp, "rebuild"], { + stdio: "inherit", + }); + if (result.status !== 0) process.exitCode = result.status ?? 1; + else { + const directory = `prebuilds/${process.platform}-${process.arch}`; + mkdirSync(directory, { recursive: true }); + copyFileSync("build/Release/identity.node", `${directory}/node.napi.node`); + } +} diff --git a/scripts/pack-release.mjs b/scripts/pack-release.mjs new file mode 100644 index 0000000..7c9ba02 --- /dev/null +++ b/scripts/pack-release.mjs @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; + +const required = ["darwin-arm64", "darwin-x64", "win32-arm64", "win32-x64"].map( + (target) => `prebuilds/${target}/node.napi.node`, +); +for (const file of required) assert.ok(existsSync(file), `Missing ${file}`); +const packed = spawnSync( + process.execPath, + [process.env.npm_execpath, "pack", "--json", "--ignore-scripts"], + { + encoding: "utf8", + timeout: 60000, + }, +); +assert.equal(packed.status, 0, packed.stderr); +const [archive] = JSON.parse(packed.stdout); +const files = new Set(archive.files.map(({ path }) => path)); +for (const file of required) + assert.ok(files.has(file), `Tarball missing ${file}`); +console.log(archive.filename); diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs new file mode 100644 index 0000000..93fe22c --- /dev/null +++ b/scripts/package-smoke.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const root = mkdtempSync(join(tmpdir(), "unique-pid-package-")); +function npm(args, cwd) { + const result = spawnSync( + process.execPath, + [process.env.npm_execpath, ...args], + { + cwd, + encoding: "utf8", + timeout: 60000, + }, + ); + assert.equal(result.status, 0, result.stderr); + return result.stdout; +} +try { + const packed = JSON.parse( + npm( + ["pack", "--json", "--ignore-scripts", "--pack-destination", root], + process.cwd(), + ), + ); + npm( + [ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--package-lock=false", + join(root, packed[0].filename), + ], + root, + ); + const code = + "import {capture,check} from 'unique-pid'; const r=capture(process.pid); if(!r.ok) throw Error(JSON.stringify(r)); const c=check(r.value); if(!c.ok || c.value!=='same') throw Error(JSON.stringify(c)); console.log('Installed package verified without build scripts');"; + const checked = spawnSync( + process.execPath, + ["--input-type=module", "-e", code], + { cwd: root, encoding: "utf8", timeout: 10000 }, + ); + assert.equal(checked.status, 0, checked.stderr); + process.stdout.write(checked.stdout); +} finally { + rmSync(root, { recursive: true, force: true }); +} diff --git a/src/darwin.c b/src/darwin.c new file mode 100644 index 0000000..14e99b5 --- /dev/null +++ b/src/darwin.c @@ -0,0 +1,29 @@ +#include "identity.h" +#include +#include +#include +#include +#include +#include + +void observe_process(unsigned int pid, struct identity *result) { + struct proc_bsdinfo bsd = {0}; + errno = 0; + int count = proc_pidinfo((int)pid, PROC_PIDTBSDINFO, 0, &bsd, sizeof(bsd)); + if (count != sizeof(bsd)) { + result->status = count <= 0 && errno == ESRCH ? "gone" + : errno == EPERM || errno == EACCES ? "denied" : "unknown"; + return; + } + if (bsd.pbi_pid != pid || bsd.pbi_start_tvusec >= 1000000) return; + size_t size = sizeof(result->boot); + if (sysctlbyname("kern.bootsessionuuid", result->boot, &size, NULL, 0) != 0) { + result->status = errno == EPERM || errno == EACCES ? "denied" : "unknown"; + return; + } + if (size < 2 || size > sizeof(result->boot) || result->boot[size - 1] != '\0') return; + for (size_t i = 0; i < size; i++) result->boot[i] = (char)tolower((unsigned char)result->boot[i]); + snprintf(result->start, sizeof(result->start), "%" PRIu64 ":%" PRIu64, + bsd.pbi_start_tvsec, bsd.pbi_start_tvusec); + result->status = "found"; +} diff --git a/src/identity.c b/src/identity.c index c1f7e97..cf23137 100644 --- a/src/identity.c +++ b/src/identity.c @@ -1,78 +1,41 @@ #define NAPI_VERSION 8 #include -#include -#include #include #include -#include #include -#include -#include +#include "identity.h" #define CHECK(call) do { if ((call) != napi_ok) { napi_throw_error(env, NULL, "Node-API failure"); return NULL; } } while (0) -static napi_value result(napi_env env, const char *status, int error) { - napi_value object, value; - CHECK(napi_create_object(env, &object)); - CHECK(napi_create_string_utf8(env, status, NAPI_AUTO_LENGTH, &value)); - CHECK(napi_set_named_property(env, object, "status", value)); - CHECK(napi_create_int32(env, error, &value)); - CHECK(napi_set_named_property(env, object, "errno", value)); - return object; -} - static napi_value read_identity(napi_env env, napi_callback_info info) { size_t argc = 1; - napi_value argv[1], value; + napi_value argv[1], object, value; napi_valuetype type; - double input; + double input = 0; CHECK(napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); - if (argc != 1) { - napi_throw_type_error(env, NULL, "Expected a positive PID"); - return NULL; - } - CHECK(napi_typeof(env, argv[0], &type)); - if (type != napi_number) { - napi_throw_type_error(env, NULL, "Expected a numeric PID"); - return NULL; + struct identity found = {0}; + found.status = "unknown"; + if (argc == 1) { + CHECK(napi_typeof(env, argv[0], &type)); + if (type == napi_number) { + CHECK(napi_get_value_double(env, argv[0], &input)); + if (isfinite(input) && input >= 1 && input <= INT_MAX && floor(input) == input) { + observe_process((unsigned int)input, &found); + } + } } - CHECK(napi_get_value_double(env, argv[0], &input)); - if (!isfinite(input) || input < 1 || input > INT_MAX || floor(input) != input) { - napi_throw_range_error(env, NULL, "Invalid PID"); - return NULL; - } - pid_t pid = (pid_t)input; - struct proc_bsdinfo bsd = {0}; - errno = 0; - int count = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsd, sizeof(bsd)); - int error = errno; - if (count != sizeof(bsd)) { - return result(env, count <= 0 && error == ESRCH ? "gone" : "unknown", error); - } - if (bsd.pbi_pid != (uint32_t)pid || bsd.pbi_start_tvusec >= 1000000) { - return result(env, "unknown", EIO); - } - char boot[64] = {0}; - size_t boot_size = sizeof(boot); - if (sysctlbyname("kern.bootsessionuuid", boot, &boot_size, NULL, 0) != 0) { - return result(env, "unknown", errno); - } - if (boot_size < 2 || boot_size > sizeof(boot) || boot[boot_size - 1] != '\0') { - return result(env, "unknown", EIO); + CHECK(napi_create_object(env, &object)); + CHECK(napi_create_string_utf8(env, found.status, NAPI_AUTO_LENGTH, &value)); + CHECK(napi_set_named_property(env, object, "status", value)); + if (strcmp(found.status, "found") != 0) return object; + CHECK(napi_create_string_utf8(env, found.start, NAPI_AUTO_LENGTH, &value)); + CHECK(napi_set_named_property(env, object, "startTime", value)); + if (found.boot[0]) { + CHECK(napi_create_string_utf8(env, found.boot, NAPI_AUTO_LENGTH, &value)); + } else { + CHECK(napi_get_null(env, &value)); } - napi_value object = result(env, "found", 0); - if (object == NULL) return NULL; - CHECK(napi_create_uint32(env, bsd.pbi_pid, &value)); - CHECK(napi_set_named_property(env, object, "pid", value)); - CHECK(napi_create_string_utf8(env, boot, NAPI_AUTO_LENGTH, &value)); CHECK(napi_set_named_property(env, object, "bootId", value)); - char seconds[32], micros[32]; - snprintf(seconds, sizeof(seconds), "%" PRIu64, bsd.pbi_start_tvsec); - snprintf(micros, sizeof(micros), "%" PRIu64, bsd.pbi_start_tvusec); - CHECK(napi_create_string_utf8(env, seconds, NAPI_AUTO_LENGTH, &value)); - CHECK(napi_set_named_property(env, object, "seconds", value)); - CHECK(napi_create_string_utf8(env, micros, NAPI_AUTO_LENGTH, &value)); - CHECK(napi_set_named_property(env, object, "micros", value)); return object; } diff --git a/src/identity.h b/src/identity.h new file mode 100644 index 0000000..67af1b5 --- /dev/null +++ b/src/identity.h @@ -0,0 +1,9 @@ +#ifndef UNIQUE_PID_IDENTITY_H +#define UNIQUE_PID_IDENTITY_H +struct identity { + const char *status; + char start[64]; + char boot[64]; +}; +void observe_process(unsigned int pid, struct identity *result); +#endif diff --git a/src/win32.c b/src/win32.c new file mode 100644 index 0000000..c8f3edc --- /dev/null +++ b/src/win32.c @@ -0,0 +1,30 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include "identity.h" + +void observe_process(unsigned int pid, struct identity *result) { + HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, (DWORD)pid); + if (handle == NULL) { + DWORD error = GetLastError(); + result->status = error == ERROR_INVALID_PARAMETER ? "gone" + : error == ERROR_ACCESS_DENIED ? "denied" : "unknown"; + return; + } + FILETIME creation, exit_time, kernel, user; + if (!GetProcessTimes(handle, &creation, &exit_time, &kernel, &user)) { + result->status = GetLastError() == ERROR_ACCESS_DENIED ? "denied" : "unknown"; + CloseHandle(handle); + return; + } + DWORD state = WaitForSingleObject(handle, 0); + if (state == WAIT_TIMEOUT) { + uint64_t ticks = ((uint64_t)creation.dwHighDateTime << 32) | creation.dwLowDateTime; + snprintf(result->start, sizeof(result->start), "%" PRIu64, ticks); + result->status = "found"; + } else { + result->status = state == WAIT_OBJECT_0 ? "gone" : "unknown"; + } + CloseHandle(handle); +} diff --git a/test/child.mjs b/test/child.mjs new file mode 100644 index 0000000..573d9e0 --- /dev/null +++ b/test/child.mjs @@ -0,0 +1,13 @@ +import { capture } from "../index.mjs"; +const expiry = setTimeout(() => process.exit(2), 20000); +process.on("message", (message) => { + if (message === "rename") { + process.title = "unique ) name"; + process.send("renamed"); + } + if (message === "stop") { + clearTimeout(expiry); + process.disconnect(); + } +}); +process.send(capture(process.pid)); diff --git a/test/hidepid.fixture.mjs b/test/hidepid.fixture.mjs new file mode 100644 index 0000000..2adc608 --- /dev/null +++ b/test/hidepid.fixture.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fork } from "node:child_process"; +import { once } from "node:events"; +import { capture, check } from "../index.mjs"; + +if (process.argv[2] === "probe") { + const pid = Number(process.argv[3]); + assert.throws(() => readFileSync(`/proc/${pid}/stat`), { code: "ENOENT" }); + const result = check(process.argv[4]); + assert.equal(result.ok, false); + assert.equal(result.error.code, "ACCESS_DENIED"); +} else { + const token = capture(process.pid); + assert.equal(token.ok, true); + const child = fork( + new URL(import.meta.url), + ["probe", String(process.pid), token.value], + { + uid: 65534, + gid: 65534, + stdio: "inherit", + }, + ); + const [code] = await once(child, "exit"); + assert.equal(code, 0); +} diff --git a/test/identity.mjs b/test/identity.mjs deleted file mode 100644 index 26aac16..0000000 --- a/test/identity.mjs +++ /dev/null @@ -1,118 +0,0 @@ -import assert from "node:assert/strict"; -import { fork, spawnSync } from "node:child_process"; -import { once } from "node:events"; -import { createRequire } from "node:module"; -import { fileURLToPath } from "node:url"; -import { capture, check, decode } from "../index.mjs"; - -const mode = process.argv[2]; -if (mode === "child") { - const expiry = setTimeout(() => process.exit(2), 15000); - process.on("message", (message) => { - if (message === "rename") { - process.title = "identity-poc-renamed"; - process.send("renamed"); - } - if (message === "stop") { - clearTimeout(expiry); - process.disconnect(); - } - }); - process.send(capture(process.pid)); -} else if (mode === "verify") { - console.log(JSON.stringify(check(process.argv[3]))); -} else { - const file = fileURLToPath(import.meta.url); - const child = fork(file, ["child"], { execArgv: [], env: { PATH: "" } }); - const exited = once(child, "exit"); - const [selfCaptured] = await once(child, "message"); - let token; - try { - const captured = capture(child.pid); - assert.equal(captured.status, "captured"); - token = captured.token; - assert.equal( - token, - selfCaptured.token, - "parent and child read exactly the same kernel identity", - ); - for (let i = 0; i < 100; i++) assert.equal(capture(child.pid).token, token); - console.log("PASS: child/parent identity agrees, 100 exact repeat reads"); - const verified = spawnSync(process.execPath, [file, "verify", token], { - env: { PATH: "" }, - encoding: "utf8", - timeout: 5000, - }); - assert.equal(verified.status, 0, verified.stderr); - assert.equal(JSON.parse(verified.stdout).status, "same"); - console.log( - "PASS: serialized token verified by a fresh Node process with empty PATH", - ); - const modified = (patch) => - "pi1." + - Buffer.from(JSON.stringify({ ...decode(token), ...patch })).toString( - "base64url", - ); - const identity = decode(token); - assert.equal( - check( - modified({ micros: String((Number(identity.micros) + 1) % 1000000) }), - ).status, - "different", - ); - assert.equal( - check(modified({ bootId: "00000000-0000-0000-0000-000000000000" })) - .status, - "different", - ); - console.log("PASS: one-microsecond and boot-session mismatches rejected"); - for (const bad of [ - null, - "", - "pi1.!!!!", - "pi1.a", - token + "=", - "x".repeat(1025), - modified({ pid: 0 }), - modified({ pid: -1 }), - modified({ pid: 1.5 }), - modified({ v: 2 }), - modified({ platform: "linux" }), - modified({ seconds: "01" }), - modified({ seconds: "18446744073709551616" }), - modified({ micros: "1000000" }), - modified({ surprise: true }), - ]) - assert.throws(() => check(bad)); - const native = createRequire(import.meta.url)( - "../build/Release/identity.node", - ); - for (const pid of [undefined, "1", NaN, Infinity, 0, -1, 1.5, 2147483648]) { - assert.throws(() => native.read(pid)); - } - console.log("PASS: malformed tokens and invalid PIDs refused"); - child.send("rename"); - await once(child, "message"); - assert.equal(check(token).status, "same"); - console.log("PASS: command-title changes do not change identity"); - const ps = spawnSync("/bin/ps", ["-p", String(child.pid), "-o", "pid="], { - timeout: 2000, - }); - if (process.env.EXPECT_PS_DENIED === "1") - assert.equal(ps.error?.code, "EPERM"); - console.log( - JSON.stringify({ - psStatus: ps.status, - psError: ps.error?.code, - node: process.version, - }), - ); - } finally { - if (child.connected) child.send("stop"); - const [code, signal] = await exited; - assert.equal(code, 0); - assert.equal(signal, null); - } - assert.equal(check(token).status, "gone"); - console.log("PASS: exited child reported gone; child stopped through IPC"); -} diff --git a/test/identity.test.mjs b/test/identity.test.mjs new file mode 100644 index 0000000..5f4d421 --- /dev/null +++ b/test/identity.test.mjs @@ -0,0 +1,160 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fork, spawnSync } from "node:child_process"; +import { once } from "node:events"; +import { fileURLToPath } from "node:url"; +import { capture, check, decode } from "../index.mjs"; + +const encode = (value) => + "upid1." + Buffer.from(JSON.stringify(value)).toString("base64url"); + +test("exact identity survives serialization, fresh readers, and title changes", async () => { + const child = fork(fileURLToPath(new URL("./child.mjs", import.meta.url)), { + execArgv: [], + env: { ...process.env, PATH: "", Path: "" }, + }); + const exited = once(child, "exit"); + let token; + try { + const [self] = await once(child, "message"); + const captured = capture(child.pid); + assert.equal(captured.ok, true, JSON.stringify(captured)); + assert.deepEqual(captured, self); + token = captured.value; + for (let i = 0; i < 100; i++) + assert.deepEqual(capture(child.pid), captured); + const code = `import {check} from ${JSON.stringify(new URL("../index.mjs", import.meta.url).href)}; console.log(JSON.stringify(check(process.argv[1])));`; + const other = spawnSync( + process.execPath, + ["--input-type=module", "-e", code, token], + { + encoding: "utf8", + env: { ...process.env, PATH: "", Path: "" }, + timeout: 5000, + }, + ); + assert.equal(other.status, 0, other.stderr); + assert.deepEqual(JSON.parse(other.stdout), { ok: true, value: "same" }); + const identity = decode(token).value; + const parts = identity.startTime.split(":"); + if (parts.length === 2) + parts[1] = String((BigInt(parts[1]) + 1n) % 1000000n); + else parts[0] = String(BigInt(parts[0]) + 1n); + assert.deepEqual( + check(encode({ ...identity, startTime: parts.join(":") })), + { ok: true, value: "different" }, + ); + if (identity.bootId !== null) { + const bootId = identity.bootId.replace(/^[a-f0-9]/, (x) => + x === "0" ? "1" : "0", + ); + assert.deepEqual(check(encode({ ...identity, bootId })), { + ok: true, + value: "different", + }); + } + child.send("rename"); + await once(child, "message"); + assert.deepEqual(check(token), { ok: true, value: "same" }); + if (process.env.EXPECT_PS_DENIED === "1") { + const ps = spawnSync("/bin/ps", ["-p", String(child.pid), "-o", "pid="], { + timeout: 2000, + }); + assert.equal(ps.error?.code, "EPERM"); + } + } finally { + if (child.connected) child.send("stop"); + const [code, signal] = await exited; + assert.equal(code, 0); + assert.equal(signal, null); + } + assert.deepEqual(check(token), { ok: true, value: "gone" }); + assert.equal(capture(child.pid).error.code, "NOT_FOUND"); +}); + +test("invalid inputs are results, including hostile objects", () => { + const hostile = new Proxy( + {}, + { + get() { + throw new Error("do not coerce"); + }, + }, + ); + for (const pid of [ + undefined, + null, + "1", + 0, + -1, + 1.5, + NaN, + Infinity, + 2147483648, + 1n, + Symbol(), + hostile, + ]) { + assert.deepEqual(capture(pid), { + ok: false, + error: { + code: "INVALID_ARGUMENT", + message: "PID must be a positive 32-bit integer", + }, + }); + } + for (const token of [ + undefined, + null, + hostile, + Symbol(), + "", + "upid1.!!!!", + "upid1.a", + "x".repeat(1025), + ]) { + assert.equal(check(token).error.code, "INVALID_TOKEN"); + assert.equal(decode(token).error.code, "INVALID_TOKEN"); + } +}); + +test("invalid token schemas, encodings, and precision loss are rejected", () => { + const identity = { + version: 1, + platform: "darwin", + pid: 123, + bootId: "01234567-0123-0123-0123-0123456789ab", + startTime: "123:456", + }; + for (const patch of [ + { pid: 0 }, + { pid: -1 }, + { pid: 1.5 }, + { version: 2 }, + { platform: "unknown" }, + { startTime: "01:2" }, + { startTime: "123:1000000" }, + { startTime: "18446744073709551616:0" }, + { startTime: 123 }, + { surprise: true }, + ]) { + assert.equal( + decode(encode({ ...identity, ...patch })).error.code, + "INVALID_TOKEN", + ); + } + assert.equal(decode(encode(identity) + "=").error.code, "INVALID_TOKEN"); + assert.equal( + decode("upid1." + Buffer.from([0xff]).toString("base64url")).error.code, + "INVALID_TOKEN", + ); + const windows = { + ...identity, + platform: "win32", + bootId: null, + startTime: "18446744073709551615", + }; + assert.deepEqual(decode(encode(windows)), { ok: true, value: windows }); + if (process.platform !== "win32") + assert.deepEqual(check(encode(windows)), { ok: true, value: "different" }); +}); diff --git a/test/linux.test.mjs b/test/linux.test.mjs new file mode 100644 index 0000000..14f429e --- /dev/null +++ b/test/linux.test.mjs @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { parseStat } from "../lib/linux.mjs"; + +test("proc stat parser preserves full-width ticks and handles parentheses in comm", () => { + const line = + "123 (odd ) name\n) S " + + Array(18).fill("0").join(" ") + + " 18446744073709551615 0"; + assert.deepEqual(parseStat(line, 123), { + state: "S", + startTime: "18446744073709551615", + }); + for (const bad of [ + line.replace("123 (", "124 ("), + "123 (name) S", + line.replace("18446744073709551615", "18446744073709551616"), + line.replace(") S ", ") ? "), + ]) { + assert.equal(parseStat(bad, 123), null); + } +}); diff --git a/test/loading.test.mjs b/test/loading.test.mjs new file mode 100644 index 0000000..a41ed94 --- /dev/null +++ b/test/loading.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { cpSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { pathToFileURL } from "node:url"; + +test( + "missing addon does not prevent import or throw from public calls", + { skip: process.platform === "linux" }, + () => { + const dir = mkdtempSync(join(tmpdir(), "unique-pid-loading-")); + try { + cpSync(new URL("../index.mjs", import.meta.url), join(dir, "index.mjs")); + cpSync(new URL("../lib", import.meta.url), join(dir, "lib"), { + recursive: true, + }); + const code = `import {capture,decode} from ${JSON.stringify(pathToFileURL(join(dir, "index.mjs")).href)}; console.log(JSON.stringify([capture(process.pid),decode(null)]));`; + const run = spawnSync( + process.execPath, + ["--input-type=module", "-e", code], + { encoding: "utf8", timeout: 5000 }, + ); + assert.equal(run.status, 0, run.stderr); + assert.deepEqual( + JSON.parse(run.stdout).map((result) => result.error.code), + ["NATIVE_UNAVAILABLE", "INVALID_TOKEN"], + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, +); diff --git a/test/types.mts b/test/types.mts index 23d7f96..5faa2fe 100644 --- a/test/types.mts +++ b/test/types.mts @@ -1,10 +1,18 @@ -import { capture, check, decode } from "../index.mjs"; - +import { capture, check, decode, type ErrorCode } from "../index.mjs"; const result = capture(process.pid); -if (result.status === "captured") { - const token: string = result.token; - const pid: number = decode(token).pid; - const status: "same" | "different" | "gone" | "unknown" = check(token).status; - void pid; - void status; +if (result.ok) { + const token: string = result.value; + const identity = decode(token); + if (identity.ok) { + const pid: number = identity.value.pid; + void pid; + } + const checked = check(token); + if (checked.ok) { + const status: "same" | "different" | "gone" = checked.value; + void status; + } +} else { + const code: ErrorCode = result.error.code; + void code; }