diff --git a/.configurations/configuration.dsc.yaml b/.configurations/configuration.dsc.yaml index 9e38aaba78a412..8c637d7dae819a 100644 --- a/.configurations/configuration.dsc.yaml +++ b/.configurations/configuration.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.NativeDesktop - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.configurations/configuration.vsBuildTools.dsc.yaml b/.configurations/configuration.vsBuildTools.dsc.yaml index 5434b44b3e0459..3e3a51ca90260f 100644 --- a/.configurations/configuration.vsBuildTools.dsc.yaml +++ b/.configurations/configuration.vsBuildTools.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.VCTools - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.configurations/configuration.vsEnterprise.dsc.yaml b/.configurations/configuration.vsEnterprise.dsc.yaml index 4faf7d77d371d6..124a391b7843fa 100644 --- a/.configurations/configuration.vsEnterprise.dsc.yaml +++ b/.configurations/configuration.vsEnterprise.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.NativeDesktop - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.configurations/configuration.vsProfessional.dsc.yaml b/.configurations/configuration.vsProfessional.dsc.yaml index e094059e826c0e..7d2a8b36984ccf 100644 --- a/.configurations/configuration.vsProfessional.dsc.yaml +++ b/.configurations/configuration.vsProfessional.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.NativeDesktop - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.github/label-pr-config.yml b/.github/label-pr-config.yml index 4ff8bee4c56d64..8b36066473f415 100644 --- a/.github/label-pr-config.yml +++ b/.github/label-pr-config.yml @@ -104,6 +104,7 @@ subSystemLabels: /^lib\/internal\/url\.js$/: whatwg-url /^lib\/internal\/modules\/esm/: esm /^lib\/internal\/modules/: module + /^lib\/internal\/source_map/: source maps /^lib\/internal\/webstreams/: web streams /^lib\/internal\/test_runner/: test_runner /^lib\/internal\/v8\//: v8 module @@ -128,6 +129,8 @@ exlusiveLabels: /^test\/report\//: test, report /^test\/fixtures\/es-module/: test, esm /^test\/es-module\//: test, esm + /^test\/fixtures\/source-map/: test, source maps + /^test\/fixtures\/test-426/: test, source maps /^test\/fixtures\/wpt\/streams\//: test, web streams /^test\/fixtures\/typescript/: test, strip-types /^test\/module-hooks\//: test, module, loaders @@ -214,6 +217,7 @@ allJsSubSystems: - url - util - v8 + - vfs - vm - wasi - worker diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5ff9daaa630d2a..01d7f37e38149e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,6 +9,7 @@ permissions: jobs: analyze: + if: github.repository == 'nodejs/node' name: Analyze runs-on: ubuntu-slim permissions: diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 8f652a91782aea..75ace50d2a071f 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -4,7 +4,6 @@ on: pull_request: branches: - main - - v[0-9]+.x-staging env: NODE_VERSION: lts/* diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 80e7f8294d693f..0b82dd2aac04c7 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -13,6 +13,7 @@ permissions: jobs: build-lto: + if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 07f05ecbbca57f..39be55d20d5fd8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -20,6 +20,7 @@ permissions: read-all jobs: analysis: + if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' name: Scorecard analysis # cannot use ubuntu-slim here because ossf/scorecard-action is dockerized # cannot use ubuntu-24.04-arm here because the docker image is x86 only diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml index 6471391171b0c5..47fadf9a3e113c 100644 --- a/.github/workflows/test-internet.yml +++ b/.github/workflows/test-internet.yml @@ -44,7 +44,7 @@ permissions: jobs: test-internet: - if: github.event_name == 'schedule' && github.repository == 'nodejs/node' || github.event.pull_request.draft == false + if: (github.event_name == 'schedule' && github.repository == 'nodejs/node') || (github.event.pull_request && github.event.pull_request.draft == false) runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitignore b/.gitignore index 0b8f1a405bda3a..69c1dd205316fa 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ /tags /tags.* /doc/api.xml +/docs/ /node /node_g /gon-config.json diff --git a/.mailmap b/.mailmap index 0422fba4472601..0860e8e0147889 100644 --- a/.mailmap +++ b/.mailmap @@ -350,7 +350,7 @@ Mathias Buus Mathias Pettersson Matt Lang Matt Reed -Matteo Collina +Matteo Collina Matthew Lye Matthew Turner Matthias Bastian diff --git a/BUILDING.md b/BUILDING.md index f7364e1499febd..de50ddd53b8673 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -117,7 +117,7 @@ platforms. This is true regardless of entries in the table below. | GNU/Linux | riscv64 | kernel >= 5.19, glibc >= 2.36 | Experimental | GCC >= 14 or Clang >= 19 for native builds[^7] | | Windows | x64 | >= Windows 10/Server 2016 | Tier 1 | [^2],[^3] | | Windows | arm64 | >= Windows 10 | Tier 2 | | -| macOS | x64 | >= 13.5 | Tier 1 | For notes about compilation see [^4] | +| macOS | x64 | >= 13.5 | Tier 2 | For notes about compilation see [^4] | | macOS | arm64 | >= 13.5 | Tier 1 | | | SmartOS | x64 | >= 18 | Tier 2 | | | AIX | ppc64be >=power9 | >= 7.2 TL04 | Tier 2 | | @@ -175,10 +175,10 @@ Binaries at are produced on: | aix-ppc64 | AIX 7.2 TL04 on PPC64BE with GCC 12[^5] | | darwin-x64 | macOS 15, Xcode 16 with -mmacosx-version-min=13.5 | | darwin-arm64 (and .pkg) | macOS 15 (arm64), Xcode 16 with -mmacosx-version-min=13.5 | -| linux-arm64 | RHEL 8 with Clang 19.1 and gcc-toolset-14-libatomic-devel[^6] | -| linux-ppc64le | RHEL 8 with Clang 19.1 and gcc-toolset-14-libatomic-devel[^6] | -| linux-s390x | RHEL 8 with Clang 19.1 and gcc-toolset-14-libatomic-devel[^6] | -| linux-x64 | RHEL 8 with Clang 19.1 and gcc-toolset-14-libatomic-devel[^6] | +| linux-arm64 | RHEL 8 with Clang 20.1 and gcc-toolset-14-libatomic-devel[^6] | +| linux-ppc64le | RHEL 8 with Clang 20.1 and gcc-toolset-14-libatomic-devel[^6] | +| linux-s390x | RHEL 8 with Clang 20.1 and gcc-toolset-14-libatomic-devel[^6] | +| linux-x64 | RHEL 8 with Clang 20.1 and gcc-toolset-14-libatomic-devel[^6] | | win-arm64 | Windows Server 2022 (x64) with Visual Studio 2022 | | win-x64 | Windows Server 2022 (x64) with Visual Studio 2022 | @@ -237,7 +237,8 @@ tarball and/or browse the git repository checked out at the relevant tag. ### Prerequisites * [A supported version of Python][Python versions] for building and testing. -* Memory: at least 8GB of RAM is typically required when compiling with 4 parallel jobs (e.g: `make -j4`) +* A Rust toolchain if [building Node.js with Temporal support](#building-nodejs-with-temporal-support). +* Memory: at least 8GB of RAM is typically required when compiling with 4 parallel jobs (e.g: `make -j4`). ### Unix and macOS @@ -796,6 +797,7 @@ easily. These files will install the following * `Python 3.14` * `Visual Studio 2022` (Build Tools, Community, Professional or Enterprise Edition) and "Desktop development with C++" workload, Clang and ClangToolset optional components +* `Rust Toolchain MSVC` * `NetWide Assembler` The following Desired State Configuration (DSC) files are available: @@ -1048,13 +1050,20 @@ enable FIPS support in Node.js. Node.js supports the [Temporal](https://github.com/tc39/proposal-temporal) APIs, when linking statically or dynamically with a version of [temporal\_rs](https://github.com/boa-dev/temporal). - -Temporal support is enabled by default starting in Node.js 26. Building it -requires a Rust toolchain: +Building it requires a Rust toolchain: * rustc >= 1.82 (with LLVM >= 19) * cargo >= 1.82 +Refer to [Install Rust](https://rust-lang.org/tools/install/) for instructions. +Individual packages such as `rust` and `cargo` in some operating system distributions may be considered +as an alternative, for example in CI environments. +Consult with relevant operating system documentation to ensure that packages +meet the minimum version specified above, +as packaged versions may lag behind the `stable` version installed by the official instructions. +Avoid mixing `rustup` together with `rust` and `cargo` package installations, due to +potential version conflicts. + If `--v8-enable-temporal-support` and `--v8-disable-temporal-support` are both omitted, `configure.py` probes for `cargo` and `rustc`. If either is missing, a warning is printed and Temporal support is disabled. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7a9c06d1c734..b12d8d62f24808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,8 @@ release. -26.2.0
+26.3.0
+26.2.0
26.1.0
26.0.0
diff --git a/Makefile b/Makefile index 1e1264619d24bb..961682e607272c 100644 --- a/Makefile +++ b/Makefile @@ -856,7 +856,7 @@ VERSION=v$(RAWVER) .PHONY: doc-only .NOTPARALLEL: doc-only -doc-only: $(apidoc_dirs) $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json out/doc/apilinks.json ## Builds the docs with the local or the global Node.js binary. +doc-only: $(apidoc_dirs) $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json out/doc/llms.txt out/doc/apilinks.json ## Builds the docs with the local or the global Node.js binary. .PHONY: doc doc: $(NODE_EXE) doc-only ## Build Node.js, and then build the documentation with the new binary. @@ -901,6 +901,22 @@ $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json &: $(a fi endif +out/doc/llms.txt: $(apidoc_sources) tools/doc/node_modules | out/doc + @if [ "$(shell $(node_use_openssl_and_icu))" != "true" ]; then \ + echo "Skipping $@ (no crypto and/or no ICU)"; \ + else \ + $(call available-node, \ + $(DOC_KIT) generate \ + -t llms-txt \ + -i doc/api/*.md \ + --ignore $(skip_apidoc_files) \ + -o $(@D) \ + -c ./CHANGELOG.md \ + -v $(VERSION) \ + --type-map doc/type-map.json \ + ) \ + fi + out/doc/apilinks.json: $(wildcard lib/*.js) tools/doc/node_modules | out/doc @if [ "$(shell $(node_use_openssl_and_icu))" != "true" ]; then \ echo "Skipping $@ (no crypto and/or no ICU)"; \ diff --git a/SECURITY.md b/SECURITY.md index 0e88d7b50702fa..55251f7da7993b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -85,6 +85,7 @@ When reporting security vulnerabilities, reporters must adhere to the following 4. **Report Quality** * Provide clear, detailed steps to reproduce the vulnerability. + * Include reproducible code written in JavaScript. * Include only the minimum proof of concept required to demonstrate the issue. * Remove any malicious payloads or components that could cause harm. @@ -376,7 +377,7 @@ the community they pose. #### Permission Model Boundaries (`--permission`) The Node.js [Permission Model](https://nodejs.org/api/permissions.html) -(`--experimental-permission`) is an opt-in mechanism that limits which +(`--permission`) is an opt-in mechanism that limits which resources a Node.js process may access. It is designed to reduce the blast radius of mistakes in trusted application code, **not** to act as a security boundary against intentional misuse or a compromised process. diff --git a/benchmark/fs/readfile-utf8-fastpath.js b/benchmark/fs/readfile-utf8-fastpath.js new file mode 100644 index 00000000000000..9bf00717c5f0b2 --- /dev/null +++ b/benchmark/fs/readfile-utf8-fastpath.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common.js'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../../test/common/tmpdir'); + +const bench = common.createBenchmark(main, { + size: [64, 1024, 16384, 262144, 4194304], + content: ['ascii', 'latin1', 'utf8_mixed'], + source: ['path', 'fd'], + n: [3e3], +}); + +function buildContent(kind, size) { + if (kind === 'ascii') { + return Buffer.alloc(size, 0x61); // 'a' + } + if (kind === 'latin1') { + // 'é' in UTF-8 is 0xC3 0xA9 (2 bytes per char) + const pair = Buffer.from([0xC3, 0xA9]); + const buf = Buffer.alloc(size); + for (let i = 0; i + 2 <= size; i += 2) pair.copy(buf, i); + return buf; + } + if (kind === 'utf8_mixed') { + // mixed ASCII + 3-byte CJK (U+4E2D 中 = E4 B8 AD) + const cjk = Buffer.from([0xE4, 0xB8, 0xAD]); + const buf = Buffer.alloc(size); + let i = 0; + while (i + 4 <= size) { + buf[i++] = 0x61; + cjk.copy(buf, i); + i += 3; + } + return buf; + } + throw new Error('unknown content: ' + kind); +} + +function main({ n, size, content, source }) { + tmpdir.refresh(); + const file = path.join(tmpdir.path, `bench-${content}-${size}.bin`); + fs.writeFileSync(file, buildContent(content, size)); + + let arg; + let shouldClose = false; + if (source === 'fd') { + arg = fs.openSync(file, 'r'); + shouldClose = true; + } else { + arg = file; + } + + bench.start(); + for (let i = 0; i < n; i++) { + fs.readFileSync(arg, 'utf8'); + } + bench.end(n); + + if (shouldClose) fs.closeSync(arg); +} diff --git a/benchmark/streams/compose.js b/benchmark/streams/compose.js index b98596ffbd1411..283ad8b7e30b32 100644 --- a/benchmark/streams/compose.js +++ b/benchmark/streams/compose.js @@ -9,16 +9,35 @@ const { } = require('node:stream'); const bench = common.createBenchmark(main, { - n: [1e3], + type: ['creation', 'throughput'], + n: [1, 1e3], + streams: [100], + chunks: [1e4], +}, { + combinationFilter({ type, n }) { + return type === 'creation' ? n === 1e3 : n === 1; + }, + test: { + n: [1, 1e3], + type: ['creation', 'throughput'], + }, }); -function main({ n }) { +function main({ type, n, streams, chunks }) { + switch (type) { + case 'creation': + return benchCreation(n, streams); + case 'throughput': + return benchThroughput(n, streams, chunks); + } +} + +function benchCreation(n, numberOfPassThroughs) { const cachedPassThroughs = []; const cachedReadables = []; const cachedWritables = []; for (let i = 0; i < n; i++) { - const numberOfPassThroughs = 100; const passThroughs = []; for (let i = 0; i < numberOfPassThroughs; i++) { @@ -40,3 +59,41 @@ function main({ n }) { } bench.end(n); } + +function benchThroughput(n, numberOfPassThroughs, chunks) { + const chunk = Buffer.alloc(1024); + + let i = 0; + bench.start(); + + function run() { + if (i++ === n) { + bench.end(n * chunks); + return; + } + + const passThroughs = []; + for (let i = 0; i < numberOfPassThroughs; i++) { + passThroughs.push(new PassThrough()); + } + + let remaining = chunks; + const composed = compose(...passThroughs); + composed.on('data', () => {}); + composed.on('end', run); + + write(); + + function write() { + while (remaining-- > 0) { + if (!composed.write(chunk)) { + composed.once('drain', write); + return; + } + } + composed.end(); + } + } + + run(); +} diff --git a/benchmark/util/style-text.js b/benchmark/util/style-text.js index f04a26646e052d..c50a225fd39331 100644 --- a/benchmark/util/style-text.js +++ b/benchmark/util/style-text.js @@ -5,9 +5,22 @@ const common = require('../common.js'); const { styleText } = require('node:util'); const assert = require('node:assert'); +// 1000 distinct hex colors to exercise the cache under high-miss conditions. +// Spread evenly across hue space so colors are valid and maximally varied. +const kHexColorCount = 1000; +const toHex = (n) => n.toString(16).padStart(2, '0'); +const hexColors = Array.from({ length: kHexColorCount }, (_, i) => { + const r = (i * 37) & 0xff; + const g = (i * 73) & 0xff; + const b = (i * 137) & 0xff; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +}); + const bench = common.createBenchmark(main, { messageType: ['string', 'number', 'boolean', 'invalid'], - format: ['red', 'italic', 'invalid', '#ff0000'], + // '#rotating' cycles through kHexColorCount distinct colors to simulate + // the high-miss-rate / large-cache scenario (e.g. user-randomised colors). + format: ['red', 'italic', 'invalid', '#ff0000', '#rotating'], validateStream: [1, 0], n: [1e3], }); @@ -31,9 +44,10 @@ function main({ messageType, format, validateStream, n }) { bench.start(); for (let i = 0; i < n; i++) { + const fmt = format === '#rotating' ? hexColors[i % kHexColorCount] : format; let colored = ''; try { - colored = styleText(format, str, { validateStream }); + colored = styleText(fmt, str, { validateStream }); assert.ok(colored); // Attempt to avoid dead-code elimination } catch { // eslint-disable no-empty diff --git a/benchmark/util/text-decoder.js b/benchmark/util/text-decoder.js index 1aa60f2dd0bcd6..ecfba045c52fab 100644 --- a/benchmark/util/text-decoder.js +++ b/benchmark/util/text-decoder.js @@ -6,26 +6,42 @@ const bench = common.createBenchmark(main, { encoding: ['utf-8', 'windows-1252', 'iso-8859-3'], ignoreBOM: [0, 1], fatal: [0, 1], + type: ['SharedArrayBuffer', 'ArrayBuffer', 'Buffer'], + content: ['ascii', 'one-byte-string', 'two-byte-string'], len: [256, 1024 * 16, 1024 * 128], n: [1e3], - type: ['SharedArrayBuffer', 'ArrayBuffer', 'Buffer'], }); -function main({ encoding, len, n, ignoreBOM, type, fatal }) { +function buildContent(content, len) { + let base; + switch (content) { + case 'ascii': base = 'a'; break; + case 'one-byte-string': base = '\xff'; break; + case 'two-byte-string': base = 'ğ'; break; + } + const unitBytes = Buffer.byteLength(base, 'utf8'); + const copies = Math.max(1, Math.floor(len / unitBytes)); + return Buffer.from(base.repeat(copies)); +} + +function main({ encoding, len, n, ignoreBOM, type, fatal, content }) { const decoder = new TextDecoder(encoding, { ignoreBOM, fatal }); + const seed = buildContent(content, len); let buf; switch (type) { case 'SharedArrayBuffer': { - buf = new SharedArrayBuffer(len); + buf = new SharedArrayBuffer(seed.length); + new Uint8Array(buf).set(seed); break; } case 'ArrayBuffer': { - buf = new ArrayBuffer(len); + buf = new ArrayBuffer(seed.length); + new Uint8Array(buf).set(seed); break; } case 'Buffer': { - buf = Buffer.allocUnsafe(len); + buf = seed; break; } } diff --git a/common.gypi b/common.gypi index c0a3d785a3dab3..26c2ef3675fd0e 100644 --- a/common.gypi +++ b/common.gypi @@ -272,6 +272,13 @@ }, }, },], + ['(enable_thin_lto=="true" or enable_lto=="true") and lto_jobs!=""', { + 'msvs_settings': { + 'VCLinkerTool': { + 'AdditionalOptions': ['/opt:lldltojobs=<(lto_jobs)'], + }, + }, + },], ], 'target_conditions': [ ['_toolset=="target"', { diff --git a/configure.py b/configure.py index eea76312119385..11ff7f0eebe8ea 100755 --- a/configure.py +++ b/configure.py @@ -225,6 +225,14 @@ help="Enable compiling with thin lto of a binary. This feature is only available " "on windows.") +parser.add_argument("--lto-jobs", + action="store", + dest="lto_jobs", + default=None, + help="Set the number of parallel LTO code generation jobs during linking. " + "Defaults to the number of CPU cores. Lower values reduce peak memory " + "usage at the cost of longer link times. Only effective with LTO enabled.") + parser.add_argument("--link-module", action="append", dest="linked_module", @@ -1951,18 +1959,29 @@ def configure_node(o): msvc_dir = target_arch # 'x64' or 'arm64' vc_tools_dir = os.environ.get('VCToolsInstallDir', '') - if vc_tools_dir: - clang_profile_lib = os.path.join(vc_tools_dir, 'lib', msvc_dir, lib_name) - if os.path.isfile(clang_profile_lib): - o['variables']['clang_profile_lib'] = clang_profile_lib - else: - raise Exception( - f'PGO profile runtime library not found at {clang_profile_lib}. ' - 'Ensure the ClangCL toolset is installed.') - else: + if not vc_tools_dir: raise Exception( 'VCToolsInstallDir not set. Run from a Visual Studio command prompt.') + # Primary location: VS2026 and VS2022 x64 + candidates = [os.path.join(vc_tools_dir, 'lib', msvc_dir, lib_name)] + + # Secondary location: VS2022 arm64 fallback + clang_major = options.clang_cl.split('.', 1)[0] + candidates.append(os.path.normpath(os.path.join( + vc_tools_dir, '..', '..', 'Llvm', msvc_dir, + 'lib', 'clang', clang_major, 'lib', 'windows', lib_name))) + + clang_profile_lib = next( + (p for p in candidates if os.path.isfile(p)), None) + if clang_profile_lib: + o['variables']['clang_profile_lib'] = clang_profile_lib + else: + raise Exception( + f'PGO profile runtime library {lib_name} not found. Searched:\n ' + + '\n '.join(candidates) + + '\nEnsure the ClangCL toolset is installed.') + if flavor != 'win' and options.enable_thin_lto: raise Exception( 'Use --enable-lto instead.') @@ -1995,6 +2014,7 @@ def configure_node(o): o['variables']['enable_lto'] = b(options.enable_lto) o['variables']['enable_thin_lto'] = b(options.enable_thin_lto) + o['variables']['lto_jobs'] = options.lto_jobs or '' if options.node_use_large_pages or options.node_use_large_pages_script_lld: warn('''The `--use-largepages` and `--use-largepages-script-lld` options diff --git a/deps/inspector_protocol/inspector_protocol.gyp b/deps/inspector_protocol/inspector_protocol.gyp index 0eb551c769f55d..bd03b7ded1220f 100644 --- a/deps/inspector_protocol/inspector_protocol.gyp +++ b/deps/inspector_protocol/inspector_protocol.gyp @@ -14,7 +14,6 @@ 'crdtp/json.h', 'crdtp/json_platform.cc', 'crdtp/json_platform.h', - 'crdtp/maybe.h', 'crdtp/parser_handler.h', 'crdtp/protocol_core.cc', 'crdtp/protocol_core.h', diff --git a/deps/npm/docs/content/commands/npm-approve-scripts.md b/deps/npm/docs/content/commands/npm-approve-scripts.md new file mode 100644 index 00000000000000..e3445447c79052 --- /dev/null +++ b/deps/npm/docs/content/commands/npm-approve-scripts.md @@ -0,0 +1,125 @@ +--- +title: npm-approve-scripts +section: 1 +description: Approve install scripts for specific dependencies +--- + +### Synopsis + +```bash +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +``` + +Note: This command is unaware of workspaces. + +### Description + +Manages the `allowScripts` field in your project's `package.json`, which +records which of your dependencies are permitted to run install scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +sources). This command is the recommended way to maintain that field. + +In the current release, this field is advisory: install scripts still run +by default, but installs print a list of packages whose scripts have not +been reviewed. A future release will block unreviewed install scripts. + +There are three modes: + +```bash +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +``` + +`` matches every installed version of that package. By default the +command writes pinned entries (`pkg@1.2.3`), which keep their approval +narrowed to the specific version you reviewed. Pass `--no-allow-scripts-pin` to write +name-only entries that allow any future version. + +`--all` approves every package with unreviewed install scripts in one go. + +`--allow-scripts-pending` is read-only: it lists every package whose install scripts +are not yet covered by `allowScripts`, without modifying `package.json`. + +`approve-scripts` honours the asymmetric pin rule: if you re-approve a +package whose installed version has changed, the existing pin is rewritten +to track the new installed version. Multi-version statements +(`pkg@1 || 2`) are left alone, since they likely capture intent that +the command cannot infer. Existing `false` entries always win; +`approve-scripts` will not silently re-allow a package you previously +denied. + +### Examples + +```bash +# Approve all currently-installed install scripts after reviewing them +npm approve-scripts --all + +# Approve specific packages, pinned to their installed version +npm approve-scripts canvas sharp + +# Approve name-only (any version of this package is allowed) +npm approve-scripts --no-allow-scripts-pin canvas + +# Preview which packages still need review +npm approve-scripts --allow-scripts-pending +``` + +### Configuration + +#### `all` + +* Default: false +* Type: Boolean + +When running `npm outdated` and `npm ls`, setting `--all` will show all +outdated or installed packages, rather than only those directly depended +upon by the current project. + + + +#### `allow-scripts-pending` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +`allowScripts` policy, without modifying `package.json`. Only meaningful for +`npm approve-scripts`. + + + +#### `allow-scripts-pin` + +* Default: true +* Type: Boolean + +Write pinned (`pkg@version`) entries when approving install scripts. Set to +`false` to write name-only entries that allow any version. Has no effect on +`npm deny-scripts`, which always writes name-only entries regardless of this +setting. + + + +#### `json` + +* Default: false +* Type: Boolean + +Whether or not to output JSON data, rather than the normal output. + +* In `npm pkg set` it enables parsing set values with JSON.parse() before + saving them to your `package.json`. + +Not supported by all npm commands. + + + +### See Also + +* [npm deny-scripts](/commands/npm-deny-scripts) +* [npm install](/commands/npm-install) +* [npm rebuild](/commands/npm-rebuild) +* [package.json](/configuring-npm/package-json) diff --git a/deps/npm/docs/content/commands/npm-ci.md b/deps/npm/docs/content/commands/npm-ci.md index 45162af8d61d57..bc460070459604 100644 --- a/deps/npm/docs/content/commands/npm-ci.md +++ b/deps/npm/docs/content/commands/npm-ci.md @@ -189,6 +189,42 @@ run any pre- or post-scripts. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -197,16 +233,85 @@ run any pre- or post-scripts. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-dedupe.md b/deps/npm/docs/content/commands/npm-dedupe.md index e7fd142d00c07c..8186ee2c1f31c7 100644 --- a/deps/npm/docs/content/commands/npm-dedupe.md +++ b/deps/npm/docs/content/commands/npm-dedupe.md @@ -184,6 +184,42 @@ run any pre- or post-scripts. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -192,12 +228,31 @@ run any pre- or post-scripts. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` diff --git a/deps/npm/docs/content/commands/npm-deny-scripts.md b/deps/npm/docs/content/commands/npm-deny-scripts.md new file mode 100644 index 00000000000000..51915b09fe1dfc --- /dev/null +++ b/deps/npm/docs/content/commands/npm-deny-scripts.md @@ -0,0 +1,109 @@ +--- +title: npm-deny-scripts +section: 1 +description: Deny install scripts for specific dependencies +--- + +### Synopsis + +```bash +npm deny-scripts [ ...] +npm deny-scripts --all +``` + +Note: This command is unaware of workspaces. + +### Description + +The companion command to [`npm approve-scripts`](/commands/npm-approve-scripts). +Writes `false` entries into the `allowScripts` field of your project's +`package.json`, recording that a dependency must not run install scripts +even if a future version would otherwise be eligible. + +In the current release, install scripts still run by default, so `deny-scripts` +only affects how installs of denied packages are reported. A future release +will block unreviewed install scripts and respect deny entries at install +time. + +```bash +npm deny-scripts [ ...] +npm deny-scripts --all +``` + +`` matches every installed version of that package. Denies are always +written name-only (`"pkg": false`), regardless of `--allow-scripts-pin`. Pinning a deny +to a specific version would silently re-allow scripts for any other version +of the same package, which defeats the purpose; the command picks the +safer default for you. + +`--all` denies every package with unreviewed install scripts. + +If a `true` (pinned or name-only) entry exists for a package and you then +deny it, the existing allow entries are removed so the name-only deny is +unambiguous. + +### Examples + +```bash +# Deny a specific package outright +npm deny-scripts telemetry-pkg + +# Deny everything that has install scripts and isn't already approved +npm deny-scripts --all +``` + +### Configuration + +#### `all` + +* Default: false +* Type: Boolean + +When running `npm outdated` and `npm ls`, setting `--all` will show all +outdated or installed packages, rather than only those directly depended +upon by the current project. + + + +#### `allow-scripts-pending` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +`allowScripts` policy, without modifying `package.json`. Only meaningful for +`npm approve-scripts`. + + + +#### `allow-scripts-pin` + +* Default: true +* Type: Boolean + +Write pinned (`pkg@version`) entries when approving install scripts. Set to +`false` to write name-only entries that allow any version. Has no effect on +`npm deny-scripts`, which always writes name-only entries regardless of this +setting. + + + +#### `json` + +* Default: false +* Type: Boolean + +Whether or not to output JSON data, rather than the normal output. + +* In `npm pkg set` it enables parsing set values with JSON.parse() before + saving them to your `package.json`. + +Not supported by all npm commands. + + + +### See Also + +* [npm approve-scripts](/commands/npm-approve-scripts) +* [npm install](/commands/npm-install) +* [package.json](/configuring-npm/package-json) diff --git a/deps/npm/docs/content/commands/npm-exec.md b/deps/npm/docs/content/commands/npm-exec.md index 72c63163be4d2f..13a0939209a5ea 100644 --- a/deps/npm/docs/content/commands/npm-exec.md +++ b/deps/npm/docs/content/commands/npm-exec.md @@ -158,6 +158,56 @@ the specified workspaces, and not on the root project. This value is not exported to the environment for child processes. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + ### Examples Run the version of `tap` in the local dependencies, with the provided arguments: diff --git a/deps/npm/docs/content/commands/npm-install-ci-test.md b/deps/npm/docs/content/commands/npm-install-ci-test.md index 6b9681d202c991..4528f63dfe28e8 100644 --- a/deps/npm/docs/content/commands/npm-install-ci-test.md +++ b/deps/npm/docs/content/commands/npm-install-ci-test.md @@ -142,6 +142,42 @@ run any pre- or post-scripts. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -150,16 +186,85 @@ run any pre- or post-scripts. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-install-test.md b/deps/npm/docs/content/commands/npm-install-test.md index 8291409edfb835..5a2f33a84ca96d 100644 --- a/deps/npm/docs/content/commands/npm-install-test.md +++ b/deps/npm/docs/content/commands/npm-install-test.md @@ -219,6 +219,42 @@ run any pre- or post-scripts. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -227,16 +263,85 @@ run any pre- or post-scripts. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true @@ -264,7 +369,13 @@ If the requested version is a `dist-tag` and the given tag does not pass the will be used. For example, `foo@latest` might install `foo@1.2` even though `latest` is `2.0`. -This config cannot be used with: `min-release-age` +If `before` and `min-release-age` are both set in the same source, `before` +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one. + + #### `min-release-age` @@ -277,9 +388,11 @@ are no versions available for the current set of dependencies, the command will error. This flag is a complement to `before`, which accepts an exact date instead -of a relative number of days. - -This config cannot be used with: `before` +of a relative number of days. The two may coexist (e.g. `min-release-age` in +your `.npmrc` is preserved when npm internally spawns a sub-process with +`--before` while preparing a `git:` or `github:` dependency); when both +apply, `before` wins within a single source and across sources the standard +precedence rules apply. This value is not exported to the environment for child processes. diff --git a/deps/npm/docs/content/commands/npm-install.md b/deps/npm/docs/content/commands/npm-install.md index 77a34667725c3f..7bc00701e7bf2d 100644 --- a/deps/npm/docs/content/commands/npm-install.md +++ b/deps/npm/docs/content/commands/npm-install.md @@ -561,6 +561,42 @@ run any pre- or post-scripts. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -569,16 +605,85 @@ run any pre- or post-scripts. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true @@ -606,7 +711,13 @@ If the requested version is a `dist-tag` and the given tag does not pass the will be used. For example, `foo@latest` might install `foo@1.2` even though `latest` is `2.0`. -This config cannot be used with: `min-release-age` +If `before` and `min-release-age` are both set in the same source, `before` +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one. + + #### `min-release-age` @@ -619,9 +730,11 @@ are no versions available for the current set of dependencies, the command will error. This flag is a complement to `before`, which accepts an exact date instead -of a relative number of days. - -This config cannot be used with: `before` +of a relative number of days. The two may coexist (e.g. `min-release-age` in +your `.npmrc` is preserved when npm internally spawns a sub-process with +`--before` while preparing a `git:` or `github:` dependency); when both +apply, `before` wins within a single source and across sources the standard +precedence rules apply. This value is not exported to the environment for child processes. diff --git a/deps/npm/docs/content/commands/npm-link.md b/deps/npm/docs/content/commands/npm-link.md index 31af87bfa3006d..37efda66408fbd 100644 --- a/deps/npm/docs/content/commands/npm-link.md +++ b/deps/npm/docs/content/commands/npm-link.md @@ -248,6 +248,42 @@ run any pre- or post-scripts. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -256,12 +292,31 @@ run any pre- or post-scripts. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` diff --git a/deps/npm/docs/content/commands/npm-ls.md b/deps/npm/docs/content/commands/npm-ls.md index 556e77976c1a8f..c0a341e46fd10b 100644 --- a/deps/npm/docs/content/commands/npm-ls.md +++ b/deps/npm/docs/content/commands/npm-ls.md @@ -23,7 +23,7 @@ Note that nested packages will *also* show the paths to the specified packages. For example, running `npm ls promzard` in npm's source tree will show: ```bash -npm@11.13.0 /path/to/npm +npm@11.16.0 /path/to/npm └─┬ init-package-json@0.0.4 └── promzard@0.1.5 ``` diff --git a/deps/npm/docs/content/commands/npm-outdated.md b/deps/npm/docs/content/commands/npm-outdated.md index f635cb90565d2f..92e15e9602013d 100644 --- a/deps/npm/docs/content/commands/npm-outdated.md +++ b/deps/npm/docs/content/commands/npm-outdated.md @@ -165,7 +165,13 @@ If the requested version is a `dist-tag` and the given tag does not pass the will be used. For example, `foo@latest` might install `foo@1.2` even though `latest` is `2.0`. -This config cannot be used with: `min-release-age` +If `before` and `min-release-age` are both set in the same source, `before` +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one. + + #### `min-release-age` @@ -178,9 +184,11 @@ are no versions available for the current set of dependencies, the command will error. This flag is a complement to `before`, which accepts an exact date instead -of a relative number of days. - -This config cannot be used with: `before` +of a relative number of days. The two may coexist (e.g. `min-release-age` in +your `.npmrc` is preserved when npm internally spawns a sub-process with +`--before` while preparing a `git:` or `github:` dependency); when both +apply, `before` wins within a single source and across sources the standard +precedence rules apply. This value is not exported to the environment for child processes. diff --git a/deps/npm/docs/content/commands/npm-publish.md b/deps/npm/docs/content/commands/npm-publish.md index c69e187429eabb..04c020b3563f7d 100644 --- a/deps/npm/docs/content/commands/npm-publish.md +++ b/deps/npm/docs/content/commands/npm-publish.md @@ -117,7 +117,7 @@ the package submitted to the registry. * Default: 'public' for new packages, existing packages it will not change the current level -* Type: null, "restricted", or "public" +* Type: null, "restricted", "public", or "private" If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. @@ -129,6 +129,8 @@ packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. +The value `private` is an alias for `restricted`. + #### `dry-run` diff --git a/deps/npm/docs/content/commands/npm-rebuild.md b/deps/npm/docs/content/commands/npm-rebuild.md index 9fb43567ac2eb4..18b1d37c779956 100644 --- a/deps/npm/docs/content/commands/npm-rebuild.md +++ b/deps/npm/docs/content/commands/npm-rebuild.md @@ -100,6 +100,56 @@ run any pre- or post-scripts. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `workspace` * Default: diff --git a/deps/npm/docs/content/commands/npm-stage.md b/deps/npm/docs/content/commands/npm-stage.md new file mode 100644 index 00000000000000..cda1b493f9ac4f --- /dev/null +++ b/deps/npm/docs/content/commands/npm-stage.md @@ -0,0 +1,253 @@ +--- +title: npm-stage +section: 1 +description: Stage packages for publishing +--- + +### Synopsis + +```bash +npm stage +``` + +Note: This command is unaware of workspaces. + +### Description + +Staged publishing allows package maintainers to require proof-of-presence +for all publishes. Proof-of-presence is where a human is involved, +interjects, and provides authentication (2FA) during an action — in this +case, publishing an npm package. + +Typically when maintainers use automated workflows to publish, +proof-of-presence is lacking as there's no convenient way to interject the +process and provide 2FA, as is the case for publishing with a granular +access token with bypass and the trusted publishing flow. Staged publishing +allows users to have their automated workflows stage a package without a 2FA +prompt, deferring the act of 2FA, allowing the maintainer to approve the +staged package and publish at a later point. + +The `npm stage publish` command packs the current working directory and +places that version of the package into the registry in a state where it's +not available for public access, allowing maintainers to approve the package +at a later point in time. The act of staging does not prompt for 2FA and can be done with any token +type, the act of approving will. + +Key behaviors: + +* Staged packages share the same semver version unique index as published + packages — you cannot publish a version that already exists as a staged + version for that package. +* You can still publish packages normally while you have staged packages + pending. +* You can stage multiple versions of the same package. +* `npm stage publish` has parity with `npm publish` and will respect + `"private": true` in `package.json`, refusing to stage the package. + +### Prerequisites + +Before using `npm stage` commands, ensure the following requirements are met: + +* **Write permissions on the package:** You must have write access to the + package you're configuring. +* **Package must exist:** The package you're configuring must already exist + on the npm registry. +* **2FA enabled on your account:** Commands that require 2FA will prompt you + to authenticate. If you don't already have 2FA enabled on your account, + you must enable it before using these commands. + +### Subcommands + +* `npm stage publish []` - Stage a package for publishing +* `npm stage list []` - List all staged package versions +* `npm stage view ` - View details of a specific staged package +* `npm stage approve ` - Approve a staged package for publishing +* `npm stage reject ` - Reject a staged package +* `npm stage download ` - Download the tarball for inspection + +### 2FA Requirements by Subcommand + +| Command | Requires 2FA | Notes | +| --- | --- | --- | +| `npm stage publish` | No | Designed for automated workflows; defers 2FA to approval | +| `npm stage list` | No | View staged packages | +| `npm stage view` | No | View staged package details | +| `npm stage approve` | Yes | Prompts for 2FA to publish the staged package | +| `npm stage reject` | Yes | Prompts for 2FA to permanently remove the staged package | +| `npm stage download` | No | Downloads the tarball for local inspection | + +### Tag Behavior + +The `--tag` flag follows the same logic as `npm publish`. If no tag is +provided, the `latest` tag is used by default. For pre-release versions +(e.g., `1.0.0-beta.1`) and non-latest semver versions, the tag must be +explicitly provided — otherwise the CLI will error, just as `npm publish` +would. + +The tag is an immutable property of the staged package. Once a package is +staged with a given tag, the tag cannot be changed. If you need to stage the +same version with a different tag, you must first reject the existing staged +package using `npm stage reject` and then re-stage it with the desired tag. + +### Token Behavior + +The key difference with staged publishing is that `npm stage publish` never +requires a 2FA prompt, regardless of token type. This is what makes it +suitable for automated workflows. The goal of `npm stage publish` is +deferring proof-of-presence to a later point in time. + +| Token Type | `npm stage publish` | `npm publish` | +| --- | --- | --- | +| GAT with bypass | Can stage | Can publish (if allowed by package publishing access) | +| GAT without bypass | Can stage | 2FA prompt (if allowed by package publishing access) | +| Session token | Can stage | 2FA prompt | +| Trust token (OIDC) | Can stage (if allowed) | Can publish (if allowed) | + +### Trust Relationship Permissions + +With staged publishing, trust relationships now support granular command +permissions. Shortlived tokens issued through trust relationships can only be +used with `npm stage publish` and `npm publish`. Shortlived tokens cannot run +`npm stage` subcommands. + +`npm trust ` supports `--allow-publish` and `--allow-stage-publish` +to control which commands are available through each trust relationship. + +### Best Practices + +**Note:** The addition of staged publishing does not make your account or org +more secure. Maintainers must still use the best practices listed below. + +1. **Delete Granular Access Tokens (GAT) with bypass 2FA enabled.** + Now with staged publishing, we've eliminated the need for a GAT token + that can bypass 2FA. We encourage you to delete all your tokens with + bypass enabled and switch to using a trust relationship in your automated + workflows, or create a GAT without bypass and use `npm stage publish`. + +2. **Disallow tokens from publishing at the package level.** + All packages have their own access controls under "package access" + allowing packages to be published with bypass tokens, which is no longer + a necessity. We encourage you to select "Require two-factor + authentication and disallow tokens (recommended)" for all your packages + on the package access page. + +3. **Configure trust relationship permissions to prevent `npm publish`.** + We encourage you to only enable `npm stage publish` on your trust + relationships and disable `npm publish`. + +### Configuration + +### `npm stage publish` + +Stage a package for publishing, deferring proof-of-presence (2FA) to a later point in time + +#### Synopsis + +```bash +npm stage publish +``` + +#### Flags + +| Flag | Default | Type | Description | +| --- | --- | --- | --- | +| `--tag` | "latest" | String | If you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. It is the tag added to the package@version specified in the `npm dist-tag add` command, if no explicit tag is given. When used by the `npm diff` command, this is the tag used to fetch the tarball that will be compared with the local files by default. If used in the `npm publish` command, this is the tag that will be added to the package submitted to the registry. | +| `--access` | 'public' for new packages, existing packages it will not change the current level | null, "restricted", "public", or "private" | If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. Unscoped packages cannot be set to `restricted`. Note: This defaults to not changing the current access level for existing packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. The value `private` is an alias for `restricted`. | +| `--dry-run` | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, `install`, `update`, `dedupe`, `uninstall`, as well as `pack` and `publish`. Note: This is NOT honored by other network related commands, eg `dist-tags`, `owner`, etc. | +| `--otp` | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with `npm access`. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | +| `--workspace`, `-w` | | String (can be set multiple times) | Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the `workspace` config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the `npm init` command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. | +| `--workspaces` | null | null or Boolean | Set to true to run the command in the context of **all** configured workspaces. Explicitly setting this to false will cause commands like `install` to ignore workspaces altogether. When not set explicitly: - Commands that operate on the `node_modules` tree (install, update, etc.) will link workspaces into the `node_modules` folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, _unless_ one or more workspaces are specified in the `workspace` config. | +| `--include-workspace-root` | false | Boolean | Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the `workspace` config, or all workspaces via the `workspaces` flag, will cause npm to operate only on the specified workspaces, and not on the root project. | +| `--provenance` | false | Boolean | When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. | + +### `npm stage list` + +List all staged package versions + +#### Synopsis + +```bash +npm stage list [] +``` + +#### Flags + +| Flag | Default | Type | Description | +| --- | --- | --- | --- | +| `--json` | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In `npm pkg set` it enables parsing set values with JSON.parse() before saving them to your `package.json`. Not supported by all npm commands. | +| `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | + +### `npm stage view` + +View details of a specific staged package + +#### Synopsis + +```bash +npm stage view +``` + +#### Flags + +| Flag | Default | Type | Description | +| --- | --- | --- | --- | +| `--json` | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In `npm pkg set` it enables parsing set values with JSON.parse() before saving them to your `package.json`. Not supported by all npm commands. | +| `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | + +### `npm stage approve` + +Approve a staged package, publishing it to the npm registry + +#### Synopsis + +```bash +npm stage approve +``` + +#### Flags + +| Flag | Default | Type | Description | +| --- | --- | --- | --- | +| `--otp` | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with `npm access`. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | +| `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | + +### `npm stage reject` + +Reject a staged package, removing it from the registry + +#### Synopsis + +```bash +npm stage reject +``` + +#### Flags + +| Flag | Default | Type | Description | +| --- | --- | --- | --- | +| `--otp` | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with `npm access`. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | +| `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | + +### `npm stage download` + +Download the tarball of a staged package for inspection + +#### Synopsis + +```bash +npm stage download +``` + +#### Flags + +| Flag | Default | Type | Description | +| --- | --- | --- | --- | +| `--json` | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In `npm pkg set` it enables parsing set values with JSON.parse() before saving them to your `package.json`. Not supported by all npm commands. | +| `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | + + +### See Also + +* [npm publish](/commands/npm-publish) +* [npm unpublish](/commands/npm-unpublish) +* [npm trust](/commands/npm-trust) diff --git a/deps/npm/docs/content/commands/npm-trust.md b/deps/npm/docs/content/commands/npm-trust.md index e780330db878de..5aac2035b38644 100644 --- a/deps/npm/docs/content/commands/npm-trust.md +++ b/deps/npm/docs/content/commands/npm-trust.md @@ -28,6 +28,17 @@ The `[package]` argument specifies the package name. If omitted, npm will use th Each trust relationship has its own set of configuration options and flags based on the OIDC claims provided by that provider. OIDC claims come from the CI/CD provider and include information such as repository name, workflow file, or environment. Since each provider's claims differ, the available flags and configuration keys are not universal—npm matches the claims supported by each provider's OIDC configuration. For specific details on which claims and flags are supported for a given provider, use `npm trust --help`. +### Permissions + +When creating a trust relationship, you must specify at least one permission flag to indicate which operations the trusted publisher is allowed to perform: + +* `--allow-publish`: Allows the trusted publisher to run `npm publish` for the package. +* `--allow-stage-publish`: Allows the trusted publisher to run `npm stage` for the package. The alias `--allow-staged-publish` is also accepted. + +At least one of these flags is required when creating a trust configuration. You can specify both to grant both permissions. + +### Provider Options + The required options depend on the CI/CD provider you're configuring. Detailed information about each option is available in the [managing trusted publisher configurations](https://docs.npmjs.com/trusted-publishers#managing-trusted-publisher-configurations) section of the npm documentation. If a provider is repository-based and the option is not provided, npm will use the `repository.url` field from your `package.json`, if available. Currently, the registry only supports one configuration per package. If you attempt to create a new trust relationship when one already exists, it will result in an error. To replace an existing configuration: @@ -53,7 +64,7 @@ Create a trusted relationship between a package and GitHub Actions #### Synopsis ```bash -npm trust github [package] --file [--repo|--repository] [--env|--environment] [-y|--yes] +npm trust github [package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes] ``` #### Flags @@ -63,6 +74,8 @@ npm trust github [package] --file [--repo|--repository] [--env|--environment] [- | `--file` | null | String (required) | Name of workflow file within a repositories .GitHub folder (must end in yaml, yml) | | `--repository`, `--repo` | null | String | Name of the repository in the format owner/repo | | `--environment`, `--env` | null | String | CI environment name | +| `--allow-publish` | false | Boolean | Allow npm publish for this trusted publisher configuration | +| `--allow-stage-publish`, `--allow-staged-publish` | false | Boolean | Allow npm stage publish for this trusted publisher configuration | | `--dry-run` | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, `install`, `update`, `dedupe`, `uninstall`, as well as `pack` and `publish`. Note: This is NOT honored by other network related commands, eg `dist-tags`, `owner`, etc. | | `--json` | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In `npm pkg set` it enables parsing set values with JSON.parse() before saving them to your `package.json`. Not supported by all npm commands. | | `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | @@ -75,7 +88,7 @@ Create a trusted relationship between a package and GitLab CI/CD #### Synopsis ```bash -npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes] +npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes] ``` #### Flags @@ -85,6 +98,8 @@ npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--envir | `--file` | null | String (required) | Name of pipeline file (e.g., .gitlab-ci.yml) | | `--project` | null | String | Name of the project in the format group/project or group/subgroup/project | | `--environment`, `--env` | null | String | CI environment name | +| `--allow-publish` | false | Boolean | Allow npm publish for this trusted publisher configuration | +| `--allow-stage-publish`, `--allow-staged-publish` | false | Boolean | Allow npm stage publish for this trusted publisher configuration | | `--dry-run` | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, `install`, `update`, `dedupe`, `uninstall`, as well as `pack` and `publish`. Note: This is NOT honored by other network related commands, eg `dist-tags`, `owner`, etc. | | `--json` | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In `npm pkg set` it enables parsing set values with JSON.parse() before saving them to your `package.json`. Not supported by all npm commands. | | `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | @@ -97,7 +112,7 @@ Create a trusted relationship between a package and CircleCI #### Synopsis ```bash -npm trust circleci [package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [-y|--yes] +npm trust circleci [package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [--allow-publish] [--allow-stage-publish] [-y|--yes] ``` #### Flags @@ -109,6 +124,8 @@ npm trust circleci [package] --org-id --project-id --pipeline-defi | `--pipeline-definition-id` | null | String (required) | CircleCI pipeline definition UUID | | `--vcs-origin` | null | String (required) | CircleCI repository origin in format 'provider/owner/repo' | | `--context-id` | null | null or String (can be set multiple times) | CircleCI context UUID to match | +| `--allow-publish` | false | Boolean | Allow npm publish for this trusted publisher configuration | +| `--allow-stage-publish`, `--allow-staged-publish` | false | Boolean | Allow npm stage publish for this trusted publisher configuration | | `--dry-run` | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, `install`, `update`, `dedupe`, `uninstall`, as well as `pack` and `publish`. Note: This is NOT honored by other network related commands, eg `dist-tags`, `owner`, etc. | | `--json` | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In `npm pkg set` it enables parsing set values with JSON.parse() before saving them to your `package.json`. Not supported by all npm commands. | | `--registry` | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | diff --git a/deps/npm/docs/content/commands/npm-update.md b/deps/npm/docs/content/commands/npm-update.md index 979f1c1de333d9..86b54eed070924 100644 --- a/deps/npm/docs/content/commands/npm-update.md +++ b/deps/npm/docs/content/commands/npm-update.md @@ -303,6 +303,56 @@ run any pre- or post-scripts. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true @@ -330,7 +380,13 @@ If the requested version is a `dist-tag` and the given tag does not pass the will be used. For example, `foo@latest` might install `foo@1.2` even though `latest` is `2.0`. -This config cannot be used with: `min-release-age` +If `before` and `min-release-age` are both set in the same source, `before` +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one. + + #### `min-release-age` @@ -343,9 +399,11 @@ are no versions available for the current set of dependencies, the command will error. This flag is a complement to `before`, which accepts an exact date instead -of a relative number of days. - -This config cannot be used with: `before` +of a relative number of days. The two may coexist (e.g. `min-release-age` in +your `.npmrc` is preserved when npm internally spawns a sub-process with +`--before` while preparing a `git:` or `github:` dependency); when both +apply, `before` wins within a single source and across sources the standard +precedence rules apply. This value is not exported to the environment for child processes. diff --git a/deps/npm/docs/content/commands/npm-version.md b/deps/npm/docs/content/commands/npm-version.md index cd504b37b7f5eb..9016c8071f7593 100644 --- a/deps/npm/docs/content/commands/npm-version.md +++ b/deps/npm/docs/content/commands/npm-version.md @@ -229,6 +229,8 @@ The exact order of execution is as follows: 6. Run the `postversion` script. Use it to clean up the file system or automatically push the commit and/or tag. +For the `preversion`, `version` and `postversion` scripts, npm also sets the [environment variables](/using-npm/scripts#environment) `npm_old_version` and `npm_new_version`. + Take the following example: ```json diff --git a/deps/npm/docs/content/commands/npm.md b/deps/npm/docs/content/commands/npm.md index 242c61509bc1c7..ba1890149fcdd0 100644 --- a/deps/npm/docs/content/commands/npm.md +++ b/deps/npm/docs/content/commands/npm.md @@ -14,7 +14,7 @@ Note: This command is unaware of workspaces. ### Version -11.13.0 +11.16.0 ### Description diff --git a/deps/npm/docs/content/using-npm/config.md b/deps/npm/docs/content/using-npm/config.md index 96df0ae0058c69..d1167e0d14880b 100644 --- a/deps/npm/docs/content/using-npm/config.md +++ b/deps/npm/docs/content/using-npm/config.md @@ -140,7 +140,7 @@ safer to use a registry-provided authentication bearer token stored in the * Default: 'public' for new packages, existing packages it will not change the current level -* Type: null, "restricted", or "public" +* Type: null, "restricted", "public", or "private" If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. @@ -152,6 +152,8 @@ packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. +The value `private` is an alias for `restricted`. + #### `all` @@ -165,6 +167,42 @@ upon by the current project. +#### `allow-directory` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any directories to be installed. `none` prevents any +directories from being installed. `root` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like `npm view` + + + +#### `allow-file` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +`all` allows any tarball file to be installed. `none` prevents any tarball +file from being installed. `root` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like `npm view` + + + #### `allow-git` * Default: "all" @@ -173,12 +211,31 @@ upon by the current project. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. `all` allows any git dependencies to be fetched and installed. `none` prevents any git dependencies from being fetched and installed. `root` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like `npm view` + + + +#### `allow-remote` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +`all` allows any url to be installed. `none` prevents any url from being +installed. `root` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like `npm view` @@ -193,6 +250,51 @@ to the same value as the current version. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `allow-scripts-pending` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +`allowScripts` policy, without modifying `package.json`. Only meaningful for +`npm approve-scripts`. + + + +#### `allow-scripts-pin` + +* Default: true +* Type: Boolean + +Write pinned (`pkg@version`) entries when approving install scripts. Set to +`false` to write name-only entries that allow any version. Has no effect on +`npm deny-scripts`, which always writes name-only entries regardless of this +setting. + + + #### `audit` * Default: true @@ -240,7 +342,13 @@ If the requested version is a `dist-tag` and the given tag does not pass the will be used. For example, `foo@latest` might install `foo@1.2` even though `latest` is `2.0`. -This config cannot be used with: `min-release-age` +If `before` and `min-release-age` are both set in the same source, `before` +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one. + + #### `bin-links` @@ -382,6 +490,18 @@ are same as `cpu` field of package.json, which comes from `process.arch`. +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `depth` * Default: `Infinity` if `--all` is set; otherwise, `0` @@ -1094,9 +1214,11 @@ are no versions available for the current set of dependencies, the command will error. This flag is a complement to `before`, which accepts an exact date instead -of a relative number of days. - -This config cannot be used with: `before` +of a relative number of days. The two may coexist (e.g. `min-release-age` in +your `.npmrc` is preserved when npm internally spawns a sub-process with +`--before` while preparing a `git:` or `github:` dependency); when both +apply, `before` wins within a single source and across sources the standard +precedence rules apply. This value is not exported to the environment for child processes. @@ -1706,6 +1828,22 @@ this to work properly. +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + #### `strict-peer-deps` * Default: false diff --git a/deps/npm/docs/content/using-npm/scripts.md b/deps/npm/docs/content/using-npm/scripts.md index 91de8f22d47f0a..dcae0c66da0e3f 100644 --- a/deps/npm/docs/content/using-npm/scripts.md +++ b/deps/npm/docs/content/using-npm/scripts.md @@ -290,6 +290,13 @@ For example, if you had `{"name":"foo", "version":"1.2.5"}` in your package.json See [`package.json`](/configuring-npm/package-json) for more on package configs. +#### versioning variables + +For versioning scripts (`preversion`, `version`, `postversion`), npm sets these environment variables: + +* `npm_old_version` - The version before being bumped +* `npm_new_version` – The version after being bumped + #### current lifecycle event Lastly, the `npm_lifecycle_event` environment variable is set to whichever stage of the cycle is being executed. diff --git a/deps/npm/docs/lib/index.js b/deps/npm/docs/lib/index.js index d7a5e83ccf5062..9779d546572930 100644 --- a/deps/npm/docs/lib/index.js +++ b/deps/npm/docs/lib/index.js @@ -151,10 +151,12 @@ const generateFlagsTable = (definitionPool) => { if (!defaultVal) { defaultVal = String(def.default) } + defaultVal = defaultVal.replace(/\n/g, ' ').trim() let typeVal = def.typeDescription || String(def.type) if (def.required) { typeVal = `${typeVal} (required)` } + typeVal = typeVal.replace(/\n/g, ' ').trim() const desc = (def.description || '').replace(/\n/g, ' ').trim() return `| ${flagsStr} | ${defaultVal} | ${typeVal} | ${desc} |` }) diff --git a/deps/npm/docs/output/commands/npm-access.html b/deps/npm/docs/output/commands/npm-access.html index 329fad79ba9d15..983b44e86a639d 100644 --- a/deps/npm/docs/output/commands/npm-access.html +++ b/deps/npm/docs/output/commands/npm-access.html @@ -186,9 +186,9 @@
-

+

npm-access - @11.13.0 + @11.16.0

Set access level on published packages
diff --git a/deps/npm/docs/output/commands/npm-adduser.html b/deps/npm/docs/output/commands/npm-adduser.html index 52fc19c7942228..8945c6ef6cb33b 100644 --- a/deps/npm/docs/output/commands/npm-adduser.html +++ b/deps/npm/docs/output/commands/npm-adduser.html @@ -186,9 +186,9 @@
-

+

npm-adduser - @11.13.0 + @11.16.0

Add a registry user account
diff --git a/deps/npm/docs/output/commands/npm-approve-scripts.html b/deps/npm/docs/output/commands/npm-approve-scripts.html new file mode 100644 index 00000000000000..1849ae8c5011c4 --- /dev/null +++ b/deps/npm/docs/output/commands/npm-approve-scripts.html @@ -0,0 +1,304 @@ + + +npm-approve-scripts + + + + + +
+
+

+ npm-approve-scripts + @11.16.0 +

+Approve install scripts for specific dependencies +
+ +
+

Table of contents

+ +
+ +

Synopsis

+
npm approve-scripts <pkg> [<pkg> ...]
+npm approve-scripts --all
+npm approve-scripts --allow-scripts-pending
+
+

Note: This command is unaware of workspaces.

+

Description

+

Manages the allowScripts field in your project's package.json, which +records which of your dependencies are permitted to run install scripts +(preinstall, install, postinstall, and prepare for non-registry +sources). This command is the recommended way to maintain that field.

+

In the current release, this field is advisory: install scripts still run +by default, but installs print a list of packages whose scripts have not +been reviewed. A future release will block unreviewed install scripts.

+

There are three modes:

+
npm approve-scripts <pkg> [<pkg> ...]
+npm approve-scripts --all
+npm approve-scripts --allow-scripts-pending
+
+

<pkg> matches every installed version of that package. By default the +command writes pinned entries (pkg@1.2.3), which keep their approval +narrowed to the specific version you reviewed. Pass --no-allow-scripts-pin to write +name-only entries that allow any future version.

+

--all approves every package with unreviewed install scripts in one go.

+

--allow-scripts-pending is read-only: it lists every package whose install scripts +are not yet covered by allowScripts, without modifying package.json.

+

approve-scripts honours the asymmetric pin rule: if you re-approve a +package whose installed version has changed, the existing pin is rewritten +to track the new installed version. Multi-version statements +(pkg@1 || 2) are left alone, since they likely capture intent that +the command cannot infer. Existing false entries always win; +approve-scripts will not silently re-allow a package you previously +denied.

+

Examples

+
# Approve all currently-installed install scripts after reviewing them
+npm approve-scripts --all
+
+# Approve specific packages, pinned to their installed version
+npm approve-scripts canvas sharp
+
+# Approve name-only (any version of this package is allowed)
+npm approve-scripts --no-allow-scripts-pin canvas
+
+# Preview which packages still need review
+npm approve-scripts --allow-scripts-pending
+
+

Configuration

+

all

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

When running npm outdated and npm ls, setting --all will show all +outdated or installed packages, rather than only those directly depended +upon by the current project.

+

allow-scripts-pending

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

List packages with install scripts that are not yet covered by the +allowScripts policy, without modifying package.json. Only meaningful for +npm approve-scripts.

+

allow-scripts-pin

+
    +
  • Default: true
  • +
  • Type: Boolean
  • +
+

Write pinned (pkg@version) entries when approving install scripts. Set to +false to write name-only entries that allow any version. Has no effect on +npm deny-scripts, which always writes name-only entries regardless of this +setting.

+

json

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

Whether or not to output JSON data, rather than the normal output.

+
    +
  • In npm pkg set it enables parsing set values with JSON.parse() before +saving them to your package.json.
  • +
+

Not supported by all npm commands.

+

See Also

+
+ + +
+ + + + \ No newline at end of file diff --git a/deps/npm/docs/output/commands/npm-audit.html b/deps/npm/docs/output/commands/npm-audit.html index e935f919dfd483..f018a7ae7f1c57 100644 --- a/deps/npm/docs/output/commands/npm-audit.html +++ b/deps/npm/docs/output/commands/npm-audit.html @@ -186,9 +186,9 @@
-

+

npm-audit - @11.13.0 + @11.16.0

Run a security audit
diff --git a/deps/npm/docs/output/commands/npm-bugs.html b/deps/npm/docs/output/commands/npm-bugs.html index f6eb47691c1842..45ca5bec8ef537 100644 --- a/deps/npm/docs/output/commands/npm-bugs.html +++ b/deps/npm/docs/output/commands/npm-bugs.html @@ -186,9 +186,9 @@
-

+

npm-bugs - @11.13.0 + @11.16.0

Report bugs for a package in a web browser
diff --git a/deps/npm/docs/output/commands/npm-cache.html b/deps/npm/docs/output/commands/npm-cache.html index 761f44801dcd58..0e561b39dabdaa 100644 --- a/deps/npm/docs/output/commands/npm-cache.html +++ b/deps/npm/docs/output/commands/npm-cache.html @@ -186,9 +186,9 @@
-

+

npm-cache - @11.13.0 + @11.16.0

Manipulates packages cache
diff --git a/deps/npm/docs/output/commands/npm-ci.html b/deps/npm/docs/output/commands/npm-ci.html index 8762c8463177d5..745a22ea53c966 100644 --- a/deps/npm/docs/output/commands/npm-ci.html +++ b/deps/npm/docs/output/commands/npm-ci.html @@ -186,16 +186,16 @@
-

+

npm-ci - @11.13.0 + @11.16.0

Clean install a project

Table of contents

- +

Synopsis

@@ -333,6 +333,34 @@

ignore-scripts

npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

+

allow-directory

+
    +
  • Default: "all"
  • +
  • Type: "all", "none", or "root"
  • +
+

Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

+

all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

+

allow-file

+
    +
  • Default: "all"
  • +
  • Type: "all", "none", or "root"
  • +
+

Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

+

all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

allow-git

  • Default: "all"
  • @@ -341,12 +369,65 @@

    allow-git

    Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

    +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

    all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

    +

    allow-remote

    +
      +
    • Default: "all"
    • +
    • Type: "all", "none", or "root"
    • +
    +

    Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

    +

    all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

    +

    allow-scripts

    +
      +
    • Default: ""
    • +
    • Type: String (can be set multiple times)
    • +
    +

    Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

    +

    This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

    +

    Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

    +

    strict-allow-scripts

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

    +

    Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

    +

    dangerously-allow-all-scripts

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

    audit

    • Default: true
    • diff --git a/deps/npm/docs/output/commands/npm-completion.html b/deps/npm/docs/output/commands/npm-completion.html index 8d92430c9b6822..866085f42351f5 100644 --- a/deps/npm/docs/output/commands/npm-completion.html +++ b/deps/npm/docs/output/commands/npm-completion.html @@ -186,9 +186,9 @@
      -

      +

      npm-completion - @11.13.0 + @11.16.0

      Tab Completion for npm
      diff --git a/deps/npm/docs/output/commands/npm-config.html b/deps/npm/docs/output/commands/npm-config.html index 9f23af17c2ba2a..3f9be6dedfb64a 100644 --- a/deps/npm/docs/output/commands/npm-config.html +++ b/deps/npm/docs/output/commands/npm-config.html @@ -186,9 +186,9 @@
      -

      +

      npm-config - @11.13.0 + @11.16.0

      Manage the npm configuration files
      diff --git a/deps/npm/docs/output/commands/npm-dedupe.html b/deps/npm/docs/output/commands/npm-dedupe.html index c4ded5d1e93e0e..e15165a5d00e44 100644 --- a/deps/npm/docs/output/commands/npm-dedupe.html +++ b/deps/npm/docs/output/commands/npm-dedupe.html @@ -186,16 +186,16 @@
      -

      +

      npm-dedupe - @11.13.0 + @11.16.0

      Reduce duplication in the package tree

      Table of contents

      - +

      Synopsis

      @@ -323,6 +323,34 @@

      ignore-scripts

      npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

      +

      allow-directory

      +
        +
      • Default: "all"
      • +
      • Type: "all", "none", or "root"
      • +
      +

      Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

      +

      all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

      +

      allow-file

      +
        +
      • Default: "all"
      • +
      • Type: "all", "none", or "root"
      • +
      +

      Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

      +

      all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

      allow-git

      • Default: "all"
      • @@ -331,11 +359,26 @@

        allow-git

        Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

        +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

        all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

        +

        allow-remote

        +
          +
        • Default: "all"
        • +
        • Type: "all", "none", or "root"
        • +
        +

        Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

        +

        all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

        audit

          diff --git a/deps/npm/docs/output/commands/npm-deny-scripts.html b/deps/npm/docs/output/commands/npm-deny-scripts.html new file mode 100644 index 00000000000000..e9b18afb88b2a2 --- /dev/null +++ b/deps/npm/docs/output/commands/npm-deny-scripts.html @@ -0,0 +1,290 @@ + + +npm-deny-scripts + + + + + +
          +
          +

          + npm-deny-scripts + @11.16.0 +

          +Deny install scripts for specific dependencies +
          + +
          +

          Table of contents

          + +
          + +

          Synopsis

          +
          npm deny-scripts <pkg> [<pkg> ...]
          +npm deny-scripts --all
          +
          +

          Note: This command is unaware of workspaces.

          +

          Description

          +

          The companion command to npm approve-scripts. +Writes false entries into the allowScripts field of your project's +package.json, recording that a dependency must not run install scripts +even if a future version would otherwise be eligible.

          +

          In the current release, install scripts still run by default, so deny-scripts +only affects how installs of denied packages are reported. A future release +will block unreviewed install scripts and respect deny entries at install +time.

          +
          npm deny-scripts <pkg> [<pkg> ...]
          +npm deny-scripts --all
          +
          +

          <pkg> matches every installed version of that package. Denies are always +written name-only ("pkg": false), regardless of --allow-scripts-pin. Pinning a deny +to a specific version would silently re-allow scripts for any other version +of the same package, which defeats the purpose; the command picks the +safer default for you.

          +

          --all denies every package with unreviewed install scripts.

          +

          If a true (pinned or name-only) entry exists for a package and you then +deny it, the existing allow entries are removed so the name-only deny is +unambiguous.

          +

          Examples

          +
          # Deny a specific package outright
          +npm deny-scripts telemetry-pkg
          +
          +# Deny everything that has install scripts and isn't already approved
          +npm deny-scripts --all
          +
          +

          Configuration

          +

          all

          +
            +
          • Default: false
          • +
          • Type: Boolean
          • +
          +

          When running npm outdated and npm ls, setting --all will show all +outdated or installed packages, rather than only those directly depended +upon by the current project.

          +

          allow-scripts-pending

          +
            +
          • Default: false
          • +
          • Type: Boolean
          • +
          +

          List packages with install scripts that are not yet covered by the +allowScripts policy, without modifying package.json. Only meaningful for +npm approve-scripts.

          +

          allow-scripts-pin

          +
            +
          • Default: true
          • +
          • Type: Boolean
          • +
          +

          Write pinned (pkg@version) entries when approving install scripts. Set to +false to write name-only entries that allow any version. Has no effect on +npm deny-scripts, which always writes name-only entries regardless of this +setting.

          +

          json

          +
            +
          • Default: false
          • +
          • Type: Boolean
          • +
          +

          Whether or not to output JSON data, rather than the normal output.

          +
            +
          • In npm pkg set it enables parsing set values with JSON.parse() before +saving them to your package.json.
          • +
          +

          Not supported by all npm commands.

          +

          See Also

          +
          + + +
          + + + + \ No newline at end of file diff --git a/deps/npm/docs/output/commands/npm-deprecate.html b/deps/npm/docs/output/commands/npm-deprecate.html index 5bc588535e092e..9bda62f1a891ca 100644 --- a/deps/npm/docs/output/commands/npm-deprecate.html +++ b/deps/npm/docs/output/commands/npm-deprecate.html @@ -186,9 +186,9 @@
          -

          +

          npm-deprecate - @11.13.0 + @11.16.0

          Deprecate a version of a package
          diff --git a/deps/npm/docs/output/commands/npm-diff.html b/deps/npm/docs/output/commands/npm-diff.html index a65a7c15e9dfd8..7b72340cb35e86 100644 --- a/deps/npm/docs/output/commands/npm-diff.html +++ b/deps/npm/docs/output/commands/npm-diff.html @@ -186,9 +186,9 @@
          -

          +

          npm-diff - @11.13.0 + @11.16.0

          The registry diff command
          diff --git a/deps/npm/docs/output/commands/npm-dist-tag.html b/deps/npm/docs/output/commands/npm-dist-tag.html index 374fcd8ceaa270..3b95fe2e3ebc7e 100644 --- a/deps/npm/docs/output/commands/npm-dist-tag.html +++ b/deps/npm/docs/output/commands/npm-dist-tag.html @@ -186,9 +186,9 @@
          -

          +

          npm-dist-tag - @11.13.0 + @11.16.0

          Modify package distribution tags
          diff --git a/deps/npm/docs/output/commands/npm-docs.html b/deps/npm/docs/output/commands/npm-docs.html index 1f445117b93cab..a12dd65697c1df 100644 --- a/deps/npm/docs/output/commands/npm-docs.html +++ b/deps/npm/docs/output/commands/npm-docs.html @@ -186,9 +186,9 @@
          -

          +

          npm-docs - @11.13.0 + @11.16.0

          Open documentation for a package in a web browser
          diff --git a/deps/npm/docs/output/commands/npm-doctor.html b/deps/npm/docs/output/commands/npm-doctor.html index b16a27077ccfd7..d9094606fa2a14 100644 --- a/deps/npm/docs/output/commands/npm-doctor.html +++ b/deps/npm/docs/output/commands/npm-doctor.html @@ -186,9 +186,9 @@
          -

          +

          npm-doctor - @11.13.0 + @11.16.0

          Check the health of your npm environment
          diff --git a/deps/npm/docs/output/commands/npm-edit.html b/deps/npm/docs/output/commands/npm-edit.html index 3660272c77fc56..a5f7d958498c21 100644 --- a/deps/npm/docs/output/commands/npm-edit.html +++ b/deps/npm/docs/output/commands/npm-edit.html @@ -186,9 +186,9 @@
          -

          +

          npm-edit - @11.13.0 + @11.16.0

          Edit an installed package
          diff --git a/deps/npm/docs/output/commands/npm-exec.html b/deps/npm/docs/output/commands/npm-exec.html index eaab3d41205b80..333fd7312a6140 100644 --- a/deps/npm/docs/output/commands/npm-exec.html +++ b/deps/npm/docs/output/commands/npm-exec.html @@ -186,16 +186,16 @@
          -

          +

          npm-exec - @11.13.0 + @11.16.0

          Run a command from a local or remote npm package

          Table of contents

          - +

          Synopsis

          @@ -307,6 +307,44 @@

          include-workspace-root

          all workspaces via the workspaces flag, will cause npm to operate only on the specified workspaces, and not on the root project.

          This value is not exported to the environment for child processes.

          +

          allow-scripts

          +
            +
          • Default: ""
          • +
          • Type: String (can be set multiple times)
          • +
          +

          Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

          +

          This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

          +

          Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

          +

          strict-allow-scripts

          +
            +
          • Default: false
          • +
          • Type: Boolean
          • +
          +

          If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

          +

          Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

          +

          dangerously-allow-all-scripts

          +
            +
          • Default: false
          • +
          • Type: Boolean
          • +
          +

          If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

          Examples

          Run the version of tap in the local dependencies, with the provided arguments:

          $ npm exec -- tap --bail test/foo.js
          diff --git a/deps/npm/docs/output/commands/npm-explain.html b/deps/npm/docs/output/commands/npm-explain.html
          index bd64b49a29f4b5..feef6d1cf95315 100644
          --- a/deps/npm/docs/output/commands/npm-explain.html
          +++ b/deps/npm/docs/output/commands/npm-explain.html
          @@ -186,9 +186,9 @@
           
           
          -

          +

          npm-explain - @11.13.0 + @11.16.0

          Explain installed packages
          diff --git a/deps/npm/docs/output/commands/npm-explore.html b/deps/npm/docs/output/commands/npm-explore.html index aed39e77a6c4dd..0985c9bb2e8a19 100644 --- a/deps/npm/docs/output/commands/npm-explore.html +++ b/deps/npm/docs/output/commands/npm-explore.html @@ -186,9 +186,9 @@
          -

          +

          npm-explore - @11.13.0 + @11.16.0

          Browse an installed package
          diff --git a/deps/npm/docs/output/commands/npm-find-dupes.html b/deps/npm/docs/output/commands/npm-find-dupes.html index bbe3222494c0b7..c013dd0db414bf 100644 --- a/deps/npm/docs/output/commands/npm-find-dupes.html +++ b/deps/npm/docs/output/commands/npm-find-dupes.html @@ -186,9 +186,9 @@
          -

          +

          npm-find-dupes - @11.13.0 + @11.16.0

          Find duplication in the package tree
          diff --git a/deps/npm/docs/output/commands/npm-fund.html b/deps/npm/docs/output/commands/npm-fund.html index 85b66955b516b3..927ce09fa8cde9 100644 --- a/deps/npm/docs/output/commands/npm-fund.html +++ b/deps/npm/docs/output/commands/npm-fund.html @@ -186,9 +186,9 @@
          -

          +

          npm-fund - @11.13.0 + @11.16.0

          Retrieve funding information
          diff --git a/deps/npm/docs/output/commands/npm-get.html b/deps/npm/docs/output/commands/npm-get.html index eb4bde934ce801..675a4fcecb9855 100644 --- a/deps/npm/docs/output/commands/npm-get.html +++ b/deps/npm/docs/output/commands/npm-get.html @@ -186,9 +186,9 @@
          -

          +

          npm-get - @11.13.0 + @11.16.0

          Get a value from the npm configuration
          diff --git a/deps/npm/docs/output/commands/npm-help-search.html b/deps/npm/docs/output/commands/npm-help-search.html index cad8a79a483727..ba77fc89ae508a 100644 --- a/deps/npm/docs/output/commands/npm-help-search.html +++ b/deps/npm/docs/output/commands/npm-help-search.html @@ -186,9 +186,9 @@
          -

          +

          npm-help-search - @11.13.0 + @11.16.0

          Search npm help documentation
          diff --git a/deps/npm/docs/output/commands/npm-help.html b/deps/npm/docs/output/commands/npm-help.html index 519fde8cf07841..5c83c72e328208 100644 --- a/deps/npm/docs/output/commands/npm-help.html +++ b/deps/npm/docs/output/commands/npm-help.html @@ -186,9 +186,9 @@
          -

          +

          npm-help - @11.13.0 + @11.16.0

          Get help on npm
          diff --git a/deps/npm/docs/output/commands/npm-init.html b/deps/npm/docs/output/commands/npm-init.html index a26b848890dffd..321a462a5f0162 100644 --- a/deps/npm/docs/output/commands/npm-init.html +++ b/deps/npm/docs/output/commands/npm-init.html @@ -186,9 +186,9 @@
          -

          +

          npm-init - @11.13.0 + @11.16.0

          Create a package.json file
          diff --git a/deps/npm/docs/output/commands/npm-install-ci-test.html b/deps/npm/docs/output/commands/npm-install-ci-test.html index bb65682760d46e..2658ecedd0efb2 100644 --- a/deps/npm/docs/output/commands/npm-install-ci-test.html +++ b/deps/npm/docs/output/commands/npm-install-ci-test.html @@ -186,16 +186,16 @@
          -

          +

          npm-install-ci-test - @11.13.0 + @11.16.0

          Install a project with a clean slate and run tests

          Table of contents

          - +

          Synopsis

          @@ -297,6 +297,34 @@

          ignore-scripts

          npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

          +

          allow-directory

          +
            +
          • Default: "all"
          • +
          • Type: "all", "none", or "root"
          • +
          +

          Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

          +

          all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

          +

          allow-file

          +
            +
          • Default: "all"
          • +
          • Type: "all", "none", or "root"
          • +
          +

          Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

          +

          all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

          allow-git

          • Default: "all"
          • @@ -305,12 +333,65 @@

            allow-git

            Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

            +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

            all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

            +

            allow-remote

            +
              +
            • Default: "all"
            • +
            • Type: "all", "none", or "root"
            • +
            +

            Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

            +

            all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

            +

            allow-scripts

            +
              +
            • Default: ""
            • +
            • Type: String (can be set multiple times)
            • +
            +

            Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

            +

            This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

            +

            Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

            +

            strict-allow-scripts

            +
              +
            • Default: false
            • +
            • Type: Boolean
            • +
            +

            If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

            +

            Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

            +

            dangerously-allow-all-scripts

            +
              +
            • Default: false
            • +
            • Type: Boolean
            • +
            +

            If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

            audit

            • Default: true
            • diff --git a/deps/npm/docs/output/commands/npm-install-test.html b/deps/npm/docs/output/commands/npm-install-test.html index dff8ea97a81ae3..cda0bf383f0fe5 100644 --- a/deps/npm/docs/output/commands/npm-install-test.html +++ b/deps/npm/docs/output/commands/npm-install-test.html @@ -186,16 +186,16 @@
              -

              +

              npm-install-test - @11.13.0 + @11.16.0

              Install package(s) and run tests

              Table of contents

              - +

              Synopsis

              @@ -353,6 +353,34 @@

              ignore-scripts

              npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

              +

              allow-directory

              +
                +
              • Default: "all"
              • +
              • Type: "all", "none", or "root"
              • +
              +

              Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

              +

              all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

              +

              allow-file

              +
                +
              • Default: "all"
              • +
              • Type: "all", "none", or "root"
              • +
              +

              Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

              +

              all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

              allow-git

              • Default: "all"
              • @@ -361,12 +389,65 @@

                allow-git

                Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

                +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

                +

                allow-remote

                +
                  +
                • Default: "all"
                • +
                • Type: "all", "none", or "root"
                • +
                +

                Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                +

                all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

                +

                allow-scripts

                +
                  +
                • Default: ""
                • +
                • Type: String (can be set multiple times)
                • +
                +

                Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

                +

                This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

                +

                Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

                +

                strict-allow-scripts

                +
                  +
                • Default: false
                • +
                • Type: Boolean
                • +
                +

                If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

                +

                Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

                +

                dangerously-allow-all-scripts

                +
                  +
                • Default: false
                • +
                • Type: Boolean
                • +
                +

                If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

                audit

                • Default: true
                • @@ -389,7 +470,11 @@

                  before

                  --before filter, the most recent version less than or equal to that tag will be used. For example, foo@latest might install foo@1.2 even though latest is 2.0.

                  -

                  This config cannot be used with: min-release-age

                  +

                  If before and min-release-age are both set in the same source, before +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one.

                  min-release-age

                  • Default: null
                  • @@ -400,8 +485,11 @@

                    min-release-age

                    are no versions available for the current set of dependencies, the command will error.

                    This flag is a complement to before, which accepts an exact date instead -of a relative number of days.

                    -

                    This config cannot be used with: before

                    +of a relative number of days. The two may coexist (e.g. min-release-age in +your .npmrc is preserved when npm internally spawns a sub-process with +--before while preparing a git: or github: dependency); when both +apply, before wins within a single source and across sources the standard +precedence rules apply.

                    This value is not exported to the environment for child processes.

                      diff --git a/deps/npm/docs/output/commands/npm-install.html b/deps/npm/docs/output/commands/npm-install.html index 69d2207db9df91..c9ae37e393238c 100644 --- a/deps/npm/docs/output/commands/npm-install.html +++ b/deps/npm/docs/output/commands/npm-install.html @@ -186,16 +186,16 @@
                      -

                      +

                      npm-install - @11.13.0 + @11.16.0

                      Install a package

                      Table of contents

                      - +

                      Synopsis

                      @@ -628,6 +628,34 @@

                      ignore-scripts

                      npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

                      +

                      allow-directory

                      +
                        +
                      • Default: "all"
                      • +
                      • Type: "all", "none", or "root"
                      • +
                      +

                      Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                      +

                      all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

                      +

                      allow-file

                      +
                        +
                      • Default: "all"
                      • +
                      • Type: "all", "none", or "root"
                      • +
                      +

                      Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

                      +

                      all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

                      allow-git

                      • Default: "all"
                      • @@ -636,12 +664,65 @@

                        allow-git

                        Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

                        +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                        all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

                        +

                        allow-remote

                        +
                          +
                        • Default: "all"
                        • +
                        • Type: "all", "none", or "root"
                        • +
                        +

                        Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                        +

                        all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

                        +

                        allow-scripts

                        +
                          +
                        • Default: ""
                        • +
                        • Type: String (can be set multiple times)
                        • +
                        +

                        Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

                        +

                        This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

                        +

                        Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

                        +

                        strict-allow-scripts

                        +
                          +
                        • Default: false
                        • +
                        • Type: Boolean
                        • +
                        +

                        If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

                        +

                        Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

                        +

                        dangerously-allow-all-scripts

                        +
                          +
                        • Default: false
                        • +
                        • Type: Boolean
                        • +
                        +

                        If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

                        audit

                        • Default: true
                        • @@ -664,7 +745,11 @@

                          before

                          --before filter, the most recent version less than or equal to that tag will be used. For example, foo@latest might install foo@1.2 even though latest is 2.0.

                          -

                          This config cannot be used with: min-release-age

                          +

                          If before and min-release-age are both set in the same source, before +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one.

                          min-release-age

                          • Default: null
                          • @@ -675,8 +760,11 @@

                            min-release-age

                            are no versions available for the current set of dependencies, the command will error.

                            This flag is a complement to before, which accepts an exact date instead -of a relative number of days.

                            -

                            This config cannot be used with: before

                            +of a relative number of days. The two may coexist (e.g. min-release-age in +your .npmrc is preserved when npm internally spawns a sub-process with +--before while preparing a git: or github: dependency); when both +apply, before wins within a single source and across sources the standard +precedence rules apply.

                            This value is not exported to the environment for child processes.

                              diff --git a/deps/npm/docs/output/commands/npm-link.html b/deps/npm/docs/output/commands/npm-link.html index aed7d7f511bfad..dcc559329dfc51 100644 --- a/deps/npm/docs/output/commands/npm-link.html +++ b/deps/npm/docs/output/commands/npm-link.html @@ -186,16 +186,16 @@
                              -

                              +

                              npm-link - @11.13.0 + @11.16.0

                              Symlink a package folder

                              Table of contents

                              - +

                              Synopsis

                              @@ -366,6 +366,34 @@

                              ignore-scripts

                              npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

                              +

                              allow-directory

                              +
                                +
                              • Default: "all"
                              • +
                              • Type: "all", "none", or "root"
                              • +
                              +

                              Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                              +

                              all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

                              +

                              allow-file

                              +
                                +
                              • Default: "all"
                              • +
                              • Type: "all", "none", or "root"
                              • +
                              +

                              Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

                              +

                              all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

                              allow-git

                              • Default: "all"
                              • @@ -374,11 +402,26 @@

                                allow-git

                                Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

                                +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                                all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

                                +

                                allow-remote

                                +
                                  +
                                • Default: "all"
                                • +
                                • Type: "all", "none", or "root"
                                • +
                                +

                                Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                                +

                                all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

                                audit

                                  diff --git a/deps/npm/docs/output/commands/npm-ll.html b/deps/npm/docs/output/commands/npm-ll.html index ce891f5976b960..52f363b8b17d68 100644 --- a/deps/npm/docs/output/commands/npm-ll.html +++ b/deps/npm/docs/output/commands/npm-ll.html @@ -186,9 +186,9 @@
                                  -

                                  +

                                  npm-ll - @11.13.0 + @11.16.0

                                  List installed packages
                                  diff --git a/deps/npm/docs/output/commands/npm-login.html b/deps/npm/docs/output/commands/npm-login.html index a442b2e7e76669..eac37fc5a66665 100644 --- a/deps/npm/docs/output/commands/npm-login.html +++ b/deps/npm/docs/output/commands/npm-login.html @@ -186,9 +186,9 @@
                                  -

                                  +

                                  npm-login - @11.13.0 + @11.16.0

                                  Login to a registry user account
                                  diff --git a/deps/npm/docs/output/commands/npm-logout.html b/deps/npm/docs/output/commands/npm-logout.html index 28482bfd2a53f1..0930332b862b4d 100644 --- a/deps/npm/docs/output/commands/npm-logout.html +++ b/deps/npm/docs/output/commands/npm-logout.html @@ -186,9 +186,9 @@
                                  -

                                  +

                                  npm-logout - @11.13.0 + @11.16.0

                                  Log out of the registry
                                  diff --git a/deps/npm/docs/output/commands/npm-ls.html b/deps/npm/docs/output/commands/npm-ls.html index 2f5dd885004c16..cbf7f4e8208c93 100644 --- a/deps/npm/docs/output/commands/npm-ls.html +++ b/deps/npm/docs/output/commands/npm-ls.html @@ -186,9 +186,9 @@
                                  -

                                  +

                                  npm-ls - @11.13.0 + @11.16.0

                                  List installed packages
                                  @@ -209,7 +209,7 @@

                                  Description

                                  Positional arguments are name@version-range identifiers, which will limit the results to only the paths to the packages named. Note that nested packages will also show the paths to the specified packages. For example, running npm ls promzard in npm's source tree will show:

                                  -
                                  npm@11.13.0 /path/to/npm
                                  +
                                  npm@11.16.0 /path/to/npm
                                   └─┬ init-package-json@0.0.4
                                     └── promzard@0.1.5
                                   
                                  diff --git a/deps/npm/docs/output/commands/npm-org.html b/deps/npm/docs/output/commands/npm-org.html index 02bd129659dc32..99e8c472dc5f74 100644 --- a/deps/npm/docs/output/commands/npm-org.html +++ b/deps/npm/docs/output/commands/npm-org.html @@ -186,9 +186,9 @@
                                  -

                                  +

                                  npm-org - @11.13.0 + @11.16.0

                                  Manage orgs
                                  diff --git a/deps/npm/docs/output/commands/npm-outdated.html b/deps/npm/docs/output/commands/npm-outdated.html index 2b4702dc5eefdd..cb154b4a234c7e 100644 --- a/deps/npm/docs/output/commands/npm-outdated.html +++ b/deps/npm/docs/output/commands/npm-outdated.html @@ -186,9 +186,9 @@
                                  -

                                  +

                                  npm-outdated - @11.13.0 + @11.16.0

                                  Check for outdated packages
                                  @@ -328,7 +328,11 @@

                                  before

                                  --before filter, the most recent version less than or equal to that tag will be used. For example, foo@latest might install foo@1.2 even though latest is 2.0.

                                  -

                                  This config cannot be used with: min-release-age

                                  +

                                  If before and min-release-age are both set in the same source, before +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one.

                                  min-release-age

                                  • Default: null
                                  • @@ -339,8 +343,11 @@

                                    min-release-age

                                    are no versions available for the current set of dependencies, the command will error.

                                    This flag is a complement to before, which accepts an exact date instead -of a relative number of days.

                                    -

                                    This config cannot be used with: before

                                    +of a relative number of days. The two may coexist (e.g. min-release-age in +your .npmrc is preserved when npm internally spawns a sub-process with +--before while preparing a git: or github: dependency); when both +apply, before wins within a single source and across sources the standard +precedence rules apply.

                                    This value is not exported to the environment for child processes.

                                    See Also

                                      diff --git a/deps/npm/docs/output/commands/npm-owner.html b/deps/npm/docs/output/commands/npm-owner.html index 7519b7eafa17bd..fa568741602212 100644 --- a/deps/npm/docs/output/commands/npm-owner.html +++ b/deps/npm/docs/output/commands/npm-owner.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-owner - @11.13.0 + @11.16.0

                                      Manage package owners
                                      diff --git a/deps/npm/docs/output/commands/npm-pack.html b/deps/npm/docs/output/commands/npm-pack.html index 3290b6a50cb74f..a99ae2dba99c61 100644 --- a/deps/npm/docs/output/commands/npm-pack.html +++ b/deps/npm/docs/output/commands/npm-pack.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-pack - @11.13.0 + @11.16.0

                                      Create a tarball from a package
                                      diff --git a/deps/npm/docs/output/commands/npm-ping.html b/deps/npm/docs/output/commands/npm-ping.html index 4fe67beb9b00a9..bd867fbd3ef12b 100644 --- a/deps/npm/docs/output/commands/npm-ping.html +++ b/deps/npm/docs/output/commands/npm-ping.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-ping - @11.13.0 + @11.16.0

                                      Ping npm registry
                                      diff --git a/deps/npm/docs/output/commands/npm-pkg.html b/deps/npm/docs/output/commands/npm-pkg.html index 82928d2a503eae..145b47fde4e069 100644 --- a/deps/npm/docs/output/commands/npm-pkg.html +++ b/deps/npm/docs/output/commands/npm-pkg.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-pkg - @11.13.0 + @11.16.0

                                      Manages your package.json
                                      diff --git a/deps/npm/docs/output/commands/npm-prefix.html b/deps/npm/docs/output/commands/npm-prefix.html index 6bbab9ec8b6baa..2bccd93bb26aae 100644 --- a/deps/npm/docs/output/commands/npm-prefix.html +++ b/deps/npm/docs/output/commands/npm-prefix.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-prefix - @11.13.0 + @11.16.0

                                      Display prefix
                                      diff --git a/deps/npm/docs/output/commands/npm-profile.html b/deps/npm/docs/output/commands/npm-profile.html index a201918112143a..9e3d975aa3c2ca 100644 --- a/deps/npm/docs/output/commands/npm-profile.html +++ b/deps/npm/docs/output/commands/npm-profile.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-profile - @11.13.0 + @11.16.0

                                      Change settings on your registry profile
                                      diff --git a/deps/npm/docs/output/commands/npm-prune.html b/deps/npm/docs/output/commands/npm-prune.html index 485ad8c114bca1..f6a356f53cc238 100644 --- a/deps/npm/docs/output/commands/npm-prune.html +++ b/deps/npm/docs/output/commands/npm-prune.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-prune - @11.13.0 + @11.16.0

                                      Remove extraneous packages
                                      diff --git a/deps/npm/docs/output/commands/npm-publish.html b/deps/npm/docs/output/commands/npm-publish.html index 9f16f9a0732631..b9c7824c8456d7 100644 --- a/deps/npm/docs/output/commands/npm-publish.html +++ b/deps/npm/docs/output/commands/npm-publish.html @@ -186,9 +186,9 @@
                                      -

                                      +

                                      npm-publish - @11.13.0 + @11.16.0

                                      Publish a package
                                      @@ -279,7 +279,7 @@

                                      access

                                      • Default: 'public' for new packages, existing packages it will not change the current level
                                      • -
                                      • Type: null, "restricted", or "public"
                                      • +
                                      • Type: null, "restricted", "public", or "private"

                                      If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted.

                                      @@ -287,6 +287,7 @@

                                      access

                                      Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would.

                                      +

                                      The value private is an alias for restricted.

                                      dry-run

                                      • Default: false
                                      • diff --git a/deps/npm/docs/output/commands/npm-query.html b/deps/npm/docs/output/commands/npm-query.html index caadf8dffcf929..efa6bd81f130f8 100644 --- a/deps/npm/docs/output/commands/npm-query.html +++ b/deps/npm/docs/output/commands/npm-query.html @@ -186,9 +186,9 @@
                                        -

                                        +

                                        npm-query - @11.13.0 + @11.16.0

                                        Dependency selector query
                                        diff --git a/deps/npm/docs/output/commands/npm-rebuild.html b/deps/npm/docs/output/commands/npm-rebuild.html index 51386d3290beab..0aff44579c5075 100644 --- a/deps/npm/docs/output/commands/npm-rebuild.html +++ b/deps/npm/docs/output/commands/npm-rebuild.html @@ -186,16 +186,16 @@
                                        -

                                        +

                                        npm-rebuild - @11.13.0 + @11.16.0

                                        Rebuild a package

                                        Table of contents

                                        - +

                                        Synopsis

                                        @@ -269,6 +269,44 @@

                                        ignore-scripts

                                        npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

                                        +

                                        allow-scripts

                                        +
                                          +
                                        • Default: ""
                                        • +
                                        • Type: String (can be set multiple times)
                                        • +
                                        +

                                        Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

                                        +

                                        This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

                                        +

                                        Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

                                        +

                                        strict-allow-scripts

                                        +
                                          +
                                        • Default: false
                                        • +
                                        • Type: Boolean
                                        • +
                                        +

                                        If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

                                        +

                                        Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

                                        +

                                        dangerously-allow-all-scripts

                                        +
                                          +
                                        • Default: false
                                        • +
                                        • Type: Boolean
                                        • +
                                        +

                                        If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

                                        workspace

                                        • Default:
                                        • diff --git a/deps/npm/docs/output/commands/npm-repo.html b/deps/npm/docs/output/commands/npm-repo.html index 1b4db65a41f8a1..0efe01e32391d1 100644 --- a/deps/npm/docs/output/commands/npm-repo.html +++ b/deps/npm/docs/output/commands/npm-repo.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-repo - @11.13.0 + @11.16.0

                                          Open package repository page in the browser
                                          diff --git a/deps/npm/docs/output/commands/npm-restart.html b/deps/npm/docs/output/commands/npm-restart.html index 3bfb26e5b83c35..d58d9d362a03a9 100644 --- a/deps/npm/docs/output/commands/npm-restart.html +++ b/deps/npm/docs/output/commands/npm-restart.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-restart - @11.13.0 + @11.16.0

                                          Restart a package
                                          diff --git a/deps/npm/docs/output/commands/npm-root.html b/deps/npm/docs/output/commands/npm-root.html index f224de42549133..8f1a7319765932 100644 --- a/deps/npm/docs/output/commands/npm-root.html +++ b/deps/npm/docs/output/commands/npm-root.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-root - @11.13.0 + @11.16.0

                                          Display npm root
                                          diff --git a/deps/npm/docs/output/commands/npm-run.html b/deps/npm/docs/output/commands/npm-run.html index c61048f31cf8e2..c234db61b936de 100644 --- a/deps/npm/docs/output/commands/npm-run.html +++ b/deps/npm/docs/output/commands/npm-run.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-run - @11.13.0 + @11.16.0

                                          Run arbitrary package scripts
                                          diff --git a/deps/npm/docs/output/commands/npm-sbom.html b/deps/npm/docs/output/commands/npm-sbom.html index ddee5f40af3896..df30ae75770012 100644 --- a/deps/npm/docs/output/commands/npm-sbom.html +++ b/deps/npm/docs/output/commands/npm-sbom.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-sbom - @11.13.0 + @11.16.0

                                          Generate a Software Bill of Materials (SBOM)
                                          diff --git a/deps/npm/docs/output/commands/npm-search.html b/deps/npm/docs/output/commands/npm-search.html index 5c01a0fd8bfe66..63efdaad281e8b 100644 --- a/deps/npm/docs/output/commands/npm-search.html +++ b/deps/npm/docs/output/commands/npm-search.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-search - @11.13.0 + @11.16.0

                                          Search for packages
                                          diff --git a/deps/npm/docs/output/commands/npm-set.html b/deps/npm/docs/output/commands/npm-set.html index b5bd610f6d999a..988c341f8fee77 100644 --- a/deps/npm/docs/output/commands/npm-set.html +++ b/deps/npm/docs/output/commands/npm-set.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-set - @11.13.0 + @11.16.0

                                          Set a value in the npm configuration
                                          diff --git a/deps/npm/docs/output/commands/npm-shrinkwrap.html b/deps/npm/docs/output/commands/npm-shrinkwrap.html index ac415a958f178d..46c96bdef91111 100644 --- a/deps/npm/docs/output/commands/npm-shrinkwrap.html +++ b/deps/npm/docs/output/commands/npm-shrinkwrap.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-shrinkwrap - @11.13.0 + @11.16.0

                                          Lock down dependency versions for publication
                                          diff --git a/deps/npm/docs/output/commands/npm-stage.html b/deps/npm/docs/output/commands/npm-stage.html new file mode 100644 index 00000000000000..e98b5e5aca18a7 --- /dev/null +++ b/deps/npm/docs/output/commands/npm-stage.html @@ -0,0 +1,609 @@ + + +npm-stage + + + + + +
                                          +
                                          +

                                          + npm-stage + @11.16.0 +

                                          +Stage packages for publishing +
                                          + +
                                          +

                                          Table of contents

                                          + +
                                          + +

                                          Synopsis

                                          +
                                          npm stage
                                          +
                                          +

                                          Note: This command is unaware of workspaces.

                                          +

                                          Description

                                          +

                                          Staged publishing allows package maintainers to require proof-of-presence +for all publishes. Proof-of-presence is where a human is involved, +interjects, and provides authentication (2FA) during an action — in this +case, publishing an npm package.

                                          +

                                          Typically when maintainers use automated workflows to publish, +proof-of-presence is lacking as there's no convenient way to interject the +process and provide 2FA, as is the case for publishing with a granular +access token with bypass and the trusted publishing flow. Staged publishing +allows users to have their automated workflows stage a package without a 2FA +prompt, deferring the act of 2FA, allowing the maintainer to approve the +staged package and publish at a later point.

                                          +

                                          The npm stage publish command packs the current working directory and +places that version of the package into the registry in a state where it's +not available for public access, allowing maintainers to approve the package +at a later point in time. The act of staging does not prompt for 2FA and can be done with any token +type, the act of approving will.

                                          +

                                          Key behaviors:

                                          +
                                            +
                                          • Staged packages share the same semver version unique index as published +packages — you cannot publish a version that already exists as a staged +version for that package.
                                          • +
                                          • You can still publish packages normally while you have staged packages +pending.
                                          • +
                                          • You can stage multiple versions of the same package.
                                          • +
                                          • npm stage publish has parity with npm publish and will respect +"private": true in package.json, refusing to stage the package.
                                          • +
                                          +

                                          Prerequisites

                                          +

                                          Before using npm stage commands, ensure the following requirements are met:

                                          +
                                            +
                                          • Write permissions on the package: You must have write access to the +package you're configuring.
                                          • +
                                          • Package must exist: The package you're configuring must already exist +on the npm registry.
                                          • +
                                          • 2FA enabled on your account: Commands that require 2FA will prompt you +to authenticate. If you don't already have 2FA enabled on your account, +you must enable it before using these commands.
                                          • +
                                          +

                                          Subcommands

                                          +
                                            +
                                          • npm stage publish [<package-spec>] - Stage a package for publishing
                                          • +
                                          • npm stage list [<package-spec>] - List all staged package versions
                                          • +
                                          • npm stage view <stage-id> - View details of a specific staged package
                                          • +
                                          • npm stage approve <stage-id> - Approve a staged package for publishing
                                          • +
                                          • npm stage reject <stage-id> - Reject a staged package
                                          • +
                                          • npm stage download <stage-id> - Download the tarball for inspection
                                          • +
                                          +

                                          2FA Requirements by Subcommand

                                          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                          CommandRequires 2FANotes
                                          npm stage publishNoDesigned for automated workflows; defers 2FA to approval
                                          npm stage listNoView staged packages
                                          npm stage viewNoView staged package details
                                          npm stage approveYesPrompts for 2FA to publish the staged package
                                          npm stage rejectYesPrompts for 2FA to permanently remove the staged package
                                          npm stage downloadNoDownloads the tarball for local inspection
                                          +

                                          Tag Behavior

                                          +

                                          The --tag flag follows the same logic as npm publish. If no tag is +provided, the latest tag is used by default. For pre-release versions +(e.g., 1.0.0-beta.1) and non-latest semver versions, the tag must be +explicitly provided — otherwise the CLI will error, just as npm publish +would.

                                          +

                                          The tag is an immutable property of the staged package. Once a package is +staged with a given tag, the tag cannot be changed. If you need to stage the +same version with a different tag, you must first reject the existing staged +package using npm stage reject and then re-stage it with the desired tag.

                                          +

                                          Token Behavior

                                          +

                                          The key difference with staged publishing is that npm stage publish never +requires a 2FA prompt, regardless of token type. This is what makes it +suitable for automated workflows. The goal of npm stage publish is +deferring proof-of-presence to a later point in time.

                                          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                          Token Typenpm stage publishnpm publish
                                          GAT with bypassCan stageCan publish (if allowed by package publishing access)
                                          GAT without bypassCan stage2FA prompt (if allowed by package publishing access)
                                          Session tokenCan stage2FA prompt
                                          Trust token (OIDC)Can stage (if allowed)Can publish (if allowed)
                                          +

                                          Trust Relationship Permissions

                                          +

                                          With staged publishing, trust relationships now support granular command +permissions. Shortlived tokens issued through trust relationships can only be +used with npm stage publish and npm publish. Shortlived tokens cannot run +npm stage subcommands.

                                          +

                                          npm trust <provider> supports --allow-publish and --allow-stage-publish +to control which commands are available through each trust relationship.

                                          +

                                          Best Practices

                                          +

                                          Note: The addition of staged publishing does not make your account or org +more secure. Maintainers must still use the best practices listed below.

                                          +
                                            +
                                          1. +

                                            Delete Granular Access Tokens (GAT) with bypass 2FA enabled. +Now with staged publishing, we've eliminated the need for a GAT token +that can bypass 2FA. We encourage you to delete all your tokens with +bypass enabled and switch to using a trust relationship in your automated +workflows, or create a GAT without bypass and use npm stage publish.

                                            +
                                          2. +
                                          3. +

                                            Disallow tokens from publishing at the package level. +All packages have their own access controls under "package access" +allowing packages to be published with bypass tokens, which is no longer +a necessity. We encourage you to select "Require two-factor +authentication and disallow tokens (recommended)" for all your packages +on the package access page.

                                            +
                                          4. +
                                          5. +

                                            Configure trust relationship permissions to prevent npm publish. +We encourage you to only enable npm stage publish on your trust +relationships and disable npm publish.

                                            +
                                          6. +
                                          +

                                          Configuration

                                          +

                                          npm stage publish

                                          +

                                          Stage a package for publishing, deferring proof-of-presence (2FA) to a later point in time

                                          +

                                          Synopsis

                                          +
                                          npm stage publish <package-spec>
                                          +
                                          +

                                          Flags

                                          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                          FlagDefaultTypeDescription
                                          --tag"latest"StringIf you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. It is the tag added to the package@version specified in the npm dist-tag add command, if no explicit tag is given. When used by the npm diff command, this is the tag used to fetch the tarball that will be compared with the local files by default. If used in the npm publish command, this is the tag that will be added to the package submitted to the registry.
                                          --access'public' for new packages, existing packages it will not change the current levelnull, "restricted", "public", or "private"If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted. Unscoped packages cannot be set to restricted. Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would. The value private is an alias for restricted.
                                          --dry-runfalseBooleanIndicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, install, update, dedupe, uninstall, as well as pack and publish. Note: This is NOT honored by other network related commands, eg dist-tags, owner, etc.
                                          --otpnullnull or StringThis is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with npm access. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one.
                                          --workspace, -wString (can be set multiple times)Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the workspace config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the npm init command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project.
                                          --workspacesnullnull or BooleanSet to true to run the command in the context of all configured workspaces. Explicitly setting this to false will cause commands like install to ignore workspaces altogether. When not set explicitly: - Commands that operate on the node_modules tree (install, update, etc.) will link workspaces into the node_modules folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, unless one or more workspaces are specified in the workspace config.
                                          --include-workspace-rootfalseBooleanInclude the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the workspace config, or all workspaces via the workspaces flag, will cause npm to operate only on the specified workspaces, and not on the root project.
                                          --provenancefalseBooleanWhen publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from.
                                          +

                                          npm stage list

                                          +

                                          List all staged package versions

                                          +

                                          Synopsis

                                          +
                                          npm stage list [<package-spec>]
                                          +
                                          +

                                          Flags

                                          + + + + + + + + + + + + + + + + + + + + + + + +
                                          FlagDefaultTypeDescription
                                          --jsonfalseBooleanWhether or not to output JSON data, rather than the normal output. * In npm pkg set it enables parsing set values with JSON.parse() before saving them to your package.json. Not supported by all npm commands.
                                          --registry"https://registry.npmjs.org/"URLThe base URL of the npm registry.
                                          +

                                          npm stage view

                                          +

                                          View details of a specific staged package

                                          +

                                          Synopsis

                                          +
                                          npm stage view <stage-id>
                                          +
                                          +

                                          Flags

                                          + + + + + + + + + + + + + + + + + + + + + + + +
                                          FlagDefaultTypeDescription
                                          --jsonfalseBooleanWhether or not to output JSON data, rather than the normal output. * In npm pkg set it enables parsing set values with JSON.parse() before saving them to your package.json. Not supported by all npm commands.
                                          --registry"https://registry.npmjs.org/"URLThe base URL of the npm registry.
                                          +

                                          npm stage approve

                                          +

                                          Approve a staged package, publishing it to the npm registry

                                          +

                                          Synopsis

                                          +
                                          npm stage approve <stage-id>
                                          +
                                          +

                                          Flags

                                          + + + + + + + + + + + + + + + + + + + + + + + +
                                          FlagDefaultTypeDescription
                                          --otpnullnull or StringThis is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with npm access. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one.
                                          --registry"https://registry.npmjs.org/"URLThe base URL of the npm registry.
                                          +

                                          npm stage reject

                                          +

                                          Reject a staged package, removing it from the registry

                                          +

                                          Synopsis

                                          +
                                          npm stage reject <stage-id>
                                          +
                                          +

                                          Flags

                                          + + + + + + + + + + + + + + + + + + + + + + + +
                                          FlagDefaultTypeDescription
                                          --otpnullnull or StringThis is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with npm access. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one.
                                          --registry"https://registry.npmjs.org/"URLThe base URL of the npm registry.
                                          +

                                          npm stage download

                                          +

                                          Download the tarball of a staged package for inspection

                                          +

                                          Synopsis

                                          +
                                          npm stage download <stage-id>
                                          +
                                          +

                                          Flags

                                          + + + + + + + + + + + + + + + + + + + + + + + +
                                          FlagDefaultTypeDescription
                                          --jsonfalseBooleanWhether or not to output JSON data, rather than the normal output. * In npm pkg set it enables parsing set values with JSON.parse() before saving them to your package.json. Not supported by all npm commands.
                                          --registry"https://registry.npmjs.org/"URLThe base URL of the npm registry.
                                          +

                                          See Also

                                          +
                                          + + +
                                          + + + + \ No newline at end of file diff --git a/deps/npm/docs/output/commands/npm-star.html b/deps/npm/docs/output/commands/npm-star.html index 977756cc679403..5ecd1df01d01d2 100644 --- a/deps/npm/docs/output/commands/npm-star.html +++ b/deps/npm/docs/output/commands/npm-star.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-star - @11.13.0 + @11.16.0

                                          Mark your favorite packages
                                          diff --git a/deps/npm/docs/output/commands/npm-stars.html b/deps/npm/docs/output/commands/npm-stars.html index 66164fa7780b94..d4e8bb1ae9764d 100644 --- a/deps/npm/docs/output/commands/npm-stars.html +++ b/deps/npm/docs/output/commands/npm-stars.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-stars - @11.13.0 + @11.16.0

                                          View packages marked as favorites
                                          diff --git a/deps/npm/docs/output/commands/npm-start.html b/deps/npm/docs/output/commands/npm-start.html index c5079ac37a31da..bcc5463e6ddfe3 100644 --- a/deps/npm/docs/output/commands/npm-start.html +++ b/deps/npm/docs/output/commands/npm-start.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-start - @11.13.0 + @11.16.0

                                          Start a package
                                          diff --git a/deps/npm/docs/output/commands/npm-stop.html b/deps/npm/docs/output/commands/npm-stop.html index 92500df777600a..abbb05aa873c1d 100644 --- a/deps/npm/docs/output/commands/npm-stop.html +++ b/deps/npm/docs/output/commands/npm-stop.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-stop - @11.13.0 + @11.16.0

                                          Stop a package
                                          diff --git a/deps/npm/docs/output/commands/npm-team.html b/deps/npm/docs/output/commands/npm-team.html index f83bbe0ee6bbe3..a1d0941e2e541b 100644 --- a/deps/npm/docs/output/commands/npm-team.html +++ b/deps/npm/docs/output/commands/npm-team.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-team - @11.13.0 + @11.16.0

                                          Manage organization teams and team memberships
                                          diff --git a/deps/npm/docs/output/commands/npm-test.html b/deps/npm/docs/output/commands/npm-test.html index 9466dd432374db..d5fb0a1ded18a7 100644 --- a/deps/npm/docs/output/commands/npm-test.html +++ b/deps/npm/docs/output/commands/npm-test.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-test - @11.13.0 + @11.16.0

                                          Test a package
                                          diff --git a/deps/npm/docs/output/commands/npm-token.html b/deps/npm/docs/output/commands/npm-token.html index 7b956c64d1d11e..10163668153233 100644 --- a/deps/npm/docs/output/commands/npm-token.html +++ b/deps/npm/docs/output/commands/npm-token.html @@ -186,9 +186,9 @@
                                          -

                                          +

                                          npm-token - @11.13.0 + @11.16.0

                                          Manage your authentication tokens
                                          diff --git a/deps/npm/docs/output/commands/npm-trust.html b/deps/npm/docs/output/commands/npm-trust.html index d2f7cfb44c024a..e269490efdff39 100644 --- a/deps/npm/docs/output/commands/npm-trust.html +++ b/deps/npm/docs/output/commands/npm-trust.html @@ -186,16 +186,16 @@
                                          -

                                          +

                                          npm-trust - @11.13.0 + @11.16.0

                                          Manage trusted publishing relationships between packages and CI/CD providers

                                          Table of contents

                                          - +

                                          Synopsis

                                          @@ -214,6 +214,14 @@

                                          Description

                                          For a comprehensive overview of trusted publishing, see the npm trusted publishers documentation.

                                          The [package] argument specifies the package name. If omitted, npm will use the name from the package.json in the current directory.

                                          Each trust relationship has its own set of configuration options and flags based on the OIDC claims provided by that provider. OIDC claims come from the CI/CD provider and include information such as repository name, workflow file, or environment. Since each provider's claims differ, the available flags and configuration keys are not universal—npm matches the claims supported by each provider's OIDC configuration. For specific details on which claims and flags are supported for a given provider, use npm trust <provider> --help.

                                          +

                                          Permissions

                                          +

                                          When creating a trust relationship, you must specify at least one permission flag to indicate which operations the trusted publisher is allowed to perform:

                                          +
                                            +
                                          • --allow-publish: Allows the trusted publisher to run npm publish for the package.
                                          • +
                                          • --allow-stage-publish: Allows the trusted publisher to run npm stage for the package. The alias --allow-staged-publish is also accepted.
                                          • +
                                          +

                                          At least one of these flags is required when creating a trust configuration. You can specify both to grant both permissions.

                                          +

                                          Provider Options

                                          The required options depend on the CI/CD provider you're configuring. Detailed information about each option is available in the managing trusted publisher configurations section of the npm documentation. If a provider is repository-based and the option is not provided, npm will use the repository.url field from your package.json, if available.

                                          Currently, the registry only supports one configuration per package. If you attempt to create a new trust relationship when one already exists, it will result in an error. To replace an existing configuration:

                                            @@ -229,7 +237,7 @@

                                            Configuration

                                            npm trust github

                                            Create a trusted relationship between a package and GitHub Actions

                                            Synopsis

                                            -
                                            npm trust github [package] --file [--repo|--repository] [--env|--environment] [-y|--yes]
                                            +
                                            npm trust github [package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]
                                             

                                            Flags

                                            @@ -261,6 +269,18 @@

                                            Flags

                                            + + + + + + + + + + + + @@ -289,7 +309,7 @@

                                            Flags

                                            npm trust gitlab

                                            Create a trusted relationship between a package and GitLab CI/CD

                                            Synopsis

                                            -
                                            npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes]
                                            +
                                            npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]
                                             

                                            Flags

                                            CI environment name
                                            --allow-publishfalseBooleanAllow npm publish for this trusted publisher configuration
                                            --allow-stage-publish, --allow-staged-publishfalseBooleanAllow npm stage publish for this trusted publisher configuration
                                            --dry-run false Boolean
                                            @@ -321,6 +341,18 @@

                                            Flags

                                            + + + + + + + + + + + + @@ -349,7 +381,7 @@

                                            Flags

                                            npm trust circleci

                                            Create a trusted relationship between a package and CircleCI

                                            Synopsis

                                            -
                                            npm trust circleci [package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [-y|--yes]
                                            +
                                            npm trust circleci [package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [--allow-publish] [--allow-stage-publish] [-y|--yes]
                                             

                                            Flags

                                            CI environment name
                                            --allow-publishfalseBooleanAllow npm publish for this trusted publisher configuration
                                            --allow-stage-publish, --allow-staged-publishfalseBooleanAllow npm stage publish for this trusted publisher configuration
                                            --dry-run false Boolean
                                            @@ -393,6 +425,18 @@

                                            Flags

                                            + + + + + + + + + + + + diff --git a/deps/npm/docs/output/commands/npm-undeprecate.html b/deps/npm/docs/output/commands/npm-undeprecate.html index f4a4530423d3f2..45fcb65deed0ac 100644 --- a/deps/npm/docs/output/commands/npm-undeprecate.html +++ b/deps/npm/docs/output/commands/npm-undeprecate.html @@ -186,9 +186,9 @@
                                            -

                                            +

                                            npm-undeprecate - @11.13.0 + @11.16.0

                                            Undeprecate a version of a package
                                            diff --git a/deps/npm/docs/output/commands/npm-uninstall.html b/deps/npm/docs/output/commands/npm-uninstall.html index dbad4b89328bfa..bdeb05ee1b30f8 100644 --- a/deps/npm/docs/output/commands/npm-uninstall.html +++ b/deps/npm/docs/output/commands/npm-uninstall.html @@ -186,9 +186,9 @@
                                            -

                                            +

                                            npm-uninstall - @11.13.0 + @11.16.0

                                            Remove a package
                                            diff --git a/deps/npm/docs/output/commands/npm-unpublish.html b/deps/npm/docs/output/commands/npm-unpublish.html index 2437c08a3d38b1..eec4aee8c603e7 100644 --- a/deps/npm/docs/output/commands/npm-unpublish.html +++ b/deps/npm/docs/output/commands/npm-unpublish.html @@ -186,9 +186,9 @@
                                            -

                                            +

                                            npm-unpublish - @11.13.0 + @11.16.0

                                            Remove a package from the registry
                                            diff --git a/deps/npm/docs/output/commands/npm-unstar.html b/deps/npm/docs/output/commands/npm-unstar.html index 78b27c53c751dc..7d3d40d29928a3 100644 --- a/deps/npm/docs/output/commands/npm-unstar.html +++ b/deps/npm/docs/output/commands/npm-unstar.html @@ -186,9 +186,9 @@
                                            -

                                            +

                                            npm-unstar - @11.13.0 + @11.16.0

                                            Remove an item from your favorite packages
                                            diff --git a/deps/npm/docs/output/commands/npm-update.html b/deps/npm/docs/output/commands/npm-update.html index 9396c8a6834ed1..1558e84ce06729 100644 --- a/deps/npm/docs/output/commands/npm-update.html +++ b/deps/npm/docs/output/commands/npm-update.html @@ -186,16 +186,16 @@
                                            -

                                            +

                                            npm-update - @11.13.0 + @11.16.0

                                            Update packages

                                            Table of contents

                                            - +

                                            Synopsis

                                            @@ -407,6 +407,44 @@

                                            ignore-scripts

                                            npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

                                            +

                                            allow-scripts

                                            +
                                              +
                                            • Default: ""
                                            • +
                                            • Type: String (can be set multiple times)
                                            • +
                                            +

                                            Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

                                            +

                                            This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

                                            +

                                            Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

                                            +

                                            strict-allow-scripts

                                            +
                                              +
                                            • Default: false
                                            • +
                                            • Type: Boolean
                                            • +
                                            +

                                            If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

                                            +

                                            Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

                                            +

                                            dangerously-allow-all-scripts

                                            +
                                              +
                                            • Default: false
                                            • +
                                            • Type: Boolean
                                            • +
                                            +

                                            If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

                                            audit

                                            • Default: true
                                            • @@ -429,7 +467,11 @@

                                              before

                                              --before filter, the most recent version less than or equal to that tag will be used. For example, foo@latest might install foo@1.2 even though latest is 2.0.

                                              -

                                              This config cannot be used with: min-release-age

                                              +

                                              If before and min-release-age are both set in the same source, before +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one.

                                              min-release-age

                                              • Default: null
                                              • @@ -440,8 +482,11 @@

                                                min-release-age

                                                are no versions available for the current set of dependencies, the command will error.

                                                This flag is a complement to before, which accepts an exact date instead -of a relative number of days.

                                                -

                                                This config cannot be used with: before

                                                +of a relative number of days. The two may coexist (e.g. min-release-age in +your .npmrc is preserved when npm internally spawns a sub-process with +--before while preparing a git: or github: dependency); when both +apply, before wins within a single source and across sources the standard +precedence rules apply.

                                                This value is not exported to the environment for child processes.

                                                  diff --git a/deps/npm/docs/output/commands/npm-version.html b/deps/npm/docs/output/commands/npm-version.html index f80e5161e98b82..4deac0758f8d5d 100644 --- a/deps/npm/docs/output/commands/npm-version.html +++ b/deps/npm/docs/output/commands/npm-version.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  npm-version - @11.13.0 + @11.16.0

                                                  Bump a package version
                                                  @@ -367,6 +367,7 @@

                                                  Description

                                                • Run the postversion script. Use it to clean up the file system or automatically push the commit and/or tag.
                                                • +

                                                  For the preversion, version and postversion scripts, npm also sets the environment variables npm_old_version and npm_new_version.

                                                  Take the following example:

                                                  {
                                                     "scripts": {
                                                  diff --git a/deps/npm/docs/output/commands/npm-view.html b/deps/npm/docs/output/commands/npm-view.html
                                                  index 2636be46817ead..71fac734e58daa 100644
                                                  --- a/deps/npm/docs/output/commands/npm-view.html
                                                  +++ b/deps/npm/docs/output/commands/npm-view.html
                                                  @@ -186,9 +186,9 @@
                                                   
                                                   
                                                  -

                                                  +

                                                  npm-view - @11.13.0 + @11.16.0

                                                  View registry info
                                                  diff --git a/deps/npm/docs/output/commands/npm-whoami.html b/deps/npm/docs/output/commands/npm-whoami.html index 2b456c4450622d..f92bd67d1f5d55 100644 --- a/deps/npm/docs/output/commands/npm-whoami.html +++ b/deps/npm/docs/output/commands/npm-whoami.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  npm-whoami - @11.13.0 + @11.16.0

                                                  Display npm username
                                                  diff --git a/deps/npm/docs/output/commands/npm.html b/deps/npm/docs/output/commands/npm.html index d9807c608a8289..52befd0760479b 100644 --- a/deps/npm/docs/output/commands/npm.html +++ b/deps/npm/docs/output/commands/npm.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  npm - @11.13.0 + @11.16.0

                                                  javascript package manager
                                                  @@ -203,7 +203,7 @@

                                                  Table of contents

                                                  Note: This command is unaware of workspaces.

                                                  Version

                                                  -

                                                  11.13.0

                                                  +

                                                  11.16.0

                                                  Description

                                                  npm is the package manager for the Node JavaScript platform. It puts modules in place so that node can find them, and manages dependency conflicts intelligently.

                                                  diff --git a/deps/npm/docs/output/commands/npx.html b/deps/npm/docs/output/commands/npx.html index ce14ad68a0a9a9..5786f4332f3b6f 100644 --- a/deps/npm/docs/output/commands/npx.html +++ b/deps/npm/docs/output/commands/npx.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  npx - @11.13.0 + @11.16.0

                                                  Run a command from a local or remote npm package
                                                  diff --git a/deps/npm/docs/output/configuring-npm/folders.html b/deps/npm/docs/output/configuring-npm/folders.html index 2ebf3eb7ee4ea0..c88270a3799930 100644 --- a/deps/npm/docs/output/configuring-npm/folders.html +++ b/deps/npm/docs/output/configuring-npm/folders.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  Folders - @11.13.0 + @11.16.0

                                                  Folder structures used by npm
                                                  diff --git a/deps/npm/docs/output/configuring-npm/install.html b/deps/npm/docs/output/configuring-npm/install.html index 1c928d1e45ad9b..cb308af4d962a9 100644 --- a/deps/npm/docs/output/configuring-npm/install.html +++ b/deps/npm/docs/output/configuring-npm/install.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  Install - @11.13.0 + @11.16.0

                                                  Download and install node and npm
                                                  diff --git a/deps/npm/docs/output/configuring-npm/npm-global.html b/deps/npm/docs/output/configuring-npm/npm-global.html index 2ebf3eb7ee4ea0..c88270a3799930 100644 --- a/deps/npm/docs/output/configuring-npm/npm-global.html +++ b/deps/npm/docs/output/configuring-npm/npm-global.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  Folders - @11.13.0 + @11.16.0

                                                  Folder structures used by npm
                                                  diff --git a/deps/npm/docs/output/configuring-npm/npm-json.html b/deps/npm/docs/output/configuring-npm/npm-json.html index b7b4f7545043b0..057c119042240f 100644 --- a/deps/npm/docs/output/configuring-npm/npm-json.html +++ b/deps/npm/docs/output/configuring-npm/npm-json.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  package.json - @11.13.0 + @11.16.0

                                                  Specifics of npm's package.json handling
                                                  diff --git a/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html b/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html index 5205dd37e7dbae..2c585ff171c2c7 100644 --- a/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html +++ b/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  npm-shrinkwrap.json - @11.13.0 + @11.16.0

                                                  A publishable lockfile
                                                  diff --git a/deps/npm/docs/output/configuring-npm/npmrc.html b/deps/npm/docs/output/configuring-npm/npmrc.html index 0980887d978241..c90a8b19c6be09 100644 --- a/deps/npm/docs/output/configuring-npm/npmrc.html +++ b/deps/npm/docs/output/configuring-npm/npmrc.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  .npmrc - @11.13.0 + @11.16.0

                                                  The npm config files
                                                  diff --git a/deps/npm/docs/output/configuring-npm/package-json.html b/deps/npm/docs/output/configuring-npm/package-json.html index b7b4f7545043b0..057c119042240f 100644 --- a/deps/npm/docs/output/configuring-npm/package-json.html +++ b/deps/npm/docs/output/configuring-npm/package-json.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  package.json - @11.13.0 + @11.16.0

                                                  Specifics of npm's package.json handling
                                                  diff --git a/deps/npm/docs/output/configuring-npm/package-lock-json.html b/deps/npm/docs/output/configuring-npm/package-lock-json.html index 9cefaff6c908e1..64a2dbb13601d2 100644 --- a/deps/npm/docs/output/configuring-npm/package-lock-json.html +++ b/deps/npm/docs/output/configuring-npm/package-lock-json.html @@ -186,9 +186,9 @@
                                                  -

                                                  +

                                                  package-lock.json - @11.13.0 + @11.16.0

                                                  A manifestation of the manifest
                                                  diff --git a/deps/npm/docs/output/using-npm/config.html b/deps/npm/docs/output/using-npm/config.html index 83655e009e7117..687d077639eda6 100644 --- a/deps/npm/docs/output/using-npm/config.html +++ b/deps/npm/docs/output/using-npm/config.html @@ -186,16 +186,16 @@
                                                  -

                                                  +

                                                  Config - @11.13.0 + @11.16.0

                                                  About npm configuration

                                                  Table of contents

                                                  -
                                                  +

                                                  Description

                                                  @@ -307,7 +307,7 @@

                                                  access

                                                  • Default: 'public' for new packages, existing packages it will not change the current level
                                                  • -
                                                  • Type: null, "restricted", or "public"
                                                  • +
                                                  • Type: null, "restricted", "public", or "private"

                                                  If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted.

                                                  @@ -315,6 +315,7 @@

                                                  access

                                                  Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would.

                                                  +

                                                  The value private is an alias for restricted.

                                                  all

                                                  • Default: false
                                                  • @@ -323,6 +324,34 @@

                                                    all

                                                    When running npm outdated and npm ls, setting --all will show all outdated or installed packages, rather than only those directly depended upon by the current project.

                                                    +

                                                    allow-directory

                                                    +
                                                      +
                                                    • Default: "all"
                                                    • +
                                                    • Type: "all", "none", or "root"
                                                    • +
                                                    +

                                                    Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                                                    +

                                                    all allows any directories to be installed. none prevents any +directories from being installed. root only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like npm view

                                                    +

                                                    allow-file

                                                    +
                                                      +
                                                    • Default: "all"
                                                    • +
                                                    • Type: "all", "none", or "root"
                                                    • +
                                                    +

                                                    Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed.

                                                    +

                                                    all allows any tarball file to be installed. none prevents any tarball +file from being installed. root only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like npm view

                                                    allow-git

                                                    • Default: "all"
                                                    • @@ -331,11 +360,26 @@

                                                      allow-git

                                                      Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed.

                                                      +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                                                      all allows any git dependencies to be fetched and installed. none prevents any git dependencies from being fetched and installed. root only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like npm view

                                                      +

                                                      allow-remote

                                                      +
                                                        +
                                                      • Default: "all"
                                                      • +
                                                      • Type: "all", "none", or "root"
                                                      • +
                                                      +

                                                      Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed.

                                                      +

                                                      all allows any url to be installed. none prevents any url from being +installed. root only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like npm view

                                                      allow-same-version

                                                        @@ -344,6 +388,40 @@

                                                        allow-same-version

                                                      Prevents throwing an error when npm version is used to set the new version to the same value as the current version.

                                                      +

                                                      allow-scripts

                                                      +
                                                        +
                                                      • Default: ""
                                                      • +
                                                      • Type: String (can be set multiple times)
                                                      • +
                                                      +

                                                      Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

                                                      +

                                                      This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

                                                      +

                                                      Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

                                                      +

                                                      allow-scripts-pending

                                                      +
                                                        +
                                                      • Default: false
                                                      • +
                                                      • Type: Boolean
                                                      • +
                                                      +

                                                      List packages with install scripts that are not yet covered by the +allowScripts policy, without modifying package.json. Only meaningful for +npm approve-scripts.

                                                      +

                                                      allow-scripts-pin

                                                      +
                                                        +
                                                      • Default: true
                                                      • +
                                                      • Type: Boolean
                                                      • +
                                                      +

                                                      Write pinned (pkg@version) entries when approving install scripts. Set to +false to write name-only entries that allow any version. Has no effect on +npm deny-scripts, which always writes name-only entries regardless of this +setting.

                                                      audit

                                                      • Default: true
                                                      • @@ -380,7 +458,11 @@

                                                        before

                                                        --before filter, the most recent version less than or equal to that tag will be used. For example, foo@latest might install foo@1.2 even though latest is 2.0.

                                                        -

                                                        This config cannot be used with: min-release-age

                                                        +

                                                        If before and min-release-age are both set in the same source, before +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one.

                                                        • Default: true
                                                        • @@ -476,6 +558,15 @@

                                                          cpu

                                                        Override CPU architecture of native modules to install. Acceptable values are same as cpu field of package.json, which comes from process.arch.

                                                        +

                                                        dangerously-allow-all-scripts

                                                        +
                                                          +
                                                        • Default: false
                                                        • +
                                                        • Type: Boolean
                                                        • +
                                                        +

                                                        If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

                                                        depth

                                                        • Default: Infinity if --all is set; otherwise, 0
                                                        • @@ -990,8 +1081,11 @@

                                                          min-release-age

                                                          are no versions available for the current set of dependencies, the command will error.

                                                          This flag is a complement to before, which accepts an exact date instead -of a relative number of days.

                                                          -

                                                          This config cannot be used with: before

                                                          +of a relative number of days. The two may coexist (e.g. min-release-age in +your .npmrc is preserved when npm internally spawns a sub-process with +--before while preparing a git: or github: dependency); when both +apply, before wins within a single source and across sources the standard +precedence rules apply.

                                                          This value is not exported to the environment for child processes.

                                                          name

                                                            @@ -1419,6 +1513,18 @@

                                                            sign-git-tag

                                                            -s to add a signature.

                                                            Note that git requires you to have set up GPG keys in your git configs for this to work properly.

                                                            +

                                                            strict-allow-scripts

                                                            +
                                                              +
                                                            • Default: false
                                                            • +
                                                            • Type: Boolean
                                                            • +
                                                            +

                                                            If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

                                                            +

                                                            Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

                                                            strict-peer-deps

                                                            • Default: false
                                                            • diff --git a/deps/npm/docs/output/using-npm/dependency-selectors.html b/deps/npm/docs/output/using-npm/dependency-selectors.html index 35ad12b72bacd2..da260de09888ec 100644 --- a/deps/npm/docs/output/using-npm/dependency-selectors.html +++ b/deps/npm/docs/output/using-npm/dependency-selectors.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Dependency Selectors - @11.13.0 + @11.16.0

                                                              Dependency Selector Syntax & Querying
                                                              diff --git a/deps/npm/docs/output/using-npm/developers.html b/deps/npm/docs/output/using-npm/developers.html index 45ef5924245796..9f8825dccd2dda 100644 --- a/deps/npm/docs/output/using-npm/developers.html +++ b/deps/npm/docs/output/using-npm/developers.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Developers - @11.13.0 + @11.16.0

                                                              Developer guide
                                                              diff --git a/deps/npm/docs/output/using-npm/logging.html b/deps/npm/docs/output/using-npm/logging.html index c34d3d7c8f32fd..675c116ed70c64 100644 --- a/deps/npm/docs/output/using-npm/logging.html +++ b/deps/npm/docs/output/using-npm/logging.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Logging - @11.13.0 + @11.16.0

                                                              Why, What & How we Log
                                                              diff --git a/deps/npm/docs/output/using-npm/orgs.html b/deps/npm/docs/output/using-npm/orgs.html index 7beb2e2cd5e0c2..4da8761b61ffca 100644 --- a/deps/npm/docs/output/using-npm/orgs.html +++ b/deps/npm/docs/output/using-npm/orgs.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Organizations - @11.13.0 + @11.16.0

                                                              Working with teams & organizations
                                                              diff --git a/deps/npm/docs/output/using-npm/package-spec.html b/deps/npm/docs/output/using-npm/package-spec.html index e75963512474ca..b682f1889687a2 100644 --- a/deps/npm/docs/output/using-npm/package-spec.html +++ b/deps/npm/docs/output/using-npm/package-spec.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Package spec - @11.13.0 + @11.16.0

                                                              Package name specifier
                                                              diff --git a/deps/npm/docs/output/using-npm/registry.html b/deps/npm/docs/output/using-npm/registry.html index 54d3618031c5c3..e6efaed669f708 100644 --- a/deps/npm/docs/output/using-npm/registry.html +++ b/deps/npm/docs/output/using-npm/registry.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Registry - @11.13.0 + @11.16.0

                                                              The JavaScript Package Registry
                                                              diff --git a/deps/npm/docs/output/using-npm/removal.html b/deps/npm/docs/output/using-npm/removal.html index 068f758e2e872f..0d58d278fa6f8f 100644 --- a/deps/npm/docs/output/using-npm/removal.html +++ b/deps/npm/docs/output/using-npm/removal.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Removal - @11.13.0 + @11.16.0

                                                              Cleaning the slate
                                                              diff --git a/deps/npm/docs/output/using-npm/scope.html b/deps/npm/docs/output/using-npm/scope.html index 26a0718f94a470..4004c513323c3b 100644 --- a/deps/npm/docs/output/using-npm/scope.html +++ b/deps/npm/docs/output/using-npm/scope.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Scope - @11.13.0 + @11.16.0

                                                              Scoped packages
                                                              diff --git a/deps/npm/docs/output/using-npm/scripts.html b/deps/npm/docs/output/using-npm/scripts.html index dc8aed1c724c2c..15ca8072c3450f 100644 --- a/deps/npm/docs/output/using-npm/scripts.html +++ b/deps/npm/docs/output/using-npm/scripts.html @@ -186,16 +186,16 @@
                                                              -

                                                              +

                                                              Scripts - @11.13.0 + @11.16.0

                                                              How npm handles the "scripts" field

                                                              Table of contents

                                                              - +

                                                              Description

                                                              @@ -459,6 +459,12 @@

                                                              package.json vars

                                                              For example, if you had {"name":"foo", "version":"1.2.5"} in your package.json file, then your package scripts would have the npm_package_name environment variable set to "foo", and the npm_package_version set to "1.2.5". You can access these variables in your code with process.env.npm_package_name and process.env.npm_package_version.

                                                              Note: In npm 7 and later, most package.json fields are no longer provided as environment variables. Scripts that need access to other package.json fields should read the package.json file directly. The npm_package_json environment variable provides the path to the file for this purpose.

                                                              See package.json for more on package configs.

                                                              +

                                                              versioning variables

                                                              +

                                                              For versioning scripts (preversion, version, postversion), npm sets these environment variables:

                                                              +
                                                                +
                                                              • npm_old_version - The version before being bumped
                                                              • +
                                                              • npm_new_version – The version after being bumped
                                                              • +

                                                              current lifecycle event

                                                              Lastly, the npm_lifecycle_event environment variable is set to whichever stage of the cycle is being executed. So, you could have a single script used for different parts of the process which switches based on what's currently happening.

                                                              diff --git a/deps/npm/docs/output/using-npm/workspaces.html b/deps/npm/docs/output/using-npm/workspaces.html index af84abf262b3db..a544b68ce46c23 100644 --- a/deps/npm/docs/output/using-npm/workspaces.html +++ b/deps/npm/docs/output/using-npm/workspaces.html @@ -186,9 +186,9 @@
                                                              -

                                                              +

                                                              Workspaces - @11.13.0 + @11.16.0

                                                              Working with workspaces
                                                              diff --git a/deps/npm/lib/commands/approve-scripts.js b/deps/npm/lib/commands/approve-scripts.js new file mode 100644 index 00000000000000..929c692112f16c --- /dev/null +++ b/deps/npm/lib/commands/approve-scripts.js @@ -0,0 +1,10 @@ +const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js') + +class ApproveScripts extends AllowScriptsCmd { + static description = 'Approve install scripts for specific dependencies' + static name = 'approve-scripts' + static usage = [' [ ...]', '--all', '--allow-scripts-pending'] + static verb = 'approve' +} + +module.exports = ApproveScripts diff --git a/deps/npm/lib/commands/ci.js b/deps/npm/lib/commands/ci.js index f6c97aea30f70a..e82438543295a1 100644 --- a/deps/npm/lib/commands/ci.js +++ b/deps/npm/lib/commands/ci.js @@ -1,4 +1,6 @@ const reifyFinish = require('../utils/reify-finish.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') const runScript = require('@npmcli/run-script') const fs = require('node:fs/promises') const path = require('node:path') @@ -21,7 +23,13 @@ class CI extends ArboristWorkspaceCmd { 'strict-peer-deps', 'foreground-scripts', 'ignore-scripts', + 'allow-directory', + 'allow-file', 'allow-git', + 'allow-remote', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', 'audit', 'bin-links', 'fund', @@ -40,12 +48,14 @@ class CI extends ArboristWorkspaceCmd { const ignoreScripts = this.npm.config.get('ignore-scripts') const where = this.npm.prefix const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const opts = { ...this.npm.flatOptions, packageLock: true, // npm ci should never skip lock files path: where, save: false, // npm ci should never modify the lockfile or package.json workspaces: this.workspaceNames, + allowScripts: allowScriptsPolicy, } // generate an inventory from the virtual tree in the lockfile @@ -66,6 +76,7 @@ class CI extends ArboristWorkspaceCmd { // We need a new one because the virtual tree fromt the lockfile can have extraneous dependencies in it that won't install on this platform const arb = new Arborist(opts) await arb.buildIdealTree() + await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: opts }) // Verifies that the packages from the ideal tree will match the same versions that are present in the virtual tree (lock file). const errors = validateLockfile(virtualInventory, arb.idealTree.inventory) diff --git a/deps/npm/lib/commands/config.js b/deps/npm/lib/commands/config.js index 015850c48304a6..0a8b84aba2666d 100644 --- a/deps/npm/lib/commands/config.js +++ b/deps/npm/lib/commands/config.js @@ -5,7 +5,7 @@ const { EOL } = require('node:os') const localeCompare = require('@isaacs/string-locale-compare')('en') const pkgJson = require('@npmcli/package-json') const { defaults, definitions, nerfDarts, proxyEnv } = require('@npmcli/config/lib/definitions') -const { log, output } = require('proc-log') +const { log, output, input } = require('proc-log') const BaseCommand = require('../base-cmd.js') const { redact } = require('@npmcli/redact') @@ -266,7 +266,7 @@ ${defData} `.split('\n').join(EOL) await mkdir(dirname(file), { recursive: true }) await writeFile(file, tmpData, 'utf8') - await new Promise((res, rej) => { + await input.start(() => new Promise((res, rej) => { const [bin, ...args] = e.split(/\s+/) const editor = spawn(bin, [...args, file], { stdio: 'inherit' }) editor.on('exit', (code) => { @@ -275,7 +275,7 @@ ${defData} } return res() }) - }) + })) } async fix () { diff --git a/deps/npm/lib/commands/dedupe.js b/deps/npm/lib/commands/dedupe.js index a931cabd646043..347031b60a78a6 100644 --- a/deps/npm/lib/commands/dedupe.js +++ b/deps/npm/lib/commands/dedupe.js @@ -14,7 +14,10 @@ class Dedupe extends ArboristWorkspaceCmd { 'omit', 'include', 'ignore-scripts', + 'allow-directory', + 'allow-file', 'allow-git', + 'allow-remote', 'audit', 'bin-links', 'fund', diff --git a/deps/npm/lib/commands/deny-scripts.js b/deps/npm/lib/commands/deny-scripts.js new file mode 100644 index 00000000000000..53b0cdd3cc50a6 --- /dev/null +++ b/deps/npm/lib/commands/deny-scripts.js @@ -0,0 +1,10 @@ +const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js') + +class DenyScripts extends AllowScriptsCmd { + static description = 'Deny install scripts for specific dependencies' + static name = 'deny-scripts' + static usage = [' [ ...]', '--all'] + static verb = 'deny' +} + +module.exports = DenyScripts diff --git a/deps/npm/lib/commands/edit.js b/deps/npm/lib/commands/edit.js index 1140c59efa3e40..0b1a200264d982 100644 --- a/deps/npm/lib/commands/edit.js +++ b/deps/npm/lib/commands/edit.js @@ -1,6 +1,7 @@ const { resolve } = require('node:path') const { lstat } = require('node:fs/promises') const cp = require('node:child_process') +const { input } = require('proc-log') const completion = require('../utils/installed-shallow.js') const BaseCommand = require('../base-cmd.js') @@ -46,16 +47,17 @@ class Edit extends BaseCommand { const dir = resolve(this.npm.dir, path) await lstat(dir) - await new Promise((res, rej) => { + await input.start(() => new Promise((res, rej) => { const [bin, ...spawnArgs] = this.npm.config.get('editor').split(/\s+/) const editor = cp.spawn(bin, [...spawnArgs, dir], { stdio: 'inherit' }) - editor.on('exit', async (code) => { + editor.on('exit', (code) => { if (code) { return rej(new Error(`editor process exited with code: ${code}`)) } - await this.npm.exec('rebuild', [dir]).then(res).catch(rej) + res() }) - }) + })) + await this.npm.exec('rebuild', [dir]) } } diff --git a/deps/npm/lib/commands/exec.js b/deps/npm/lib/commands/exec.js index 5b1d117889a1ee..23c47a0cc1ad77 100644 --- a/deps/npm/lib/commands/exec.js +++ b/deps/npm/lib/commands/exec.js @@ -1,5 +1,6 @@ const { resolve } = require('node:path') const libexec = require('libnpmexec') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') const BaseCommand = require('../base-cmd.js') class Exec extends BaseCommand { @@ -10,6 +11,9 @@ class Exec extends BaseCommand { 'workspace', 'workspaces', 'include-workspace-root', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', ] static name = 'exec' @@ -74,8 +78,16 @@ class Exec extends BaseCommand { throw this.usageError() } + // Resolve the install-script policy from the user/global .npmrc layer + // only. The RFC requires exec/npx to ignore any project + // package.json#allowScripts; CLI flags still apply. + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm, { + skipProjectConfig: true, + }) + return libexec({ ...flatOptions, + allowScripts: allowScriptsPolicy, // we explicitly set packageLockOnly to false because if it's true when we try to install a missing package, we won't actually install it packageLockOnly: false, // what the user asked to run args[0] is run by default diff --git a/deps/npm/lib/commands/install.js b/deps/npm/lib/commands/install.js index 5970fddfdfe4fa..0bc3591d4af731 100644 --- a/deps/npm/lib/commands/install.js +++ b/deps/npm/lib/commands/install.js @@ -5,6 +5,8 @@ const runScript = require('@npmcli/run-script') const pacote = require('pacote') const checks = require('npm-install-checks') const reifyFinish = require('../utils/reify-finish.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Install extends ArboristWorkspaceCmd { @@ -27,7 +29,13 @@ class Install extends ArboristWorkspaceCmd { 'package-lock-only', 'foreground-scripts', 'ignore-scripts', + 'allow-directory', + 'allow-file', 'allow-git', + 'allow-remote', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', 'audit', 'before', 'min-release-age', @@ -135,14 +143,17 @@ class Install extends ArboristWorkspaceCmd { } const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const opts = { ...this.npm.flatOptions, auditLevel: null, path: where, add: args, workspaces: this.workspaceNames, + allowScripts: allowScriptsPolicy, } const arb = new Arborist(opts) + await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: opts }) await arb.reify(opts) if (!args.length && !isGlobalInstall && !ignoreScripts) { diff --git a/deps/npm/lib/commands/link.js b/deps/npm/lib/commands/link.js index e166a0051299a7..ca656ad18f5ca0 100644 --- a/deps/npm/lib/commands/link.js +++ b/deps/npm/lib/commands/link.js @@ -25,7 +25,10 @@ class Link extends ArboristWorkspaceCmd { 'omit', 'include', 'ignore-scripts', + 'allow-directory', + 'allow-file', 'allow-git', + 'allow-remote', 'audit', 'bin-links', 'fund', diff --git a/deps/npm/lib/commands/outdated.js b/deps/npm/lib/commands/outdated.js index 9140cdbc9fea51..882ad2cc9d28a2 100644 --- a/deps/npm/lib/commands/outdated.js +++ b/deps/npm/lib/commands/outdated.js @@ -31,6 +31,7 @@ class Outdated extends ArboristWorkspaceCmd { 'global', 'workspace', 'before', + 'min-release-age', ] #tree diff --git a/deps/npm/lib/commands/publish.js b/deps/npm/lib/commands/publish.js index 98478ae1e95f1d..450c51858ba017 100644 --- a/deps/npm/lib/commands/publish.js +++ b/deps/npm/lib/commands/publish.js @@ -1,4 +1,4 @@ -const { log, output } = require('proc-log') +const { log, output, META } = require('proc-log') const semver = require('semver') const pack = require('libnpmpack') const libpub = require('libnpmpublish').publish @@ -14,11 +14,17 @@ const { getContents, logTar } = require('../utils/tar.js') const { flatten } = require('@npmcli/config/lib/definitions') const pkgJson = require('@npmcli/package-json') const BaseCommand = require('../base-cmd.js') -const { oidc } = require('../../lib/utils/oidc.js') +const { oidc } = require('../utils/oidc.js') class Publish extends BaseCommand { static description = 'Publish a package' static name = 'publish' + static stage = false + + get isStage () { + return this.constructor.stage + } + static params = [ 'tag', 'access', @@ -60,13 +66,17 @@ class Publish extends BaseCommand { if (err.code !== 'EPRIVATE') { throw err } - log.warn('publish', `Skipping workspace ${this.npm.chalk.cyan(name)}, marked as ${this.npm.chalk.bold('private')}`) + log.warn(this.#command, `Skipping workspace ${this.npm.chalk.cyan(name)}, marked as ${this.npm.chalk.bold('private')}`) } } } + get #command () { + return this.isStage ? 'stage' : 'publish' + } + async #publish (args, { workspace } = {}) { - log.verbose('publish', replaceInfo(args)) + log.verbose(this.#command, replaceInfo(args)) const unicode = this.npm.config.get('unicode') const dryRun = this.npm.config.get('dry-run') @@ -138,7 +148,6 @@ class Publish extends BaseCommand { const noCreds = !(creds.token || creds.username || creds.certfile && creds.keyfile) const outputRegistry = replaceInfo(registry) - // if a workspace package is marked private then we skip it if (workspace && manifest.private) { throw Object.assign( new Error(`This package has been marked as private @@ -150,7 +159,7 @@ class Publish extends BaseCommand { if (noCreds) { const msg = `This command requires you to be logged in to ${outputRegistry}` if (dryRun) { - log.warn('', `${msg} (dry-run)`) + log.warn(this.#command, `${msg} (dry-run)`) } else { throw Object.assign(new Error(msg), { code: 'ENEEDAUTH' }) } @@ -171,20 +180,36 @@ class Publish extends BaseCommand { } const access = opts.access === null ? 'default' : opts.access - let msg = `Publishing to ${outputRegistry} with tag ${defaultTag} and ${access} access` + const verb = this.isStage ? 'Staging' : 'Publishing' + let msg = `${verb} to ${outputRegistry} with tag ${defaultTag} and ${access} access` if (dryRun) { msg = `${msg} (dry-run)` } log.notice('', msg) + let stageId if (!dryRun) { - await otplease(this.npm, opts, o => libpub(manifest, tarballData, o)) + if (this.isStage) { + // Stage intentionally bypasses otplease — 2FA is deferred to approve/reject + const res = await libpub(manifest, tarballData, { + ...opts, + command: this.#command, + stage: true, + }) + stageId = res.stageId + } else { + await otplease(this.npm, opts, o => libpub(manifest, tarballData, o)) + } } // In json mode we don't log until the publish has completed as this will add it to the output only if completes successfully if (json) { - logPkg() + if (stageId) { + pkgContents.stageId = stageId + } + logTar(pkgContents, { + unicode, json, key: pkgContents.name, redact: stageId ? false : undefined }) } if (spec.type === 'directory' && !ignoreScripts) { @@ -204,7 +229,15 @@ class Publish extends BaseCommand { } if (!json && !silent) { - output.standard(`+ ${pkgContents.id}`) + if (this.isStage) { + const stagedMsg = stageId + ? `+ ${pkgContents.id} (staged with id ${stageId})` + : `+ ${pkgContents.id} (staged)` + output.standard(stagedMsg, { [META]: true, redact: false }) + log.notice(this.#command, `package ${pkgContents.id} has been staged with tag ${defaultTag}`) + } else { + output.standard(`+ ${pkgContents.id}`) + } } } @@ -245,8 +278,8 @@ class Publish extends BaseCommand { const changes = [] const pkg = await pkgJson.fix(spec.fetchSpec, { changes }) if (changes.length && logWarnings) { - log.warn('publish', 'npm auto-corrected some errors in your package.json when publishing. Please run "npm pkg fix" to address these errors.') - log.warn('publish', `errors corrected:\n${changes.join('\n')}`) + log.warn(this.#command, 'npm auto-corrected some errors in your package.json when publishing. Please run "npm pkg fix" to address these errors.') + log.warn(this.#command, `errors corrected:\n${changes.join('\n')}`) } // Prepare is the special function for publishing, different than normalize const { content } = await pkg.prepare() @@ -254,7 +287,7 @@ class Publish extends BaseCommand { } else { manifest = await pacote.manifest(spec, { ...opts, - fullmetadata: true, + fullMetadata: true, fullReadJson: true, }) } diff --git a/deps/npm/lib/commands/rebuild.js b/deps/npm/lib/commands/rebuild.js index a23df39f1560be..333a879026cbc1 100644 --- a/deps/npm/lib/commands/rebuild.js +++ b/deps/npm/lib/commands/rebuild.js @@ -1,8 +1,11 @@ const { resolve } = require('node:path') -const { output } = require('proc-log') +const { log, output } = require('proc-log') const npa = require('npm-package-arg') const semver = require('semver') const ArboristWorkspaceCmd = require('../arborist-cmd.js') +const checkAllowScripts = require('../utils/check-allow-scripts.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') class Rebuild extends ArboristWorkspaceCmd { static description = 'Rebuild a package' @@ -12,6 +15,9 @@ class Rebuild extends ArboristWorkspaceCmd { 'bin-links', 'foreground-scripts', 'ignore-scripts', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', ...super.params, ] @@ -26,9 +32,11 @@ class Rebuild extends ArboristWorkspaceCmd { const globalTop = resolve(this.npm.globalDir, '..') const where = this.npm.global ? globalTop : this.npm.prefix const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const arb = new Arborist({ ...this.npm.flatOptions, path: where, + allowScripts: allowScriptsPolicy, // TODO when extending ReifyCmd // workspaces: this.workspaceNames, }) @@ -50,11 +58,28 @@ class Rebuild extends ArboristWorkspaceCmd { }) const nodes = tree.inventory.filter(node => this.isNode(specs, node)) + await strictAllowScriptsPreflight({ arb, npm: this.npm }) await arb.rebuild({ nodes }) } else { + await arb.loadActual() + await strictAllowScriptsPreflight({ arb, npm: this.npm }) await arb.rebuild() } + // Phase 1 advisory: list any packages whose install scripts ran (or + // would have run) and are not yet covered by allowScripts. Rebuild + // doesn't go through reifyFinish, so the walker is invoked here. + const unreviewed = await checkAllowScripts({ arb, npm: this.npm }) + if (unreviewed.length > 0) { + const count = unreviewed.length + const noun = count === 1 ? 'package has' : 'packages have' + log.warn( + 'rebuild', + `${count} ${noun} install scripts not yet covered by allowScripts. ` + + 'Run `npm approve-scripts --allow-scripts-pending` to review.' + ) + } + output.standard('rebuilt dependencies successfully') } diff --git a/deps/npm/lib/commands/stage/approve.js b/deps/npm/lib/commands/stage/approve.js new file mode 100644 index 00000000000000..619015d0c8a55e --- /dev/null +++ b/deps/npm/lib/commands/stage/approve.js @@ -0,0 +1,35 @@ +const { log, output, META } = require('proc-log') +const npmFetch = require('npm-registry-fetch') +const { otplease } = require('../../utils/auth.js') +const { validateUUID } = require('../../utils/validate-uuid.js') +const BaseCommand = require('../../base-cmd.js') + +class StageApprove extends BaseCommand { + static description = 'Approve a staged package, publishing it to the npm registry' + static name = 'approve' + static usage = [''] + static params = ['otp', 'registry'] + static positionals = 1 + + async exec (args) { + if (!args[0]) { + throw this.usageError('Missing required ') + } + const stageId = args[0] + validateUUID(stageId, 'stage-id') + const opts = { ...this.npm.flatOptions } + + log.notice('', `Approving staged package ${stageId}`) + + await otplease(this.npm, opts, o => + npmFetch.json(`/-/stage/${stageId}/approve`, { + ...o, + method: 'POST', + }) + ) + + output.standard(`Staged package ${stageId} approved and published successfully.`, { [META]: true, redact: false }) + } +} + +module.exports = StageApprove diff --git a/deps/npm/lib/commands/stage/download.js b/deps/npm/lib/commands/stage/download.js new file mode 100644 index 00000000000000..e5b7711aee54d3 --- /dev/null +++ b/deps/npm/lib/commands/stage/download.js @@ -0,0 +1,70 @@ +const { log, output, META } = require('proc-log') +const { writeFile } = require('node:fs/promises') +const { resolve } = require('node:path') +const tar = require('tar') +const npmFetch = require('npm-registry-fetch') +const { getContents, logTar } = require('../../utils/tar.js') +const { validateUUID } = require('../../utils/validate-uuid.js') +const BaseCommand = require('../../base-cmd.js') + +class StageDownload extends BaseCommand { + static description = 'Download the tarball of a staged package for inspection' + static name = 'download' + static usage = [''] + static params = ['json', 'registry'] + static positionals = 1 + + async exec (args) { + if (!args[0]) { + throw this.usageError('Missing required ') + } + const stageId = args[0] + validateUUID(stageId, 'stage-id') + const opts = { ...this.npm.flatOptions } + const unicode = this.npm.config.get('unicode') + const json = this.npm.config.get('json') + + log.notice('', `Downloading staged package ${stageId}`) + + const res = await npmFetch(`/-/stage/${stageId}/tarball`, opts) + const data = Buffer.from(await res.arrayBuffer()) + + const manifest = await this.#readManifestFromTarball(data) + const pkgContents = await getContents(manifest, data) + logTar(pkgContents, { unicode, json, key: pkgContents.name }) + + const safeName = pkgContents.name.replace('@', '').replace('/', '-') + const filename = `${safeName}-${pkgContents.version}-${stageId}.tgz` + const dest = resolve(process.cwd(), filename) + + await writeFile(dest, data) + if (!json) { + output.standard(filename, { [META]: true, redact: false }) + } + } + + async #readManifestFromTarball (tarballData) { + let manifestJson + const stream = tar.t({ + onentry (entry) { + if (entry.path === 'package/package.json') { + const chunks = [] + entry.on('data', c => chunks.push(c)) + entry.on('end', () => { + manifestJson = JSON.parse(Buffer.concat(chunks).toString()) + }) + } else { + entry.resume() + } + }, + }) + // node-tar uses Minipass which processes synchronously on .end() + stream.end(tarballData) + if (!manifestJson) { + throw new Error('Could not read package.json from tarball') + } + return manifestJson + } +} + +module.exports = StageDownload diff --git a/deps/npm/lib/commands/stage/index.js b/deps/npm/lib/commands/stage/index.js new file mode 100644 index 00000000000000..51b41b4f0249d9 --- /dev/null +++ b/deps/npm/lib/commands/stage/index.js @@ -0,0 +1,25 @@ +const BaseCommand = require('../../base-cmd.js') + +class Stage extends BaseCommand { + static description = 'Stage packages for publishing, deferring proof-of-presence (2FA) to a later point in time' + static name = 'stage' + + static subcommands = { + publish: require('./publish.js'), + list: require('./list.js'), + view: require('./view.js'), + approve: require('./approve.js'), + reject: require('./reject.js'), + download: require('./download.js'), + } + + static async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length === 2) { + return Object.keys(Stage.subcommands) + } + return [] + } +} + +module.exports = Stage diff --git a/deps/npm/lib/commands/stage/list.js b/deps/npm/lib/commands/stage/list.js new file mode 100644 index 00000000000000..bcfb45affb00b0 --- /dev/null +++ b/deps/npm/lib/commands/stage/list.js @@ -0,0 +1,72 @@ +const { output, META } = require('proc-log') +const npa = require('npm-package-arg') +const npmFetch = require('npm-registry-fetch') +const { logStageItem } = require('../../utils/key-values.js') +const BaseCommand = require('../../base-cmd.js') + +class StageList extends BaseCommand { + static description = 'List all staged package versions' + static name = 'list' + static usage = ['[]'] + static params = ['json', 'registry'] + + async exec (args) { + let packageFilter = null + if (args[0]) { + const spec = npa(args[0]) + if (spec.rawSpec !== '*') { + throw this.usageError('Version specifiers are not supported for listing staged packages') + } + packageFilter = spec.name + } + const opts = { ...this.npm.flatOptions } + const json = this.npm.config.get('json') + + const allItems = await this.#fetchAllPages(opts, packageFilter) + + if (json) { + output.standard(JSON.stringify(allItems, null, 2), { [META]: true, redact: false }) + return + } + + if (allItems.length === 0) { + if (packageFilter) { + output.standard(`No staged versions of package name "${packageFilter}".`) + } else { + output.standard('No staged packages found.') + } + return + } + + for (let i = 0; i < allItems.length; i++) { + if (i > 0) { + output.standard('') + } + logStageItem(allItems[i], { chalk: this.npm.chalk }) + } + } + + async #fetchAllPages (opts, packageFilter) { + const items = [] + let page = 0 + const perPage = 100 + while (true) { + const query = { page, perPage } + if (packageFilter) { + query.package = packageFilter + } + const res = await npmFetch.json('/-/stage', { + ...opts, + query, + }) + items.push(...res.items) + if (items.length >= res.total || res.items.length < perPage) { + break + } + page++ + } + return items + } +} + +module.exports = StageList diff --git a/deps/npm/lib/commands/stage/publish.js b/deps/npm/lib/commands/stage/publish.js new file mode 100644 index 00000000000000..ff3fa3ad2b9ca3 --- /dev/null +++ b/deps/npm/lib/commands/stage/publish.js @@ -0,0 +1,13 @@ +const Publish = require('../publish.js') + +class StagePublish extends Publish { + static description = 'Stage a package for publishing, deferring proof-of-presence (2FA) to a later point in time' + static name = 'publish' + static stage = true + static params = Publish.params + static usage = Publish.usage + static workspaces = true + static ignoreImplicitWorkspace = false +} + +module.exports = StagePublish diff --git a/deps/npm/lib/commands/stage/reject.js b/deps/npm/lib/commands/stage/reject.js new file mode 100644 index 00000000000000..2f29a95ea4e964 --- /dev/null +++ b/deps/npm/lib/commands/stage/reject.js @@ -0,0 +1,37 @@ +const { log, output, META } = require('proc-log') +const npmFetch = require('npm-registry-fetch') +const { otplease } = require('../../utils/auth.js') +const { validateUUID } = require('../../utils/validate-uuid.js') +const BaseCommand = require('../../base-cmd.js') + +class StageReject extends BaseCommand { + static description = 'Reject a staged package, removing it from the registry' + static name = 'reject' + static usage = [''] + static params = ['otp', 'registry'] + static positionals = 1 + + async exec (args) { + if (!args[0]) { + throw this.usageError('Missing required ') + } + const stageId = args[0] + validateUUID(stageId, 'stage-id') + const opts = { ...this.npm.flatOptions } + + log.notice('', `Rejecting staged package ${stageId}`) + log.warn('', 'Rejecting will permanently delete this staged publish record and tarball from the registry.') + + await otplease(this.npm, opts, o => + npmFetch(`/-/stage/${stageId}`, { + ...o, + method: 'DELETE', + ignoreBody: true, + }) + ) + + output.standard(`Staged package ${stageId} has been rejected.`, { [META]: true, redact: false }) + } +} + +module.exports = StageReject diff --git a/deps/npm/lib/commands/stage/view.js b/deps/npm/lib/commands/stage/view.js new file mode 100644 index 00000000000000..7f7f6634568704 --- /dev/null +++ b/deps/npm/lib/commands/stage/view.js @@ -0,0 +1,34 @@ +const { output, META } = require('proc-log') +const npmFetch = require('npm-registry-fetch') +const { logStageItem } = require('../../utils/key-values.js') +const { validateUUID } = require('../../utils/validate-uuid.js') +const BaseCommand = require('../../base-cmd.js') + +class StageView extends BaseCommand { + static description = 'View details of a specific staged package' + static name = 'view' + static usage = [''] + static params = ['json', 'registry'] + static positionals = 1 + + async exec (args) { + if (!args[0]) { + throw this.usageError('Missing required ') + } + const stageId = args[0] + validateUUID(stageId, 'stage-id') + const opts = { ...this.npm.flatOptions } + const json = this.npm.config.get('json') + + const item = await npmFetch.json(`/-/stage/${stageId}`, opts) + + if (json) { + output.standard(JSON.stringify(item, null, 2), { [META]: true, redact: false }) + return + } + + logStageItem(item, { chalk: this.npm.chalk }) + } +} + +module.exports = StageView diff --git a/deps/npm/lib/commands/trust/circleci.js b/deps/npm/lib/commands/trust/circleci.js index 34d25b80182688..5444ccd09ce6e8 100644 --- a/deps/npm/lib/commands/trust/circleci.js +++ b/deps/npm/lib/commands/trust/circleci.js @@ -1,9 +1,8 @@ const Definition = require('@npmcli/config/lib/definitions/definition.js') const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') const TrustCommand = require('../../trust-cmd.js') - -// UUID validation regex -const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i +const { trustDefinitions } = require('../../trust-cmd.js') +const { validateUUID } = require('../../utils/validate-uuid.js') class TrustCircleCI extends TrustCommand { static description = 'Create a trusted relationship between a package and CircleCI' @@ -13,7 +12,7 @@ class TrustCircleCI extends TrustCommand { static providerEntity = 'CircleCI pipeline' static usage = [ - '[package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [-y|--yes]', + '[package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [--allow-publish] [--allow-stage-publish] [-y|--yes]', ] static definitions = [ @@ -46,6 +45,8 @@ class TrustCircleCI extends TrustCommand { type: [null, String, Array], description: 'CircleCI context UUID to match', }), + trustDefinitions['allow-publish'], + trustDefinitions['allow-stage-publish'], // globals are alphabetical globalDefinitions['dry-run'], globalDefinitions.json, @@ -54,9 +55,7 @@ class TrustCircleCI extends TrustCommand { ] validateUuid (value, fieldName) { - if (!UUID_REGEX.test(value)) { - throw new Error(`${fieldName} must be a valid UUID`) - } + validateUUID(value, fieldName) } validateVcsOrigin (value) { diff --git a/deps/npm/lib/commands/trust/github.js b/deps/npm/lib/commands/trust/github.js index 870314b717a759..c3434fe40770eb 100644 --- a/deps/npm/lib/commands/trust/github.js +++ b/deps/npm/lib/commands/trust/github.js @@ -1,6 +1,7 @@ const Definition = require('@npmcli/config/lib/definitions/definition.js') const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') const TrustCommand = require('../../trust-cmd.js') +const { trustDefinitions } = require('../../trust-cmd.js') const path = require('node:path') class TrustGitHub extends TrustCommand { @@ -16,7 +17,7 @@ class TrustGitHub extends TrustCommand { static entityKey = 'repository' static usage = [ - '[package] --file [--repo|--repository] [--env|--environment] [-y|--yes]', + '[package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]', ] static definitions = [ @@ -38,6 +39,8 @@ class TrustGitHub extends TrustCommand { description: 'CI environment name', alias: ['env'], }), + trustDefinitions['allow-publish'], + trustDefinitions['allow-stage-publish'], // globals are alphabetical globalDefinitions['dry-run'], globalDefinitions.json, diff --git a/deps/npm/lib/commands/trust/gitlab.js b/deps/npm/lib/commands/trust/gitlab.js index e6456244ea1850..809e05ed200201 100644 --- a/deps/npm/lib/commands/trust/gitlab.js +++ b/deps/npm/lib/commands/trust/gitlab.js @@ -1,6 +1,7 @@ const Definition = require('@npmcli/config/lib/definitions/definition.js') const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') const TrustCommand = require('../../trust-cmd.js') +const { trustDefinitions } = require('../../trust-cmd.js') const path = require('node:path') class TrustGitLab extends TrustCommand { @@ -16,7 +17,7 @@ class TrustGitLab extends TrustCommand { static entityKey = 'project' static usage = [ - '[package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes]', + '[package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]', ] static definitions = [ @@ -37,6 +38,8 @@ class TrustGitLab extends TrustCommand { description: 'CI environment name', alias: ['env'], }), + trustDefinitions['allow-publish'], + trustDefinitions['allow-stage-publish'], // globals are alphabetical globalDefinitions['dry-run'], globalDefinitions.json, diff --git a/deps/npm/lib/commands/update.js b/deps/npm/lib/commands/update.js index ed1416d70c13e2..22f77390b25a31 100644 --- a/deps/npm/lib/commands/update.js +++ b/deps/npm/lib/commands/update.js @@ -1,6 +1,8 @@ const path = require('node:path') const { log } = require('proc-log') const reifyFinish = require('../utils/reify-finish.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Update extends ArboristWorkspaceCmd { @@ -19,8 +21,12 @@ class Update extends ArboristWorkspaceCmd { 'package-lock', 'foreground-scripts', 'ignore-scripts', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', 'audit', 'before', + 'min-release-age', 'bin-links', 'fund', 'dry-run', @@ -50,15 +56,19 @@ class Update extends ArboristWorkspaceCmd { } const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const opts = { ...this.npm.flatOptions, path: where, save, workspaces: this.workspaceNames, + allowScripts: allowScriptsPolicy, } const arb = new Arborist(opts) - await arb.reify({ ...opts, update }) + const reifyOpts = { ...opts, update } + await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: reifyOpts }) + await arb.reify(reifyOpts) await reifyFinish(this.npm, arb) } } diff --git a/deps/npm/lib/trust-cmd.js b/deps/npm/lib/trust-cmd.js index 5fab8df1d21aa2..7fabe3e421aa6a 100644 --- a/deps/npm/lib/trust-cmd.js +++ b/deps/npm/lib/trust-cmd.js @@ -6,9 +6,29 @@ const { read: _read } = require('read') const { input, output, log, META } = require('proc-log') const gitinfo = require('hosted-git-info') const pkgJson = require('@npmcli/package-json') +const Definition = require('@npmcli/config/lib/definitions/definition.js') const NPM_FRONTEND = 'https://www.npmjs.com' +const PERMISSIONS = { + CREATE_PACKAGE: 'createPackage', + CREATE_STAGED_PACKAGE: 'createStagedPackage', +} + +const trustDefinitions = { + 'allow-publish': new Definition('allow-publish', { + default: false, + type: Boolean, + description: 'Allow npm publish for this trusted publisher configuration', + }), + 'allow-stage-publish': new Definition('allow-stage-publish', { + default: false, + type: Boolean, + description: 'Allow npm stage publish for this trusted publisher configuration', + alias: ['allow-staged-publish'], + }), +} + class TrustCommand extends BaseCommand { // Helper to format template strings with color // Blue text with reset color for interpolated values @@ -45,8 +65,22 @@ class TrustCommand extends BaseCommand { })) } + static permissionLabels = { + [PERMISSIONS.CREATE_PACKAGE]: 'publish', + [PERMISSIONS.CREATE_STAGED_PACKAGE]: 'stage publish', + } + + static formatPermissions (permissions) { + if (!Array.isArray(permissions) || permissions.length === 0) { + return null + } + return permissions + .map(p => TrustCommand.permissionLabels[p] || p) + .join(', ') + } + logOptions (options, pad = true) { - const { values, warnings, fromPackageJson, urls } = { warnings: [], ...options } + const { values, warnings, fromPackageJson, urls, permissions } = { warnings: [], ...options } if (warnings && warnings.length > 0) { for (const warningMsg of warnings) { log.warn('trust', warningMsg) @@ -55,8 +89,12 @@ class TrustCommand extends BaseCommand { const json = this.config.get('json') if (json) { + const jsonValues = { ...options.values } + if (permissions) { + jsonValues.permissions = permissions + } // Disable redaction: trust config values (e.g. CircleCI UUIDs) are not secrets - output.standard(JSON.stringify(options.values, null, 2), { [META]: true, redact: false }) + output.standard(JSON.stringify(jsonValues, null, 2), { [META]: true, redact: false }) return } @@ -82,6 +120,10 @@ class TrustCommand extends BaseCommand { lines.push(parts.join(' ')) } } + const formattedPermissions = TrustCommand.formatPermissions(permissions) + if (formattedPermissions) { + lines.push(`${chalk.reset('permissions')}: ${chalk.green(formattedPermissions)}`) + } if (pad) { output.standard() } @@ -165,6 +207,22 @@ class TrustCommand extends BaseCommand { const { providerName, providerEntity, providerHostname } = this.constructor const dryRun = this.config.get('dry-run') const yes = this.config.get('yes') // deep-lore this allows for --no-yes + + const allowPublish = flags['allow-publish'] + const allowStagePublish = flags['allow-stage-publish'] + + if (!allowPublish && !allowStagePublish) { + throw new Error('At least one permission flag is required (--allow-publish, --allow-stage-publish)') + } + + const permissions = [] + if (allowPublish) { + permissions.push(PERMISSIONS.CREATE_PACKAGE) + } + if (allowStagePublish) { + permissions.push(PERMISSIONS.CREATE_STAGED_PACKAGE) + } + const options = await this.flagsToOptions({ positionalArgs, flags, providerHostname }) this.dialogue`Establishing trust between ${options.values.package} package and ${providerName}` this.dialogue`Anyone with ${providerEntity} write access can publish to ${options.values.package}` @@ -172,12 +230,13 @@ class TrustCommand extends BaseCommand { if (!this.registryIsDefault) { this.warn`Registry ${this.npm.config.get('registry')} may not support trusted publishing` } - this.logOptions(options) + this.logOptions({ ...options, permissions }) if (dryRun) { return } await this.confirmOperation(yes) const trustConfig = this.constructor.optionsToBody(options.values) + trustConfig.permissions = permissions const response = await this.createConfig(options.values.package, [trustConfig]) const body = await response.json() this.dialogue`Trust configuration created successfully for ${options.values.package} with the following settings:` @@ -273,8 +332,9 @@ class TrustCommand extends BaseCommand { const items = Array.isArray(body) ? body : [body] for (const config of items) { const values = this.constructor.bodyToOptions(config) + const permissions = config.permissions output.standard() - this.logOptions({ values }, false) + this.logOptions({ values, permissions }, false) } output.standard() } @@ -282,3 +342,4 @@ class TrustCommand extends BaseCommand { module.exports = TrustCommand module.exports.NPM_FRONTEND = NPM_FRONTEND +module.exports.trustDefinitions = trustDefinitions diff --git a/deps/npm/lib/utils/allow-scripts-cmd.js b/deps/npm/lib/utils/allow-scripts-cmd.js new file mode 100644 index 00000000000000..c1ff242abeaa82 --- /dev/null +++ b/deps/npm/lib/utils/allow-scripts-cmd.js @@ -0,0 +1,245 @@ +const { log, output } = require('proc-log') +const pkgJson = require('@npmcli/package-json') +const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js') +const checkAllowScripts = require('./check-allow-scripts.js') +const resolveAllowScripts = require('./resolve-allow-scripts.js') +const { + applyApprovalForPackage, + applyDenyForPackage, + nameKeyFor, +} = require('./allow-scripts-writer.js') +const BaseCommand = require('../base-cmd.js') + +// Shared implementation for `npm approve-scripts` and `npm deny-scripts`. +// Subclasses set `verb` to `'approve'` or `'deny'`. +// +// Extends `BaseCommand` rather than `ArboristCmd` on purpose. Per RFC, +// `allowScripts` is read from the workspace root's `package.json` only; +// individual workspaces don't have their own `allowScripts` field, and +// running approve/deny inside a sub-workspace is identical to running +// it at the root. There's no per-workspace targeting to do, so the +// `--workspace` / `--workspaces` / `--include-workspace-root` params +// from `ArboristCmd` would be misleading no-ops. +class AllowScriptsCmd extends BaseCommand { + static params = ['all', 'allow-scripts-pending', 'allow-scripts-pin', 'json'] + static ignoreImplicitWorkspace = false + + // Subclasses set `static verb = 'approve' | 'deny'`. + get verb () { + /* istanbul ignore next: every concrete subclass declares static verb */ + return this.constructor.verb + } + + async exec (args) { + if (this.npm.global) { + throw Object.assign( + new Error(`\`npm ${this.constructor.name}\` does not work for global installs`), + { code: 'EGLOBAL' } + ) + } + + const pending = !!this.npm.config.get('allow-scripts-pending') + const all = !!this.npm.config.get('all') + + if (pending && (args.length > 0 || all)) { + throw this.usageError( + '`--allow-scripts-pending` cannot be combined with positional arguments or `--all`.' + ) + } + if (!pending && !all && args.length === 0) { + throw this.usageError() + } + if (this.verb === 'deny' && pending) { + throw this.usageError('`npm deny-scripts --allow-scripts-pending` is not supported.') + } + + const Arborist = require('@npmcli/arborist') + const { policy } = await resolveAllowScripts(this.npm) + const arb = new Arborist({ + ...this.npm.flatOptions, + path: this.npm.prefix, + allowScripts: policy, + }) + await arb.loadActual() + + const unreviewed = await checkAllowScripts({ arb, npm: this.npm }) + + if (pending) { + return this.runPending(unreviewed) + } + + if (all) { + return this.runAll(unreviewed) + } + + return this.runPositional(args, arb) + } + + runPending (unreviewed) { + if (unreviewed.length === 0) { + output.standard('No packages with unreviewed install scripts.') + return + } + const count = unreviewed.length + const has = count === 1 ? 'has' : 'have' + const pkg = count === 1 ? 'package' : 'packages' + output.standard( + `${count} ${pkg} ${has} install scripts not yet covered by allowScripts:` + ) + for (const { node, scripts } of unreviewed) { + const { name, version } = trustedDisplay(node) + /* istanbul ignore next: every test node has a name */ + const display = name || '' + const ver = version ? `@${version}` : '' + const events = Object.entries(scripts) + .map(([event, cmd]) => `${event}: ${cmd}`) + .join('; ') + output.standard(` ${display}${ver} (${events})`) + } + output.standard('') + output.standard( + 'Run `npm approve-scripts ` to allow, or `npm deny-scripts ` to deny.' + ) + } + + async runAll (unreviewed) { + if (unreviewed.length === 0) { + output.standard('No packages with unreviewed install scripts.') + return + } + // Bundled dependencies cannot be allowlisted in Phase 1 (RFC defers + // this to a follow-up because matching by name@version from the + // bundled tarball would reintroduce manifest confusion). Exclude + // them from `--all` so we don't silently write a policy entry under + // attacker-controlled identity. + const candidates = unreviewed.filter(({ node }) => !node.inBundle) + const skipped = unreviewed.length - candidates.length + if (skipped > 0) { + /* istanbul ignore next: plural variant covered separately */ + const noun = skipped === 1 ? 'dependency' : 'dependencies' + log.warn( + this.logTitle, + `Skipping ${skipped} bundled ${noun}; bundled deps with install ` + + 'scripts cannot be allowlisted in this release.' + ) + } + if (candidates.length === 0) { + output.standard('No packages eligible for approval.') + return + } + const groups = this.groupByPackage(candidates.map(({ node }) => node)) + await this.writePolicyChanges(groups) + } + + async runPositional (args, arb) { + const matched = this.findNodesForArgs(args, arb) + const groups = this.groupByPackage(matched) + if (Object.keys(groups).length === 0) { + throw Object.assign( + new Error(`No installed packages match: ${args.join(', ')}`), + { code: 'ENOMATCH' } + ) + } + await this.writePolicyChanges(groups) + } + + findNodesForArgs (args, arb) { + // Match positional args against each node's trusted name. Registry + // deps use the URL-derived name; non-registry deps fall back to the + // dependency edge name. Bundled deps are excluded for the same reason + // as --all. + const wanted = new Set(args) + const matched = [] + for (const node of arb.actualTree.inventory.values()) { + if (node.isProjectRoot || node.isWorkspace || node.inBundle) { + continue + } + const { name } = trustedDisplay(node) + if (name && wanted.has(name)) { + matched.push(node) + } + } + return matched + } + + get logTitle () { + return this.constructor.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + } + + groupByPackage (nodes) { + const groups = {} + for (const node of nodes) { + const key = nameKeyFor(node) + /* istanbul ignore if: callers prefilter via inBundle and trustedDisplay so untrusted nodes don't reach here */ + if (!key) { + log.warn( + this.logTitle, + `skipping ${node.name || ''}: no trusted identity for policy key` + ) + continue + } + if (!groups[key]) { + groups[key] = [] + } + groups[key].push(node) + } + return groups + } + + async writePolicyChanges (groups) { + const pin = this.npm.config.get('allow-scripts-pin') !== false + + const pkg = await pkgJson.load(this.npm.prefix) + const content = pkg.content + const existing = content.allowScripts && typeof content.allowScripts === 'object' + ? content.allowScripts + : {} + + let updated = existing + const summary = [] + + for (const [name, nodes] of Object.entries(groups)) { + const result = this.verb === 'approve' + ? applyApprovalForPackage(updated, nodes, { pin }) + : applyDenyForPackage(updated, nodes) + + if (result.warning) { + log.warn(this.logTitle, result.warning) + } + updated = result.allowScripts + summary.push({ name, changes: result.changes }) + } + + /* istanbul ignore else: writePolicyChanges only called when changes are expected */ + if (updated !== existing) { + pkg.update({ allowScripts: updated }) + await pkg.save() + } + + this.printSummary(summary) + } + + printSummary (summary) { + if (this.npm.flatOptions.json) { + output.buffer({ allowScripts: summary }) + return + } + const verb = this.verb === 'approve' ? 'Approved' : 'Denied' + let touched = 0 + for (const { name, changes } of summary) { + if (changes.length === 0) { + continue + } + touched++ + output.standard(`${verb} ${name}:`) + for (const { key, change } of changes) { + output.standard(` ${change} ${key}`) + } + } + if (touched === 0) { + output.standard(`Nothing to ${this.verb}; allowScripts unchanged.`) + } + } +} + +module.exports = AllowScriptsCmd diff --git a/deps/npm/lib/utils/allow-scripts-writer.js b/deps/npm/lib/utils/allow-scripts-writer.js new file mode 100644 index 00000000000000..5f43bbebeedefa --- /dev/null +++ b/deps/npm/lib/utils/allow-scripts-writer.js @@ -0,0 +1,323 @@ +const npa = require('npm-package-arg') +const { log } = require('proc-log') +const { getTrustedRegistryIdentity } = require('@npmcli/arborist/lib/script-allowed.js') + +// Pure helpers that implement the RFC's pin-mismatch table for +// `npm approve-scripts` and `npm deny-scripts`. +// +// Approving writes either `"": true` or `"": true` to the +// project's `allowScripts` field, depending on `--allow-scripts-pin` and the currently +// installed versions. +// +// Denying always writes `"": false`, regardless of `--allow-scripts-pin`, per the +// RFC's asymmetric-pin rule. + +// Convert an arborist Node into the spec string used for a versioned policy +// entry. Returns `null` if the node cannot be represented as a versioned key +// derived from trusted sources (lockfile URL for registry, hosted shortcut +// for git, the resolved file path for local installs). Never falls back to +// `node.packageName` / `node.version`, which are tarball-controlled. +const versionedKeyFor = (node) => { + if (!node) { + return null + } + /* istanbul ignore next: callers guarantee a string resolved */ + const resolved = typeof node.resolved === 'string' ? node.resolved : '' + if (resolved.startsWith('git')) { + try { + const parsed = npa(resolved) + if (parsed.hosted) { + const committish = parsed.gitCommittish || parsed.hosted.committish + const base = parsed.hosted.shortcut({ noCommittish: true }) + return committish ? `${base}#${committish}` : base + } + } catch { + /* istanbul ignore next: npa already parsed this string in keyTargetsNode */ + return null + } + return null + } + if (/^https?:\/\//.test(resolved)) { + const trusted = getTrustedRegistryIdentity(node) + if (trusted && trusted.version) { + return `${trusted.name}@${trusted.version}` + } + // Registry node with a resolved URL that versionFromTgz couldn't + // parse (private-registry mirror, alternate CDN URL shape). Leave a + // breadcrumb so users notice when policy keys are silently pruned. + log.silly( + 'allow-scripts', + `unable to derive trusted versioned key for ${node.path || node.name || ''} ` + + `(resolved: ${resolved}); key will be pruned on next save` + ) + return null + } + /* istanbul ignore next: 'file:' and '/' branches are each covered separately */ + if (resolved.startsWith('file:') || resolved.startsWith('/')) { + return resolved + } + // No trusted source. Refuse to compose a key from attacker-controlled + // `node.packageName` / `node.version`. + /* istanbul ignore next: callers filter out non-registry/non-file nodes before reaching this fallback */ + return null +} + +// Convert an arborist Node into the spec string used for a name-only policy +// entry. Same trust rules as versionedKeyFor — returns `null` rather than +// falling back to tarball-controlled fields. +const nameKeyFor = (node) => { + if (!node) { + return null + } + /* istanbul ignore next: callers guarantee a string resolved */ + const resolved = typeof node.resolved === 'string' ? node.resolved : '' + if (resolved.startsWith('git')) { + try { + const parsed = npa(resolved) + if (parsed.hosted) { + return parsed.hosted.shortcut({ noCommittish: true }) + } + } catch { + /* istanbul ignore next: npa already parsed this string in keyTargetsNode */ + return null + } + return null + } + if (resolved.startsWith('file:') || resolved.startsWith('/')) { + return resolved + } + // Registry deps: only the URL-derived (or edges-derived, in the + // omit-lockfile case) trusted name is acceptable. + const trusted = getTrustedRegistryIdentity(node) + return trusted ? trusted.name : null +} + +const isSingleVersionPin = (key) => { + try { + const parsed = npa(key) + return parsed.type === 'version' + } catch { + return false + } +} + +// Build the warning string emitted when an existing deny entry blocks +// an approval. Per RFC, a name-only deny ("pkg": false) is widest and +// the only remediation is to remove the entry. A versioned deny +// ("pkg@1.2.3": false or a disjunction) blocks only specific versions; +// the user can either widen it via `npm deny-scripts ` or remove +// it to approve the currently-installed version only. +const denyWarning = (key, subject, name) => { + if (isNameOnlyKey(key)) { + return `${key} is denied; remove the entry from allowScripts to approve ${subject}.` + } + /* istanbul ignore next: name fallback is defensive; callers pass nameKeyFor(sample) */ + const widenTarget = name || 'this package' + return `${key} is a versioned deny; run \`npm deny-scripts ${widenTarget}\` ` + + `to widen the deny to all versions of ${widenTarget}, or remove the entry ` + + `to approve ${subject}.` +} + +const isNameOnlyKey = (key) => { + try { + const parsed = npa(key) + if (parsed.type === 'tag') { + return true + } + if (parsed.type === 'range') { + return parsed.fetchSpec === '*' + || parsed.rawSpec === '' + || parsed.rawSpec === '*' + } + return false + } catch { + /* istanbul ignore next: keys reaching this helper have already parsed via keyTargetsNode */ + return false + } +} + +// Does this policy key target this node by identity (ignoring the +// allow/deny value)? +// +// Registry keys (`tag`, `range`, `version`) require a trusted identity on +// the node. If the node has no `getTrustedRegistryIdentity` result, the +// key does not match — never fall back to `node.name`, which is the +// install-directory name and is forgeable through aliases / manifest +// confusion. +const keyTargetsNode = (key, node) => { + let parsed + try { + parsed = npa(key) + } catch { + return false + } + switch (parsed.type) { + case 'tag': + case 'range': + case 'version': { + const trusted = getTrustedRegistryIdentity(node) + if (!trusted) { + return false + } + return trusted.name === parsed.name + } + case 'git': { + let resolvedParsed + try { + resolvedParsed = node.resolved ? npa(node.resolved) : null + } catch { + /* istanbul ignore next */ + return false + } + const keyHost = parsed.hosted?.ssh({ noCommittish: true }) + const nodeHost = resolvedParsed?.hosted?.ssh({ noCommittish: true }) + return !!(keyHost && nodeHost && keyHost === nodeHost) + } + case 'file': + case 'directory': + case 'remote': + return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec + default: + return false + } +} + +// Apply approvals for all currently-installed versions of a single package. +// +// `nodes` must all share an identity (same package name for registry deps, +// or same hosted shortcut for git deps, etc.). The caller is responsible +// for grouping nodes correctly. +// +// Returns `{ allowScripts, changes, warning }` where: +// - `allowScripts` is the new object (the input is never mutated) +// - `changes` is a list of `{ key, change }` entries describing edits +// - `warning` is an optional message to surface to the user +const applyApprovalForPackage = (existing, nodes, { pin = true } = {}) => { + const allowScripts = { ...existing } + const changes = [] + + if (!Array.isArray(nodes) || nodes.length === 0) { + return { allowScripts, changes } + } + + const sample = nodes[0] + const name = nameKeyFor(sample) + + // Deny-wins: any existing false that targets any installed version aborts. + for (const node of nodes) { + for (const [key, value] of Object.entries(allowScripts)) { + if (value === false && keyTargetsNode(key, node)) { + /* istanbul ignore next: name fallback covers the empty-name edge case */ + const subject = name || 'this package' + return { + allowScripts, + changes, + warning: denyWarning(key, subject, name), + } + } + } + } + + if (!pin) { + // Name-only mode: collapse any single-version pins for this package + // into a single name-only entry. + for (const key of Object.keys(allowScripts)) { + if ( + keyTargetsNode(key, sample) && + key !== name && + isSingleVersionPin(key) && + allowScripts[key] === true + ) { + delete allowScripts[key] + } + } + + /* istanbul ignore else: name === null is the no-identity path tested separately */ + if (name && allowScripts[name] !== true) { + allowScripts[name] = true + changes.push({ key: name, change: 'added' }) + } + return { allowScripts, changes } + } + + // Pin mode. For each currently installed version, write a single-version + // pin if one is not already in place. Stale single-version pins for this + // package are removed. Per the RFC's pin-mismatch table, an existing + // name-only entry (`pkg: true`) is replaced by `pkg@x.y.z: true` once + // every installed version has a pin. + const installedKeys = new Set(nodes.map(versionedKeyFor).filter(Boolean)) + + for (const key of Object.keys(allowScripts)) { + if ( + keyTargetsNode(key, sample) && + isSingleVersionPin(key) && + allowScripts[key] === true && + !installedKeys.has(key) + ) { + delete allowScripts[key] + changes.push({ key, change: 'removed-stale' }) + } + } + + for (const key of installedKeys) { + if (allowScripts[key] !== true) { + allowScripts[key] = true + changes.push({ key, change: 'added' }) + } + } + + // Upgrade: drop the name-only entry once every installed version has a + // pin. The operation is convergent: running the command twice produces + // the same shape regardless of the starting state. + if ( + installedKeys.size > 0 && + name && + !installedKeys.has(name) && + allowScripts[name] === true + ) { + delete allowScripts[name] + changes.push({ key: name, change: 'replaced-by-pin' }) + } + + return { allowScripts, changes } +} + +// Apply a deny for a single package. Always name-only; ignores `--allow-scripts-pin`. +const applyDenyForPackage = (existing, nodes) => { + const allowScripts = { ...existing } + const changes = [] + + if (!Array.isArray(nodes) || nodes.length === 0) { + return { allowScripts, changes } + } + + const sample = nodes[0] + const name = nameKeyFor(sample) + if (!name) { + return { allowScripts, changes } + } + + // Drop any pinned allow entries for this package: the name-only deny + // overrides them anyway, and leaving them in place is confusing. + for (const key of Object.keys(allowScripts)) { + if (keyTargetsNode(key, sample) && key !== name) { + delete allowScripts[key] + changes.push({ key, change: 'removed-pinned-allow' }) + } + } + + if (allowScripts[name] !== false) { + allowScripts[name] = false + changes.push({ key: name, change: 'added' }) + } + return { allowScripts, changes } +} + +module.exports = { + applyApprovalForPackage, + applyDenyForPackage, + versionedKeyFor, + nameKeyFor, + keyTargetsNode, + isSingleVersionPin, +} diff --git a/deps/npm/lib/utils/check-allow-scripts.js b/deps/npm/lib/utils/check-allow-scripts.js new file mode 100644 index 00000000000000..5ef2bfb74cf153 --- /dev/null +++ b/deps/npm/lib/utils/check-allow-scripts.js @@ -0,0 +1,54 @@ +const isScriptAllowed = require('@npmcli/arborist/lib/script-allowed.js') +const getInstallScripts = require('@npmcli/arborist/lib/install-scripts.js') + +// Walks arb.actualTree.inventory and returns the list of dep nodes that +// have install-relevant lifecycle scripts and are not yet covered (or +// explicitly denied) by the allowScripts policy. +// +// Returns an array of `{ node, scripts }` entries. `scripts` is an object +// describing the relevant lifecycle scripts that would run. + +const checkAllowScripts = async ({ arb, npm, tree }) => { + const ignoreScripts = !!arb.options?.ignoreScripts + const dangerouslyAllowAll = !!npm?.flatOptions?.dangerouslyAllowAllScripts + + if (ignoreScripts || dangerouslyAllowAll) { + return [] + } + + // Defaults to actualTree (post-reify) but accepts an explicit tree so + // callers can pre-flight against the idealTree before scripts run. + const targetTree = tree || arb.actualTree + if (!targetTree?.inventory) { + return [] + } + + const policy = arb.options?.allowScripts || null + + const unreviewed = [] + for (const node of targetTree.inventory.values()) { + if (node.isProjectRoot || node.isWorkspace) { + continue + } + if (node.isLink) { + // Linked workspace dependencies are managed by the workspace owner. + continue + } + + const verdict = isScriptAllowed(node, policy) + if (verdict === true || verdict === false) { + continue + } + + const scripts = await getInstallScripts(node) + if (Object.keys(scripts).length === 0) { + continue + } + + unreviewed.push({ node, scripts }) + } + + return unreviewed +} + +module.exports = checkAllowScripts diff --git a/deps/npm/lib/utils/cmd-list.js b/deps/npm/lib/utils/cmd-list.js index 9cd2346f0ff98f..1909df0d045469 100644 --- a/deps/npm/lib/utils/cmd-list.js +++ b/deps/npm/lib/utils/cmd-list.js @@ -5,6 +5,7 @@ const abbrev = require('abbrev') const commands = [ 'access', 'adduser', + 'approve-scripts', 'audit', 'bugs', 'cache', @@ -12,6 +13,7 @@ const commands = [ 'completion', 'config', 'dedupe', + 'deny-scripts', 'deprecate', 'diff', 'dist-tag', @@ -55,6 +57,7 @@ const commands = [ 'search', 'set', 'shrinkwrap', + 'stage', 'star', 'stars', 'start', diff --git a/deps/npm/lib/utils/display.js b/deps/npm/lib/utils/display.js index 122a7f6e8c577a..72e76bd36731b0 100644 --- a/deps/npm/lib/utils/display.js +++ b/deps/npm/lib/utils/display.js @@ -1,6 +1,7 @@ const { log, output, input, META } = require('proc-log') const { explain } = require('./explain-eresolve.js') const { formatWithOptions } = require('./format') +const { redactLog } = require('@npmcli/redact') // This is the general approach to color: // Eventually this will be exposed somewhere we can refer to these by name. @@ -98,14 +99,16 @@ const getArrayOrObject = (items) => { return Object.assign({}, ...items.filter(o => isPlainObject(o))) } +const redactValue = (obj) => JSON.parse(redactLog(JSON.stringify(obj))) + const getJsonBuffer = ({ [JSON_ERROR_KEY]: metaError }, buffer) => { const items = [] // meta also contains the meta object passed to flush const errors = metaError ? [metaError] : [] // index 1 is the meta, 2 is the logged argument - for (const [, { [JSON_ERROR_KEY]: error }, obj] of buffer) { + for (const [, { [JSON_ERROR_KEY]: error, redact = true }, obj] of buffer) { if (obj) { - items.push(obj) + items.push(redact ? redactValue(obj) : obj) } if (error) { errors.push(error) @@ -291,7 +294,9 @@ class Display { if (this.#json) { const json = getJsonBuffer(meta, this.#outputState.buffer) if (json) { - this.#writeOutput(output.KEYS.standard, meta, JSON.stringify(json, null, 2)) + // Per-item redaction already applied in getJsonBuffer, skip string-level redaction + const jsonMeta = { ...meta, redact: false } + this.#writeOutput(output.KEYS.standard, jsonMeta, JSON.stringify(json, null, 2)) } } else { this.#outputState.buffer.forEach((item) => this.#writeOutput(...item)) diff --git a/deps/npm/lib/utils/key-values.js b/deps/npm/lib/utils/key-values.js new file mode 100644 index 00000000000000..cf54304da6b4b2 --- /dev/null +++ b/deps/npm/lib/utils/key-values.js @@ -0,0 +1,42 @@ +const { output, META } = require('proc-log') + +const defaultPredicate = (key, value, chalk) => { + if (value === null || value === undefined) { + return null + } + return chalk.green(value) +} + +function logObject (values, { chalk, json, predicate = defaultPredicate }) { + if (json) { + output.standard(JSON.stringify(values, null, 2), { [META]: true, redact: false }) + return + } + + const lines = [] + for (const [key, value] of Object.entries(values)) { + const formatted = predicate(key, value, chalk) + if (formatted !== null) { + lines.push(`${chalk.cyan(key)}: ${formatted}`) + } + } + if (lines.length) { + output.standard(lines.join('\n'), { [META]: true, redact: false }) + } +} + +function logStageItem (item, { chalk }) { + const { id, packageName, version, tag, createdAt, actor, actorType, shasum, ...rest } = item + logObject({ + id, + 'package name': packageName, + version, + tag, + 'date staged': createdAt, + 'staged by': actorType ? `${actor} (${actorType})` : actor, + shasum, + ...rest, + }, { chalk }) +} + +module.exports = { logObject, logStageItem, defaultPredicate } diff --git a/deps/npm/lib/utils/reify-finish.js b/deps/npm/lib/utils/reify-finish.js index 5e1330f4937bbd..1041c53fdb9357 100644 --- a/deps/npm/lib/utils/reify-finish.js +++ b/deps/npm/lib/utils/reify-finish.js @@ -1,4 +1,6 @@ const reifyOutput = require('./reify-output.js') +const checkAllowScripts = require('./check-allow-scripts.js') +const warnWorkspaceAllowScripts = require('./warn-workspace-allow-scripts.js') const ini = require('ini') const { writeFile } = require('node:fs/promises') const { resolve } = require('node:path') @@ -15,7 +17,9 @@ const reifyFinish = async (npm, arb) => { } } } - reifyOutput(npm, arb) + warnWorkspaceAllowScripts(arb.actualTree) + const unreviewedScripts = await checkAllowScripts({ arb, npm }) + reifyOutput(npm, arb, { unreviewedScripts }) } module.exports = reifyFinish diff --git a/deps/npm/lib/utils/reify-output.js b/deps/npm/lib/utils/reify-output.js index 99427faaf66488..b1e1ffbcddd175 100644 --- a/deps/npm/lib/utils/reify-output.js +++ b/deps/npm/lib/utils/reify-output.js @@ -14,11 +14,12 @@ const { depth } = require('treeverse') const ms = require('ms') const npmAuditReport = require('npm-audit-report') const { readTree: getFundingInfo } = require('libnpmfund') +const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js') const auditError = require('./audit-error.js') -// TODO: output JSON if flatOptions.json is true -const reifyOutput = (npm, arb) => { +const reifyOutput = (npm, arb, extras = {}) => { const { diff, actualTree } = arb + const unreviewedScripts = extras.unreviewedScripts || [] // note: fails and crashes if we're running audit fix and there was an error which is a good thing, because there's no point printing all this other stuff in that case! const auditReport = auditError(npm, arb.auditReport) ? null : arb.auditReport @@ -113,11 +114,23 @@ const reifyOutput = (npm, arb) => { summary.audit = npm.command === 'audit' ? auditReport : auditReport.toJSON().metadata } + if (unreviewedScripts.length) { + summary.unreviewedScripts = unreviewedScripts.map(({ node, scripts }) => { + const { name, version } = trustedDisplay(node) + return { + name, + version, + path: node.path, + scripts, + } + }) + } output.buffer(summary) } else { packagesChangedMessage(npm, summary) packagesFundingMessage(npm, summary) printAuditReport(npm, auditReport) + unreviewedScriptsMessage(npm, unreviewedScripts) } } @@ -217,4 +230,39 @@ const packagesFundingMessage = (npm, { funding }) => { output.standard(' run `npm fund` for details') } +const unreviewedScriptsMessage = (npm, unreviewedScripts) => { + if (!unreviewedScripts.length) { + return + } + + // Goes through log.warn so it respects --loglevel / --silent and lands + // on stderr like every other "FYI, here's something to know" message. + // stdout is reserved for things the user explicitly asked to see + // (npm ls, npm view). + const count = unreviewedScripts.length + const pkg = count === 1 ? 'package has' : 'packages have' + const header = `${count} ${pkg} install scripts not yet covered by allowScripts:` + + const lines = unreviewedScripts.map(({ node, scripts }) => { + const { name, version } = trustedDisplay(node) + /* istanbul ignore next: every test node has a name */ + const display = name || '' + const ver = version ? `@${version}` : '' + const events = Object.entries(scripts) + .map(([event, cmd]) => `${event}: ${cmd}`) + .join('; ') + return ` ${display}${ver} (${events})` + }) + + log.warn( + 'allow-scripts', + [ + header, + ...lines, + '', + 'Run `npm approve-scripts --allow-scripts-pending` to review, or `npm approve-scripts ` to allow.', + ].join('\n') + ) +} + module.exports = reifyOutput diff --git a/deps/npm/lib/utils/resolve-allow-scripts.js b/deps/npm/lib/utils/resolve-allow-scripts.js new file mode 100644 index 00000000000000..b658e1a68ad0cf --- /dev/null +++ b/deps/npm/lib/utils/resolve-allow-scripts.js @@ -0,0 +1,181 @@ +const { log } = require('proc-log') +const npa = require('npm-package-arg') +const pkgJson = require('@npmcli/package-json') +const { isExactVersionDisjunction } = require('@npmcli/arborist/lib/script-allowed.js') +const parseAllowScriptsList = require('@npmcli/config/lib/parse-allow-scripts-list.js') + +const buildPolicyFromNames = (names) => { + /* istanbul ignore if: callers only pass non-empty arrays */ + if (!names.length) { + return null + } + const policy = {} + for (const name of names) { + policy[name] = true + } + return policy +} + +// Read the `allow-scripts` value from one or more named config sources and +// build a policy object. Returns `null` if none of the sources has a value. +const policyFromSources = (npm, sources) => { + for (const where of sources) { + const value = npm.config.get?.('allow-scripts', where) + if (value === undefined) { + continue + } + const names = parseAllowScriptsList(value) + /* istanbul ignore else: parseAllowScriptsList returns non-empty when value is set */ + if (names.length) { + return buildPolicyFromNames(names) + } + } + return null +} + +const validatePolicy = (policy, sourceLabel) => { + // Drop and warn about keys with forbidden semver ranges (^, ~, >=, <, *). + // The RFC only permits exact versions joined by `||`. Bare names like + // `canvas` and explicit name-only wildcards (`canvas@*`) are allowed. + if (!policy) { + return policy + } + const cleaned = {} + for (const [key, value] of Object.entries(policy)) { + let parsed + try { + parsed = npa(key) + } catch { + log.warn('allow-scripts', `${sourceLabel}: ignoring unparseable entry "${key}"`) + continue + } + if (parsed.type === 'tag') { + // `pkg@latest`, `pkg@next`, etc. look like a pin but behave name- + // only — the matcher has no way to verify what the tag points at + // when scripts run. Reject for the same reason as semver ranges. + log.warn( + 'allow-scripts', + `${sourceLabel}: ignoring "${key}" — dist-tag specs (@latest, @next, ...) are not allowed; ` + + 'use exact versions joined by "||", or the bare package name, instead' + ) + continue + } + if (parsed.type === 'range') { + const isNameOnly = parsed.fetchSpec === '*' + || parsed.rawSpec === '' + || parsed.rawSpec === '*' + if (!isNameOnly && !isExactVersionDisjunction(parsed.fetchSpec)) { + log.warn( + 'allow-scripts', + `${sourceLabel}: ignoring "${key}" — semver ranges (^, ~, >=, <) are not allowed; ` + + 'use exact versions joined by "||" instead' + ) + continue + } + } + cleaned[key] = value + } + return Object.keys(cleaned).length > 0 ? cleaned : null +} + +// Resolve the effective allowScripts policy from the layered sources. +// Returns `{ policy, source }` where: +// - `policy` is an object map of `package-spec` -> boolean, or `null` if +// no layer has any configuration +// - `source` is one of `'cli'`, `'package.json'`, `'.npmrc'`, or `null` +// +// Precedence order (highest to lowest), per RFC npm/rfcs#868: +// 1. CLI flags (--allow-scripts) and env vars +// 2. Root `package.json#allowScripts` +// 3. `.npmrc` cascade (project, user, global) +// +// The project `package.json` layer is skipped when: +// - `npm.global` is true (no project context exists for global installs) +// - `skipProjectConfig` is true (e.g. npm exec / npx, which per the RFC +// consult only user/global .npmrc) +// +// In both skipped cases, the CLI and .npmrc layers are still consulted; +// only the project package.json layer is skipped. +// +// The first source with any configuration wins for the entire install; +// lower layers are ignored. A `log.warn` is emitted whenever a setting is +// being suppressed by a higher-priority source. +// +// Reads `package.json` from `npm.prefix` (not `npm.localPrefix`) so an +// install run from a workspace sub-directory still picks up the project +// root's policy. +const resolveAllowScripts = async (npm, { skipProjectConfig = false } = {}) => { + // Independently probe each RFC layer. + const cliPolicy = policyFromSources(npm, ['cli', 'env']) + const npmrcPolicy = policyFromSources(npm, ['project', 'user', 'global', 'builtin']) + + // The --allow-scripts CLI flag is intended for one-off and global + // contexts (npm exec, npx, npm install -g). In a project-scoped install, + // team policy belongs in package.json or .npmrc, so reject the flag + // outright to avoid the "works on my machine" footgun. + if (cliPolicy && !npm.global && !skipProjectConfig) { + throw Object.assign( + new Error( + '--allow-scripts is not allowed in project-scoped installs. ' + + 'Add the entries to the "allowScripts" field in package.json, ' + + 'or to .npmrc, instead.' + ), + { code: 'EALLOWSCRIPTS' } + ) + } + + // Project package.json is consulted only when the caller is operating + // inside a real project (not -g, not npx). + let pkgPolicy = null + if (!npm.global && !skipProjectConfig) { + try { + const { content } = await pkgJson.normalize(npm.prefix) + if (content?.allowScripts && typeof content.allowScripts === 'object') { + const entries = Object.entries(content.allowScripts) + if (entries.length > 0) { + pkgPolicy = Object.fromEntries(entries) + } + } + } catch (err) { + log.silly('allow-scripts', 'no package.json at prefix', err.message) + } + } + + // Validate each candidate layer: drop forbidden ranges, warn the user. + const cli = validatePolicy(cliPolicy, 'CLI flag') + const pkg = validatePolicy(pkgPolicy, 'package.json') + const rc = validatePolicy(npmrcPolicy, '.npmrc') + + // Apply RFC precedence. + if (cli) { + // Note: `pkg` is never set alongside `cli` here. Project package.json is + // only read when `!npm.global && !skipProjectConfig`, but in that same + // case a CLI policy throws above. With `npm.global` or skipProjectConfig + // set, package.json is never consulted. + if (rc) { + log.warn( + 'allow-scripts', + '.npmrc allow-scripts setting is being ignored because --allow-scripts was passed on the command line' + ) + } + return { policy: cli, source: 'cli' } + } + + if (pkg) { + if (rc) { + log.warn( + 'allow-scripts', + '.npmrc allow-scripts setting is being ignored because package.json declares its own allowScripts field' + ) + } + return { policy: pkg, source: 'package.json' } + } + + if (rc) { + return { policy: rc, source: '.npmrc' } + } + + return { policy: null, source: null } +} + +module.exports = resolveAllowScripts diff --git a/deps/npm/lib/utils/sbom-cyclonedx.js b/deps/npm/lib/utils/sbom-cyclonedx.js index f8283397989d5b..fe368e968baaa7 100644 --- a/deps/npm/lib/utils/sbom-cyclonedx.js +++ b/deps/npm/lib/utils/sbom-cyclonedx.js @@ -170,13 +170,20 @@ const toCyclonedxItem = (node, { packageType }) => { } const toCyclonedxDependency = (node, nodes) => { - return { - ref: toCyclonedxID(node), - dependsOn: [...node.edgesOut.values()] + // A node can have multiple outgoing edges resolving to the same + // `name@version` (e.g. via npm aliases like `foo: npm:bar@1` alongside a + // direct `bar: ^1` dep), which would produce duplicate entries in + // `dependsOn`. CycloneDX 1.5 requires unique items, so dedupe by ref. + const dependsOn = [...new Set( + [...node.edgesOut.values()] // Filter out edges that are linking to nodes not in the list .filter(edge => nodes.find(n => n === edge.to)) .map(edge => toCyclonedxID(edge.to)) - .filter(id => id), + .filter(id => id) + )] + return { + ref: toCyclonedxID(node), + dependsOn, } } diff --git a/deps/npm/lib/utils/sbom-spdx.js b/deps/npm/lib/utils/sbom-spdx.js index 38824f263681d0..8ea75c688bc862 100644 --- a/deps/npm/lib/utils/sbom-spdx.js +++ b/deps/npm/lib/utils/sbom-spdx.js @@ -48,11 +48,23 @@ const spdxOutput = ({ npm, nodes, packageType }) => { } seen.add(node) + // A node can have multiple outgoing edges resolving to the same + // `name@version` of the same edge type (e.g. via npm aliases), which + // would produce identical relationship triples. Dedupe per source node. + const seenRels = new Set() const rels = [...node.edgesOut.values()] // Filter out edges that are linking to nodes not in the list .filter(edge => nodes.find(n => n === edge.to)) .map(edge => toSpdxRelationship(node, edge)) .filter(rel => rel) + .filter(rel => { + const key = `${rel.spdxElementId}|${rel.relatedSpdxElement}|${rel.relationshipType}` + if (seenRels.has(key)) { + return false + } + seenRels.add(key) + return true + }) relationships.push(...rels) } diff --git a/deps/npm/lib/utils/strict-allow-scripts-preflight.js b/deps/npm/lib/utils/strict-allow-scripts-preflight.js new file mode 100644 index 00000000000000..a3f83ea4b662bc --- /dev/null +++ b/deps/npm/lib/utils/strict-allow-scripts-preflight.js @@ -0,0 +1,61 @@ +const checkAllowScripts = require('./check-allow-scripts.js') + +// Pre-flight check for `--strict-allow-scripts`. Call after arborist has +// been constructed but before `arb.reify()` runs, so that install scripts +// never execute when strict mode would block them. +// +// Behaviour: +// - No-op unless `npm.flatOptions.strictAllowScripts` is set. +// - Bypassed by `--dangerously-allow-all-scripts` and `--ignore-scripts` +// (the per-flag short-circuits already live in checkAllowScripts). +// - Builds the ideal tree (idempotent — subsequent reify reuses it), +// walks it for nodes whose install scripts have not been covered by +// the `allowScripts` policy, and throws if any are found. +const strictAllowScriptsPreflight = async ({ arb, npm, idealTreeOpts }) => { + if (!npm?.flatOptions?.strictAllowScripts) { + return + } + + // Prefer the idealTree when reify is about to run; fall back to + // actualTree for npm rebuild (which never builds an ideal tree). + let tree + if (idealTreeOpts !== undefined) { + // `npm ci` builds the ideal tree before calling the preflight, so + // skip the rebuild when one already exists. `npm install` calls the + // preflight before reify and needs us to build. + if (!arb.idealTree) { + await arb.buildIdealTree(idealTreeOpts) + } + tree = arb.idealTree + } else { + tree = arb.actualTree + } + + const unreviewed = await checkAllowScripts({ arb, npm, tree }) + if (unreviewed.length === 0) { + return + } + + const lines = unreviewed.map(({ node, scripts }) => { + const events = Object.entries(scripts) + .map(([event, body]) => `${event}: ${body}`) + .join('; ') + const name = node.package?.name || node.name + const version = node.package?.version || '' + const label = version ? `${name}@${version}` : name + return ` ${label} (${events})` + }).join('\n') + + throw Object.assign( + new Error( + `--strict-allow-scripts: ${unreviewed.length} package(s) have install ` + + `scripts not covered by allowScripts:\n${lines}\n` + + 'Approve them with `npm approve-scripts`, deny them with ' + + '`npm deny-scripts`, or bypass this check with ' + + '`--dangerously-allow-all-scripts`.' + ), + { code: 'ESTRICTALLOWSCRIPTS' } + ) +} + +module.exports = strictAllowScriptsPreflight diff --git a/deps/npm/lib/utils/tar.js b/deps/npm/lib/utils/tar.js index a744dca3132578..6b6870678c0973 100644 --- a/deps/npm/lib/utils/tar.js +++ b/deps/npm/lib/utils/tar.js @@ -1,15 +1,16 @@ const tar = require('tar') const ssri = require('ssri') -const { log, output } = require('proc-log') +const { log, output, META } = require('proc-log') const formatBytes = require('./format-bytes.js') const localeCompare = require('@isaacs/string-locale-compare')('en', { sensitivity: 'case', numeric: true, }) -const logTar = (tarball, { unicode = false, json, key } = {}) => { +const logTar = (tarball, { unicode = false, json, key, redact } = {}) => { if (json) { - output.buffer(key == null ? tarball : { [key]: tarball }) + const meta = redact === false ? { [META]: true, redact: false } : undefined + output.buffer(key == null ? tarball : { [key]: tarball }, meta) return } log.notice('') diff --git a/deps/npm/lib/utils/validate-uuid.js b/deps/npm/lib/utils/validate-uuid.js new file mode 100644 index 00000000000000..d5842429303f60 --- /dev/null +++ b/deps/npm/lib/utils/validate-uuid.js @@ -0,0 +1,10 @@ +// UUID validation regex +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + +const validateUUID = (value, fieldName) => { + if (!UUID_REGEX.test(value)) { + throw new Error(`${fieldName} must be a valid UUID`) + } +} + +module.exports = { UUID_REGEX, validateUUID } diff --git a/deps/npm/lib/utils/warn-workspace-allow-scripts.js b/deps/npm/lib/utils/warn-workspace-allow-scripts.js new file mode 100644 index 00000000000000..e46e6cf4d2a10b --- /dev/null +++ b/deps/npm/lib/utils/warn-workspace-allow-scripts.js @@ -0,0 +1,40 @@ +const { log } = require('proc-log') + +// The allowScripts policy MUST live at the project root (RFC npm/rfcs#868). +// A non-root workspace declaring its own allowScripts is almost always a +// mistake: that policy would be silently ignored at install time. +// +// `findWorkspaceAllowScripts` returns the list of offending workspace nodes. +// `warnWorkspaceAllowScripts` is the side-effecting variant that emits one +// install-time `log.warn` per offender. + +const findWorkspaceAllowScripts = (tree) => { + const offenders = [] + if (!tree?.inventory) { + return offenders + } + for (const node of tree.inventory.values()) { + if (!node.isWorkspace || node.isProjectRoot) { + continue + } + if (node.package?.allowScripts !== undefined) { + offenders.push(node) + } + } + return offenders +} + +const warnWorkspaceAllowScripts = (tree) => { + for (const node of findWorkspaceAllowScripts(tree)) { + const name = node.packageName || node.name + log.warn( + 'allow-scripts', + `allowScripts in workspace ${name} (${node.path}) is ignored. ` + + 'Move the field to the project root package.json.' + ) + } +} + +module.exports = warnWorkspaceAllowScripts +module.exports.warnWorkspaceAllowScripts = warnWorkspaceAllowScripts +module.exports.findWorkspaceAllowScripts = findWorkspaceAllowScripts diff --git a/deps/npm/man/man1/npm-access.1 b/deps/npm/man/man1/npm-access.1 index 13c9af41efc167..1137b4d5fbee07 100644 --- a/deps/npm/man/man1/npm-access.1 +++ b/deps/npm/man/man1/npm-access.1 @@ -1,4 +1,4 @@ -.TH "NPM-ACCESS" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-ACCESS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-access\fR - Set access level on published packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-adduser.1 b/deps/npm/man/man1/npm-adduser.1 index a9ce18423f7286..b52bf3ed7f980e 100644 --- a/deps/npm/man/man1/npm-adduser.1 +++ b/deps/npm/man/man1/npm-adduser.1 @@ -1,4 +1,4 @@ -.TH "NPM-ADDUSER" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-ADDUSER" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-adduser\fR - Add a registry user account .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-approve-scripts.1 b/deps/npm/man/man1/npm-approve-scripts.1 new file mode 100644 index 00000000000000..b6ac279a025584 --- /dev/null +++ b/deps/npm/man/man1/npm-approve-scripts.1 @@ -0,0 +1,113 @@ +.TH "NPM-APPROVE-SCRIPTS" "1" "May 2026" "NPM@11.16.0" "" +.SH "NAME" +\fBnpm-approve-scripts\fR - Approve install scripts for specific dependencies +.SS "Synopsis" +.P +.RS 2 +.nf +npm approve-scripts \[lB] ...\[rB] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +.fi +.RE +.P +Note: This command is unaware of workspaces. +.SS "Description" +.P +Manages the \fBallowScripts\fR field in your project's \fBpackage.json\fR, which records which of your dependencies are permitted to run install scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry sources). This command is the recommended way to maintain that field. +.P +In the current release, this field is advisory: install scripts still run by default, but installs print a list of packages whose scripts have not been reviewed. A future release will block unreviewed install scripts. +.P +There are three modes: +.P +.RS 2 +.nf +npm approve-scripts \[lB] ...\[rB] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +.fi +.RE +.P +\fB\fR matches every installed version of that package. By default the command writes pinned entries (\fBpkg@1.2.3\fR), which keep their approval narrowed to the specific version you reviewed. Pass \fB--no-allow-scripts-pin\fR to write name-only entries that allow any future version. +.P +\fB--all\fR approves every package with unreviewed install scripts in one go. +.P +\fB--allow-scripts-pending\fR is read-only: it lists every package whose install scripts are not yet covered by \fBallowScripts\fR, without modifying \fBpackage.json\fR. +.P +\fBapprove-scripts\fR honours the asymmetric pin rule: if you re-approve a package whose installed version has changed, the existing pin is rewritten to track the new installed version. Multi-version statements (\fBpkg@1 || 2\fR) are left alone, since they likely capture intent that the command cannot infer. Existing \fBfalse\fR entries always win; \fBapprove-scripts\fR will not silently re-allow a package you previously denied. +.SS "Examples" +.P +.RS 2 +.nf +# Approve all currently-installed install scripts after reviewing them +npm approve-scripts --all + +# Approve specific packages, pinned to their installed version +npm approve-scripts canvas sharp + +# Approve name-only (any version of this package is allowed) +npm approve-scripts --no-allow-scripts-pin canvas + +# Preview which packages still need review +npm approve-scripts --allow-scripts-pending +.fi +.RE +.SS "Configuration" +.SS "\fBall\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +When running \fBnpm outdated\fR and \fBnpm ls\fR, setting \fB--all\fR will show all outdated or installed packages, rather than only those directly depended upon by the current project. +.SS "\fBallow-scripts-pending\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +List packages with install scripts that are not yet covered by the \fBallowScripts\fR policy, without modifying \fBpackage.json\fR. Only meaningful for \fBnpm approve-scripts\fR. +.SS "\fBallow-scripts-pin\fR" +.RS 0 +.IP \(bu 4 +Default: true +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Write pinned (\fBpkg@version\fR) entries when approving install scripts. Set to \fBfalse\fR to write name-only entries that allow any version. Has no effect on \fBnpm deny-scripts\fR, which always writes name-only entries regardless of this setting. +.SS "\fBjson\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Whether or not to output JSON data, rather than the normal output. +.RS 0 +.IP \(bu 4 +In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. +.RE 0 + +.P +Not supported by all npm commands. +.SS "See Also" +.RS 0 +.IP \(bu 4 +npm help deny-scripts +.IP \(bu 4 +npm help install +.IP \(bu 4 +npm help rebuild +.IP \(bu 4 +\fBpackage.json\fR \fI\(la/configuring-npm/package-json\(ra\fR +.RE 0 diff --git a/deps/npm/man/man1/npm-audit.1 b/deps/npm/man/man1/npm-audit.1 index 6760fe1e954125..f066529a8b8530 100644 --- a/deps/npm/man/man1/npm-audit.1 +++ b/deps/npm/man/man1/npm-audit.1 @@ -1,4 +1,4 @@ -.TH "NPM-AUDIT" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-AUDIT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-audit\fR - Run a security audit .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-bugs.1 b/deps/npm/man/man1/npm-bugs.1 index 474b08f5d76a26..6c9d745c20f3f2 100644 --- a/deps/npm/man/man1/npm-bugs.1 +++ b/deps/npm/man/man1/npm-bugs.1 @@ -1,4 +1,4 @@ -.TH "NPM-BUGS" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-BUGS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-bugs\fR - Report bugs for a package in a web browser .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-cache.1 b/deps/npm/man/man1/npm-cache.1 index e20349de377056..17e38319e16223 100644 --- a/deps/npm/man/man1/npm-cache.1 +++ b/deps/npm/man/man1/npm-cache.1 @@ -1,4 +1,4 @@ -.TH "NPM-CACHE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-CACHE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-cache\fR - Manipulates packages cache .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ci.1 b/deps/npm/man/man1/npm-ci.1 index 21f461e0fdd1db..467929979eda6c 100644 --- a/deps/npm/man/man1/npm-ci.1 +++ b/deps/npm/man/man1/npm-ci.1 @@ -1,4 +1,4 @@ -.TH "NPM-CI" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-CI" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ci\fR - Clean install a project .SS "Synopsis" @@ -168,6 +168,30 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -177,9 +201,57 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-completion.1 b/deps/npm/man/man1/npm-completion.1 index ad07723f8e19dd..c6a82af87d93fa 100644 --- a/deps/npm/man/man1/npm-completion.1 +++ b/deps/npm/man/man1/npm-completion.1 @@ -1,4 +1,4 @@ -.TH "NPM-COMPLETION" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-COMPLETION" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-completion\fR - Tab Completion for npm .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-config.1 b/deps/npm/man/man1/npm-config.1 index cb4f7c4a21f478..28ae9ed07de461 100644 --- a/deps/npm/man/man1/npm-config.1 +++ b/deps/npm/man/man1/npm-config.1 @@ -1,4 +1,4 @@ -.TH "NPM-CONFIG" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-CONFIG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-config\fR - Manage the npm configuration files .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-dedupe.1 b/deps/npm/man/man1/npm-dedupe.1 index 97eb2af71fde23..c62112ff7f9a8c 100644 --- a/deps/npm/man/man1/npm-dedupe.1 +++ b/deps/npm/man/man1/npm-dedupe.1 @@ -1,4 +1,4 @@ -.TH "NPM-DEDUPE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-DEDUPE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-dedupe\fR - Reduce duplication in the package tree .SS "Synopsis" @@ -165,6 +165,30 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -174,9 +198,21 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-deny-scripts.1 b/deps/npm/man/man1/npm-deny-scripts.1 new file mode 100644 index 00000000000000..a466da7a30dc8f --- /dev/null +++ b/deps/npm/man/man1/npm-deny-scripts.1 @@ -0,0 +1,99 @@ +.TH "NPM-DENY-SCRIPTS" "1" "May 2026" "NPM@11.16.0" "" +.SH "NAME" +\fBnpm-deny-scripts\fR - Deny install scripts for specific dependencies +.SS "Synopsis" +.P +.RS 2 +.nf +npm deny-scripts \[lB] ...\[rB] +npm deny-scripts --all +.fi +.RE +.P +Note: This command is unaware of workspaces. +.SS "Description" +.P +The companion command to npm help approve-scripts. Writes \fBfalse\fR entries into the \fBallowScripts\fR field of your project's \fBpackage.json\fR, recording that a dependency must not run install scripts even if a future version would otherwise be eligible. +.P +In the current release, install scripts still run by default, so \fBdeny-scripts\fR only affects how installs of denied packages are reported. A future release will block unreviewed install scripts and respect deny entries at install time. +.P +.RS 2 +.nf +npm deny-scripts \[lB] ...\[rB] +npm deny-scripts --all +.fi +.RE +.P +\fB\fR matches every installed version of that package. Denies are always written name-only (\fB"pkg": false\fR), regardless of \fB--allow-scripts-pin\fR. Pinning a deny to a specific version would silently re-allow scripts for any other version of the same package, which defeats the purpose; the command picks the safer default for you. +.P +\fB--all\fR denies every package with unreviewed install scripts. +.P +If a \fBtrue\fR (pinned or name-only) entry exists for a package and you then deny it, the existing allow entries are removed so the name-only deny is unambiguous. +.SS "Examples" +.P +.RS 2 +.nf +# Deny a specific package outright +npm deny-scripts telemetry-pkg + +# Deny everything that has install scripts and isn't already approved +npm deny-scripts --all +.fi +.RE +.SS "Configuration" +.SS "\fBall\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +When running \fBnpm outdated\fR and \fBnpm ls\fR, setting \fB--all\fR will show all outdated or installed packages, rather than only those directly depended upon by the current project. +.SS "\fBallow-scripts-pending\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +List packages with install scripts that are not yet covered by the \fBallowScripts\fR policy, without modifying \fBpackage.json\fR. Only meaningful for \fBnpm approve-scripts\fR. +.SS "\fBallow-scripts-pin\fR" +.RS 0 +.IP \(bu 4 +Default: true +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Write pinned (\fBpkg@version\fR) entries when approving install scripts. Set to \fBfalse\fR to write name-only entries that allow any version. Has no effect on \fBnpm deny-scripts\fR, which always writes name-only entries regardless of this setting. +.SS "\fBjson\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Whether or not to output JSON data, rather than the normal output. +.RS 0 +.IP \(bu 4 +In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. +.RE 0 + +.P +Not supported by all npm commands. +.SS "See Also" +.RS 0 +.IP \(bu 4 +npm help approve-scripts +.IP \(bu 4 +npm help install +.IP \(bu 4 +\fBpackage.json\fR \fI\(la/configuring-npm/package-json\(ra\fR +.RE 0 diff --git a/deps/npm/man/man1/npm-deprecate.1 b/deps/npm/man/man1/npm-deprecate.1 index 8c44f55a1a52b0..0e67565fba394c 100644 --- a/deps/npm/man/man1/npm-deprecate.1 +++ b/deps/npm/man/man1/npm-deprecate.1 @@ -1,4 +1,4 @@ -.TH "NPM-DEPRECATE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-DEPRECATE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-deprecate\fR - Deprecate a version of a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-diff.1 b/deps/npm/man/man1/npm-diff.1 index 5b4ab2fcebcb1d..1051276045fc84 100644 --- a/deps/npm/man/man1/npm-diff.1 +++ b/deps/npm/man/man1/npm-diff.1 @@ -1,4 +1,4 @@ -.TH "NPM-DIFF" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-DIFF" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-diff\fR - The registry diff command .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-dist-tag.1 b/deps/npm/man/man1/npm-dist-tag.1 index 768fe6d575ea48..a2a7e0ea36a018 100644 --- a/deps/npm/man/man1/npm-dist-tag.1 +++ b/deps/npm/man/man1/npm-dist-tag.1 @@ -1,4 +1,4 @@ -.TH "NPM-DIST-TAG" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-DIST-TAG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-dist-tag\fR - Modify package distribution tags .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-docs.1 b/deps/npm/man/man1/npm-docs.1 index ed9f35f00bebea..943c26eb53fbe3 100644 --- a/deps/npm/man/man1/npm-docs.1 +++ b/deps/npm/man/man1/npm-docs.1 @@ -1,4 +1,4 @@ -.TH "NPM-DOCS" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-DOCS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-docs\fR - Open documentation for a package in a web browser .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-doctor.1 b/deps/npm/man/man1/npm-doctor.1 index d7f5ef43edf1f4..0d40b9b45b14d2 100644 --- a/deps/npm/man/man1/npm-doctor.1 +++ b/deps/npm/man/man1/npm-doctor.1 @@ -1,4 +1,4 @@ -.TH "NPM-DOCTOR" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-DOCTOR" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-doctor\fR - Check the health of your npm environment .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-edit.1 b/deps/npm/man/man1/npm-edit.1 index 83753a5fc423ab..d57bdc0ce6f910 100644 --- a/deps/npm/man/man1/npm-edit.1 +++ b/deps/npm/man/man1/npm-edit.1 @@ -1,4 +1,4 @@ -.TH "NPM-EDIT" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-EDIT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-edit\fR - Edit an installed package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-exec.1 b/deps/npm/man/man1/npm-exec.1 index d6f20fe3b587b6..8987175e5d8410 100644 --- a/deps/npm/man/man1/npm-exec.1 +++ b/deps/npm/man/man1/npm-exec.1 @@ -1,4 +1,4 @@ -.TH "NPM-EXEC" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-EXEC" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-exec\fR - Run a command from a local or remote npm package .SS "Synopsis" @@ -167,6 +167,42 @@ Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the \fBworkspace\fR config, or all workspaces via the \fBworkspaces\fR flag, will cause npm to operate only on the specified workspaces, and not on the root project. .P This value is not exported to the environment for child processes. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "Examples" .P Run the version of \fBtap\fR in the local dependencies, with the provided arguments: diff --git a/deps/npm/man/man1/npm-explain.1 b/deps/npm/man/man1/npm-explain.1 index 3f981b60850469..ec315300c2f1ba 100644 --- a/deps/npm/man/man1/npm-explain.1 +++ b/deps/npm/man/man1/npm-explain.1 @@ -1,4 +1,4 @@ -.TH "NPM-EXPLAIN" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-EXPLAIN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-explain\fR - Explain installed packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-explore.1 b/deps/npm/man/man1/npm-explore.1 index f8fad8822edd5b..fbc61b1de01014 100644 --- a/deps/npm/man/man1/npm-explore.1 +++ b/deps/npm/man/man1/npm-explore.1 @@ -1,4 +1,4 @@ -.TH "NPM-EXPLORE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-EXPLORE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-explore\fR - Browse an installed package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-find-dupes.1 b/deps/npm/man/man1/npm-find-dupes.1 index 5ea1a5e19df5b9..57daa9d322595e 100644 --- a/deps/npm/man/man1/npm-find-dupes.1 +++ b/deps/npm/man/man1/npm-find-dupes.1 @@ -1,4 +1,4 @@ -.TH "NPM-FIND-DUPES" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-FIND-DUPES" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-find-dupes\fR - Find duplication in the package tree .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-fund.1 b/deps/npm/man/man1/npm-fund.1 index fc0e31efb0f96e..05a0177501955f 100644 --- a/deps/npm/man/man1/npm-fund.1 +++ b/deps/npm/man/man1/npm-fund.1 @@ -1,4 +1,4 @@ -.TH "NPM-FUND" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-FUND" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-fund\fR - Retrieve funding information .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-get.1 b/deps/npm/man/man1/npm-get.1 index e05435badd204c..bbcfcae3a21158 100644 --- a/deps/npm/man/man1/npm-get.1 +++ b/deps/npm/man/man1/npm-get.1 @@ -1,4 +1,4 @@ -.TH "NPM-GET" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-GET" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-get\fR - Get a value from the npm configuration .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-help-search.1 b/deps/npm/man/man1/npm-help-search.1 index ed23a840be992b..b50eb9a9ac9c5b 100644 --- a/deps/npm/man/man1/npm-help-search.1 +++ b/deps/npm/man/man1/npm-help-search.1 @@ -1,4 +1,4 @@ -.TH "NPM-HELP-SEARCH" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-HELP-SEARCH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-help-search\fR - Search npm help documentation .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-help.1 b/deps/npm/man/man1/npm-help.1 index 191480d5c3fe4e..eb4353fbd7c0ea 100644 --- a/deps/npm/man/man1/npm-help.1 +++ b/deps/npm/man/man1/npm-help.1 @@ -1,4 +1,4 @@ -.TH "NPM-HELP" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-HELP" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-help\fR - Get help on npm .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-init.1 b/deps/npm/man/man1/npm-init.1 index 6c7f7af97d5a0e..a55bf5cf7e42d3 100644 --- a/deps/npm/man/man1/npm-init.1 +++ b/deps/npm/man/man1/npm-init.1 @@ -1,4 +1,4 @@ -.TH "NPM-INIT" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-INIT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-init\fR - Create a package.json file .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-install-ci-test.1 b/deps/npm/man/man1/npm-install-ci-test.1 index c1855691bfbc14..4d0d125aac3a57 100644 --- a/deps/npm/man/man1/npm-install-ci-test.1 +++ b/deps/npm/man/man1/npm-install-ci-test.1 @@ -1,4 +1,4 @@ -.TH "NPM-INSTALL-CI-TEST" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-INSTALL-CI-TEST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-install-ci-test\fR - Install a project with a clean slate and run tests .SS "Synopsis" @@ -116,6 +116,30 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -125,9 +149,57 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-install-test.1 b/deps/npm/man/man1/npm-install-test.1 index ce4172bf42b0ad..dd238cbf6c613b 100644 --- a/deps/npm/man/man1/npm-install-test.1 +++ b/deps/npm/man/man1/npm-install-test.1 @@ -1,4 +1,4 @@ -.TH "NPM-INSTALL-TEST" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-INSTALL-TEST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-install-test\fR - Install package(s) and run tests .SS "Synopsis" @@ -193,6 +193,30 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -202,9 +226,57 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 @@ -228,7 +300,7 @@ If passed to \fBnpm install\fR, will rebuild the npm tree such that only version .P If the requested version is a \fBdist-tag\fR and the given tag does not pass the \fB--before\fR filter, the most recent version less than or equal to that tag will be used. For example, \fBfoo@latest\fR might install \fBfoo@1.2\fR even though \fBlatest\fR is \fB2.0\fR. .P -This config cannot be used with: \fBmin-release-age\fR +If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .SS "\fBmin-release-age\fR" .RS 0 .IP \(bu 4 @@ -240,9 +312,7 @@ Type: null or Number .P If set, npm will build the npm tree such that only versions that were available more than the given number of days ago will be installed. If there are no versions available for the current set of dependencies, the command will error. .P -This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. -.P -This config cannot be used with: \fBbefore\fR +This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P This value is not exported to the environment for child processes. .SS "\fBbin-links\fR" diff --git a/deps/npm/man/man1/npm-install.1 b/deps/npm/man/man1/npm-install.1 index 92df99f6437c96..b51006e58e26d1 100644 --- a/deps/npm/man/man1/npm-install.1 +++ b/deps/npm/man/man1/npm-install.1 @@ -1,4 +1,4 @@ -.TH "NPM-INSTALL" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-INSTALL" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-install\fR - Install a package .SS "Synopsis" @@ -583,6 +583,30 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -592,9 +616,57 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 @@ -618,7 +690,7 @@ If passed to \fBnpm install\fR, will rebuild the npm tree such that only version .P If the requested version is a \fBdist-tag\fR and the given tag does not pass the \fB--before\fR filter, the most recent version less than or equal to that tag will be used. For example, \fBfoo@latest\fR might install \fBfoo@1.2\fR even though \fBlatest\fR is \fB2.0\fR. .P -This config cannot be used with: \fBmin-release-age\fR +If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .SS "\fBmin-release-age\fR" .RS 0 .IP \(bu 4 @@ -630,9 +702,7 @@ Type: null or Number .P If set, npm will build the npm tree such that only versions that were available more than the given number of days ago will be installed. If there are no versions available for the current set of dependencies, the command will error. .P -This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. -.P -This config cannot be used with: \fBbefore\fR +This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P This value is not exported to the environment for child processes. .SS "\fBbin-links\fR" diff --git a/deps/npm/man/man1/npm-link.1 b/deps/npm/man/man1/npm-link.1 index 749145e10948f0..5fc0057bfe9b6b 100644 --- a/deps/npm/man/man1/npm-link.1 +++ b/deps/npm/man/man1/npm-link.1 @@ -1,4 +1,4 @@ -.TH "NPM-LINK" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-LINK" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-link\fR - Symlink a package folder .SS "Synopsis" @@ -224,6 +224,30 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -233,9 +257,21 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-ll.1 b/deps/npm/man/man1/npm-ll.1 index d7eb6abb2be468..860cb296e86a60 100644 --- a/deps/npm/man/man1/npm-ll.1 +++ b/deps/npm/man/man1/npm-ll.1 @@ -1,4 +1,4 @@ -.TH "NPM-LL" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-LL" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ll\fR - List installed packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-login.1 b/deps/npm/man/man1/npm-login.1 index 159897bd393f39..037be1840a4f1a 100644 --- a/deps/npm/man/man1/npm-login.1 +++ b/deps/npm/man/man1/npm-login.1 @@ -1,4 +1,4 @@ -.TH "NPM-LOGIN" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-LOGIN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-login\fR - Login to a registry user account .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-logout.1 b/deps/npm/man/man1/npm-logout.1 index 2f8be327eb1816..d3fdd55251f4ab 100644 --- a/deps/npm/man/man1/npm-logout.1 +++ b/deps/npm/man/man1/npm-logout.1 @@ -1,4 +1,4 @@ -.TH "NPM-LOGOUT" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-LOGOUT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-logout\fR - Log out of the registry .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ls.1 b/deps/npm/man/man1/npm-ls.1 index 0a6f9a49df3c5d..ac695c5c633c55 100644 --- a/deps/npm/man/man1/npm-ls.1 +++ b/deps/npm/man/man1/npm-ls.1 @@ -1,4 +1,4 @@ -.TH "NPM-LS" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-LS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ls\fR - List installed packages .SS "Synopsis" @@ -20,7 +20,7 @@ Positional arguments are \fBname@version-range\fR identifiers, which will limit .P .RS 2 .nf -npm@11.13.0 /path/to/npm +npm@11.16.0 /path/to/npm └─┬ init-package-json@0.0.4 └── promzard@0.1.5 .fi diff --git a/deps/npm/man/man1/npm-org.1 b/deps/npm/man/man1/npm-org.1 index 4afed2e97b2cc5..25f1ca4680cd27 100644 --- a/deps/npm/man/man1/npm-org.1 +++ b/deps/npm/man/man1/npm-org.1 @@ -1,4 +1,4 @@ -.TH "NPM-ORG" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-ORG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-org\fR - Manage orgs .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-outdated.1 b/deps/npm/man/man1/npm-outdated.1 index 59a87bd862c9c7..462141f446fa46 100644 --- a/deps/npm/man/man1/npm-outdated.1 +++ b/deps/npm/man/man1/npm-outdated.1 @@ -1,4 +1,4 @@ -.TH "NPM-OUTDATED" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-OUTDATED" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-outdated\fR - Check for outdated packages .SS "Synopsis" @@ -180,7 +180,7 @@ If passed to \fBnpm install\fR, will rebuild the npm tree such that only version .P If the requested version is a \fBdist-tag\fR and the given tag does not pass the \fB--before\fR filter, the most recent version less than or equal to that tag will be used. For example, \fBfoo@latest\fR might install \fBfoo@1.2\fR even though \fBlatest\fR is \fB2.0\fR. .P -This config cannot be used with: \fBmin-release-age\fR +If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .SS "\fBmin-release-age\fR" .RS 0 .IP \(bu 4 @@ -192,9 +192,7 @@ Type: null or Number .P If set, npm will build the npm tree such that only versions that were available more than the given number of days ago will be installed. If there are no versions available for the current set of dependencies, the command will error. .P -This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. -.P -This config cannot be used with: \fBbefore\fR +This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P This value is not exported to the environment for child processes. .SS "See Also" diff --git a/deps/npm/man/man1/npm-owner.1 b/deps/npm/man/man1/npm-owner.1 index 9be1dad02c0ca6..7a90c8e0c28856 100644 --- a/deps/npm/man/man1/npm-owner.1 +++ b/deps/npm/man/man1/npm-owner.1 @@ -1,4 +1,4 @@ -.TH "NPM-OWNER" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-OWNER" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-owner\fR - Manage package owners .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-pack.1 b/deps/npm/man/man1/npm-pack.1 index 7491a4154ec658..945b3f42e3c910 100644 --- a/deps/npm/man/man1/npm-pack.1 +++ b/deps/npm/man/man1/npm-pack.1 @@ -1,4 +1,4 @@ -.TH "NPM-PACK" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PACK" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-pack\fR - Create a tarball from a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ping.1 b/deps/npm/man/man1/npm-ping.1 index cf20722885fa78..0c9f579acb2cd5 100644 --- a/deps/npm/man/man1/npm-ping.1 +++ b/deps/npm/man/man1/npm-ping.1 @@ -1,4 +1,4 @@ -.TH "NPM-PING" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PING" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ping\fR - Ping npm registry .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-pkg.1 b/deps/npm/man/man1/npm-pkg.1 index 8622f1fe5aaba7..525cc2fa92f67d 100644 --- a/deps/npm/man/man1/npm-pkg.1 +++ b/deps/npm/man/man1/npm-pkg.1 @@ -1,4 +1,4 @@ -.TH "NPM-PKG" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PKG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-pkg\fR - Manages your package.json .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-prefix.1 b/deps/npm/man/man1/npm-prefix.1 index 83b8d8cc0df771..0ad1dced9b9fca 100644 --- a/deps/npm/man/man1/npm-prefix.1 +++ b/deps/npm/man/man1/npm-prefix.1 @@ -1,4 +1,4 @@ -.TH "NPM-PREFIX" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PREFIX" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-prefix\fR - Display prefix .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-profile.1 b/deps/npm/man/man1/npm-profile.1 index 703f320dc298c3..ca1cdd366d80e1 100644 --- a/deps/npm/man/man1/npm-profile.1 +++ b/deps/npm/man/man1/npm-profile.1 @@ -1,4 +1,4 @@ -.TH "NPM-PROFILE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PROFILE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-profile\fR - Change settings on your registry profile .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-prune.1 b/deps/npm/man/man1/npm-prune.1 index 7f3d08ca5633d2..b7dc1e212c4c89 100644 --- a/deps/npm/man/man1/npm-prune.1 +++ b/deps/npm/man/man1/npm-prune.1 @@ -1,4 +1,4 @@ -.TH "NPM-PRUNE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PRUNE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-prune\fR - Remove extraneous packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-publish.1 b/deps/npm/man/man1/npm-publish.1 index f527741ac1963a..3bbb7139839af8 100644 --- a/deps/npm/man/man1/npm-publish.1 +++ b/deps/npm/man/man1/npm-publish.1 @@ -1,4 +1,4 @@ -.TH "NPM-PUBLISH" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-PUBLISH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-publish\fR - Publish a package .SS "Synopsis" @@ -120,7 +120,7 @@ If used in the \fBnpm publish\fR command, this is the tag that will be added to .IP \(bu 4 Default: 'public' for new packages, existing packages it will not change the current level .IP \(bu 4 -Type: null, "restricted", or "public" +Type: null, "restricted", "public", or "private" .RE 0 .P @@ -130,6 +130,8 @@ Unscoped packages cannot be set to \fBrestricted\fR. .P Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. +.P +The value \fBprivate\fR is an alias for \fBrestricted\fR. .SS "\fBdry-run\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-query.1 b/deps/npm/man/man1/npm-query.1 index d8e792f3d0be1f..cc73ad1e92470f 100644 --- a/deps/npm/man/man1/npm-query.1 +++ b/deps/npm/man/man1/npm-query.1 @@ -1,4 +1,4 @@ -.TH "NPM-QUERY" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-QUERY" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-query\fR - Dependency selector query .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-rebuild.1 b/deps/npm/man/man1/npm-rebuild.1 index c19b76fe7597ec..af0562603369e6 100644 --- a/deps/npm/man/man1/npm-rebuild.1 +++ b/deps/npm/man/man1/npm-rebuild.1 @@ -1,4 +1,4 @@ -.TH "NPM-REBUILD" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-REBUILD" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-rebuild\fR - Rebuild a package .SS "Synopsis" @@ -101,6 +101,42 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBworkspace\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-repo.1 b/deps/npm/man/man1/npm-repo.1 index 71c912c1f672b6..00b1754b495937 100644 --- a/deps/npm/man/man1/npm-repo.1 +++ b/deps/npm/man/man1/npm-repo.1 @@ -1,4 +1,4 @@ -.TH "NPM-REPO" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-REPO" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-repo\fR - Open package repository page in the browser .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-restart.1 b/deps/npm/man/man1/npm-restart.1 index 75f8758112eb29..5fb602be957ba2 100644 --- a/deps/npm/man/man1/npm-restart.1 +++ b/deps/npm/man/man1/npm-restart.1 @@ -1,4 +1,4 @@ -.TH "NPM-RESTART" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-RESTART" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-restart\fR - Restart a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-root.1 b/deps/npm/man/man1/npm-root.1 index b8b6f31b2d979c..79ab7c6debcb3e 100644 --- a/deps/npm/man/man1/npm-root.1 +++ b/deps/npm/man/man1/npm-root.1 @@ -1,4 +1,4 @@ -.TH "NPM-ROOT" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-ROOT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-root\fR - Display npm root .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-run.1 b/deps/npm/man/man1/npm-run.1 index c6c0bf5325f5d9..f20c43ab6a7113 100644 --- a/deps/npm/man/man1/npm-run.1 +++ b/deps/npm/man/man1/npm-run.1 @@ -1,4 +1,4 @@ -.TH "NPM-RUN" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-RUN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-run\fR - Run arbitrary package scripts .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-sbom.1 b/deps/npm/man/man1/npm-sbom.1 index 34d5972e568556..e04ac0e689169a 100644 --- a/deps/npm/man/man1/npm-sbom.1 +++ b/deps/npm/man/man1/npm-sbom.1 @@ -1,4 +1,4 @@ -.TH "NPM-SBOM" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-SBOM" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-sbom\fR - Generate a Software Bill of Materials (SBOM) .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-search.1 b/deps/npm/man/man1/npm-search.1 index b901d6872d770c..51c9a99e58aefe 100644 --- a/deps/npm/man/man1/npm-search.1 +++ b/deps/npm/man/man1/npm-search.1 @@ -1,4 +1,4 @@ -.TH "NPM-SEARCH" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-SEARCH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-search\fR - Search for packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-set.1 b/deps/npm/man/man1/npm-set.1 index a9a1506e540526..280d610a6e32c4 100644 --- a/deps/npm/man/man1/npm-set.1 +++ b/deps/npm/man/man1/npm-set.1 @@ -1,4 +1,4 @@ -.TH "NPM-SET" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-SET" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-set\fR - Set a value in the npm configuration .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-shrinkwrap.1 b/deps/npm/man/man1/npm-shrinkwrap.1 index 2c872c9723d832..0f7214bda3fd09 100644 --- a/deps/npm/man/man1/npm-shrinkwrap.1 +++ b/deps/npm/man/man1/npm-shrinkwrap.1 @@ -1,4 +1,4 @@ -.TH "NPM-SHRINKWRAP" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-SHRINKWRAP" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-shrinkwrap\fR - Lock down dependency versions for publication .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-stage.1 b/deps/npm/man/man1/npm-stage.1 new file mode 100644 index 00000000000000..0fe39e8ff8cffe --- /dev/null +++ b/deps/npm/man/man1/npm-stage.1 @@ -0,0 +1,178 @@ +.TH "NPM-STAGE" "1" "May 2026" "NPM@11.16.0" "" +.SH "NAME" +\fBnpm-stage\fR - Stage packages for publishing +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage +.fi +.RE +.P +Note: This command is unaware of workspaces. +.SS "Description" +.P +Staged publishing allows package maintainers to require proof-of-presence for all publishes. Proof-of-presence is where a human is involved, interjects, and provides authentication (2FA) during an action \[em] in this case, publishing an npm package. +.P +Typically when maintainers use automated workflows to publish, proof-of-presence is lacking as there's no convenient way to interject the process and provide 2FA, as is the case for publishing with a granular access token with bypass and the trusted publishing flow. Staged publishing allows users to have their automated workflows stage a package without a 2FA prompt, deferring the act of 2FA, allowing the maintainer to approve the staged package and publish at a later point. +.P +The \fBnpm stage publish\fR command packs the current working directory and places that version of the package into the registry in a state where it's not available for public access, allowing maintainers to approve the package at a later point in time. The act of staging does not prompt for 2FA and can be done with any token type, the act of approving will. +.P +Key behaviors: +.RS 0 +.IP \(bu 4 +Staged packages share the same semver version unique index as published packages \[em] you cannot publish a version that already exists as a staged version for that package. +.IP \(bu 4 +You can still publish packages normally while you have staged packages pending. +.IP \(bu 4 +You can stage multiple versions of the same package. +.IP \(bu 4 +\fBnpm stage publish\fR has parity with \fBnpm publish\fR and will respect \fB"private": true\fR in \fBpackage.json\fR, refusing to stage the package. +.RE 0 + +.SS "Prerequisites" +.P +Before using \fBnpm stage\fR commands, ensure the following requirements are met: +.RS 0 +.IP \(bu 4 +\fBWrite permissions on the package:\fR You must have write access to the package you're configuring. +.IP \(bu 4 +\fBPackage must exist:\fR The package you're configuring must already exist on the npm registry. +.IP \(bu 4 +\fB2FA enabled on your account:\fR Commands that require 2FA will prompt you to authenticate. If you don't already have 2FA enabled on your account, you must enable it before using these commands. +.RE 0 + +.SS "Subcommands" +.RS 0 +.IP \(bu 4 +\fBnpm stage publish \[lB]\[rB]\fR - Stage a package for publishing +.IP \(bu 4 +\fBnpm stage list \[lB]\[rB]\fR - List all staged package versions +.IP \(bu 4 +\fBnpm stage view \fR - View details of a specific staged package +.IP \(bu 4 +\fBnpm stage approve \fR - Approve a staged package for publishing +.IP \(bu 4 +\fBnpm stage reject \fR - Reject a staged package +.IP \(bu 4 +\fBnpm stage download \fR - Download the tarball for inspection +.RE 0 + +.SS "2FA Requirements by Subcommand" +.P +| Command | Requires 2FA | Notes | | --- | --- | --- | | \fBnpm stage publish\fR | No | Designed for automated workflows; defers 2FA to approval | | \fBnpm stage list\fR | No | View staged packages | | \fBnpm stage view\fR | No | View staged package details | | \fBnpm stage approve\fR | Yes | Prompts for 2FA to publish the staged package | | \fBnpm stage reject\fR | Yes | Prompts for 2FA to permanently remove the staged package | | \fBnpm stage download\fR | No | Downloads the tarball for local inspection | +.SS "Tag Behavior" +.P +The \fB--tag\fR flag follows the same logic as \fBnpm publish\fR. If no tag is provided, the \fBlatest\fR tag is used by default. For pre-release versions (e.g., \fB1.0.0-beta.1\fR) and non-latest semver versions, the tag must be explicitly provided \[em] otherwise the CLI will error, just as \fBnpm publish\fR would. +.P +The tag is an immutable property of the staged package. Once a package is staged with a given tag, the tag cannot be changed. If you need to stage the same version with a different tag, you must first reject the existing staged package using \fBnpm stage reject\fR and then re-stage it with the desired tag. +.SS "Token Behavior" +.P +The key difference with staged publishing is that \fBnpm stage publish\fR never requires a 2FA prompt, regardless of token type. This is what makes it suitable for automated workflows. The goal of \fBnpm stage publish\fR is deferring proof-of-presence to a later point in time. +.P +| Token Type | \fBnpm stage publish\fR | \fBnpm publish\fR | | --- | --- | --- | | GAT with bypass | Can stage | Can publish (if allowed by package publishing access) | | GAT without bypass | Can stage | 2FA prompt (if allowed by package publishing access) | | Session token | Can stage | 2FA prompt | | Trust token (OIDC) | Can stage (if allowed) | Can publish (if allowed) | +.SS "Trust Relationship Permissions" +.P +With staged publishing, trust relationships now support granular command permissions. Shortlived tokens issued through trust relationships can only be used with \fBnpm stage publish\fR and \fBnpm publish\fR. Shortlived tokens cannot run \fBnpm stage\fR subcommands. +.P +\fBnpm trust \fR supports \fB--allow-publish\fR and \fB--allow-stage-publish\fR to control which commands are available through each trust relationship. +.SS "Best Practices" +.P +\fBNote:\fR The addition of staged publishing does not make your account or org more secure. Maintainers must still use the best practices listed below. +.RS 0 +.IP 1. 4 +\fBDelete Granular Access Tokens (GAT) with bypass 2FA enabled.\fR Now with staged publishing, we've eliminated the need for a GAT token that can bypass 2FA. We encourage you to delete all your tokens with bypass enabled and switch to using a trust relationship in your automated workflows, or create a GAT without bypass and use \fBnpm stage publish\fR. +.IP 2. 4 +\fBDisallow tokens from publishing at the package level.\fR All packages have their own access controls under "package access" allowing packages to be published with bypass tokens, which is no longer a necessity. We encourage you to select "Require two-factor authentication and disallow tokens (recommended)" for all your packages on the package access page. +.IP 3. 4 +\fBConfigure trust relationship permissions to prevent \fBnpm publish\fB.\fR We encourage you to only enable \fBnpm stage publish\fR on your trust relationships and disable \fBnpm publish\fR. +.RE 0 + +.SS "Configuration" +.SS "\fBnpm stage publish\fR" +.P +Stage a package for publishing, deferring proof-of-presence (2FA) to a later point in time +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage publish +.fi +.RE +.SS "Flags" +.P +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--tag\fR | "latest" | String | If you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. It is the tag added to the package@version specified in the \fBnpm dist-tag add\fR command, if no explicit tag is given. When used by the \fBnpm diff\fR command, this is the tag used to fetch the tarball that will be compared with the local files by default. If used in the \fBnpm publish\fR command, this is the tag that will be added to the package submitted to the registry. | | \fB--access\fR | 'public' for new packages, existing packages it will not change the current level | null, "restricted", "public", or "private" | If you do not want your scoped package to be publicly viewable (and installable) set \fB--access=restricted\fR. Unscoped packages cannot be set to \fBrestricted\fR. Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. The value \fBprivate\fR is an alias for \fBrestricted\fR. | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--otp\fR | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with \fBnpm access\fR. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | | \fB--workspace\fR, \fB-w\fR | | String (can be set multiple times) | Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the \fBworkspace\fR config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the \fBnpm init\fR command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. | | \fB--workspaces\fR | null | null or Boolean | Set to true to run the command in the context of \fBall\fR configured workspaces. Explicitly setting this to false will cause commands like \fBinstall\fR to ignore workspaces altogether. When not set explicitly: - Commands that operate on the \fBnode_modules\fR tree (install, update, etc.) will link workspaces into the \fBnode_modules\fR folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, \fIunless\fR one or more workspaces are specified in the \fBworkspace\fR config. | | \fB--include-workspace-root\fR | false | Boolean | Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the \fBworkspace\fR config, or all workspaces via the \fBworkspaces\fR flag, will cause npm to operate only on the specified workspaces, and not on the root project. | | \fB--provenance\fR | false | Boolean | When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. | +.SS "\fBnpm stage list\fR" +.P +List all staged package versions +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage list \[lB]\[rB] +.fi +.RE +.SS "Flags" +.P +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | +.SS "\fBnpm stage view\fR" +.P +View details of a specific staged package +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage view +.fi +.RE +.SS "Flags" +.P +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | +.SS "\fBnpm stage approve\fR" +.P +Approve a staged package, publishing it to the npm registry +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage approve +.fi +.RE +.SS "Flags" +.P +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--otp\fR | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with \fBnpm access\fR. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | +.SS "\fBnpm stage reject\fR" +.P +Reject a staged package, removing it from the registry +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage reject +.fi +.RE +.SS "Flags" +.P +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--otp\fR | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with \fBnpm access\fR. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | +.SS "\fBnpm stage download\fR" +.P +Download the tarball of a staged package for inspection +.SS "Synopsis" +.P +.RS 2 +.nf +npm stage download +.fi +.RE +.SS "Flags" +.P +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | +.SS "See Also" +.RS 0 +.IP \(bu 4 +npm help publish +.IP \(bu 4 +npm help unpublish +.IP \(bu 4 +npm help trust +.RE 0 diff --git a/deps/npm/man/man1/npm-star.1 b/deps/npm/man/man1/npm-star.1 index ffc44d6728e6c4..385d6e8a23249e 100644 --- a/deps/npm/man/man1/npm-star.1 +++ b/deps/npm/man/man1/npm-star.1 @@ -1,4 +1,4 @@ -.TH "NPM-STAR" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-STAR" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-star\fR - Mark your favorite packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-stars.1 b/deps/npm/man/man1/npm-stars.1 index 40e1fac243e72e..f028349ba81f8c 100644 --- a/deps/npm/man/man1/npm-stars.1 +++ b/deps/npm/man/man1/npm-stars.1 @@ -1,4 +1,4 @@ -.TH "NPM-STARS" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-STARS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-stars\fR - View packages marked as favorites .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-start.1 b/deps/npm/man/man1/npm-start.1 index e5d324c7a459c5..de0605d2fa8fa2 100644 --- a/deps/npm/man/man1/npm-start.1 +++ b/deps/npm/man/man1/npm-start.1 @@ -1,4 +1,4 @@ -.TH "NPM-START" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-START" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-start\fR - Start a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-stop.1 b/deps/npm/man/man1/npm-stop.1 index 4f3f31a8111ddb..57cadfb2fa80bd 100644 --- a/deps/npm/man/man1/npm-stop.1 +++ b/deps/npm/man/man1/npm-stop.1 @@ -1,4 +1,4 @@ -.TH "NPM-STOP" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-STOP" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-stop\fR - Stop a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-team.1 b/deps/npm/man/man1/npm-team.1 index a3a69befb065d4..06d7b94acb7f24 100644 --- a/deps/npm/man/man1/npm-team.1 +++ b/deps/npm/man/man1/npm-team.1 @@ -1,4 +1,4 @@ -.TH "NPM-TEAM" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-TEAM" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-team\fR - Manage organization teams and team memberships .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-test.1 b/deps/npm/man/man1/npm-test.1 index 50a813a18c1f08..6c1108360e5789 100644 --- a/deps/npm/man/man1/npm-test.1 +++ b/deps/npm/man/man1/npm-test.1 @@ -1,4 +1,4 @@ -.TH "NPM-TEST" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-TEST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-test\fR - Test a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-token.1 b/deps/npm/man/man1/npm-token.1 index 396df16d27d5c2..41a69ed9d9c355 100644 --- a/deps/npm/man/man1/npm-token.1 +++ b/deps/npm/man/man1/npm-token.1 @@ -1,4 +1,4 @@ -.TH "NPM-TOKEN" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-TOKEN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-token\fR - Manage your authentication tokens .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-trust.1 b/deps/npm/man/man1/npm-trust.1 index 91744a56b13b4d..7b3c4d7990a8c9 100644 --- a/deps/npm/man/man1/npm-trust.1 +++ b/deps/npm/man/man1/npm-trust.1 @@ -1,4 +1,4 @@ -.TH "NPM-TRUST" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-TRUST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-trust\fR - Manage trusted publishing relationships between packages and CI/CD providers .SS "Synopsis" @@ -29,6 +29,19 @@ For a comprehensive overview of trusted publishing, see the \fBnpm trusted publi The \fB\[lB]package\[rB]\fR argument specifies the package name. If omitted, npm will use the name from the \fBpackage.json\fR in the current directory. .P Each trust relationship has its own set of configuration options and flags based on the OIDC claims provided by that provider. OIDC claims come from the CI/CD provider and include information such as repository name, workflow file, or environment. Since each provider's claims differ, the available flags and configuration keys are not universal\[em]npm matches the claims supported by each provider's OIDC configuration. For specific details on which claims and flags are supported for a given provider, use \fBnpm trust --help\fR. +.SS "Permissions" +.P +When creating a trust relationship, you must specify at least one permission flag to indicate which operations the trusted publisher is allowed to perform: +.RS 0 +.IP \(bu 4 +\fB--allow-publish\fR: Allows the trusted publisher to run \fBnpm publish\fR for the package. +.IP \(bu 4 +\fB--allow-stage-publish\fR: Allows the trusted publisher to run \fBnpm stage\fR for the package. The alias \fB--allow-staged-publish\fR is also accepted. +.RE 0 + +.P +At least one of these flags is required when creating a trust configuration. You can specify both to grant both permissions. +.SS "Provider Options" .P The required options depend on the CI/CD provider you're configuring. Detailed information about each option is available in the \fBmanaging trusted publisher configurations\fR \fI\(lahttps://docs.npmjs.com/trusted-publishers#managing-trusted-publisher-configurations\(ra\fR section of the npm documentation. If a provider is repository-based and the option is not provided, npm will use the \fBrepository.url\fR field from your \fBpackage.json\fR, if available. .P @@ -57,12 +70,12 @@ Create a trusted relationship between a package and GitHub Actions .P .RS 2 .nf -npm trust github \[lB]package\[rB] --file \[lB]--repo|--repository\[rB] \[lB]--env|--environment\[rB] \[lB]-y|--yes\[rB] +npm trust github \[lB]package\[rB] --file \[lB]--repo|--repository\[rB] \[lB]--env|--environment\[rB] \[lB]--allow-publish\[rB] \[lB]--allow-stage-publish\[rB] \[lB]-y|--yes\[rB] .fi .RE .SS "Flags" .P -| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--file\fR | null | String (required) | Name of workflow file within a repositories .GitHub folder (must end in yaml, yml) | | \fB--repository\fR, \fB--repo\fR | null | String | Name of the repository in the format owner/repo | | \fB--environment\fR, \fB--env\fR | null | String | CI environment name | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | | \fB--yes\fR, \fB-y\fR | null | null or Boolean | Automatically answer "yes" to any prompts that npm might print on the command line. | +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--file\fR | null | String (required) | Name of workflow file within a repositories .GitHub folder (must end in yaml, yml) | | \fB--repository\fR, \fB--repo\fR | null | String | Name of the repository in the format owner/repo | | \fB--environment\fR, \fB--env\fR | null | String | CI environment name | | \fB--allow-publish\fR | false | Boolean | Allow npm publish for this trusted publisher configuration | | \fB--allow-stage-publish\fR, \fB--allow-staged-publish\fR | false | Boolean | Allow npm stage publish for this trusted publisher configuration | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | | \fB--yes\fR, \fB-y\fR | null | null or Boolean | Automatically answer "yes" to any prompts that npm might print on the command line. | .SS "\fBnpm trust gitlab\fR" .P Create a trusted relationship between a package and GitLab CI/CD @@ -70,12 +83,12 @@ Create a trusted relationship between a package and GitLab CI/CD .P .RS 2 .nf -npm trust gitlab \[lB]package\[rB] --file \[lB]--project|--repo|--repository\[rB] \[lB]--env|--environment\[rB] \[lB]-y|--yes\[rB] +npm trust gitlab \[lB]package\[rB] --file \[lB]--project|--repo|--repository\[rB] \[lB]--env|--environment\[rB] \[lB]--allow-publish\[rB] \[lB]--allow-stage-publish\[rB] \[lB]-y|--yes\[rB] .fi .RE .SS "Flags" .P -| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--file\fR | null | String (required) | Name of pipeline file (e.g., .gitlab-ci.yml) | | \fB--project\fR | null | String | Name of the project in the format group/project or group/subgroup/project | | \fB--environment\fR, \fB--env\fR | null | String | CI environment name | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | | \fB--yes\fR, \fB-y\fR | null | null or Boolean | Automatically answer "yes" to any prompts that npm might print on the command line. | +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--file\fR | null | String (required) | Name of pipeline file (e.g., .gitlab-ci.yml) | | \fB--project\fR | null | String | Name of the project in the format group/project or group/subgroup/project | | \fB--environment\fR, \fB--env\fR | null | String | CI environment name | | \fB--allow-publish\fR | false | Boolean | Allow npm publish for this trusted publisher configuration | | \fB--allow-stage-publish\fR, \fB--allow-staged-publish\fR | false | Boolean | Allow npm stage publish for this trusted publisher configuration | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | | \fB--yes\fR, \fB-y\fR | null | null or Boolean | Automatically answer "yes" to any prompts that npm might print on the command line. | .SS "\fBnpm trust circleci\fR" .P Create a trusted relationship between a package and CircleCI @@ -83,12 +96,12 @@ Create a trusted relationship between a package and CircleCI .P .RS 2 .nf -npm trust circleci \[lB]package\[rB] --org-id --project-id --pipeline-definition-id --vcs-origin \[lB]--context-id ...\[rB] \[lB]-y|--yes\[rB] +npm trust circleci \[lB]package\[rB] --org-id --project-id --pipeline-definition-id --vcs-origin \[lB]--context-id ...\[rB] \[lB]--allow-publish\[rB] \[lB]--allow-stage-publish\[rB] \[lB]-y|--yes\[rB] .fi .RE .SS "Flags" .P -| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--org-id\fR | null | String (required) | CircleCI organization UUID | | \fB--project-id\fR | null | String (required) | CircleCI project UUID | | \fB--pipeline-definition-id\fR | null | String (required) | CircleCI pipeline definition UUID | | \fB--vcs-origin\fR | null | String (required) | CircleCI repository origin in format 'provider/owner/repo' | | \fB--context-id\fR | null | null or String (can be set multiple times) | CircleCI context UUID to match | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | | \fB--yes\fR, \fB-y\fR | null | null or Boolean | Automatically answer "yes" to any prompts that npm might print on the command line. | +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--org-id\fR | null | String (required) | CircleCI organization UUID | | \fB--project-id\fR | null | String (required) | CircleCI project UUID | | \fB--pipeline-definition-id\fR | null | String (required) | CircleCI pipeline definition UUID | | \fB--vcs-origin\fR | null | String (required) | CircleCI repository origin in format 'provider/owner/repo' | | \fB--context-id\fR | null | null or String (can be set multiple times) | CircleCI context UUID to match | | \fB--allow-publish\fR | false | Boolean | Allow npm publish for this trusted publisher configuration | | \fB--allow-stage-publish\fR, \fB--allow-staged-publish\fR | false | Boolean | Allow npm stage publish for this trusted publisher configuration | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--json\fR | false | Boolean | Whether or not to output JSON data, rather than the normal output. * In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. Not supported by all npm commands. | | \fB--registry\fR | "https://registry.npmjs.org/" | URL | The base URL of the npm registry. | | \fB--yes\fR, \fB-y\fR | null | null or Boolean | Automatically answer "yes" to any prompts that npm might print on the command line. | .SS "\fBnpm trust list\fR" .P List trusted relationships for a package diff --git a/deps/npm/man/man1/npm-undeprecate.1 b/deps/npm/man/man1/npm-undeprecate.1 index 86b6ed7fc73ae7..8bb2936691c5c4 100644 --- a/deps/npm/man/man1/npm-undeprecate.1 +++ b/deps/npm/man/man1/npm-undeprecate.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNDEPRECATE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-UNDEPRECATE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-undeprecate\fR - Undeprecate a version of a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-uninstall.1 b/deps/npm/man/man1/npm-uninstall.1 index 0d89545bc3d13a..10845d76cb050f 100644 --- a/deps/npm/man/man1/npm-uninstall.1 +++ b/deps/npm/man/man1/npm-uninstall.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNINSTALL" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-UNINSTALL" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-uninstall\fR - Remove a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-unpublish.1 b/deps/npm/man/man1/npm-unpublish.1 index 465ee175b738e8..1386bb85ce279d 100644 --- a/deps/npm/man/man1/npm-unpublish.1 +++ b/deps/npm/man/man1/npm-unpublish.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNPUBLISH" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-UNPUBLISH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-unpublish\fR - Remove a package from the registry .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-unstar.1 b/deps/npm/man/man1/npm-unstar.1 index 60a6e600a49ddc..4fabb116f4242f 100644 --- a/deps/npm/man/man1/npm-unstar.1 +++ b/deps/npm/man/man1/npm-unstar.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNSTAR" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-UNSTAR" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-unstar\fR - Remove an item from your favorite packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-update.1 b/deps/npm/man/man1/npm-update.1 index 0eacbf369b37d5..e157c16d8a30ee 100644 --- a/deps/npm/man/man1/npm-update.1 +++ b/deps/npm/man/man1/npm-update.1 @@ -1,4 +1,4 @@ -.TH "NPM-UPDATE" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-UPDATE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-update\fR - Update packages .SS "Synopsis" @@ -277,6 +277,42 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 @@ -300,7 +336,7 @@ If passed to \fBnpm install\fR, will rebuild the npm tree such that only version .P If the requested version is a \fBdist-tag\fR and the given tag does not pass the \fB--before\fR filter, the most recent version less than or equal to that tag will be used. For example, \fBfoo@latest\fR might install \fBfoo@1.2\fR even though \fBlatest\fR is \fB2.0\fR. .P -This config cannot be used with: \fBmin-release-age\fR +If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .SS "\fBmin-release-age\fR" .RS 0 .IP \(bu 4 @@ -312,9 +348,7 @@ Type: null or Number .P If set, npm will build the npm tree such that only versions that were available more than the given number of days ago will be installed. If there are no versions available for the current set of dependencies, the command will error. .P -This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. -.P -This config cannot be used with: \fBbefore\fR +This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P This value is not exported to the environment for child processes. .SS "\fBbin-links\fR" diff --git a/deps/npm/man/man1/npm-version.1 b/deps/npm/man/man1/npm-version.1 index 70feae9fc4509b..78612356235a44 100644 --- a/deps/npm/man/man1/npm-version.1 +++ b/deps/npm/man/man1/npm-version.1 @@ -1,4 +1,4 @@ -.TH "NPM-VERSION" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-VERSION" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-version\fR - Bump a package version .SS "Synopsis" @@ -226,6 +226,8 @@ Commit and tag. Run the \fBpostversion\fR script. Use it to clean up the file system or automatically push the commit and/or tag. .RE 0 +.P +For the \fBpreversion\fR, \fBversion\fR and \fBpostversion\fR scripts, npm also sets the \fBenvironment variables\fR \fI\(la/using-npm/scripts#environment\(ra\fR \fBnpm_old_version\fR and \fBnpm_new_version\fR. .P Take the following example: .P diff --git a/deps/npm/man/man1/npm-view.1 b/deps/npm/man/man1/npm-view.1 index e587954fc4be25..5ac8d354fbb51d 100644 --- a/deps/npm/man/man1/npm-view.1 +++ b/deps/npm/man/man1/npm-view.1 @@ -1,4 +1,4 @@ -.TH "NPM-VIEW" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-VIEW" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-view\fR - View registry info .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-whoami.1 b/deps/npm/man/man1/npm-whoami.1 index a944920ca2e7c5..a0c1956514c26e 100644 --- a/deps/npm/man/man1/npm-whoami.1 +++ b/deps/npm/man/man1/npm-whoami.1 @@ -1,4 +1,4 @@ -.TH "NPM-WHOAMI" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM-WHOAMI" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-whoami\fR - Display npm username .SS "Synopsis" diff --git a/deps/npm/man/man1/npm.1 b/deps/npm/man/man1/npm.1 index a32d7b13b182e9..68369b132c9168 100644 --- a/deps/npm/man/man1/npm.1 +++ b/deps/npm/man/man1/npm.1 @@ -1,4 +1,4 @@ -.TH "NPM" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPM" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm\fR - javascript package manager .SS "Synopsis" @@ -12,7 +12,7 @@ npm Note: This command is unaware of workspaces. .SS "Version" .P -11.13.0 +11.16.0 .SS "Description" .P npm is the package manager for the Node JavaScript platform. It puts modules in place so that node can find them, and manages dependency conflicts intelligently. diff --git a/deps/npm/man/man1/npx.1 b/deps/npm/man/man1/npx.1 index 8878334eb8a297..23671ac8cfb611 100644 --- a/deps/npm/man/man1/npx.1 +++ b/deps/npm/man/man1/npx.1 @@ -1,4 +1,4 @@ -.TH "NPX" "1" "April 2026" "NPM@11.13.0" "" +.TH "NPX" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpx\fR - Run a command from a local or remote npm package .SS "Synopsis" diff --git a/deps/npm/man/man5/folders.5 b/deps/npm/man/man5/folders.5 index c9d7a23cfd2916..b0a8c9b4825ccb 100644 --- a/deps/npm/man/man5/folders.5 +++ b/deps/npm/man/man5/folders.5 @@ -1,4 +1,4 @@ -.TH "FOLDERS" "5" "April 2026" "NPM@11.13.0" "" +.TH "FOLDERS" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBFolders\fR - Folder structures used by npm .SS "Description" diff --git a/deps/npm/man/man5/install.5 b/deps/npm/man/man5/install.5 index c43ef43f621d5d..e70fd2ed7b602e 100644 --- a/deps/npm/man/man5/install.5 +++ b/deps/npm/man/man5/install.5 @@ -1,4 +1,4 @@ -.TH "INSTALL" "5" "April 2026" "NPM@11.13.0" "" +.TH "INSTALL" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBInstall\fR - Download and install node and npm .SS "Description" diff --git a/deps/npm/man/man5/npm-global.5 b/deps/npm/man/man5/npm-global.5 index c9d7a23cfd2916..b0a8c9b4825ccb 100644 --- a/deps/npm/man/man5/npm-global.5 +++ b/deps/npm/man/man5/npm-global.5 @@ -1,4 +1,4 @@ -.TH "FOLDERS" "5" "April 2026" "NPM@11.13.0" "" +.TH "FOLDERS" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBFolders\fR - Folder structures used by npm .SS "Description" diff --git a/deps/npm/man/man5/npm-json.5 b/deps/npm/man/man5/npm-json.5 index f26a307d85a111..3d0c548f7042fa 100644 --- a/deps/npm/man/man5/npm-json.5 +++ b/deps/npm/man/man5/npm-json.5 @@ -1,4 +1,4 @@ -.TH "PACKAGE.JSON" "5" "April 2026" "NPM@11.13.0" "" +.TH "PACKAGE.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBpackage.json\fR - Specifics of npm's package.json handling .SS "Description" diff --git a/deps/npm/man/man5/npm-shrinkwrap-json.5 b/deps/npm/man/man5/npm-shrinkwrap-json.5 index dff972eeacec67..104ac0703d7710 100644 --- a/deps/npm/man/man5/npm-shrinkwrap-json.5 +++ b/deps/npm/man/man5/npm-shrinkwrap-json.5 @@ -1,4 +1,4 @@ -.TH "NPM-SHRINKWRAP.JSON" "5" "April 2026" "NPM@11.13.0" "" +.TH "NPM-SHRINKWRAP.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-shrinkwrap.json\fR - A publishable lockfile .SS "Description" diff --git a/deps/npm/man/man5/npmrc.5 b/deps/npm/man/man5/npmrc.5 index ff767b87c10dcb..7c5273d6344a25 100644 --- a/deps/npm/man/man5/npmrc.5 +++ b/deps/npm/man/man5/npmrc.5 @@ -1,4 +1,4 @@ -.TH ".NPMRC" "5" "April 2026" "NPM@11.13.0" "" +.TH ".NPMRC" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fB.npmrc\fR - The npm config files .SS "Description" diff --git a/deps/npm/man/man5/package-json.5 b/deps/npm/man/man5/package-json.5 index f26a307d85a111..3d0c548f7042fa 100644 --- a/deps/npm/man/man5/package-json.5 +++ b/deps/npm/man/man5/package-json.5 @@ -1,4 +1,4 @@ -.TH "PACKAGE.JSON" "5" "April 2026" "NPM@11.13.0" "" +.TH "PACKAGE.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBpackage.json\fR - Specifics of npm's package.json handling .SS "Description" diff --git a/deps/npm/man/man5/package-lock-json.5 b/deps/npm/man/man5/package-lock-json.5 index f66b38335b28ff..40742966252fa4 100644 --- a/deps/npm/man/man5/package-lock-json.5 +++ b/deps/npm/man/man5/package-lock-json.5 @@ -1,4 +1,4 @@ -.TH "PACKAGE-LOCK.JSON" "5" "April 2026" "NPM@11.13.0" "" +.TH "PACKAGE-LOCK.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBpackage-lock.json\fR - A manifestation of the manifest .SS "Description" diff --git a/deps/npm/man/man7/config.7 b/deps/npm/man/man7/config.7 index 1b93498c84818e..d4b0b124d3f142 100644 --- a/deps/npm/man/man7/config.7 +++ b/deps/npm/man/man7/config.7 @@ -1,4 +1,4 @@ -.TH "CONFIG" "7" "April 2026" "NPM@11.13.0" "" +.TH "CONFIG" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBConfig\fR - About npm configuration .SS "Description" @@ -174,7 +174,7 @@ Warning: This should generally not be set via a command-line option. It is safer .IP \(bu 4 Default: 'public' for new packages, existing packages it will not change the current level .IP \(bu 4 -Type: null, "restricted", or "public" +Type: null, "restricted", "public", or "private" .RE 0 .P @@ -184,6 +184,8 @@ Unscoped packages cannot be set to \fBrestricted\fR. .P Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. +.P +The value \fBprivate\fR is an alias for \fBrestricted\fR. .SS "\fBall\fR" .RS 0 .IP \(bu 4 @@ -194,6 +196,30 @@ Type: Boolean .P When running \fBnpm outdated\fR and \fBnpm ls\fR, setting \fB--all\fR will show all outdated or installed packages, rather than only those directly depended upon by the current project. +.SS "\fBallow-directory\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from directories. That is, dependencies that point to a directory instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any directories to be installed. \fBnone\fR prevents any directories from being installed. \fBroot\fR only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-file\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to install dependencies from tarball files. That is, dependencies that point to a local tarball file instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any tarball file to be installed. \fBnone\fR prevents any tarball file from being installed. \fBroot\fR only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-git\fR" .RS 0 .IP \(bu 4 @@ -203,9 +229,21 @@ Type: "all", "none", or "root" .RE 0 .P -Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. +Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. +.P +\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +.SS "\fBallow-remote\fR" +.RS 0 +.IP \(bu 4 +Default: "all" +.IP \(bu 4 +Type: "all", "none", or "root" +.RE 0 + +.P +Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P -\fBall\fR allows any git dependencies to be fetched and installed. \fBnone\fR prevents any git dependencies from being fetched and installed. \fBroot\fR only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \fBnpm view\fR +\fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR .SS "\fBallow-same-version\fR" .RS 0 .IP \(bu 4 @@ -216,6 +254,40 @@ Type: Boolean .P Prevents throwing an error when \fBnpm version\fR is used to set the new version to the same value as the current version. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBallow-scripts-pending\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +List packages with install scripts that are not yet covered by the \fBallowScripts\fR policy, without modifying \fBpackage.json\fR. Only meaningful for \fBnpm approve-scripts\fR. +.SS "\fBallow-scripts-pin\fR" +.RS 0 +.IP \(bu 4 +Default: true +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Write pinned (\fBpkg@version\fR) entries when approving install scripts. Set to \fBfalse\fR to write name-only entries that allow any version. Has no effect on \fBnpm deny-scripts\fR, which always writes name-only entries regardless of this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 @@ -259,7 +331,7 @@ If passed to \fBnpm install\fR, will rebuild the npm tree such that only version .P If the requested version is a \fBdist-tag\fR and the given tag does not pass the \fB--before\fR filter, the most recent version less than or equal to that tag will be used. For example, \fBfoo@latest\fR might install \fBfoo@1.2\fR even though \fBlatest\fR is \fB2.0\fR. .P -This config cannot be used with: \fBmin-release-age\fR +If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .SS "\fBbin-links\fR" .RS 0 .IP \(bu 4 @@ -401,6 +473,16 @@ Type: null or String .P Override CPU architecture of native modules to install. Acceptable values are same as \fBcpu\fR field of package.json, which comes from \fBprocess.arch\fR. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBdepth\fR" .RS 0 .IP \(bu 4 @@ -1116,9 +1198,7 @@ Type: null or Number .P If set, npm will build the npm tree such that only versions that were available more than the given number of days ago will be installed. If there are no versions available for the current set of dependencies, the command will error. .P -This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. -.P -This config cannot be used with: \fBbefore\fR +This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P This value is not exported to the environment for child processes. .SS "\fBname\fR" @@ -1725,6 +1805,18 @@ Type: Boolean If set to true, then the \fBnpm version\fR command will tag the version using \fB-s\fR to add a signature. .P Note that git requires you to have set up GPG keys in your git configs for this to work properly. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. .SS "\fBstrict-peer-deps\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man7/dependency-selectors.7 b/deps/npm/man/man7/dependency-selectors.7 index 3b5e9f8faa2333..4b67c2a3be5892 100644 --- a/deps/npm/man/man7/dependency-selectors.7 +++ b/deps/npm/man/man7/dependency-selectors.7 @@ -1,4 +1,4 @@ -.TH "SELECTORS" "7" "April 2026" "NPM@11.13.0" "" +.TH "SELECTORS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBSelectors\fR - Dependency Selector Syntax & Querying .SS "Description" diff --git a/deps/npm/man/man7/developers.7 b/deps/npm/man/man7/developers.7 index 6ed01ced244362..5b41ed26374205 100644 --- a/deps/npm/man/man7/developers.7 +++ b/deps/npm/man/man7/developers.7 @@ -1,4 +1,4 @@ -.TH "DEVELOPERS" "7" "April 2026" "NPM@11.13.0" "" +.TH "DEVELOPERS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBDevelopers\fR - Developer guide .SS "Description" diff --git a/deps/npm/man/man7/logging.7 b/deps/npm/man/man7/logging.7 index 0a114670022595..474becd6ff6431 100644 --- a/deps/npm/man/man7/logging.7 +++ b/deps/npm/man/man7/logging.7 @@ -1,4 +1,4 @@ -.TH "LOGGING" "7" "April 2026" "NPM@11.13.0" "" +.TH "LOGGING" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBLogging\fR - Why, What & How we Log .SS "Description" diff --git a/deps/npm/man/man7/orgs.7 b/deps/npm/man/man7/orgs.7 index 502ef7d6f9b521..e0666bdf0f3389 100644 --- a/deps/npm/man/man7/orgs.7 +++ b/deps/npm/man/man7/orgs.7 @@ -1,4 +1,4 @@ -.TH "ORGANIZATIONS" "7" "April 2026" "NPM@11.13.0" "" +.TH "ORGANIZATIONS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBOrganizations\fR - Working with teams & organizations .SS "Description" diff --git a/deps/npm/man/man7/package-spec.7 b/deps/npm/man/man7/package-spec.7 index 3644ebecde9732..76d07709632b0c 100644 --- a/deps/npm/man/man7/package-spec.7 +++ b/deps/npm/man/man7/package-spec.7 @@ -1,4 +1,4 @@ -.TH "SPEC" "7" "April 2026" "NPM@11.13.0" "" +.TH "SPEC" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBspec\fR - Package name specifier .SS "Description" diff --git a/deps/npm/man/man7/registry.7 b/deps/npm/man/man7/registry.7 index 62edddcc6d591b..cbfca0c6f42d3c 100644 --- a/deps/npm/man/man7/registry.7 +++ b/deps/npm/man/man7/registry.7 @@ -1,4 +1,4 @@ -.TH "REGISTRY" "7" "April 2026" "NPM@11.13.0" "" +.TH "REGISTRY" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBRegistry\fR - The JavaScript Package Registry .SS "Description" diff --git a/deps/npm/man/man7/removal.7 b/deps/npm/man/man7/removal.7 index 3758202663fa2e..fa44b56539a008 100644 --- a/deps/npm/man/man7/removal.7 +++ b/deps/npm/man/man7/removal.7 @@ -1,4 +1,4 @@ -.TH "REMOVAL" "7" "April 2026" "NPM@11.13.0" "" +.TH "REMOVAL" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBRemoval\fR - Cleaning the slate .SS "Synopsis" diff --git a/deps/npm/man/man7/scope.7 b/deps/npm/man/man7/scope.7 index fec92c73e43eab..7857ede645fa92 100644 --- a/deps/npm/man/man7/scope.7 +++ b/deps/npm/man/man7/scope.7 @@ -1,4 +1,4 @@ -.TH "SCOPE" "7" "April 2026" "NPM@11.13.0" "" +.TH "SCOPE" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBScope\fR - Scoped packages .SS "Description" diff --git a/deps/npm/man/man7/scripts.7 b/deps/npm/man/man7/scripts.7 index f6d85659db5872..5cfab1d64cd2e9 100644 --- a/deps/npm/man/man7/scripts.7 +++ b/deps/npm/man/man7/scripts.7 @@ -1,4 +1,4 @@ -.TH "SCRIPTS" "7" "April 2026" "NPM@11.13.0" "" +.TH "SCRIPTS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBScripts\fR - How npm handles the "scripts" field .SS "Description" @@ -382,6 +382,16 @@ For example, if you had \fB{"name":"foo", "version":"1.2.5"}\fR in your package. \fBNote:\fR In npm 7 and later, most package.json fields are no longer provided as environment variables. Scripts that need access to other package.json fields should read the package.json file directly. The \fBnpm_package_json\fR environment variable provides the path to the file for this purpose. .P See \fB\[rs]fBpackage.json\[rs]fR\fR \fI\(la/configuring-npm/package-json\(ra\fR for more on package configs. +.SS "versioning variables" +.P +For versioning scripts (\fBpreversion\fR, \fBversion\fR, \fBpostversion\fR), npm sets these environment variables: +.RS 0 +.IP \(bu 4 +\fBnpm_old_version\fR - The version before being bumped +.IP \(bu 4 +\fBnpm_new_version\fR \[en] The version after being bumped +.RE 0 + .SS "current lifecycle event" .P Lastly, the \fBnpm_lifecycle_event\fR environment variable is set to whichever stage of the cycle is being executed. So, you could have a single script used for different parts of the process which switches based on what's currently happening. diff --git a/deps/npm/man/man7/workspaces.7 b/deps/npm/man/man7/workspaces.7 index e32fbf3c9b124f..be7a22047d9971 100644 --- a/deps/npm/man/man7/workspaces.7 +++ b/deps/npm/man/man7/workspaces.7 @@ -1,4 +1,4 @@ -.TH "WORKSPACES" "7" "April 2026" "NPM@11.13.0" "" +.TH "WORKSPACES" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBWorkspaces\fR - Working with workspaces .SS "Description" diff --git a/deps/npm/node_modules/@npmcli/agent/lib/agents.js b/deps/npm/node_modules/@npmcli/agent/lib/agents.js index c541b93001517e..e9624dfeb90090 100644 --- a/deps/npm/node_modules/@npmcli/agent/lib/agents.js +++ b/deps/npm/node_modules/@npmcli/agent/lib/agents.js @@ -203,4 +203,56 @@ module.exports = class Agent extends AgentBase { return super.addRequest(request, options) } + + // When connect() rejects, agent-base removes only its placeholder socket, so Node never drains this.requests[name] and requests queued past maxSockets hang forever. + // On a failure we dispatch the next queued request ourselves. + // See npm/cli#9386 and TooTallNate/proxy-agents#427. + createSocket (req, options, cb) { + super.createSocket(req, options, (err, socket) => { + if (err) { + this.#drainPendingRequests(req, options) + } + cb(err, socket) + }) + } + + // Dispatch the next request queued behind maxSockets, reusing the slot the failed connection freed. + #drainPendingRequests (failedReq, options) { + const name = this.getName(options) + const queue = this.requests[name] + if (!queue || queue.length === 0) { + return + } + + // Node's removeSocket() picks a queued request without shifting it off, so drop the failed one to avoid dispatching it twice. + const failedIndex = queue.indexOf(failedReq) + if (failedIndex !== -1) { + queue.splice(failedIndex, 1) + } + if (queue.length === 0) { + delete this.requests[name] + return + } + + // Safety belt: only dispatch if a socket slot is genuinely free. + const socketCount = this.sockets[name] ? this.sockets[name].length : 0 + if (socketCount >= this.maxSockets || this.totalSocketCount >= this.maxTotalSockets) { + return + } + + const nextReq = queue.shift() + if (queue.length === 0) { + delete this.requests[name] + } + + // All queued requests share this origin, so the failed request's options suit the next one. + // createSocket() recurses here if this connection also fails, draining the whole queue. + this.createSocket(nextReq, options, (err, socket) => { + if (err) { + nextReq.onSocket(null, err) + } else { + nextReq.onSocket(socket) + } + }) + } } diff --git a/deps/npm/node_modules/@npmcli/agent/lib/options.js b/deps/npm/node_modules/@npmcli/agent/lib/options.js index 0bf53f725f0846..a6ae490a89c3b3 100644 --- a/deps/npm/node_modules/@npmcli/agent/lib/options.js +++ b/deps/npm/node_modules/@npmcli/agent/lib/options.js @@ -37,6 +37,10 @@ const normalizeOptions = (opts) => { // remove timeout since we already used it to set our own idle timeout delete normalized.timeout + // since opts is often passed when initiating requests, it may contain + // headers, which should not be saved in an agent + delete normalized.headers + return normalized } diff --git a/deps/npm/node_modules/@npmcli/agent/package.json b/deps/npm/node_modules/@npmcli/agent/package.json index 67670a0c1c484e..8c0d358b02a717 100644 --- a/deps/npm/node_modules/@npmcli/agent/package.json +++ b/deps/npm/node_modules/@npmcli/agent/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/agent", - "version": "4.0.0", + "version": "4.0.2", "description": "the http/https agent used by the npm cli", "main": "lib/index.js", "scripts": { @@ -29,8 +29,10 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.25.0", - "publish": "true" + "version": "4.30.0", + "publish": "true", + "updateNpm": false, + "latestCiVersion": 24 }, "dependencies": { "agent-base": "^7.1.0", @@ -40,11 +42,11 @@ "socks-proxy-agent": "^8.0.3" }, "devDependencies": { - "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.25.0", - "minipass-fetch": "^4.0.1", + "@npmcli/eslint-config": "^6.0.0", + "@npmcli/template-oss": "4.30.0", + "ip-address": "^10.1.0", + "minipass-fetch": "^5.0.0", "nock": "^14.0.3", - "socksv5": "^0.0.6", "tap": "^16.3.0" }, "repository": { diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js index fdbbd4679bd80a..f16844ea73d7d1 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -13,7 +13,6 @@ const { lstat, readlink } = require('node:fs/promises') const { depth } = require('treeverse') const { log, time } = require('proc-log') const { redact } = require('@npmcli/redact') -const semver = require('semver') const { OK, @@ -29,6 +28,15 @@ const Shrinkwrap = require('../shrinkwrap.js') const { defaultLockfileVersion } = Shrinkwrap const Node = require('../node.js') const Link = require('../link.js') + +// Maps a parsed spec.type to the corresponding allow-* arborist option name. +// Hoisted to module scope so #checkAllow doesn't re-allocate it per call. +const ALLOW_OPTION_FOR_TYPE = { + git: 'allowGit', + remote: 'allowRemote', + file: 'allowFile', + directory: 'allowDirectory', +} const addRmPkgDeps = require('../add-rm-pkg-deps.js') const optionalSet = require('../optional-set.js') const { checkEngine, checkPlatform } = require('npm-install-checks') @@ -294,10 +302,6 @@ module.exports = cls => class IdealTreeBuilder extends cls { }).then(meta => Object.assign(root, { meta })) } else { return this.loadVirtual({ root }) - .then(tree => { - this.#applyRootOverridesToWorkspaces(tree) - return tree - }) } }) @@ -406,6 +410,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { global: this.options.global, installLinks: this.installLinks, legacyPeerDeps: this.legacyPeerDeps, + loadOverrides: true, root, }) } @@ -450,6 +455,11 @@ module.exports = cls => class IdealTreeBuilder extends cls { const paths = await readdirScoped(nm).catch(() => []) for (const p of paths) { const name = p.replace(/\\/g, '/') + // Match loadActual behavior: hidden entries and retired scoped package + // folders are not installed global packages. + if (/^(@[^/]+\/)?\./.test(name)) { + continue + } const updateName = this[_updateNames].includes(name) if (this[_updateAll] || updateName) { if (updateName) { @@ -648,6 +658,45 @@ module.exports = cls => class IdealTreeBuilder extends cls { return vuln.range } + // Enforces the allow-git / allow-file / allow-directory / allow-remote configs at the arborist resolution layer, before any branching into the symlink (Link) path or the manifest-fetch path. + // Pacote also enforces these inside FetcherBase.get() as defense-in-depth, but the symlink branch never reaches pacote, and the manifest cache here would bypass pacote on a cached hit. + // Throws the same { code: EALLOW${TYPE} } shape pacote uses, so callers and downstream consumers stay consistent. + #checkAllow (spec, edge) { + const optName = ALLOW_OPTION_FOR_TYPE[spec.type] + if (!optName) { + return + } + const allow = this.options[optName] ?? 'all' + if (allow === 'all') { + return + } + const isRoot = !!(edge?.from?.isProjectRoot || edge?.from?.isWorkspace) + if (allow !== 'none' && isRoot) { + return + } + throw Object.assign( + new Error(`Fetching${allow === 'root' ? ' non-root' : ''} packages of type "${spec.type}" have been disabled`), + { + code: `EALLOW${spec.type.toUpperCase()}`, + package: spec.toString(), + } + ) + } + + // Builds a Node representing a spec we failed to load (allow-* gate, network failure, ENOTARGET, etc.) and records it in #loadFailures so #pruneFailedOptional can later decide whether the failure is fatal or silently dropped for optional deps. + #failureNode (name, parent, error, edge) { + error.requiredBy = edge?.from?.location || '.' + const n = new Node({ + name, + parent, + error, + installLinks: this.installLinks, + legacyPeerDeps: this.legacyPeerDeps, + }) + this.#loadFailures.add(n) + return n + } + #queueNamedUpdates () { // ignore top nodes, since they are not loaded the same way, and // probably have their own project associated with them. @@ -913,8 +962,21 @@ This is a one-time fix-up, please be patient... // be forced to agree on a version of z. const required = new Set([edge.from]) const parent = edge.peer ? virtualRoot : null - const dep = vrDep && vrDep.satisfies(edge) ? vrDep - : await this.#nodeFromEdge(edge, parent, null, required) + let dep = vrDep && vrDep.satisfies(edge) ? vrDep : null + + // A peerOptional conflict can be resolved by finding an existing node in the tree that satisfies the edge, avoiding a registry fetch that may introduce an extraneous package. See npm/cli#9249. + // Skip the shortcut when the user has signaled an explicit re-fetch intent (npm update by name, explicit request, or audit fix), so we honor those signals rather than silently keeping the existing node. + const skipExistingShortcut = this[_updateNames].includes(edge.name) + || this.#explicitRequests.has(edge) + || (edge.to && this.auditReport?.isVulnerable(edge.to)) + if (!dep && edge.type === 'peerOptional' && !skipExistingShortcut) { + dep = this.#findHoistableNode( + /* istanbul ignore next - resolveParent is always set for non-root nodes */ + edge.from.resolveParent || edge.from, edge) + } + if (!dep) { + dep = await this.#nodeFromEdge(edge, parent, null, required) + } /* istanbul ignore next */ debug(() => { @@ -1026,7 +1088,7 @@ This is a one-time fix-up, please be patient... // This can't be changed or removed till we figure out why // The test is named "tarball deps with transitive tarball deps" promises.push(() => - this.#fetchManifest(npa.resolve(e.name, e.spec, fromPath(placed, e)), parent) + this.#fetchManifest(npa.resolve(e.name, e.spec, fromPath(placed, e)), parent, e) .catch(() => null) ) } @@ -1044,6 +1106,24 @@ This is a one-time fix-up, please be patient... return this.#buildDepStep() } + // BFS descendants of `root` for a node satisfying `edge`. + // Prefers nodes closer to root. Skips bundled nodes. + #findHoistableNode (root, edge) { + const queue = [...root.children.values()] + while (queue.length) { + const node = queue.shift() + if (node.name === edge.name + && !node.inDepBundle + && node.satisfies(edge)) { + return node + } + for (const child of node.children.values()) { + queue.push(child) + } + } + return null + } + // loads a node from an edge, and then loads its peer deps (and their peer deps, on down the line) into a virtual root parent. async #nodeFromEdge (edge, parent_, secondEdge, required) { // create a virtual root node with the same deps as the node that is requesting this one, so that we can get all the peer deps in a context where they're likely to be resolvable. @@ -1199,12 +1279,14 @@ This is a one-time fix-up, please be patient... return problems } - async #fetchManifest (spec, parent) { + async #fetchManifest (spec, parent, edge) { + // Enforce allow-* gates before consulting the manifest cache so a cached entry from a different edge cannot bypass the policy. + this.#checkAllow(spec, edge) const options = { ...this.options, avoid: this.#avoidRange(spec.name), fullMetadata: true, - _isRoot: parent?.isProjectRoot || parent?.isWorkspace, + _isRoot: !!(edge?.from?.isProjectRoot || edge?.from?.isWorkspace), } // get the intended spec and stored metadata from yarn.lock file, // if available and valid. @@ -1221,6 +1303,14 @@ This is a one-time fix-up, please be patient... } async #nodeFromSpec (name, spec, parent, edge) { + // Enforce allow-git / allow-file / allow-directory / allow-remote before any branching, so the symlink (Link) path is enforced as well as the manifest-fetch path. + // Route the failure through #loadFailures so optional-dep semantics apply (e.g. a transitive optionalDependencies entry that resolves to a disallowed git URL is silently dropped rather than failing the install). + try { + this.#checkAllow(spec, edge) + } catch (error) { + return this.#failureNode(name, parent, error, edge) + } + // pacote will slap integrity on its options, so we have to clone the object so it doesn't get mutated. // Don't bother to load the manifest for link deps, because the target might be within another package that doesn't exist yet. const { installLinks, legacyPeerDeps } = this @@ -1275,23 +1365,26 @@ This is a one-time fix-up, please be patient... // spec isn't a directory, and either isn't a workspace or the workspace we have // doesn't satisfy the edge. try to fetch a manifest and build a node from that. - return this.#fetchManifest(spec, parent) - .then(pkg => new Node({ name, pkg, parent, installLinks, legacyPeerDeps }), error => { - error.requiredBy = edge.from.location || '.' - - // failed to load the spec, either because of enotarget or - // fetch failure of some other sort. save it so we can verify - // later that it's optional; otherwise, the error is fatal. - const n = new Node({ - name, - parent, - error, - installLinks, - legacyPeerDeps, - }) - this.#loadFailures.add(n) - return n - }) + return this.#fetchManifest(spec, parent, edge) + .then( + pkg => { + // When a proxy/upstream registry returns an incomplete manifest + // (e.g. missing version field for platform-specific packages it + // hasn't cached), treat it as a load failure so that optional deps + // are properly pruned instead of written to the lockfile without + // version metadata. Only apply to registry specs — file: deps + // legitimately omit version. + if (!pkg.version && spec.registry) { + const error = Object.assign( + new Error(`incomplete manifest for ${name}, missing version`), + { code: 'EINCOMPLETEMANIFEST' } + ) + return this.#failureNode(name, parent, error, edge) + } + return new Node({ name, pkg, parent, installLinks, legacyPeerDeps }) + }, + error => this.#failureNode(name, parent, error, edge) + ) } // load all peer deps and meta-peer deps into the node's parent @@ -1507,32 +1600,6 @@ This is a one-time fix-up, please be patient... timeEnd() } - #applyRootOverridesToWorkspaces (tree) { - const rootOverrides = tree.root.package.overrides || {} - - for (const node of tree.root.inventory.values()) { - if (!node.isWorkspace) { - continue - } - - for (const depName of Object.keys(rootOverrides)) { - const edge = node.edgesOut.get(depName) - const rootNode = tree.root.children.get(depName) - - // safely skip if either edge or rootNode doesn't exist yet - if (!edge || !rootNode) { - continue - } - - const resolvedRootVersion = rootNode.package.version - if (!semver.satisfies(resolvedRootVersion, edge.spec)) { - edge.detach() - node.children.delete(depName) - } - } - } - } - #idealTreePrune () { for (const node of this.idealTree.inventory.values()) { if (node.extraneous) { diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js index 4c1faffa786f35..11581cb4fd9400 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js @@ -100,8 +100,10 @@ class Arborist extends Base { nodeVersion: process.version, ...options, Arborist: this.constructor, + allowScripts: options.allowScripts ?? null, binLinks: 'binLinks' in options ? !!options.binLinks : true, cache: options.cache || `${homedir()}/.npm/_cacache`, + dangerouslyAllowAllScripts: !!options.dangerouslyAllowAllScripts, dryRun: !!options.dryRun, formatPackageLock: 'formatPackageLock' in options ? !!options.formatPackageLock : true, force: !!options.force, @@ -288,6 +290,16 @@ class Arborist extends Base { return ret } + // Build an ideal tree (or reuse an already-built one) and return the + // resulting lockfile contents as a string, without writing to disk. + // Useful for callers that want to inspect, diff, or store a lockfile + // somewhere other than the project's `package-lock.json`. + async lockfileString (options = {}) { + await this.buildIdealTree(options) + + return this.idealTree.meta.toString(options) + } + async dedupe (options = {}) { // allow the user to set options on the ctor as well. // XXX: deprecate separate method options objects. diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js index 1f086e97b3cd59..14f432ca977459 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js @@ -97,7 +97,9 @@ module.exports = cls => class IsolatedReifier extends cls { } this.counter = 0 - this.idealGraph.workspaces = await Promise.all(Array.from(idealTree.fsChildren.values(), w => this.#workspaceProxy(w))) + // Skip extraneous fsChildren: workspaces removed from the root manifest can linger in fsChildren via the lockfile, and re-materializing them here would re-create a directory the user just deleted. + const fsChildren = Array.from(idealTree.fsChildren.values()).filter(w => !w.extraneous) + this.idealGraph.workspaces = await Promise.all(fsChildren.map(w => this.#workspaceProxy(w))) const processed = new Set() const queue = [idealTree, ...idealTree.fsChildren] while (queue.length !== 0) { @@ -333,7 +335,8 @@ module.exports = cls => class IsolatedReifier extends cls { root.inventory.set(workspace.location, workspace) root.workspaces.set(wsName, workspace.path) - // Create workspace Link. For root declared deps, link at root node_modules/. For undeclared deps, link at the workspace's own node_modules/ (self-link). + // Declared workspaces are symlinked at root node_modules/. + // Undeclared workspaces get a tree-only Link kept for diff/filter participation but not materialized on disk. const isDeclared = this.#rootDeclaredDeps.has(wsName) const wsLink = new IsolatedLink({ location: isDeclared ? join('node_modules', wsName) : join(c.localLocation, 'node_modules', wsName), @@ -346,7 +349,7 @@ module.exports = cls => class IsolatedReifier extends cls { target: workspace, }) if (!isDeclared) { - workspace.children.set(wsName, wsLink) + wsLink.isUndeclaredWorkspaceLink = true } root.children.set(wsName, wsLink) root.inventory.set(wsLink.location, wsLink) diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js index d4cce1ac02776c..e70a2186c29713 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js @@ -12,6 +12,7 @@ const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp' const { promiseRetry } = require('@gar/promise-retry') const { log, time } = require('proc-log') const { resolve } = require('node:path') +const { isScriptAllowed } = require('../script-allowed.js') const boolEnv = b => b ? '1' : '' const sortNodes = (a, b) => (a.depth - b.depth) || localeCompare(a.path, b.path) @@ -225,6 +226,18 @@ module.exports = cls => class Builder extends cls { return } + // Phase 1 allowScripts gate: a `false` verdict from the policy matcher + // means the user explicitly denied install scripts for this node, so skip + // it. `true` and `null` (unreviewed) both fall through to the existing + // detection logic — unreviewed nodes still run their scripts in Phase 1 + // and are surfaced via the post-reify advisory warning. The global + // --ignore-scripts kill switch in #build() still takes precedence, and + // --dangerously-allow-all-scripts bypasses this gate entirely. + if (!this.options.dangerouslyAllowAllScripts && + isScriptAllowed(node, this.options.allowScripts) === false) { + return + } + if (this.#oldMeta === null) { const { root: { meta } } = node this.#oldMeta = meta && meta.loadedFromDisk && diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js index 26ad0016be3a95..38fb4e37589255 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js @@ -4,6 +4,7 @@ const hgi = require('hosted-git-info') const npa = require('npm-package-arg') const packageContents = require('@npmcli/installed-package-contents') const pacote = require('pacote') +const { pickRegistry } = require('npm-registry-fetch') const promiseAllRejectLate = require('promise-all-reject-late') const runScript = require('@npmcli/run-script') const { callLimit: promiseCallLimit } = require('promise-call-limit') @@ -11,7 +12,7 @@ const { depth: dfwalk } = require('treeverse') const { dirname, resolve, relative, join, sep } = require('node:path') const { log, time } = require('proc-log') const { existsSync } = require('node:fs') -const { lstat, mkdir, readdir, rm, symlink } = require('node:fs/promises') +const { lstat, mkdir, readdir, readlink, rm, symlink } = require('node:fs/promises') const { moveFile } = require('@npmcli/fs') const { subset, intersects } = require('semver') const { walkUp } = require('walk-up-path') @@ -126,7 +127,11 @@ module.exports = cls => class Reifier extends cls { await this[_diffTrees]() await this.#reifyPackages() if (linked) { - await this.#cleanOrphanedStoreEntries() + // The sweep mutates node_modules on disk, so skip it for dry runs and lockfile-only installs (those modes also short-circuit #reifyPackages). + // The sweep itself scopes to in-filter workspaces when a filter is active, so it's safe to run for filtered installs too. + if (!this.options.dryRun && !this.options.packageLockOnly) { + await this.#cleanOrphanedStoreEntries() + } // swap back in the idealTree // so that the lockfile is preserved this.idealTree = oldTree @@ -234,7 +239,7 @@ module.exports = cls => class Reifier extends cls { this.actualTree = this.idealTree this.idealTree = null - if (!this.options.global) { + if (!this.options.global && !this.options.dryRun) { await this.actualTree.meta.save() const ignoreScripts = !!this.options.ignoreScripts // if we aren't doing a dry run or ignoring scripts and we actually made changes to the dep @@ -737,7 +742,14 @@ module.exports = cls => class Reifier extends cls { ...this.options, resolved: node.resolved, integrity: node.integrity, - _isRoot: node.parent?.isProjectRoot || node.parent?.isWorkspace, + // A node counts as "root" for allow-* enforcement if it satisfies at least one valid dependency edge declared by the project root or a workspace. + // node.parent is unsafe here: after hoisting, transitive packages can have the project root as their tree parent. + _isRoot: [...node.edgesIn].some(e => + e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace) + ), + // pacote's npa re-parses our `name@URL` spec as type=remote, so allowRemote would mis-fire on registry tarballs. + // Override only when we can prove the URL is registry-mediated; see #isRegistryResolvedTarball. + ...(this.#isRegistryResolvedTarball(node) ? { allowRemote: 'all' } : {}), }) // store nodes don't use Node class so node.package doesn't get updated if (node.isInStore) { @@ -748,6 +760,12 @@ module.exports = cls => class Reifier extends cls { } // node.isLink + + // Tree-only Link: present in the tree for diff/filter participation, never materialized on disk. + if (node.isUndeclaredWorkspaceLink) { + return + } + await rm(node.path, { recursive: true, force: true }) // symlink @@ -861,6 +879,24 @@ module.exports = cls => class Reifier extends cls { return wrapper } + // When extracting a registry-resolved package, the spec we hand to pacote is name@URL. + // pacote re-parses that with npa and gets spec.type === 'remote', so without an override the allow-remote gate would fire on every registry tarball (both =none and =root mis-fire). + // Returns true only when we are confident this is a registry-mediated install: the node's inbound edges must all be registry-typed (no exotic spec smuggled the URL in) AND the resolved URL's host must match the registry npm-registry-fetch selected for this spec, so a tampered lockfile pointing at an attacker host still hits the gate. + #isRegistryResolvedTarball (node) { + if (!node.resolved || !node.isRegistryDependency) { + return false + } + try { + // Hostnames are case-insensitive; lowercase both sides for safety even though WHATWG URL already normalizes. + const resolvedHost = new URL(node.resolved).hostname.toLowerCase() + // pickRegistry only consults spec.scope, so a bare-name (tag) parse is sufficient and avoids a node.version dependency. + const registryHost = new URL(pickRegistry(npa(node.name), this.options)).hostname.toLowerCase() + return resolvedHost === registryHost + } catch { + return false + } + } + #registryResolved (resolved) { // the default registry url is a magic value meaning "the currently // configured registry". @@ -1321,35 +1357,179 @@ module.exports = cls => class Reifier extends cls { // After a linked install, scan node_modules/.store/ and remove any directories that are not referenced by the current ideal tree. // Store entries become orphaned when dependencies are updated or removed, because the diff never sees the old store keys. + // Then sweep the top-level node_modules/ for orphaned symlinks (e.g. an uninstalled dep whose store entry was just removed) so we don't leave dangling links. async #cleanOrphanedStoreEntries () { - const storeDir = resolve(this.path, 'node_modules', '.store') + const nmDir = resolve(this.path, 'node_modules') + const storeDir = resolve(nmDir, '.store') + let entries try { entries = await readdir(storeDir) } catch { - return + entries = null } - // Collect valid store keys from the isolated ideal tree (location: node_modules/.store/{key}/node_modules/{pkg}) + // Collect valid store keys and valid top-level links per node_modules directory. + // Store entries have location node_modules/.store/{key}/node_modules/{pkg}. + // Top-level links have location {prefix}/node_modules/{pkg} or {prefix}/node_modules/@scope/{pkg}, where {prefix} is empty for the root project and the workspace's localLocation for workspace deps. + // Locations are normalized to forward slashes here because IsolatedNode/IsolatedLink locations are built with path.join, which uses backslashes on Windows. const validKeys = new Set() + const nmDirs = new Map() + const NM_PREFIX = 'node_modules/' + const STORE_MARKER = '/.store/' for (const child of this.idealTree.children.values()) { + const loc = child.location.replace(/\\/g, '/') if (child.isInStore) { - const key = child.location.split(sep)[2] + const key = loc.split('/')[2] validKeys.add(key) + continue + } + if (!child.isLink) { + continue + } + // Tree-only Links never exist on disk; skipping them lets the sweep remove any stale self-link left by an older npm version. + if (child.isUndeclaredWorkspaceLink) { + continue + } + const nmIdx = loc.lastIndexOf(NM_PREFIX) + if (nmIdx === -1 || loc.includes(STORE_MARKER)) { + continue + } + const prefix = loc.slice(0, nmIdx) + const dir = resolve(this.path, prefix, 'node_modules') + const rest = loc.slice(nmIdx + NM_PREFIX.length) + let entry + if (rest.startsWith('@')) { + const [scope, name] = rest.split('/') + entry = `${scope}${sep}${name}` + } else { + entry = rest.split('/')[0] + } + let set = nmDirs.get(dir) + if (!set) { + set = new Set() + nmDirs.set(dir, set) + } + set.add(entry) + } + + // Determine which node_modules directories to sweep. + // For an unfiltered install, sweep the project root and every workspace's node_modules even if no top-level links remain (e.g. last dep was just uninstalled). + // For a filtered install (npm install -w ), restrict the sweep to the in-scope workspaces so out-of-scope workspaces are left untouched, mirroring what the diff would do. + // When --include-workspace-root is set, the filter scope pulls in root deps too, so the root node_modules is included in the sweep. + const filteredNames = this.options.workspaces + const isFiltered = Array.isArray(filteredNames) && filteredNames.length > 0 + if (isFiltered) { + const allowedDirs = new Set() + for (const ws of this.idealTree.fsChildren) { + if (filteredNames.includes(ws.packageName) || filteredNames.includes(ws.name)) { + allowedDirs.add(resolve(ws.path, 'node_modules')) + } + } + if (this.options.includeWorkspaceRoot) { + allowedDirs.add(nmDir) + } + for (const dir of [...nmDirs.keys()]) { + if (!allowedDirs.has(dir)) { + nmDirs.delete(dir) + } + } + for (const dir of allowedDirs) { + if (!nmDirs.has(dir)) { + nmDirs.set(dir, new Set()) + } + } + } else { + if (!nmDirs.has(nmDir)) { + nmDirs.set(nmDir, new Set()) + } + for (const ws of this.idealTree.fsChildren) { + const wsNmDir = resolve(ws.path, 'node_modules') + if (!nmDirs.has(wsNmDir)) { + nmDirs.set(wsNmDir, new Set()) + } + } + } + + if (entries) { + const orphaned = entries.filter(e => !validKeys.has(e)) + if (orphaned.length) { + log.silly('reify', 'cleaning orphaned store entries', orphaned) + await promiseAllRejectLate( + orphaned.map(e => + rm(resolve(storeDir, e), { recursive: true, force: true }) + .catch(/* istanbul ignore next -- rm with force rarely fails */ + er => log.warn('cleanup', `Failed to remove orphaned store entry ${e}`, er)) + ) + ) + } + } + + for (const [dir, valid] of nmDirs) { + await this.#cleanOrphanedTopLevelLinks(dir, valid) + } + } + + // Remove node_modules/ entries that aren't represented in the ideal tree. + // Run for the project root and each workspace's node_modules. + // The linked diff path can't see these because #buildLinkedActualForDiff derives the actual tree from the ideal, so removed deps are never compared. + // Only symlinks whose target resolves inside the project root are removed — that covers store links (node_modules/.store/...) and workspace self-links (e.g. node_modules/ -> ../packages/) that npm itself created. + // Symlinks pointing outside the project (e.g. `npm link foo` without --save targeting the global prefix, or hand-made `ln -s` to an external path) and real directories are preserved. + async #cleanOrphanedTopLevelLinks (nmDir, validTopLevel) { + const projectPrefix = resolve(this.path) + sep + let dirents + try { + dirents = await readdir(nmDir, { withFileTypes: true }) + } catch { + return + } + + const isOurOrphan = async (linkPath) => { + let target + try { + target = await readlink(linkPath) + } catch { + /* istanbul ignore next -- readlink of an entry we just listed as a symlink should not fail */ + return false + } + return resolve(dirname(linkPath), target).startsWith(projectPrefix) + } + + const orphaned = [] + for (const ent of dirents) { + // skip npm-managed entries (.bin, .store, .package-lock.json, etc) + if (ent.name.startsWith('.')) { + continue + } + if (ent.name.startsWith('@')) { + let scoped + try { + scoped = await readdir(resolve(nmDir, ent.name), { withFileTypes: true }) + } catch { + /* istanbul ignore next -- readdir of an entry we just listed should not fail */ + continue + } + for (const pkgEnt of scoped) { + const key = `${ent.name}${sep}${pkgEnt.name}` + if (!validTopLevel.has(key) && pkgEnt.isSymbolicLink() && await isOurOrphan(resolve(nmDir, key))) { + orphaned.push(key) + } + } + } else if (!validTopLevel.has(ent.name) && ent.isSymbolicLink() && await isOurOrphan(resolve(nmDir, ent.name))) { + orphaned.push(ent.name) } } - const orphaned = entries.filter(e => !validKeys.has(e)) if (!orphaned.length) { return } - log.silly('reify', 'cleaning orphaned store entries', orphaned) + log.silly('reify', 'cleaning orphaned top-level links', orphaned) await promiseAllRejectLate( - orphaned.map(e => - rm(resolve(storeDir, e), { recursive: true, force: true }) + orphaned.map(name => + rm(resolve(nmDir, name), { recursive: true, force: true }) .catch(/* istanbul ignore next -- rm with force rarely fails */ - er => log.warn('cleanup', `Failed to remove orphaned store entry ${e}`, er)) + er => log.warn('cleanup', `Failed to remove orphaned link ${name}`, er)) ) ) } diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js b/deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js new file mode 100644 index 00000000000000..47a7f982c04ef1 --- /dev/null +++ b/deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js @@ -0,0 +1,88 @@ +const { isNodeGypPackage } = require('@npmcli/node-gyp') + +// Returns the install-relevant lifecycle scripts that would run for a +// given arborist Node, or `{}` if there are none. +// +// Includes: +// - explicit preinstall/install/postinstall +// - prepare, but only for non-registry sources (git, file, link, remote) +// - synthetic `node-gyp rebuild`, when `binding.gyp` is present on disk +// and the package does not opt out via `gypfile: false` or define its +// own install / preinstall script + +// Lifecycle-script enumeration boundary. +// +// IMPORTANT: this helper decides whether `prepare` should be included +// in the enumerated install scripts (true for non-registry sources only). +// It is NOT a policy-matching predicate. The policy matcher in +// script-allowed.js uses `isRegistryNode`, which is strictly tied to +// versionFromTgz(node.resolved). The two helpers exist separately on +// purpose: +// +// - `hasNonRegistryShape` (here): "should we consider running prepare +// on this node?" — a yes/no for what to enumerate. +// - `isRegistryNode` (script-allowed.js): "do we trust this node's +// identity enough to apply a policy entry?" — a security check. +// +// The looser fallback here (treating unknown-resolved nodes as registry, +// thus skipping `prepare`) is the safer default for enumeration: we'd +// rather omit a script we should have run than synthesise one for a +// non-registry source we couldn't confirm. The policy matcher's stricter +// behaviour is correct for its boundary; the two helpers must not be +// merged. +const hasNonRegistryShape = (node) => { + if (typeof node.isRegistryDependency === 'boolean') { + return !node.isRegistryDependency + } + if (!node.resolved) { + return false + } + return !/^https?:\/\/[^/]+\/.+\/-\/[^/]+-\d/.test(node.resolved) +} + +const getInstallScripts = async (node) => { + /* istanbul ignore next: arborist Nodes always carry a `package` object; + defensive fallbacks for non-arborist callers. */ + const pkg = node.package || {} + /* istanbul ignore next */ + const scripts = pkg.scripts || {} + const collected = {} + + if (scripts.preinstall) { + collected.preinstall = scripts.preinstall + } + if (scripts.install) { + collected.install = scripts.install + } + if (scripts.postinstall) { + collected.postinstall = scripts.postinstall + } + if (scripts.prepare && hasNonRegistryShape(node)) { + collected.prepare = scripts.prepare + } + + const hasExplicitGypGate = !!(collected.preinstall || collected.install) + if ( + !hasExplicitGypGate && + pkg.gypfile !== false && + await isNodeGypPackage(node.path).catch(() => false) + ) { + collected.install = 'node-gyp rebuild' + } + + // Lockfile-only nodes (e.g. `npm ci` before reify) carry + // `hasInstallScript: true` but no enumerated scripts: the lockfile + // records the presence flag but never the script bodies. Without this + // fallback the strict-allow-scripts preflight would miss them entirely + // and let postinstall run. We can't recover the real script body + // without fetching the manifest, so emit a sentinel describing that + // install scripts are present. + if (Object.keys(collected).length === 0 && node.hasInstallScript === true) { + collected.install = '(install scripts present)' + } + + return collected +} + +module.exports = getInstallScripts +module.exports.getInstallScripts = getInstallScripts diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/link.js b/deps/npm/node_modules/@npmcli/arborist/lib/link.js index 42bc1faf488609..3824dc81ffb3cb 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/link.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/link.js @@ -109,6 +109,26 @@ class Link extends Node { // so this is a no-op [_loadDeps] () {} + // When a Link receives overrides (via edgesIn), forward them to the target node which holds the actual edgesOut — but only when the OverrideSet has at least one rule that names a dep the target actually depends on. + // Without this scope, the link forwards a generic ancestor OverrideSet that has no real effect on the target's edges, but still flips the target to "has overrides", which changes downstream `canReplaceWith` / placement decisions and causes `npm ci` to re-resolve lockfile-pinned edges from the registry. + // See npm/cli#9357. + recalculateOutEdgesOverrides () { + if (!this.target || !this.overrides) { + return + } + let hasMatchingRule = false + for (const rule of this.overrides.ruleset.values()) { + if (this.target.edgesOut.has(rule.name)) { + hasMatchingRule = true + break + } + } + if (!hasMatchingRule) { + return + } + this.target.updateOverridesEdgeInAdded(this.overrides) + } + // links can't have children, only their targets can // fix it to an empty list so that we can still call // things that iterate over them, just as a no-op diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js b/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js new file mode 100644 index 00000000000000..91734fa38c1034 --- /dev/null +++ b/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js @@ -0,0 +1,340 @@ +const npa = require('npm-package-arg') +const semver = require('semver') +const versionFromTgz = require('./version-from-tgz.js') + +// Identity matcher for the allowScripts policy. +// +// Returns: +// - true: at least one allow entry matches and no deny entry matches +// - false: at least one deny entry matches (deny wins on conflict) +// - null: no entry matches (unreviewed) +// +// `policy` is a flat object of `spec-key -> boolean`, where spec-key is +// anything `npm-package-arg` can parse. `node` is an arborist Node. +// +// Identity rules (see RFC npm/rfcs#868): +// - registry deps match by the name+version parsed from the lockfile's +// resolved URL, NOT by `node.packageName` / `node.version`. Those two +// getters return `node.package.name` / `node.package.version`, which +// come from the tarball's own package.json and are therefore +// attacker-controlled. A package can publish a tarball claiming any +// name; the only trusted name is the one baked into the registry URL. +// - tarball / file / link / remote: exact match on node.resolved +// - git: match on hosted.ssh() plus a short-SHA prefix of the +// resolved committish + +const isScriptAllowed = (node, policy) => { + // Bundled dependencies cannot be allowlisted in Phase 1. The RFC defers + // allowlisting them to a follow-up RFC because matching by name@version + // from the bundled tarball would reintroduce manifest confusion (a + // bundled tarball can claim any name and version). Returning null here + // marks bundled deps as unreviewed regardless of any policy entries, so + // their install scripts surface in the Phase 1 advisory warning and + // (eventually) get blocked at the install-time gate. + if (node.inBundle) { + return null + } + + if (!policy || typeof policy !== 'object') { + return null + } + + let anyAllow = false + let anyDeny = false + + for (const [key, value] of Object.entries(policy)) { + if (!matches(node, key)) { + continue + } + if (value === false) { + anyDeny = true + continue + } + /* istanbul ignore else: policy values are strictly true/false; + defensive guard against unexpected coercions. */ + if (value === true) { + anyAllow = true + } + } + + if (anyDeny) { + return false + } + if (anyAllow) { + return true + } + return null +} + +const matches = (node, key) => { + let parsed + try { + parsed = npa(key) + } catch { + return false + } + + switch (parsed.type) { + case 'tag': + case 'range': + case 'version': + return matchRegistry(node, parsed) + case 'git': + return matchGit(node, parsed) + case 'file': + case 'directory': + return matchFileOrDir(node, parsed) + case 'remote': + return matchRemote(node, parsed) + case 'alias': + // Disallowed: aliases as policy keys do not match anything. + // The user has to address the real package name. + return false + /* istanbul ignore next: switch above covers every npa type we expect; + defensive fallback for future npa types. */ + default: + return false + } +} + +const matchRegistry = (node, parsed) => { + // If this node is not a registry dep, refuse the match. A registry-style + // key (`pkg`, `pkg@1`, `pkg@1 || 2`) must not match a tarball or git node + // even if their names happen to coincide. + if (!isRegistryNode(node)) { + return false + } + + // Derive the trusted name+version from the lockfile's resolved URL. + // Never use `node.packageName` / `node.version` here: those read from + // the tarball's own package.json and can be forged by a malicious + // publisher to bypass an allowScripts entry. + const trusted = getTrustedRegistryIdentity(node) + if (!trusted || trusted.name !== parsed.name) { + return false + } + + // `tag` covers `pkg@latest`. Rejected up front by validatePolicy in + // resolve-allow-scripts.js because tags look like a pin but can't be + // verified at install time. Defense-in-depth: if one slips through + // (e.g. arborist invoked directly without the resolver), don't match. + if (parsed.type === 'tag') { + /* istanbul ignore next: validatePolicy filters this; defensive */ + return false + } + + // `range` includes `pkg@^1`, `pkg@1 || 2`, `pkg@*`, `pkg@>=0`, and bare + // names like `pkg` (npa parses these as range with fetchSpec='*'). The + // RFC permits bare names (name-only allow) and exact versions joined by + // `||`; ranges like ^/~/>=/< are rejected because they would silently + // allow versions the user has never reviewed. + if (parsed.type === 'range') { + // Bare name or `pkg@*`: treat as name-only allow. + if (parsed.fetchSpec === '*' || parsed.rawSpec === '' || parsed.rawSpec === '*') { + return true + } + if (!trusted.version || !isExactVersionDisjunction(parsed.fetchSpec)) { + return false + } + return semver.satisfies(trusted.version, parsed.fetchSpec, { loose: true }) + } + + // `version` is an exact pin like `pkg@1.2.3`. + /* istanbul ignore else: parsed.type at this point is always 'version'; + the istanbul-ignored fallback below handles the impossible case. */ + if (parsed.type === 'version') { + return trusted.version === parsed.fetchSpec + } + + /* istanbul ignore next: parsed.type is constrained to tag/range/version + by the caller; this final fallback is defensive. */ + return false +} + +// Derive a registry node's trusted name+version. +// +// Preferred source: the lockfile's resolved URL parsed via +// versionFromTgz. arborist records the URL when it first adds the dep, +// before any tarball is unpacked, so the URL cannot be forged by the +// package's own package.json. +// +// Fallback for lockfiles produced with omit-lockfile-registry-resolved +// (where the URL is absent): take the dep name from an incoming +// dependency edge. The edge's spec was written by the consumer (or by an +// upstream package.json), not by the installed tarball. For aliases like +// `"trusted": "npm:naughty@1.0.0"`, the underlying registered package +// name is parsed out of the alias `subSpec`. The install location +// (`node_modules/trusted`) is deliberately not consulted because for +// aliases it carries only the alias name, which would let a malicious +// publisher bypass an allowScripts entry written for the real package. +// +// Version is left null in the fallback case because the only remaining +// source for it (`node.version`) reads from the tarball. +// +// Returns `{ name, version }` or `null` if no trusted identity exists. +const getTrustedRegistryIdentity = (node) => { + if (node.resolved && typeof node.resolved === 'string') { + const parsed = versionFromTgz('', node.resolved) + /* istanbul ignore else: versionFromTgz returns either a complete + { name, version } or null; partial objects are not produced. */ + if (parsed && parsed.name && parsed.version) { + return parsed + } + } + const name = nameFromEdges(node) + if (name) { + return { name, version: null } + } + return null +} + +const nameFromEdges = (node) => { + if (!node.edgesIn || typeof node.edgesIn[Symbol.iterator] !== 'function') { + return null + } + for (const edge of node.edgesIn) { + let parsed + try { + parsed = npa.resolve(edge.name, edge.spec) + } catch { + continue + } + // Aliases: trust the underlying registered package, not the alias. + if (parsed.type === 'alias' && parsed.subSpec && parsed.subSpec.registry) { + return parsed.subSpec.name + } + // Non-aliased registry edge: the edge name is the package name as + // written by the consumer / upstream, which is trusted (it is not + // read from the installed tarball). + if (parsed.registry) { + return parsed.name + } + } + return null +} + +// True if `rangeSpec` is one or more exact versions joined by `||`. Anything +// containing comparator operators (^, ~, >=, <, *) returns false. +const isExactVersionDisjunction = (rangeSpec) => { + /* istanbul ignore next: caller always passes parsed.fetchSpec, which + npa guarantees to be a non-empty string for range specs. */ + if (typeof rangeSpec !== 'string' || rangeSpec.trim() === '') { + return false + } + const parts = rangeSpec.split('||').map(p => p.trim()) + /* istanbul ignore next: String.prototype.split always returns at least + one element; defensive guard only. */ + if (parts.length === 0) { + return false + } + return parts.every(p => p !== '' && semver.valid(p) !== null) +} + +const matchGit = (node, parsed) => { + if (!node.resolved || !node.resolved.startsWith('git')) { + return false + } + + let nodeParsed + try { + nodeParsed = npa(node.resolved) + } catch { + /* istanbul ignore next: npa parsing a git URL we already validated + starts with `git` should not throw; defensive guard only. */ + return false + } + + // Compare the host/repo. Both sides should resolve to the same canonical + // ssh URL. + const noCommittish = { noCommittish: true } + const keyHost = parsed.hosted?.ssh(noCommittish) + const nodeHost = nodeParsed.hosted?.ssh(noCommittish) + if (keyHost && nodeHost) { + if (keyHost !== nodeHost) { + return false + } + } else if (parsed.fetchSpec && nodeParsed.fetchSpec) { + // Non-hosted git URLs: fall back to fetch spec. + if (parsed.fetchSpec !== nodeParsed.fetchSpec) { + return false + } + } else { + return false + } + + // If the policy key has no committish, name-only match. + const keyCommittish = parsed.gitCommittish || parsed.hosted?.committish + if (!keyCommittish) { + return true + } + + // Match the resolved full SHA against the key's committish. Users + // typically write short SHAs in the policy; the lockfile stores 40-char + // SHAs. Direction matters: the lockfile's full SHA must START WITH the + // key's short SHA, never the reverse. A longer key matching a shorter + // resolved committish would let a malformed lockfile or a divergent + // resolver allow scripts the user never approved. + const nodeCommittish = nodeParsed.gitCommittish || nodeParsed.hosted?.committish || '' + if (!nodeCommittish) { + return false + } + return nodeCommittish.startsWith(keyCommittish) +} + +const matchFileOrDir = (node, parsed) => { + if (!node.resolved) { + return false + } + return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec +} + +const matchRemote = (node, parsed) => { + if (!node.resolved) { + return false + } + return node.resolved === parsed.fetchSpec || node.resolved === parsed.saveSpec +} + +const isRegistryNode = (node) => { + // Prefer arborist's edge-based check when available (real Node objects). + // It inspects the incoming edges' specs and only returns true if every + // edge resolves to a registry spec, which is much harder to spoof than + // the URL. + if (typeof node.isRegistryDependency === 'boolean') { + return node.isRegistryDependency + } + // Fall back to URL parsing for nodes without the arborist getter + // (e.g. test fixtures, lockfiles with omit-lockfile-registry-resolved). + // Treat the node as a registry dep when: + // - resolved is missing entirely (omitLockfileRegistryResolved), + // - resolved is an https/http URL pointing at a registry tarball, or + // - resolved is undefined and the node has a version (defensive). + if (!node.resolved) { + return !!node.version + } + // Registry tarballs live at `//-/-.tgz`. + // Require a path segment before `/-/` so an attacker can't lift a + // registry-style allow entry to a hostile URL like + // `https://evil.com/-/trusted-1.0.0.tgz`. + return /^https?:\/\/[^/]+\/.+\/-\/[^/]+-\d/.test(node.resolved) +} + +// Trusted display identity for human-facing output (`npm install` +// advisory, `npm approve-scripts --allow-scripts-pending`). Same idea as +// getTrustedRegistryIdentity, but for DISPLAY only — version falls back +// to node.version when the URL doesn't carry one. Must never be used +// for policy matching. +const trustedDisplay = (node) => { + const trusted = getTrustedRegistryIdentity(node) + /* istanbul ignore next: defensive fallbacks for nodes without name/version */ + return { + name: (trusted && trusted.name) || node.name || null, + version: (trusted && trusted.version) || node.version || null, + } +} + +module.exports = isScriptAllowed +module.exports.isScriptAllowed = isScriptAllowed +module.exports.isExactVersionDisjunction = isExactVersionDisjunction +module.exports.getTrustedRegistryIdentity = getTrustedRegistryIdentity +module.exports.trustedDisplay = trustedDisplay diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js b/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js index 751c549fed63bc..6af902cdf8a48e 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js @@ -929,10 +929,24 @@ class Shrinkwrap { continue } const loc = relpath(this.path, node.path) - this.data.packages[loc] = Shrinkwrap.metaFromNode( + // Drop lockfile entries for extraneous nodes outside node_modules. These are stale workspace entries: the workspace was removed from package.json or its directory was deleted, so it should not be tracked in package-lock.json. + if (node.extraneous && !/(^|\/)node_modules\//.test(loc) && loc !== 'node_modules') { + continue + } + const meta = Shrinkwrap.metaFromNode( node, this.path, this.resolveOptions) + // Skip inert nodes — these are optional deps that failed to load + // (e.g. 404 from a proxy registry that hasn't cached the package, + // or incomplete manifest missing version field). + // #pruneFailedOptional marks them inert so they won't be reified; + // writing them to the lockfile produces invalid entries like + // {"optional": true} that cause "Invalid Version:" errors. + if (node.inert && !node.package.version) { + continue + } + this.data.packages[loc] = meta } } else if (this.#awaitingUpdate.size > 0) { for (const loc of this.#awaitingUpdate.keys()) { diff --git a/deps/npm/node_modules/@npmcli/arborist/package.json b/deps/npm/node_modules/@npmcli/arborist/package.json index d0f823e61d3481..712151e63a1c65 100644 --- a/deps/npm/node_modules/@npmcli/arborist/package.json +++ b/deps/npm/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "9.4.3", + "version": "9.7.0", "description": "Manage node_modules trees", "dependencies": { "@gar/promise-retry": "^1.0.0", diff --git a/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js b/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js index c3e5cd2b430189..2cb03709d73b5e 100644 --- a/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js +++ b/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js @@ -1,4 +1,5 @@ const Definition = require('./definition.js') +const parseAllowScriptsList = require('../parse-allow-scripts-list.js') const ciInfo = require('ci-info') const querystring = require('node:querystring') @@ -153,7 +154,7 @@ const definitions = { defaultDescription: ` 'public' for new packages, existing packages it will not change the current level `, - type: [null, 'restricted', 'public'], + type: [null, 'restricted', 'public', 'private'], description: ` If you do not want your scoped package to be publicly viewable (and installable) set \`--access=restricted\`. @@ -164,8 +165,13 @@ const definitions = { packages. Specifying a value of \`restricted\` or \`public\` during publish will change the access for an existing package the same way that \`npm access set status\` would. + + The value \`private\` is an alias for \`restricted\`. `, - flatten, + flatten (key, obj, flatOptions) { + const value = obj[key] + flatOptions.access = value === 'private' ? 'restricted' : value + }, }), all: new Definition('all', { default: false, @@ -187,6 +193,36 @@ const definitions = { `, flatten, }), + 'allow-directory': new Definition('allow-directory', { + default: 'all', + type: ['all', 'none', 'root'], + description: ` + Limits the ability for npm to install dependencies from directories. + That is, dependencies that point to a directory instead of a version or semver range. + Please note that this could leave your tree incomplete and some packages may not function as intended or designed. + Changing this setting will not remove dependencies that are already installed. + + \`all\` allows any directories to be installed. + \`none\` prevents any directories from being installed. + \`root\` only allows directories defined in your project's package.json to be installed. Also allows directory dependencies to be used for other commands like \`npm view\` + `, + flatten, + }), + 'allow-file': new Definition('allow-file', { + default: 'all', + type: ['all', 'none', 'root'], + description: ` + Limits the ability for npm to install dependencies from tarball files. + That is, dependencies that point to a local tarball file instead of a version or semver range. + Please note that this could leave your tree incomplete and some packages may not function as intended or designed. + Changing this setting will not remove dependencies that are already installed. + + \`all\` allows any tarball file to be installed. + \`none\` prevents any tarball file from being installed. + \`root\` only allows tarball files defined in your project's package.json to be installed. Also allows tarball file dependencies to be used for other commands like \`npm view\` + `, + flatten, + }), 'allow-git': new Definition('allow-git', { default: 'all', type: ['all', 'none', 'root'], @@ -194,13 +230,54 @@ const definitions = { Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. + Changing this setting will not remove dependencies that are already installed. \`all\` allows any git dependencies to be fetched and installed. \`none\` prevents any git dependencies from being fetched and installed. - \`root\` only allows git dependencies defined in your project's package.json to be fetched installed. Also allows git dependencies to be fetched for other commands like \`npm view\` + \`root\` only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \`npm view\` `, flatten, }), + 'allow-remote': new Definition('allow-remote', { + default: 'all', + type: ['all', 'none', 'root'], + description: ` + Limits the ability for npm to fetch dependencies from urls. + That is, dependencies that point to a tarball url instead of a version or semver range. + Please note that this could leave your tree incomplete and some packages may not function as intended or designed. + Changing this setting will not remove dependencies that are already installed. + + \`all\` allows any url to be installed. + \`none\` prevents any url from being installed. + \`root\` only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \`npm view\` + `, + flatten, + }), + 'allow-scripts': new Definition('allow-scripts', { + default: '', + type: [String, Array], + hint: '', + description: ` + Comma-separated list of packages whose install-time lifecycle scripts + (\`preinstall\`, \`install\`, \`postinstall\`, and \`prepare\` for + non-registry dependencies) are allowed to run. + + This setting is intended for one-off and global contexts: \`npm exec\`, + \`npx\`, and \`npm install -g\`, where no project \`package.json\` is + involved. For team-wide policy in a project, use the \`allowScripts\` + field in \`package.json\` (which also supports explicit denials), or + configure it in \`.npmrc\`. Passing \`--allow-scripts\` on the command + line during a project-scoped \`npm install\`, \`ci\`, \`update\`, or + \`rebuild\` is an error. + + Each name is matched against a dependency's resolved identity, not + against the package's self-reported name. \`--ignore-scripts\` and + \`--dangerously-allow-all-scripts\` both override this setting. + `, + flatten (key, obj, flatOptions) { + flatOptions.allowScripts = parseAllowScriptsList(obj[key]) + }, + }), also: new Definition('also', { default: null, type: [null, 'dev', 'development'], @@ -246,7 +323,6 @@ const definitions = { default: null, hint: '', type: [null, Date], - exclusive: ['min-release-age'], description: ` If passed to \`npm install\`, will rebuild the npm tree such that only versions that were available **on or before** the given date are @@ -257,6 +333,12 @@ const definitions = { pass the \`--before\` filter, the most recent version less than or equal to that tag will be used. For example, \`foo@latest\` might install \`foo@1.2\` even though \`latest\` is \`2.0\`. + + If \`before\` and \`min-release-age\` are both set in the same source, + \`before\` wins (an explicit absolute date overrides a relative window). + Across sources, the standard precedence applies (cli > env > project > + user > global), so a higher-priority source can always relax or + override a lower-priority one. `, flatten, }), @@ -484,6 +566,18 @@ const definitions = { `, flatten, }), + 'dangerously-allow-all-scripts': new Definition('dangerously-allow-all-scripts', { + default: false, + type: Boolean, + description: ` + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + dependency install script regardless of whether it was approved or + denied. Intended as a migration escape hatch only; its use is strongly + discouraged. \`--ignore-scripts\` still takes precedence over this + setting. + `, + flatten, + }), depth: new Definition('depth', { default: null, defaultDescription: ` @@ -1363,7 +1457,6 @@ const definitions = { default: null, hint: '', type: [null, Number], - exclusive: ['before'], envExport: false, description: ` If set, npm will build the npm tree such that only versions that were @@ -1372,13 +1465,18 @@ const definitions = { command will error. This flag is a complement to \`before\`, which accepts an exact date - instead of a relative number of days. + instead of a relative number of days. The two may coexist (e.g. + \`min-release-age\` in your \`.npmrc\` is preserved when npm internally + spawns a sub-process with \`--before\` while preparing a \`git:\` or + \`github:\` dependency); when both apply, \`before\` wins within a + single source and across sources the standard precedence rules apply. `, flatten: (key, obj, flatOptions) => { - if (obj['min-release-age'] !== null) { - flatOptions.before = new Date(Date.now() - (86400000 * obj['min-release-age'])) - obj.before = flatOptions.before - delete obj['min-release-age'] + const age = obj['min-release-age'] + // `hasOwn` so a `before` inherited via ConfigData's prototype chain (lib/index.js) from a lower-priority source doesn't silently win. + // The `: null` clear depends on `Config#flat` iterating sources low → high. + if (age != null && !Object.hasOwn(obj, 'before')) { + flatOptions.before = age ? new Date(Date.now() - (86400000 * age)) : null } }, }), @@ -1612,6 +1710,27 @@ const definitions = { `, flatten, }), + 'allow-scripts-pending': new Definition('allow-scripts-pending', { + default: false, + type: Boolean, + description: ` + List packages with install scripts that are not yet covered by the + \`allowScripts\` policy, without modifying \`package.json\`. Only + meaningful for \`npm approve-scripts\`. + `, + flatten, + }), + 'allow-scripts-pin': new Definition('allow-scripts-pin', { + default: true, + type: Boolean, + description: ` + Write pinned (\`pkg@version\`) entries when approving install scripts. + Set to \`false\` to write name-only entries that allow any version. + Has no effect on \`npm deny-scripts\`, which always writes name-only + entries regardless of this setting. + `, + flatten, + }), 'prefer-dedupe': new Definition('prefer-dedupe', { default: false, type: Boolean, @@ -2183,6 +2302,22 @@ const definitions = { `, flatten, }), + 'strict-allow-scripts': new Definition('strict-allow-scripts', { + default: false, + type: Boolean, + description: ` + If \`true\`, turn the install-script policy from a warning into a hard + error: any dependency with install scripts not covered by + \`allowScripts\` will fail the install instead of running with a + notice. + + Dependencies explicitly denied with \`false\` in \`allowScripts\` are + always silently skipped; this setting only affects unreviewed entries. + \`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both + override this setting. + `, + flatten, + }), 'strict-ssl': new Definition('strict-ssl', { default: true, type: Boolean, diff --git a/deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js b/deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js new file mode 100644 index 00000000000000..0f13d4a75b6349 --- /dev/null +++ b/deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js @@ -0,0 +1,23 @@ +// Parse an `allow-scripts` raw config value (string or array of strings) +// into a flat array of trimmed package-spec entries. Shared between the +// CLI/env layer (via the `allow-scripts` definition's `flatten`) and the +// package.json / .npmrc layer (in lib/utils/resolve-allow-scripts.js) so +// both paths agree on quoting, whitespace, and duplicate handling. +const parseAllowScriptsList = (raw) => { + const parts = [] + const entries = Array.isArray(raw) ? raw : (typeof raw === 'string' ? [raw] : []) + for (const entry of entries) { + if (typeof entry !== 'string') { + continue + } + for (const part of entry.split(',')) { + const trimmed = part.trim() + if (trimmed) { + parts.push(trimmed) + } + } + } + return parts +} + +module.exports = parseAllowScriptsList diff --git a/deps/npm/node_modules/@npmcli/config/package.json b/deps/npm/node_modules/@npmcli/config/package.json index 5da16efc6cc4c3..295855d76df3e0 100644 --- a/deps/npm/node_modules/@npmcli/config/package.json +++ b/deps/npm/node_modules/@npmcli/config/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/config", - "version": "10.8.1", + "version": "10.10.0", "files": [ "bin/", "lib/" diff --git a/deps/npm/node_modules/@sigstore/core/dist/dsse.js b/deps/npm/node_modules/@sigstore/core/dist/dsse.js index ca7b63630e2ba9..9dcc2649198c19 100644 --- a/deps/npm/node_modules/@sigstore/core/dist/dsse.js +++ b/deps/npm/node_modules/@sigstore/core/dist/dsse.js @@ -19,12 +19,11 @@ limitations under the License. const PAE_PREFIX = 'DSSEv1'; // DSSE Pre-Authentication Encoding function preAuthEncoding(payloadType, payload) { - const prefix = [ - PAE_PREFIX, - payloadType.length, - payloadType, - payload.length, - '', - ].join(' '); - return Buffer.concat([Buffer.from(prefix, 'ascii'), payload]); + const typeBytes = Buffer.from(payloadType, 'utf-8'); + return Buffer.concat([ + Buffer.from(`${PAE_PREFIX} ${typeBytes.length} `, 'ascii'), + typeBytes, + Buffer.from(` ${payload.length} `, 'ascii'), + payload, + ]); } diff --git a/deps/npm/node_modules/@sigstore/core/package.json b/deps/npm/node_modules/@sigstore/core/package.json index 0564a373c6fa31..82cab44654a1c9 100644 --- a/deps/npm/node_modules/@sigstore/core/package.json +++ b/deps/npm/node_modules/@sigstore/core/package.json @@ -1,6 +1,6 @@ { "name": "@sigstore/core", - "version": "3.2.0", + "version": "3.2.1", "description": "Base library for Sigstore", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/deps/npm/node_modules/@sigstore/verify/dist/key/index.js b/deps/npm/node_modules/@sigstore/verify/dist/key/index.js index c966ccb1e925ef..880ad04bd235d7 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/key/index.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/key/index.js @@ -56,9 +56,17 @@ function getSigner(cert) { else { issuer = cert.extension(OID_FULCIO_ISSUER_V1)?.value.toString('ascii'); } + const oids = cert.extensions.map((ext) => { + const oid = ext.subs[0].toOID(); + return { + oid: { id: oid.split('.').map(Number) }, + value: ext.subs[ext.subs.length - 1].value, + }; + }); const identity = { extensions: { issuer }, subjectAlternativeName: cert.subjectAltName, + oids, }; return { key: core_1.crypto.createPublicKey(cert.publicKey), diff --git a/deps/npm/node_modules/@sigstore/verify/dist/policy.js b/deps/npm/node_modules/@sigstore/verify/dist/policy.js index f5960cf047b84b..b08d083a296fb8 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/policy.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/policy.js @@ -2,7 +2,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.verifySubjectAlternativeName = verifySubjectAlternativeName; exports.verifyExtensions = verifyExtensions; +exports.verifyOIDs = verifyOIDs; const error_1 = require("./error"); +// Verifies that the signer's SAN matches the policy identity. The +// policyIdentity is treated as a JavaScript regular expression pattern and +// tested against the full signerIdentity string. For exact matching, use +// anchored patterns (e.g. '^user@example\\.com$'). function verifySubjectAlternativeName(policyIdentity, signerIdentity) { if (signerIdentity === undefined || !signerIdentity.match(policyIdentity)) { throw new error_1.PolicyError({ @@ -22,3 +27,24 @@ function verifyExtensions(policyExtensions, signerExtensions = {}) { } } } +function verifyOIDs(policyOIDs, signerOIDs = []) { + for (const policyOID of policyOIDs) { + const match = signerOIDs.find((signerOID) => oidEquals(policyOID.oid?.id, signerOID.oid?.id) && + policyOID.value.equals(signerOID.value)); + if (!match) { + /* istanbul ignore next */ + const oid = policyOID.oid?.id.join('.') ?? ''; + throw new error_1.PolicyError({ + code: 'UNTRUSTED_SIGNER_ERROR', + message: `invalid certificate extension - missing OID ${oid}`, + }); + } + } +} +function oidEquals(a, b) { + /* istanbul ignore if */ + if (a === undefined || b === undefined) { + return false; + } + return a.length === b.length && a.every((v, i) => v === b[i]); +} diff --git a/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js b/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js index 03a51083e10827..603e559831a9d8 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js @@ -12,6 +12,10 @@ function getTSATimestamp(timestamp, data, timestampAuthorities) { }; } function getTLogTimestamp(entry) { + // Only entries with an inclusion promise provide a verifiable timestamp + if (!entry.inclusionPromise) { + return undefined; + } return { type: 'transparency-log', logID: entry.logId.keyId, diff --git a/deps/npm/node_modules/@sigstore/verify/dist/verifier.js b/deps/npm/node_modules/@sigstore/verify/dist/verifier.js index 5751087ff178d2..eeba4128fabe34 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/verifier.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/verifier.js @@ -46,17 +46,22 @@ class Verifier { } // Checks that all of the timestamps in the entity are valid and returns them verifyTimestamps(entity) { - let timestampCount = 0; - const timestamps = entity.timestamps.map((timestamp) => { + const timestamps = []; + for (const timestamp of entity.timestamps) { switch (timestamp.$case) { case 'timestamp-authority': - timestampCount++; - return (0, timestamp_1.getTSATimestamp)(timestamp.timestamp, entity.signature.signature, this.trustMaterial.timestampAuthorities); - case 'transparency-log': - timestampCount++; - return (0, timestamp_1.getTLogTimestamp)(timestamp.tlogEntry); + timestamps.push((0, timestamp_1.getTSATimestamp)(timestamp.timestamp, entity.signature.signature, this.trustMaterial.timestampAuthorities)); + break; + case 'transparency-log': { + const result = (0, timestamp_1.getTLogTimestamp)(timestamp.tlogEntry); + /* istanbul ignore else */ + if (result) { + timestamps.push(result); + } + break; + } } - }); + } // Check for duplicate timestamps if (containsDupes(timestamps)) { throw new error_1.VerificationError({ @@ -64,10 +69,10 @@ class Verifier { message: 'duplicate timestamp', }); } - if (timestampCount < this.options.timestampThreshold) { + if (timestamps.length < this.options.timestampThreshold) { throw new error_1.VerificationError({ code: 'TIMESTAMP_ERROR', - message: `expected ${this.options.timestampThreshold} timestamps, got ${timestampCount}`, + message: `expected ${this.options.timestampThreshold} timestamps, got ${timestamps.length}`, }); } return timestamps.map((t) => t.timestamp); @@ -133,6 +138,11 @@ class Verifier { if (policy.extensions) { (0, policy_1.verifyExtensions)(policy.extensions, identity.extensions); } + // Check that the OIDs of the signer match the policy + /* istanbul ignore if */ + if (policy.oids) { + (0, policy_1.verifyOIDs)(policy.oids, identity.oids); + } } } exports.Verifier = Verifier; diff --git a/deps/npm/node_modules/@sigstore/verify/package.json b/deps/npm/node_modules/@sigstore/verify/package.json index 79826a80bddebf..9c4e5dc7a727a7 100644 --- a/deps/npm/node_modules/@sigstore/verify/package.json +++ b/deps/npm/node_modules/@sigstore/verify/package.json @@ -1,6 +1,6 @@ { "name": "@sigstore/verify", - "version": "3.1.0", + "version": "3.1.1", "description": "Verification of Sigstore signatures", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -28,7 +28,7 @@ "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0" + "@sigstore/core": "^3.2.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/deps/npm/node_modules/bin-links/lib/check-bin.js b/deps/npm/node_modules/bin-links/lib/check-bin.js index c5b997bb96355c..a7fc8d51b415e2 100644 --- a/deps/npm/node_modules/bin-links/lib/check-bin.js +++ b/deps/npm/node_modules/bin-links/lib/check-bin.js @@ -2,7 +2,7 @@ // either rejects or resolves to nothing. return value not relevant. const isWindows = require('./is-windows.js') const binTarget = require('./bin-target.js') -const { resolve, dirname } = require('path') +const { resolve, dirname, sep } = require('path') const readCmdShim = require('read-cmd-shim') const { readlink } = require('fs/promises') @@ -34,7 +34,9 @@ const checkLink = async ({ target, path }) => { const resolved = resolve(dirname(target), current) - if (resolved.toLowerCase().indexOf(path.toLowerCase()) !== 0) { + const resolvedLower = resolved.toLowerCase() + const pathLower = path.toLowerCase() + if (resolvedLower !== pathLower && !resolvedLower.startsWith(pathLower + sep)) { return failEEXIST({ target }) } } @@ -65,7 +67,9 @@ const checkShim = async ({ target, path }) => { const resolved = resolve(dirname(shim), current.replace(/\\/g, '/')) - if (resolved.toLowerCase().indexOf(path.toLowerCase()) !== 0) { + const resolvedLower = resolved.toLowerCase() + const pathLower = path.toLowerCase() + if (resolvedLower !== pathLower && !resolvedLower.startsWith(pathLower + sep)) { return failEEXIST({ target: shim }) } })) diff --git a/deps/npm/node_modules/bin-links/lib/link-gently.js b/deps/npm/node_modules/bin-links/lib/link-gently.js index a39d3bced57b13..c4a38f7f54b951 100644 --- a/deps/npm/node_modules/bin-links/lib/link-gently.js +++ b/deps/npm/node_modules/bin-links/lib/link-gently.js @@ -4,7 +4,7 @@ // if there's a symlink already, pointing into our pkg, remove it first // then create the symlink -const { resolve, dirname } = require('path') +const { resolve, dirname, sep } = require('path') const { lstat, mkdir, readlink, rm, symlink } = require('fs/promises') const { log } = require('proc-log') const throwSignificant = er => { @@ -63,7 +63,7 @@ const linkGently = async ({ path, to, from, absFrom, force }) => { } // skip it, already set up like we want it. target = resolve(dirname(to), target) - if (target.indexOf(path) === 0 || force) { + if (target === path || target.startsWith(path + sep) || force) { return rm(to, rmOpts).then(() => CLOBBER) } // neither skip nor clobber diff --git a/deps/npm/node_modules/bin-links/lib/shim-bin.js b/deps/npm/node_modules/bin-links/lib/shim-bin.js index 67e2702702f0a8..91a6fcc94153bc 100644 --- a/deps/npm/node_modules/bin-links/lib/shim-bin.js +++ b/deps/npm/node_modules/bin-links/lib/shim-bin.js @@ -1,4 +1,4 @@ -const { resolve, dirname } = require('path') +const { resolve, dirname, sep } = require('path') const { lstat } = require('fs/promises') const throwNonEnoent = er => { if (er.code !== 'ENOENT') { @@ -64,7 +64,8 @@ const shimBin = ({ path, to, from, absFrom, force }) => { return readCmdShim(s) .then(target => { target = resolve(dirname(to), target) - if (target.indexOf(resolve(path)) !== 0) { + const base = resolve(path) + if (target !== base && !target.startsWith(base + sep)) { return failEEXIST({ from, to, path }) } return false diff --git a/deps/npm/node_modules/bin-links/package.json b/deps/npm/node_modules/bin-links/package.json index 23f52cfc96ec46..80a63323e884b1 100644 --- a/deps/npm/node_modules/bin-links/package.json +++ b/deps/npm/node_modules/bin-links/package.json @@ -1,6 +1,6 @@ { "name": "bin-links", - "version": "6.0.0", + "version": "6.0.2", "description": "JavaScript package binary linker", "main": "./lib/index.js", "scripts": { @@ -32,7 +32,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.27.1", + "@npmcli/template-oss": "4.30.0", "require-inject": "^1.4.4", "tap": "^16.0.1" }, @@ -55,7 +55,9 @@ "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", "windowsCI": false, - "version": "4.27.1", - "publish": true + "version": "4.30.0", + "publish": true, + "updateNpm": false, + "latestCiVersion": 24 } } diff --git a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js index b9f3c6fdf6dfc7..33063dd3552cd6 100644 --- a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js +++ b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js @@ -155,7 +155,7 @@ function expand_(str, max, isTop) { } const pad = n.some(isPadded); N = []; - for (let i = x; test(i, y); i += incr) { + for (let i = x; test(i, y) && N.length < max; i += incr) { let c; if (isAlphaSequence) { c = String.fromCharCode(i); diff --git a/deps/npm/node_modules/brace-expansion/dist/esm/index.js b/deps/npm/node_modules/brace-expansion/dist/esm/index.js index 855e22cd71e824..32399e7b2f5cf1 100644 --- a/deps/npm/node_modules/brace-expansion/dist/esm/index.js +++ b/deps/npm/node_modules/brace-expansion/dist/esm/index.js @@ -151,7 +151,7 @@ function expand_(str, max, isTop) { } const pad = n.some(isPadded); N = []; - for (let i = x; test(i, y); i += incr) { + for (let i = x; test(i, y) && N.length < max; i += incr) { let c; if (isAlphaSequence) { c = String.fromCharCode(i); diff --git a/deps/npm/node_modules/brace-expansion/package.json b/deps/npm/node_modules/brace-expansion/package.json index 83a8289641532c..81524809e58612 100644 --- a/deps/npm/node_modules/brace-expansion/package.json +++ b/deps/npm/node_modules/brace-expansion/package.json @@ -1,7 +1,7 @@ { "name": "brace-expansion", "description": "Brace expansion as known from sh/bash", - "version": "5.0.5", + "version": "5.0.6", "files": [ "dist" ], diff --git a/deps/npm/node_modules/cidr-regex/package.json b/deps/npm/node_modules/cidr-regex/package.json index 1c2db1a6e28b30..c15a446e7e2136 100644 --- a/deps/npm/node_modules/cidr-regex/package.json +++ b/deps/npm/node_modules/cidr-regex/package.json @@ -1,12 +1,25 @@ { "name": "cidr-regex", - "version": "5.0.4", + "version": "5.0.5", "description": "Regular expression for matching IP addresses in CIDR notation", "author": "silverwind ", "contributors": [ "Felipe Apostol (http://flipjs.io/)" ], "repository": "silverwind/cidr-regex", + "keywords": [ + "cidr", + "regex", + "regexp", + "ip", + "ipv4", + "ipv6", + "address", + "subnet", + "network", + "notation", + "match" + ], "license": "BSD-2-Clause", "type": "module", "sideEffects": false, @@ -21,18 +34,18 @@ }, "devDependencies": { "@types/node": "25.6.0", - "@typescript/native-preview": "7.0.0-dev.20260420.1", + "@typescript/native-preview": "7.0.0-dev.20260427.1", "eslint": "10.2.1", - "eslint-config-silverwind": "131.0.5", + "eslint-config-silverwind": "132.0.0", "jest-extended": "7.0.0", - "tsdown": "0.21.9", - "tsdown-config-silverwind": "2.1.0", + "tsdown": "0.21.10", + "tsdown-config-silverwind": "2.1.1", "typescript": "6.0.3", "typescript-config-silverwind": "18.0.0", - "updates": "17.15.5", - "updates-config-silverwind": "2.1.0", - "versions": "15.0.0", - "vitest": "4.1.4", - "vitest-config-silverwind": "11.3.0" + "updates": "17.16.4", + "updates-config-silverwind": "2.1.1", + "versions": "15.0.1", + "vitest": "4.1.5", + "vitest-config-silverwind": "11.3.1" } } \ No newline at end of file diff --git a/deps/npm/node_modules/hosted-git-info/lib/hosts.js b/deps/npm/node_modules/hosted-git-info/lib/hosts.js index 6e7c123dbff8b4..a1635d3c898fed 100644 --- a/deps/npm/node_modules/hosted-git-info/lib/hosts.js +++ b/deps/npm/node_modules/hosted-git-info/lib/hosts.js @@ -110,7 +110,7 @@ hosts.gitlab = { blobpath: 'tree', editpath: '-/edit', tarballtemplate: ({ domain, user, project, committish }) => - `https://${domain}/${user}/${project}/repository/archive.tar.gz?ref=${maybeEncode(committish || 'HEAD')}`, + `https://${domain}/api/v4/projects/${maybeEncode(user + '/' + project)}/repository/archive.tar.gz?sha=${maybeEncode(committish || 'HEAD')}`, extract: (url) => { const path = url.pathname.slice(1) if (path.includes('/-/') || path.includes('/archive.tar.gz')) { @@ -198,7 +198,7 @@ hosts.sourcehut = { filetemplate: ({ domain, user, project, committish, path }) => `https://${domain}/${user}/${project}/blob/${maybeEncode(committish) || 'HEAD'}/${path}`, httpstemplate: ({ domain, user, project, committish }) => - `https://${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, + `https://${domain}/${user}/${project}${maybeJoin('#', committish)}`, tarballtemplate: ({ domain, user, project, committish }) => `https://${domain}/${user}/${project}/archive/${maybeEncode(committish) || 'HEAD'}.tar.gz`, bugstemplate: () => null, diff --git a/deps/npm/node_modules/hosted-git-info/package.json b/deps/npm/node_modules/hosted-git-info/package.json index 1e74eda1656d78..f21e546a64bfa5 100644 --- a/deps/npm/node_modules/hosted-git-info/package.json +++ b/deps/npm/node_modules/hosted-git-info/package.json @@ -1,6 +1,6 @@ { "name": "hosted-git-info", - "version": "9.0.2", + "version": "9.0.3", "description": "Provides metadata and conversions from repository urls for GitHub, Bitbucket and GitLab", "main": "./lib/index.js", "repository": { @@ -21,22 +21,23 @@ "homepage": "https://github.com/npm/hosted-git-info", "scripts": { "posttest": "npm run lint", - "snap": "tap", - "test": "tap", + "snap": "node --test --test-update-snapshots './test/**/*.js'", + "test": "node --test './test/**/*.js'", "test:coverage": "tap --coverage-report=html", "lint": "npm run eslint", "postlint": "template-oss-check", "lintfix": "npm run eslint -- --fix", "template-oss-apply": "template-oss-apply --force", - "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" + "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"", + "test:node20": "node --test test", + "test:cover": "node --test --experimental-test-coverage --test-timeout=3000 --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 './test/**/*.js'" }, "dependencies": { "lru-cache": "^11.1.0" }, "devDependencies": { - "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.25.1", - "tap": "^16.0.1" + "@npmcli/eslint-config": "^6.0.0", + "@npmcli/template-oss": "4.30.0" }, "files": [ "bin/", @@ -45,17 +46,12 @@ "engines": { "node": "^20.17.0 || >=22.9.0" }, - "tap": { - "color": 1, - "coverage": true, - "nyc-arg": [ - "--exclude", - "tap-snapshots/**" - ] - }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.25.1", - "publish": "true" + "version": "4.30.0", + "publish": "true", + "testRunner": "node:test", + "latestCiVersion": 24, + "updateNpm": false } } diff --git a/deps/npm/node_modules/ip-address/dist/common.js b/deps/npm/node_modules/ip-address/dist/common.js index 273a01e28e317d..6b76e051b44e41 100644 --- a/deps/npm/node_modules/ip-address/dist/common.js +++ b/deps/npm/node_modules/ip-address/dist/common.js @@ -2,9 +2,11 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.isInSubnet = isInSubnet; exports.isCorrect = isCorrect; +exports.prefixLengthFromMask = prefixLengthFromMask; exports.numberToPaddedHex = numberToPaddedHex; exports.stringToPaddedHex = stringToPaddedHex; exports.testBit = testBit; +const address_error_1 = require("./address-error"); function isInSubnet(address) { if (this.subnetMask < address.subnetMask) { return false; @@ -25,6 +27,25 @@ function isCorrect(defaultBits) { return this.parsedSubnet === String(this.subnetMask); }; } +/** + * Returns the prefix length (number of leading 1 bits) of a contiguous + * subnet mask. Throws `AddressError` if the mask is non-contiguous (e.g. + * `255.0.255.0`). + */ +function prefixLengthFromMask(value, totalBits) { + const binary = value.toString(2).padStart(totalBits, '0'); + if (binary.length > totalBits) { + throw new address_error_1.AddressError('Invalid subnet mask.'); + } + const firstZero = binary.indexOf('0'); + if (firstZero === -1) { + return totalBits; + } + if (binary.slice(firstZero).includes('1')) { + throw new address_error_1.AddressError('Invalid subnet mask.'); + } + return firstZero; +} function numberToPaddedHex(number) { return number.toString(16).padStart(2, '0'); } diff --git a/deps/npm/node_modules/ip-address/dist/ipv4.js b/deps/npm/node_modules/ip-address/dist/ipv4.js index 311c89c6965cb8..2c0fd182086d2d 100644 --- a/deps/npm/node_modules/ip-address/dist/ipv4.js +++ b/deps/npm/node_modules/ip-address/dist/ipv4.js @@ -28,9 +28,9 @@ exports.Address4 = void 0; const common = __importStar(require("./common")); const constants = __importStar(require("./v4/constants")); const address_error_1 = require("./address-error"); +const isCorrect4 = common.isCorrect(constants.BITS); /** * Represents an IPv4 address - * @class Address4 * @param {string} address - An IPv4 address string */ class Address4 { @@ -43,15 +43,11 @@ class Address4 { this.v4 = true; /** * Returns true if the address is correct, false otherwise - * @memberof Address4 - * @instance * @returns {Boolean} */ - this.isCorrect = common.isCorrect(constants.BITS); + this.isCorrect = isCorrect4; /** * Returns true if the given address is in the subnet of the current address - * @memberof Address4 - * @instance * @returns {boolean} */ this.isInSubnet = common.isInSubnet; @@ -69,6 +65,13 @@ class Address4 { this.addressMinusSuffix = address; this.parsedAddress = this.parse(address); } + /** + * Returns true if the given string is a valid IPv4 address (with optional + * CIDR subnet), false otherwise. Host bits in the subnet portion are + * allowed (e.g. `192.168.1.5/24` is valid); for strict network-address + * validation compare `correctForm()` to `startAddress().correctForm()`, + * or use `networkForm()`. + */ static isValid(address) { try { // eslint-disable-next-line no-new @@ -79,8 +82,11 @@ class Address4 { return false; } } - /* - * Parses a v4 address + /** + * Parses an IPv4 address string into its four octet groups and stores the + * result on `this.parsedAddress`. Called automatically by the constructor; + * you typically don't need to call it directly. Throws `AddressError` if + * the input is not a valid IPv4 address. */ parse(address) { const groups = address.split('.'); @@ -90,45 +96,110 @@ class Address4 { return groups; } /** - * Returns the correct form of an address - * @memberof Address4 - * @instance - * @returns {String} + * Returns the address in correct form: octets joined with `.` and any + * leading zeros stripped (e.g. `192.168.1.1`). For IPv4 this matches the + * canonical dotted-decimal representation. */ correctForm() { return this.parsedAddress.map((part) => parseInt(part, 10)).join('.'); } /** - * Converts a hex string to an IPv4 address object - * @memberof Address4 - * @static + * Construct an `Address4` from an address and a dotted-decimal subnet + * mask given as separate strings (e.g. as returned by Node's + * `os.networkInterfaces()`). Throws `AddressError` if the mask is + * non-contiguous (e.g. `255.0.255.0`). + * @example + * var address = Address4.fromAddressAndMask('192.168.1.1', '255.255.255.0'); + * address.subnetMask; // 24 + */ + static fromAddressAndMask(address, mask) { + const bits = common.prefixLengthFromMask(new Address4(mask).bigInt(), constants.BITS); + return new Address4(`${address}/${bits}`); + } + /** + * Construct an `Address4` from an address and a Cisco-style wildcard mask + * given as separate strings (e.g. `0.0.0.255` for a `/24`). The wildcard + * mask is the bitwise inverse of the subnet mask. Throws `AddressError` + * if the mask is non-contiguous (e.g. `0.255.0.255`). + * @example + * var address = Address4.fromAddressAndWildcardMask('10.0.0.1', '0.0.0.255'); + * address.subnetMask; // 24 + */ + static fromAddressAndWildcardMask(address, wildcardMask) { + const wildcard = new Address4(wildcardMask).bigInt(); + const allOnes = (BigInt(1) << BigInt(constants.BITS)) - BigInt(1); + // eslint-disable-next-line no-bitwise + const mask = wildcard ^ allOnes; + const bits = common.prefixLengthFromMask(mask, constants.BITS); + return new Address4(`${address}/${bits}`); + } + /** + * Construct an `Address4` from a wildcard pattern with trailing `*` + * octets. The number of trailing wildcards determines the prefix + * length: each `*` represents 8 bits. + * + * Only trailing whole-octet wildcards are supported. Partial-octet + * wildcards (e.g. `192.168.0.1*`) and interior wildcards (e.g. + * `192.*.0.1`) throw `AddressError`. + * @example + * Address4.fromWildcard('192.168.0.*').subnet; // '/24' + * Address4.fromWildcard('192.168.*.*').subnet; // '/16' + * Address4.fromWildcard('*.*.*.*').subnet; // '/0' + */ + static fromWildcard(input) { + const groups = input.split('.'); + if (groups.length !== constants.GROUPS) { + throw new address_error_1.AddressError('Wildcard pattern must have 4 octets'); + } + let firstWildcard = -1; + for (let i = 0; i < groups.length; i++) { + if (groups[i] === '*') { + if (firstWildcard === -1) { + firstWildcard = i; + } + } + else if (firstWildcard !== -1) { + throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing octets (e.g. `192.168.0.*`)'); + } + } + const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard; + const replaced = groups.map((g) => (g === '*' ? '0' : g)); + const subnetBits = constants.BITS - trailing * 8; + return new Address4(`${replaced.join('.')}/${subnetBits}`); + } + /** + * Converts a hex string to an IPv4 address object. Accepts 8 hex digits + * with optional `:` separators (e.g. `'7f000001'` or `'7f:00:00:01'`). + * Throws `AddressError` for any other length or for non-hex characters. * @param {string} hex - a hex string to convert * @returns {Address4} */ static fromHex(hex) { - const padded = hex.replace(/:/g, '').padStart(8, '0'); + const stripped = hex.replace(/:/g, ''); + if (!/^[0-9a-fA-F]{8}$/.test(stripped)) { + throw new address_error_1.AddressError('IPv4 hex must be exactly 8 hex digits'); + } const groups = []; - let i; - for (i = 0; i < 8; i += 2) { - const h = padded.slice(i, i + 2); - groups.push(parseInt(h, 16)); + for (let i = 0; i < 8; i += 2) { + groups.push(parseInt(stripped.slice(i, i + 2), 16)); } return new Address4(groups.join('.')); } /** - * Converts an integer into a IPv4 address object - * @memberof Address4 - * @static + * Converts an integer into a IPv4 address object. The integer must be a + * non-negative safe integer in the range `[0, 2**32 - 1]`; otherwise + * `AddressError` is thrown. * @param {integer} integer - a number to convert * @returns {Address4} */ static fromInteger(integer) { - return Address4.fromHex(integer.toString(16)); + if (!Number.isInteger(integer) || integer < 0 || integer > 0xffffffff) { + throw new address_error_1.AddressError('IPv4 integer must be in the range 0 to 2**32 - 1'); + } + return Address4.fromHex(integer.toString(16).padStart(8, '0')); } /** * Return an address from in-addr.arpa form - * @memberof Address4 - * @static * @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address * @returns {Adress4} * @example @@ -143,17 +214,15 @@ class Address4 { } /** * Converts an IPv4 address object to a hex string - * @memberof Address4 - * @instance * @returns {String} */ toHex() { return this.parsedAddress.map((part) => common.stringToPaddedHex(part)).join(':'); } /** - * Converts an IPv4 address object to an array of bytes - * @memberof Address4 - * @instance + * Converts an IPv4 address object to an array of bytes. + * + * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toArray())`. * @returns {Array} */ toArray() { @@ -161,8 +230,6 @@ class Address4 { } /** * Converts an IPv4 address object to an IPv6 address group - * @memberof Address4 - * @instance * @returns {String} */ toGroup6() { @@ -175,8 +242,6 @@ class Address4 { } /** * Returns the address as a `bigint` - * @memberof Address4 - * @instance * @returns {bigint} */ bigInt() { @@ -184,8 +249,6 @@ class Address4 { } /** * Helper function getting start address. - * @memberof Address4 - * @instance * @returns {bigint} */ _startAddress() { @@ -194,8 +257,6 @@ class Address4 { /** * The first address in the range given by this address' subnet. * Often referred to as the Network Address. - * @memberof Address4 - * @instance * @returns {Address4} */ startAddress() { @@ -204,8 +265,6 @@ class Address4 { /** * The first host address in the range given by this address's subnet ie * the first address after the Network Address - * @memberof Address4 - * @instance * @returns {Address4} */ startAddressExclusive() { @@ -214,8 +273,6 @@ class Address4 { } /** * Helper function getting end address. - * @memberof Address4 - * @instance * @returns {bigint} */ _endAddress() { @@ -224,8 +281,6 @@ class Address4 { /** * The last address in the range given by this address' subnet * Often referred to as the Broadcast - * @memberof Address4 - * @instance * @returns {Address4} */ endAddress() { @@ -234,8 +289,6 @@ class Address4 { /** * The last host address in the range given by this address's subnet ie * the last address prior to the Broadcast Address - * @memberof Address4 - * @instance * @returns {Address4} */ endAddressExclusive() { @@ -243,19 +296,47 @@ class Address4 { return Address4.fromBigInt(this._endAddress() - adjust); } /** - * Converts a BigInt to a v4 address object - * @memberof Address4 - * @static + * The dotted-decimal form of the subnet mask, e.g. `255.255.240.0` for + * a `/20`. Returns an `Address4`; call `.correctForm()` for the string. + * @returns {Address4} + */ + subnetMaskAddress() { + return Address4.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants.BITS - this.subnetMask)}`)); + } + /** + * The Cisco-style wildcard mask, e.g. `0.0.0.255` for a `/24`. This is + * the bitwise inverse of `subnetMaskAddress()`. Returns an `Address4`; + * call `.correctForm()` for the string. + * @returns {Address4} + */ + wildcardMask() { + return Address4.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants.BITS - this.subnetMask)}`)); + } + /** + * The network address in CIDR string form, e.g. `192.168.1.0/24` for + * `192.168.1.5/24`. For an address with no explicit subnet the prefix is + * `/32`, e.g. `networkForm()` on `192.168.1.5` returns `192.168.1.5/32`. + * @returns {string} + */ + networkForm() { + return `${this.startAddress().correctForm()}/${this.subnetMask}`; + } + /** + * Converts a BigInt to a v4 address object. The value must be in the + * range `[0, 2**32 - 1]`; otherwise `AddressError` is thrown. * @param {bigint} bigInt - a BigInt to convert * @returns {Address4} */ static fromBigInt(bigInt) { - return Address4.fromHex(bigInt.toString(16)); + if (bigInt < 0n || bigInt > 0xffffffffn) { + throw new address_error_1.AddressError('IPv4 BigInt must be in the range 0 to 2**32 - 1'); + } + return Address4.fromHex(bigInt.toString(16).padStart(8, '0')); } /** - * Convert a byte array to an Address4 object - * @memberof Address4 - * @static + * Convert a byte array to an Address4 object. + * + * To convert from a Node.js `Buffer`, spread it: `Address4.fromByteArray([...buf])`. * @param {Array} bytes - an array of 4 bytes (0-255) * @returns {Address4} */ @@ -273,8 +354,6 @@ class Address4 { } /** * Convert an unsigned byte array to an Address4 object - * @memberof Address4 - * @static * @param {Array} bytes - an array of 4 unsigned bytes (0-255) * @returns {Address4} */ @@ -288,8 +367,6 @@ class Address4 { /** * Returns the first n bits of the address, defaulting to the * subnet mask - * @memberof Address4 - * @instance * @returns {String} */ mask(mask) { @@ -300,8 +377,6 @@ class Address4 { } /** * Returns the bits in the given range as a base-2 string - * @memberof Address4 - * @instance * @returns {string} */ getBitsBase2(start, end) { @@ -309,10 +384,8 @@ class Address4 { } /** * Return the reversed ip6.arpa form of the address - * @memberof Address4 * @param {Object} options * @param {boolean} options.omitSuffix - omit the "in-addr.arpa" suffix - * @instance * @returns {String} */ reverseForm(options) { @@ -327,21 +400,62 @@ class Address4 { } /** * Returns true if the given address is a multicast address - * @memberof Address4 - * @instance * @returns {boolean} */ isMulticast() { - return this.isInSubnet(new Address4('224.0.0.0/4')); + return this.isInSubnet(MULTICAST_V4); + } + /** + * Returns true if the address is in one of the [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private address ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). + * @returns {boolean} + */ + isPrivate() { + return PRIVATE_V4.some((subnet) => this.isInSubnet(subnet)); + } + /** + * Returns true if the address is in the loopback range `127.0.0.0/8` ([RFC 1122](https://datatracker.ietf.org/doc/html/rfc1122)). + * @returns {boolean} + */ + isLoopback() { + return this.isInSubnet(LOOPBACK_V4); + } + /** + * Returns true if the address is in the link-local range `169.254.0.0/16` ([RFC 3927](https://datatracker.ietf.org/doc/html/rfc3927)). + * @returns {boolean} + */ + isLinkLocal() { + return this.isInSubnet(LINK_LOCAL_V4); + } + /** + * Returns true if the address is the unspecified address `0.0.0.0`. + * @returns {boolean} + */ + isUnspecified() { + return this.isInSubnet(UNSPECIFIED_V4); + } + /** + * Returns true if the address is the limited broadcast address `255.255.255.255` ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)). + * @returns {boolean} + */ + isBroadcast() { + return this.isInSubnet(BROADCAST_V4); + } + /** + * Returns true if the address is in the carrier-grade NAT range `100.64.0.0/10` ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)). + * @returns {boolean} + */ + isCGNAT() { + return this.isInSubnet(CGNAT_V4); } /** * Returns a zero-padded base-2 string representation of the address - * @memberof Address4 - * @instance * @returns {string} */ binaryZeroPad() { - return this.bigInt().toString(2).padStart(constants.BITS, '0'); + if (this._binaryZeroPad === undefined) { + this._binaryZeroPad = this.bigInt().toString(2).padStart(constants.BITS, '0'); + } + return this._binaryZeroPad; } /** * Groups an IPv4 address for inclusion at the end of an IPv6 address @@ -357,4 +471,15 @@ class Address4 { } } exports.Address4 = Address4; +const MULTICAST_V4 = new Address4('224.0.0.0/4'); +const PRIVATE_V4 = [ + new Address4('10.0.0.0/8'), + new Address4('172.16.0.0/12'), + new Address4('192.168.0.0/16'), +]; +const LOOPBACK_V4 = new Address4('127.0.0.0/8'); +const LINK_LOCAL_V4 = new Address4('169.254.0.0/16'); +const UNSPECIFIED_V4 = new Address4('0.0.0.0/32'); +const BROADCAST_V4 = new Address4('255.255.255.255/32'); +const CGNAT_V4 = new Address4('100.64.0.0/10'); //# sourceMappingURL=ipv4.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/ip-address/dist/ipv6.js b/deps/npm/node_modules/ip-address/dist/ipv6.js index 5f88ab63a56eb8..a78020ee7886ff 100644 --- a/deps/npm/node_modules/ip-address/dist/ipv6.js +++ b/deps/npm/node_modules/ip-address/dist/ipv6.js @@ -34,6 +34,7 @@ const ipv4_1 = require("./ipv4"); const regular_expressions_1 = require("./v6/regular-expressions"); const address_error_1 = require("./address-error"); const common_1 = require("./common"); +const isCorrect6 = common.isCorrect(constants6.BITS); function assert(condition) { if (!condition) { throw new Error('Assertion failed.'); @@ -77,7 +78,6 @@ function unsignByte(b) { } /** * Represents an IPv6 address - * @class Address6 * @param {string} address - An IPv6 address string * @param {number} [groups=8] - How many octets to parse * @example @@ -94,18 +94,14 @@ class Address6 { // #region Attributes /** * Returns true if the given address is in the subnet of the current address - * @memberof Address6 - * @instance * @returns {boolean} */ this.isInSubnet = common.isInSubnet; /** * Returns true if the address is correct, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ - this.isCorrect = common.isCorrect(constants6.BITS); + this.isCorrect = isCorrect6; if (optionalGroups === undefined) { this.groups = constants6.GROUPS; } @@ -136,6 +132,13 @@ class Address6 { this.addressMinusSuffix = address; this.parsedAddress = this.parse(this.addressMinusSuffix); } + /** + * Returns true if the given string is a valid IPv6 address (with optional + * CIDR subnet and zone identifier), false otherwise. Host bits in the + * subnet portion are allowed (e.g. `2001:db8::1/32` is valid); for strict + * network-address validation compare `correctForm()` to + * `startAddress().correctForm()`, or use `networkForm()`. + */ static isValid(address) { try { // eslint-disable-next-line no-new @@ -147,9 +150,8 @@ class Address6 { } } /** - * Convert a BigInt to a v6 address object - * @memberof Address6 - * @static + * Convert a BigInt to a v6 address object. The value must be in the + * range `[0, 2**128 - 1]`; otherwise `AddressError` is thrown. * @param {bigint} bigInt - a BigInt to convert * @returns {Address6} * @example @@ -158,19 +160,21 @@ class Address6 { * address.correctForm(); // '::e8:d4a5:1000' */ static fromBigInt(bigInt) { + if (bigInt < 0n || bigInt > (1n << BigInt(constants6.BITS)) - 1n) { + throw new address_error_1.AddressError('IPv6 BigInt must be in the range 0 to 2**128 - 1'); + } const hex = bigInt.toString(16).padStart(32, '0'); const groups = []; - let i; - for (i = 0; i < constants6.GROUPS; i++) { + for (let i = 0; i < constants6.GROUPS; i++) { groups.push(hex.slice(i * 4, (i + 1) * 4)); } return new Address6(groups.join(':')); } /** - * Convert a URL (with optional port number) to an address object - * @memberof Address6 - * @static - * @param {string} url - a URL with optional port number + * Parse a URL (with optional bracketed host and port) into an address and + * port. Returns either `{ address, port }` on success or + * `{ error, address: null, port: null }` if the URL could not be parsed. + * Ports are returned as numbers (or `null` if absent or out of range). * @example * var addressAndPort = Address6.fromURL('http://[ffff::]:8080/foo/'); * addressAndPort.address.correctForm(); // 'ffff::' @@ -229,10 +233,92 @@ class Address6 { port, }; } + /** + * Construct an `Address6` from an address and a hex subnet mask given as + * separate strings (e.g. as returned by Node's `os.networkInterfaces()`). + * Throws `AddressError` if the mask is non-contiguous (e.g. + * `ffff::ffff`). + * @example + * var address = Address6.fromAddressAndMask('fe80::1', 'ffff:ffff:ffff:ffff::'); + * address.subnetMask; // 64 + */ + static fromAddressAndMask(address, mask) { + const bits = common.prefixLengthFromMask(new Address6(mask).bigInt(), constants6.BITS); + return new Address6(`${address}/${bits}`); + } + /** + * Construct an `Address6` from an address and a Cisco-style wildcard mask + * given as separate strings (e.g. `::ffff:ffff:ffff:ffff` for a `/64`). + * The wildcard mask is the bitwise inverse of the subnet mask. Throws + * `AddressError` if the mask is non-contiguous. + * @example + * var address = Address6.fromAddressAndWildcardMask('fe80::1', '::ffff:ffff:ffff:ffff'); + * address.subnetMask; // 64 + */ + static fromAddressAndWildcardMask(address, wildcardMask) { + const wildcard = new Address6(wildcardMask).bigInt(); + const allOnes = (BigInt(1) << BigInt(constants6.BITS)) - BigInt(1); + // eslint-disable-next-line no-bitwise + const mask = wildcard ^ allOnes; + const bits = common.prefixLengthFromMask(mask, constants6.BITS); + return new Address6(`${address}/${bits}`); + } + /** + * Construct an `Address6` from a wildcard pattern with trailing `*` + * groups. The number of trailing wildcards determines the prefix + * length: each `*` represents 16 bits. `::` is expanded to zero groups + * (not wildcards) before evaluating trailing wildcards. + * + * Only trailing whole-group wildcards are supported. Partial-group + * wildcards (e.g. `2001:db8::0*`) and interior wildcards (e.g. + * `*::1`) throw `AddressError`. + * @example + * Address6.fromWildcard('2001:db8:*:*:*:*:*:*').subnet; // '/32' + * Address6.fromWildcard('2001:db8::*').subnet; // '/112' + * Address6.fromWildcard('*:*:*:*:*:*:*:*').subnet; // '/0' + */ + static fromWildcard(input) { + if (input.includes('%') || input.includes('/')) { + throw new address_error_1.AddressError('Wildcard pattern must not include a zone or CIDR suffix'); + } + const halves = input.split('::'); + if (halves.length > 2) { + throw new address_error_1.AddressError("Wildcard pattern cannot contain more than one '::'"); + } + let groups; + if (halves.length === 2) { + const left = halves[0] === '' ? [] : halves[0].split(':'); + const right = halves[1] === '' ? [] : halves[1].split(':'); + const remaining = constants6.GROUPS - left.length - right.length; + if (remaining < 1) { + throw new address_error_1.AddressError("Wildcard pattern with '::' has too many groups"); + } + groups = [...left, ...new Array(remaining).fill('0'), ...right]; + } + else { + groups = input.split(':'); + } + if (groups.length !== constants6.GROUPS) { + throw new address_error_1.AddressError('Wildcard pattern must have 8 groups'); + } + let firstWildcard = -1; + for (let i = 0; i < groups.length; i++) { + if (groups[i] === '*') { + if (firstWildcard === -1) { + firstWildcard = i; + } + } + else if (firstWildcard !== -1) { + throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing groups (e.g. `2001:db8:*:*:*:*:*:*`)'); + } + } + const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard; + const replaced = groups.map((g) => (g === '*' ? '0' : g)); + const subnetBits = constants6.BITS - trailing * 16; + return new Address6(`${replaced.join(':')}/${subnetBits}`); + } /** * Create an IPv6-mapped address given an IPv4 address - * @memberof Address6 - * @static * @param {string} address - An IPv4 address string * @returns {Address6} * @example @@ -247,8 +333,6 @@ class Address6 { } /** * Return an address from ip6.arpa form - * @memberof Address6 - * @static * @param {string} arpaFormAddress - an 'ip6.arpa' form address * @returns {Adress6} * @example @@ -273,8 +357,6 @@ class Address6 { } /** * Return the Microsoft UNC transcription of the address - * @memberof Address6 - * @instance * @returns {String} the Microsoft UNC transcription of the address */ microsoftTranscription() { @@ -282,8 +364,6 @@ class Address6 { } /** * Return the first n bits of the address, defaulting to the subnet mask - * @memberof Address6 - * @instance * @param {number} [mask=subnet] - the number of bits to mask * @returns {String} the first n bits of the address as a string */ @@ -292,8 +372,6 @@ class Address6 { } /** * Return the number of possible subnets of a given size in the address - * @memberof Address6 - * @instance * @param {number} [subnetSize=128] - the subnet size * @returns {String} */ @@ -309,8 +387,6 @@ class Address6 { } /** * Helper function getting start address. - * @memberof Address6 - * @instance * @returns {bigint} */ _startAddress() { @@ -319,8 +395,6 @@ class Address6 { /** * The first address in the range given by this address' subnet * Often referred to as the Network Address. - * @memberof Address6 - * @instance * @returns {Address6} */ startAddress() { @@ -329,8 +403,6 @@ class Address6 { /** * The first host address in the range given by this address's subnet ie * the first address after the Network Address - * @memberof Address6 - * @instance * @returns {Address6} */ startAddressExclusive() { @@ -339,8 +411,6 @@ class Address6 { } /** * Helper function getting end address. - * @memberof Address6 - * @instance * @returns {bigint} */ _endAddress() { @@ -349,8 +419,6 @@ class Address6 { /** * The last address in the range given by this address' subnet * Often referred to as the Broadcast - * @memberof Address6 - * @instance * @returns {Address6} */ endAddress() { @@ -359,8 +427,6 @@ class Address6 { /** * The last host address in the range given by this address's subnet ie * the last address prior to the Broadcast Address - * @memberof Address6 - * @instance * @returns {Address6} */ endAddressExclusive() { @@ -368,36 +434,73 @@ class Address6 { return Address6.fromBigInt(this._endAddress() - adjust); } /** - * Return the scope of the address - * @memberof Address6 - * @instance + * The hex form of the subnet mask, e.g. `ffff:ffff:ffff:ffff::` for a + * `/64`. Returns an `Address6`; call `.correctForm()` for the string. + * @returns {Address6} + */ + subnetMaskAddress() { + return Address6.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants6.BITS - this.subnetMask)}`)); + } + /** + * The Cisco-style wildcard mask, e.g. `::ffff:ffff:ffff:ffff` for a + * `/64`. This is the bitwise inverse of `subnetMaskAddress()`. Returns + * an `Address6`; call `.correctForm()` for the string. + * @returns {Address6} + */ + wildcardMask() { + return Address6.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants6.BITS - this.subnetMask)}`)); + } + /** + * The network address in CIDR string form, e.g. `2001:db8::/32` for + * `2001:db8::1/32`. For an address with no explicit subnet the prefix + * is `/128`, e.g. `networkForm()` on `2001:db8::1` returns + * `2001:db8::1/128`. + * @returns {string} + */ + networkForm() { + return `${this.startAddress().correctForm()}/${this.subnetMask}`; + } + /** + * Return the scope of the address. The 4-bit scope field + * ([RFC 4291 §2.7](https://datatracker.ietf.org/doc/html/rfc4291#section-2.7)) + * is only defined for multicast addresses; for unicast addresses the scope + * is derived from the address type per + * [RFC 4007 §6](https://datatracker.ietf.org/doc/html/rfc4007#section-6). * @returns {String} */ getScope() { - let scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)]; - if (this.getType() === 'Global unicast' && scope !== 'Link local') { - scope = 'Global'; + const type = this.getType(); + if (type === 'Multicast' || type.startsWith('Multicast ')) { + const scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)]; + return scope || 'Unknown'; + } + // RFC 4291 §2.5.3: the loopback address is treated as having Link-Local + // scope. (Multicast scope 1, "Interface-Local", is a different concept + // used only for loopback transmission of multicast.) + if (type === 'Link-local unicast' || type === 'Loopback') { + return 'Link local'; + } + // RFC 4007 §6: the unspecified address has no scope. + if (type === 'Unspecified') { + return 'Unknown'; } - return scope || 'Unknown'; + return 'Global'; } /** * Return the type of the address - * @memberof Address6 - * @instance * @returns {String} */ getType() { - for (const subnet of Object.keys(constants6.TYPES)) { - if (this.isInSubnet(new Address6(subnet))) { - return constants6.TYPES[subnet]; + for (let i = 0; i < TYPE_SUBNETS.length; i++) { + const entry = TYPE_SUBNETS[i]; + if (this.isInSubnet(entry[0])) { + return entry[1]; } } return 'Global unicast'; } /** * Return the bits in the given range as a BigInt - * @memberof Address6 - * @instance * @returns {bigint} */ getBits(start, end) { @@ -405,8 +508,6 @@ class Address6 { } /** * Return the bits in the given range as a base-2 string - * @memberof Address6 - * @instance * @returns {String} */ getBitsBase2(start, end) { @@ -414,8 +515,6 @@ class Address6 { } /** * Return the bits in the given range as a base-16 string - * @memberof Address6 - * @instance * @returns {String} */ getBitsBase16(start, end) { @@ -429,8 +528,6 @@ class Address6 { } /** * Return the bits that are set past the subnet mask length - * @memberof Address6 - * @instance * @returns {String} */ getBitsPastSubnet() { @@ -438,10 +535,8 @@ class Address6 { } /** * Return the reversed ip6.arpa form of the address - * @memberof Address6 * @param {Object} options * @param {boolean} options.omitSuffix - omit the "ip6.arpa" suffix - * @instance * @returns {String} */ reverseForm(options) { @@ -467,10 +562,10 @@ class Address6 { return 'ip6.arpa.'; } /** - * Return the correct form of the address - * @memberof Address6 - * @instance - * @returns {String} + * Returns the address in correct form, per + * [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952): leading zeros + * stripped, the longest run of zero groups collapsed to `::`, and hex digits + * lowercased (e.g. `2001:db8::1`). This is the recommended form for display. */ correctForm() { let i; @@ -514,8 +609,6 @@ class Address6 { } /** * Return a zero-padded base-2 string representation of the address - * @memberof Address6 - * @instance * @returns {String} * @example * var address = new Address6('2001:4860:4001:803::1011'); @@ -524,10 +617,22 @@ class Address6 { * // 0000000000000000000000000000000000000000000000000001000000010001' */ binaryZeroPad() { - return this.bigInt().toString(2).padStart(constants6.BITS, '0'); + if (this._binaryZeroPad === undefined) { + this._binaryZeroPad = this.bigInt().toString(2).padStart(constants6.BITS, '0'); + } + return this._binaryZeroPad; } + /** + * Parses a v4-in-v6 string (e.g. `::ffff:192.168.0.1`) by extracting the + * trailing IPv4 address into `this.address4` / `this.parsedAddress4` and + * returning the address with the v4 portion converted to two v6 groups. + * Used internally by `parse()`. + */ // TODO: Improve the semantics of this helper function parse4in6(address) { + if (address.indexOf('.') === -1) { + return address; + } const groups = address.split(':'); const lastGroup = groups.slice(-1)[0]; const address4 = lastGroup.match(constants4.RE_ADDRESS); @@ -536,7 +641,12 @@ class Address6 { this.address4 = new ipv4_1.Address4(this.parsedAddress4); for (let i = 0; i < this.address4.groups; i++) { if (/^0[0-9]+/.test(this.address4.parsedAddress[i])) { - throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", address.replace(constants4.RE_ADDRESS, this.address4.parsedAddress.map(spanLeadingZeroes4).join('.'))); + // The prefix groups haven't been through the bad-character check + // yet, so escape them before including in the error HTML. + const highlighted = this.address4.parsedAddress.map(spanLeadingZeroes4).join('.'); + const prefix = groups.slice(0, -1).map(helpers.escapeHtml).join(':'); + const separator = groups.length > 1 ? ':' : ''; + throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", `${prefix}${separator}${highlighted}`); } } this.v4 = true; @@ -545,6 +655,13 @@ class Address6 { } return address; } + /** + * Parses an IPv6 address string into its 8 hexadecimal groups (expanding + * any `::` elision and any trailing v4-in-v6 portion) and stores the result + * on `this.parsedAddress`. Called automatically by the constructor; you + * typically don't need to call it directly. Throws `AddressError` if the + * input is malformed. + */ // TODO: Make private? parse(address) { address = this.parse4in6(address); @@ -594,18 +711,16 @@ class Address6 { return groups; } /** - * Return the canonical form of the address - * @memberof Address6 - * @instance - * @returns {String} + * Returns the canonical (fully expanded) form of the address: all 8 groups, + * each padded to 4 hex digits, with no `::` collapsing + * (e.g. `2001:0db8:0000:0000:0000:0000:0000:0001`). Useful for sorting and + * byte-exact comparison. */ canonicalForm() { return this.parsedAddress.map(paddedHex).join(':'); } /** * Return the decimal form of the address - * @memberof Address6 - * @instance * @returns {String} */ decimal() { @@ -613,8 +728,6 @@ class Address6 { } /** * Return the address as a BigInt - * @memberof Address6 - * @instance * @returns {bigint} */ bigInt() { @@ -622,8 +735,6 @@ class Address6 { } /** * Return the last two groups of this address as an IPv4 address string - * @memberof Address6 - * @instance * @returns {Address4} * @example * var address = new Address6('2001:4860:4001::1825:bf11'); @@ -631,12 +742,10 @@ class Address6 { */ to4() { const binary = this.binaryZeroPad().split(''); - return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16)); + return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16).padStart(8, '0')); } /** * Return the v4-in-v6 form of the address - * @memberof Address6 - * @instance * @returns {String} */ to4in6() { @@ -650,10 +759,10 @@ class Address6 { return correct + infix + address4.address; } /** - * Return an object containing the Teredo properties of the address - * @memberof Address6 - * @instance - * @returns {Object} + * Decodes the Teredo tunneling fields embedded in this address. Returns the + * Teredo prefix, server IPv4, client IPv4, raw flag bits, cone-NAT flag, + * UDP port, and Microsoft-format flag breakdown (reserved, universal/local, + * group/individual, nonce). Only meaningful for addresses in `2001::/32`. */ inspectTeredo() { /* @@ -684,7 +793,7 @@ class Address6 { const server4 = ipv4_1.Address4.fromHex(this.getBitsBase16(32, 64)); const bitsForClient4 = this.getBits(96, 128); // eslint-disable-next-line no-bitwise - const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16)); + const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16).padStart(8, '0')); const flagsBase2 = this.getBitsBase2(64, 80); const coneNat = (0, common_1.testBit)(flagsBase2, 15); const reserved = (0, common_1.testBit)(flagsBase2, 14); @@ -707,10 +816,9 @@ class Address6 { }; } /** - * Return an object containing the 6to4 properties of the address - * @memberof Address6 - * @instance - * @returns {Object} + * Decodes the 6to4 tunneling fields embedded in this address. Returns the + * 6to4 prefix and the embedded IPv4 gateway address. Only meaningful for + * addresses in `2002::/16`. */ inspect6to4() { /* @@ -726,8 +834,6 @@ class Address6 { } /** * Return a v6 6to4 address from a v6 v4inv6 address - * @memberof Address6 - * @instance * @returns {Address6} */ to6to4() { @@ -744,9 +850,80 @@ class Address6 { return new Address6(addr6to4); } /** - * Return a byte array - * @memberof Address6 - * @instance + * Embed an IPv4 address into a NAT64 IPv6 address using the encoding + * defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052). + * The default prefix is the well-known prefix `64:ff9b::/96`. The prefix + * length must be one of 32, 40, 48, 56, 64, or 96; for prefixes shorter + * than /64 the IPv4 octets are split around the reserved bits 64–71. + * @example + * Address6.fromAddress4Nat64('192.0.2.33').correctForm(); // '64:ff9b::c000:221' + * Address6.fromAddress4Nat64('192.0.2.33', '2001:db8::/32').correctForm(); // '2001:db8:c000:221::' + */ + static fromAddress4Nat64(address, prefix = '64:ff9b::/96') { + const v4 = new ipv4_1.Address4(address); + const prefix6 = new Address6(prefix); + const pl = prefix6.subnetMask; + if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) { + throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96'); + } + const prefixBits = prefix6.binaryZeroPad(); + const v4Bits = v4.binaryZeroPad(); + let bits; + if (pl === 96) { + bits = prefixBits.slice(0, 96) + v4Bits; + } + else { + const beforeU = 64 - pl; + bits = + prefixBits.slice(0, pl) + + v4Bits.slice(0, beforeU) + + '00000000' + + v4Bits.slice(beforeU) + + '0'.repeat(128 - 72 - (32 - beforeU)); + } + const hex = BigInt(`0b${bits}`).toString(16).padStart(32, '0'); + const groups = []; + for (let i = 0; i < 8; i++) { + groups.push(hex.slice(i * 4, (i + 1) * 4)); + } + return new Address6(groups.join(':')); + } + /** + * Extract the embedded IPv4 address from a NAT64 IPv6 address using the + * encoding defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052). + * The default prefix is the well-known prefix `64:ff9b::/96`. Returns + * `null` if this address is not contained within the given prefix. + * @example + * new Address6('64:ff9b::c000:221').toAddress4Nat64()!.correctForm(); // '192.0.2.33' + */ + toAddress4Nat64(prefix = '64:ff9b::/96') { + const prefix6 = new Address6(prefix); + const pl = prefix6.subnetMask; + if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) { + throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96'); + } + if (!this.isInSubnet(prefix6)) { + return null; + } + const bits = this.binaryZeroPad(); + let v4Bits; + if (pl === 96) { + v4Bits = bits.slice(96, 128); + } + else { + const beforeU = 64 - pl; + v4Bits = bits.slice(pl, pl + beforeU) + bits.slice(72, 72 + (32 - beforeU)); + } + const octets = []; + for (let i = 0; i < 4; i++) { + octets.push(parseInt(v4Bits.slice(i * 8, (i + 1) * 8), 2).toString()); + } + return new ipv4_1.Address4(octets.join('.')); + } + /** + * Return a byte array. + * + * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toByteArray())`. * @returns {Array} */ toByteArray() { @@ -760,27 +937,27 @@ class Address6 { return bytes; } /** - * Return an unsigned byte array - * @memberof Address6 - * @instance + * Return an unsigned byte array. + * + * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toUnsignedByteArray())`. * @returns {Array} */ toUnsignedByteArray() { return this.toByteArray().map(unsignByte); } /** - * Convert a byte array to an Address6 object - * @memberof Address6 - * @static + * Convert a byte array to an Address6 object. + * + * To convert from a Node.js `Buffer`, spread it: `Address6.fromByteArray([...buf])`. * @returns {Address6} */ static fromByteArray(bytes) { return this.fromUnsignedByteArray(bytes.map(unsignByte)); } /** - * Convert an unsigned byte array to an Address6 object - * @memberof Address6 - * @static + * Convert an unsigned byte array to an Address6 object. + * + * To convert from a Node.js `Buffer`, spread it: `Address6.fromUnsignedByteArray([...buf])`. * @returns {Address6} */ static fromUnsignedByteArray(bytes) { @@ -795,8 +972,6 @@ class Address6 { } /** * Returns true if the address is in the canonical form, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isCanonical() { @@ -804,8 +979,6 @@ class Address6 { } /** * Returns true if the address is a link local address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isLinkLocal() { @@ -818,53 +991,81 @@ class Address6 { } /** * Returns true if the address is a multicast address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isMulticast() { - return this.getType() === 'Multicast'; + const type = this.getType(); + return type === 'Multicast' || type.startsWith('Multicast '); } /** - * Returns true if the address is a v4-in-v6 address, false otherwise - * @memberof Address6 - * @instance + * Returns true if the address was written in v4-in-v6 dotted-quad notation + * (e.g. `::ffff:127.0.0.1`), false otherwise. This is a notation-level flag + * and does not reflect whether the address bits lie in the IPv4-mapped + * (`::ffff:0:0/96`) subnet — for that, see {@link isMapped4}. * @returns {boolean} */ is4() { return this.v4; } + /** + * Returns true if the address is an IPv4-mapped IPv6 address in + * `::ffff:0:0/96` ([RFC 4291 §2.5.5.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2)), + * false otherwise. Unlike {@link is4}, this checks the underlying address + * bits rather than the textual notation, so `::ffff:127.0.0.1` and + * `::ffff:7f00:1` both return true. + * @returns {boolean} + */ + isMapped4() { + return this.isInSubnet(IPV4_MAPPED_SUBNET); + } /** * Returns true if the address is a Teredo address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isTeredo() { - return this.isInSubnet(new Address6('2001::/32')); + return this.isInSubnet(TEREDO_SUBNET); } /** * Returns true if the address is a 6to4 address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ is6to4() { - return this.isInSubnet(new Address6('2002::/16')); + return this.isInSubnet(SIX_TO_FOUR_SUBNET); } /** * Returns true if the address is a loopback address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isLoopback() { return this.getType() === 'Loopback'; } + /** + * Returns true if the address is a Unique Local Address in `fc00::/7` ([RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193)). ULAs are the IPv6 equivalent of IPv4 [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private addresses. + * @returns {boolean} + */ + isULA() { + return this.isInSubnet(ULA_SUBNET); + } + /** + * Returns true if the address is the unspecified address `::`. + * @returns {boolean} + */ + isUnspecified() { + return this.getType() === 'Unspecified'; + } + /** + * Returns true if the address is in the documentation prefix `2001:db8::/32` ([RFC 3849](https://datatracker.ietf.org/doc/html/rfc3849)). + * @returns {boolean} + */ + isDocumentation() { + return this.isInSubnet(DOCUMENTATION_SUBNET); + } // #endregion // #region HTML /** - * @returns {String} the address in link form with a default port of 80 + * Returns the address as an HTTP URL with the host bracketed, e.g. + * `http://[2001:db8::1]/`. If `optionalPort` is provided it is appended, + * e.g. `http://[2001:db8::1]:8080/`. */ href(optionalPort) { if (optionalPort === undefined) { @@ -876,7 +1077,12 @@ class Address6 { return `http://[${this.correctForm()}]${optionalPort}/`; } /** - * @returns {String} a link suitable for conveying the address via a URL hash + * Returns an HTML `` element whose `href` encodes the address in a URL + * hash fragment (default prefix `/#address=`). Useful for linking between + * pages of an address-inspector UI. + * @param options.className - CSS class for the rendered `` element + * @param options.prefix - hash prefix prepended to the address (default `/#address=`) + * @param options.v4 - when true, render the address in v4-in-v6 form */ link(options) { if (!options) { @@ -896,10 +1102,13 @@ class Address6 { formFunction = this.to4in6; } const form = formFunction.call(this); + const safeHref = helpers.escapeHtml(`${options.prefix}${form}`); + const safeForm = helpers.escapeHtml(form); if (options.className) { - return `${form}`; + const safeClass = helpers.escapeHtml(options.className); + return `${safeForm}`; } - return `${form}`; + return `${safeForm}`; } /** * Groups an address @@ -908,13 +1117,13 @@ class Address6 { group() { if (this.elidedGroups === 0) { // The simple case - return helpers.simpleGroup(this.address).join(':'); + return helpers.simpleGroup(this.addressMinusSuffix).join(':'); } assert(typeof this.elidedGroups === 'number'); assert(typeof this.elisionBegin === 'number'); // The elided case const output = []; - const [left, right] = this.address.split('::'); + const [left, right] = this.addressMinusSuffix.split('::'); if (left.length) { output.push(...helpers.simpleGroup(left)); } @@ -944,8 +1153,6 @@ class Address6 { /** * Generate a regular expression string that can be used to find or validate * all variations of this address - * @memberof Address6 - * @instance * @param {boolean} substringSearch * @returns {string} */ @@ -990,8 +1197,6 @@ class Address6 { /** * Generate a regular expression that can be used to find or validate all * variations of this address. - * @memberof Address6 - * @instance * @param {boolean} substringSearch * @returns {RegExp} */ @@ -1000,4 +1205,13 @@ class Address6 { } } exports.Address6 = Address6; +const TYPE_SUBNETS = Object.keys(constants6.TYPES).map((subnet) => [ + new Address6(subnet), + constants6.TYPES[subnet], +]); +const TEREDO_SUBNET = new Address6('2001::/32'); +const SIX_TO_FOUR_SUBNET = new Address6('2002::/16'); +const ULA_SUBNET = new Address6('fc00::/7'); +const DOCUMENTATION_SUBNET = new Address6('2001:db8::/32'); +const IPV4_MAPPED_SUBNET = new Address6('::ffff:0:0/96'); //# sourceMappingURL=ipv6.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/ip-address/dist/v6/constants.js b/deps/npm/node_modules/ip-address/dist/v6/constants.js index 0abc423e0a91ab..1a8cd1dd616e86 100644 --- a/deps/npm/node_modules/ip-address/dist/v6/constants.js +++ b/deps/npm/node_modules/ip-address/dist/v6/constants.js @@ -46,6 +46,11 @@ exports.TYPES = { '::1/128': 'Loopback', 'ff00::/8': 'Multicast', 'fe80::/10': 'Link-local unicast', + 'fc00::/7': 'Unique local', + '2002::/16': '6to4', + '2001:db8::/32': 'Documentation', + '64:ff9b::/96': 'NAT64 (well-known)', + '64:ff9b:1::/48': 'NAT64 (local-use)', }; /** * A regular expression that matches bad characters in an IPv6 address diff --git a/deps/npm/node_modules/ip-address/dist/v6/helpers.js b/deps/npm/node_modules/ip-address/dist/v6/helpers.js index fafca0c2712ddc..e6bae04698a66a 100644 --- a/deps/npm/node_modules/ip-address/dist/v6/helpers.js +++ b/deps/npm/node_modules/ip-address/dist/v6/helpers.js @@ -1,14 +1,23 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.escapeHtml = escapeHtml; exports.spanAllZeroes = spanAllZeroes; exports.spanAll = spanAll; exports.spanLeadingZeroes = spanLeadingZeroes; exports.simpleGroup = simpleGroup; +function escapeHtml(s) { + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} /** * @returns {String} the string with all zeroes contained in a */ function spanAllZeroes(s) { - return s.replace(/(0+)/g, '$1'); + return escapeHtml(s).replace(/(0+)/g, '$1'); } /** * @returns {String} the string with each character contained in a @@ -16,11 +25,11 @@ function spanAllZeroes(s) { function spanAll(s, offset = 0) { const letters = s.split(''); return letters - .map((n, i) => `${spanAllZeroes(n)}`) + .map((n, i) => `${spanAllZeroes(n)}`) .join(''); } function spanLeadingZeroesSimple(group) { - return group.replace(/^(0+)/, '$1'); + return escapeHtml(group).replace(/^(0+)/, '$1'); } /** * @returns {String} the string with leading zeroes contained in a diff --git a/deps/npm/node_modules/ip-address/package.json b/deps/npm/node_modules/ip-address/package.json index 5cf811e8c563af..47d109ec6f34da 100644 --- a/deps/npm/node_modules/ip-address/package.json +++ b/deps/npm/node_modules/ip-address/package.json @@ -2,77 +2,87 @@ "name": "ip-address", "description": "A library for parsing IPv4 and IPv6 IP addresses in node and the browser.", "keywords": [ - "ipv6", + "ip", "ipv4", - "browser", - "validation" + "ipv6", + "address", + "cidr", + "subnet", + "netmask", + "validate", + "validation", + "parse", + "arpa", + "bigint", + "browser" ], - "version": "10.1.0", + "version": "10.2.0", "author": "Beau Gunderson (https://beaugunderson.com/)", "license": "MIT", "main": "dist/ip-address.js", "types": "dist/ip-address.d.ts", "scripts": { - "docs": "documentation build --github --output docs --format html ./ip-address.js", + "docs": "tsx scripts/build-readme.ts", "build": "rm -rf dist; mkdir dist; tsc", - "prepack": "npm run build", - "release": "release-it", - "test-ci": "nyc mocha", + "prepack": "npm run docs && npm run build", + "test-ci": "c8 --experimental-monocart mocha", "test": "mocha", "watch": "mocha --watch" }, - "nyc": { - "extension": [ - ".ts" + "c8": { + "include": [ + "src/**/*.ts" ], "exclude": [ "**/*.d.ts", - ".eslintrc.js", - "coverage/", - "dist/", - "test/", - "tmp/" + "src/ip-address.ts", + "src/v4/constants.ts", + "src/v6/constants.ts" ], "reporter": [ "html", "lcov", "text" - ], - "all": true + ] }, "engines": { "node": ">= 12" }, + "sideEffects": false, "files": [ - "src", "dist" ], "repository": { "type": "git", "url": "git://github.com/beaugunderson/ip-address.git" }, + "overrides": { + "diff": "^8.0.3", + "serialize-javascript": "^7.0.5", + "@eslint/plugin-kit": "^0.7.1" + }, "devDependencies": { - "@types/chai": "^5.0.0", - "@types/mocha": "^10.0.8", - "@typescript-eslint/eslint-plugin": "^8.8.0", - "@typescript-eslint/parser": "^8.8.0", - "chai": "^5.1.1", - "documentation": "^14.0.3", - "eslint": "^8.50.0", + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@typescript-eslint/eslint-plugin": "^8.59.1", + "@typescript-eslint/parser": "^8.59.1", + "c8": "^11.0.0", + "chai": "^6.2.2", + "eslint": "^8.57.1", "eslint_d": "^14.0.4", "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.30.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", - "mocha": "^10.7.3", - "nyc": "^17.1.0", - "prettier": "^3.3.3", - "release-it": "^17.6.0", + "mocha": "^11.7.5", + "monocart-coverage-reports": "^2.12.11", + "prettier": "^3.8.3", "source-map-support": "^0.5.21", - "tsx": "^4.19.1", + "tsx": "^4.21.0", + "typedoc": "^0.28.19", "typescript": "<5.6.0" } } diff --git a/deps/npm/node_modules/libnpmdiff/package.json b/deps/npm/node_modules/libnpmdiff/package.json index a59017ff3d9731..08783e3ecb13e8 100644 --- a/deps/npm/node_modules/libnpmdiff/package.json +++ b/deps/npm/node_modules/libnpmdiff/package.json @@ -1,6 +1,6 @@ { "name": "libnpmdiff", - "version": "8.1.6", + "version": "8.1.9", "description": "The registry diff", "repository": { "type": "git", @@ -47,7 +47,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.4.3", + "@npmcli/arborist": "^9.7.0", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", diff --git a/deps/npm/node_modules/libnpmexec/lib/index.js b/deps/npm/node_modules/libnpmexec/lib/index.js index 3681653d8217d6..3add22cd2edca5 100644 --- a/deps/npm/node_modules/libnpmexec/lib/index.js +++ b/deps/npm/node_modules/libnpmexec/lib/index.js @@ -87,8 +87,10 @@ const missingFromTree = async ({ spec, tree, flatOptions, isNpxTree, shallow }) } // see if the package.json at `path` has an entry that matches `cmd` +// the path is a known-local directory, not a user-supplied dep, so +// allow-directory must not gate this introspection const hasPkgBin = (path, cmd, flatOptions) => - pacote.manifest(path, flatOptions) + pacote.manifest(path, { ...flatOptions, allowDirectory: 'all' }) .then(manifest => manifest?.bin?.[cmd]).catch(() => null) const exec = async (opts) => { @@ -147,6 +149,8 @@ const exec = async (opts) => { // we have to install the local package into the npx cache so that its // bin links get set up flatOptions.installLinks = false + // self-execution of a local bin, not a directory dep install + flatOptions.allowDirectory = 'all' // args[0] will exist when the package is installed packages.push(p) yes = true diff --git a/deps/npm/node_modules/libnpmexec/package.json b/deps/npm/node_modules/libnpmexec/package.json index 078c5618a4cd33..b672050048bd3f 100644 --- a/deps/npm/node_modules/libnpmexec/package.json +++ b/deps/npm/node_modules/libnpmexec/package.json @@ -1,6 +1,6 @@ { "name": "libnpmexec", - "version": "10.2.6", + "version": "10.2.9", "files": [ "bin/", "lib/" @@ -61,7 +61,7 @@ }, "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.4.3", + "@npmcli/arborist": "^9.7.0", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", diff --git a/deps/npm/node_modules/libnpmfund/package.json b/deps/npm/node_modules/libnpmfund/package.json index 62e73f5ef6436b..ab5b5a86d98339 100644 --- a/deps/npm/node_modules/libnpmfund/package.json +++ b/deps/npm/node_modules/libnpmfund/package.json @@ -1,6 +1,6 @@ { "name": "libnpmfund", - "version": "7.0.20", + "version": "7.0.23", "main": "lib/index.js", "files": [ "bin/", @@ -46,7 +46,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.4.3" + "@npmcli/arborist": "^9.7.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/deps/npm/node_modules/libnpmpack/package.json b/deps/npm/node_modules/libnpmpack/package.json index befca6090e66b8..58ff8edc24d844 100644 --- a/deps/npm/node_modules/libnpmpack/package.json +++ b/deps/npm/node_modules/libnpmpack/package.json @@ -1,6 +1,6 @@ { "name": "libnpmpack", - "version": "9.1.6", + "version": "9.1.9", "description": "Programmatic API for the bits behind npm pack", "author": "GitHub Inc.", "main": "lib/index.js", @@ -37,7 +37,7 @@ "bugs": "https://github.com/npm/libnpmpack/issues", "homepage": "https://npmjs.com/package/libnpmpack", "dependencies": { - "@npmcli/arborist": "^9.4.3", + "@npmcli/arborist": "^9.7.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" diff --git a/deps/npm/node_modules/libnpmpublish/README.md b/deps/npm/node_modules/libnpmpublish/README.md index 90b1f7c68ab4f2..4daac34feaad10 100644 --- a/deps/npm/node_modules/libnpmpublish/README.md +++ b/deps/npm/node_modules/libnpmpublish/README.md @@ -62,6 +62,13 @@ A couple of options of note: containing a [DSSE](https://github.com/secure-systems-lab/dsse)-packaged provenance statement. +* `opts.stage` - when `true`, publishes the package to a staging area instead + of making it immediately available. The registry response will include a + `stageId` (UUID) that can be used to approve or reject the staged version + later. Changes the request method to `POST` and the endpoint to + `/-/stage/package/`. The returned Response object will have a + `stageId` property. + #### `> libpub.publish(manifest, tarData, [opts]) -> Promise` Sends the package represented by the `manifest` and `tarData` to the diff --git a/deps/npm/node_modules/libnpmpublish/lib/publish.js b/deps/npm/node_modules/libnpmpublish/lib/publish.js index 933e142422b6c4..cfe85d2d29f57b 100644 --- a/deps/npm/node_modules/libnpmpublish/lib/publish.js +++ b/deps/npm/node_modules/libnpmpublish/lib/publish.js @@ -50,15 +50,20 @@ Remove the 'private' field from the package.json to publish it.`), opts ) - const res = await npmFetch(spec.escapedName, { + const stageRoute = `/-/stage/package/${spec.escapedName}` + const res = await npmFetch(opts.stage ? stageRoute : spec.escapedName, { ...opts, - method: 'PUT', + method: opts.stage ? 'POST' : 'PUT', body: metadata, - ignoreBody: true, + ignoreBody: !opts.stage, }) if (transparencyLogUrl) { res.transparencyLogUrl = transparencyLogUrl } + if (opts.stage) { + const json = await res.json() + res.stageId = json.stageId + } return res } @@ -86,7 +91,7 @@ const patchManifest = async (_manifest, opts) => { } const buildMetadata = async (registry, manifest, tarballData, spec, opts) => { - const { access, defaultTag, algorithms, provenance, provenanceFile } = opts + const { access, defaultTag, algorithms, provenance, provenanceFile, command = 'publish' } = opts const root = { _id: manifest.name, name: manifest.name, @@ -141,14 +146,14 @@ const buildMetadata = async (registry, manifest, tarballData, spec, opts) => { provenanceBundle = await generateProvenance([subject], opts) /* eslint-disable-next-line max-len */ - log.notice('publish', `Signed provenance statement with source and build information from ${ciInfo.name}`) + log.notice(command, `Signed provenance statement with source and build information from ${ciInfo.name}`) const tlogEntry = provenanceBundle?.verificationMaterial?.tlogEntries[0] /* istanbul ignore else */ if (tlogEntry) { transparencyLogUrl = `${TLOG_BASE_URL}?logIndex=${tlogEntry.logIndex}` log.notice( - 'publish', + command, `Provenance statement published to transparency log: ${transparencyLogUrl}` ) } diff --git a/deps/npm/node_modules/libnpmpublish/package.json b/deps/npm/node_modules/libnpmpublish/package.json index f90a6bf4506ecd..5b4ae66e57284c 100644 --- a/deps/npm/node_modules/libnpmpublish/package.json +++ b/deps/npm/node_modules/libnpmpublish/package.json @@ -1,6 +1,6 @@ { "name": "libnpmpublish", - "version": "11.1.3", + "version": "11.2.0", "description": "Programmatic API for the bits behind npm publish and unpublish", "author": "GitHub Inc.", "main": "lib/index.js", diff --git a/deps/npm/node_modules/libnpmversion/README.md b/deps/npm/node_modules/libnpmversion/README.md index b81a231d05ce04..d60a144bcc1bf1 100644 --- a/deps/npm/node_modules/libnpmversion/README.md +++ b/deps/npm/node_modules/libnpmversion/README.md @@ -86,6 +86,9 @@ The exact order of execution is as follows: 6. Run the `postversion` script. Use it to clean up the file system or automatically push the commit and/or tag. +For the `preversion`, `version` and `postversion` scripts, npm also sets the +environment variables `npm_old_version` and `npm_new_version`. + Take the following example: ```json diff --git a/deps/npm/node_modules/libnpmversion/package.json b/deps/npm/node_modules/libnpmversion/package.json index cac11cc36bd385..f8be6d8fdb3af7 100644 --- a/deps/npm/node_modules/libnpmversion/package.json +++ b/deps/npm/node_modules/libnpmversion/package.json @@ -1,6 +1,6 @@ { "name": "libnpmversion", - "version": "8.0.3", + "version": "8.0.4", "main": "lib/index.js", "files": [ "bin/", diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js new file mode 100644 index 00000000000000..8f6a8f12edb399 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tracing = exports.metrics = void 0; +const dummy = { hasSubscribers: false }; +exports.metrics = dummy; +exports.tracing = dummy; +//# sourceMappingURL=diagnostics-channel-browser.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/browser/index.js b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/index.js new file mode 100644 index 00000000000000..6b8268b0ea9123 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/index.js @@ -0,0 +1,1726 @@ +"use strict"; +/** + * @module LRUCache + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LRUCache = void 0; +const diagnostics_channel_js_1 = require("./diagnostics-channel.js"); +const perf_js_1 = require("./perf.js"); +const hasSubscribers = () => diagnostics_channel_js_1.metrics.hasSubscribers || diagnostics_channel_js_1.tracing.hasSubscribers; +const warned = new Set(); +/* c8 ignore start */ +const PROCESS = (typeof process === 'object' && !!process ? + process + : {}); +/* c8 ignore stop */ +const emitWarning = (msg, type, code, fn) => { + if (typeof PROCESS.emitWarning === 'function') { + PROCESS.emitWarning(msg, type, code, fn); + } + else { + //oxlint-disable-next-line no-console + console.error(`[${code}] ${type}: ${msg}`); + } +}; +const shouldWarn = (code) => !warned.has(code); +const TYPE = Symbol('type'); +const isPosInt = (n) => !!n && n === Math.floor(n) && n > 0 && isFinite(n); +// This is a little bit ridiculous, tbh. +// The maximum array length is 2^32-1 or thereabouts on most JS impls. +// And well before that point, you're caching the entire world, I mean, +// that's ~32GB of just integers for the next/prev links, plus whatever +// else to hold that many keys and values. Just filling the memory with +// zeroes at init time is brutal when you get that big. +// But why not be complete? +// Maybe in the future, these limits will have expanded. +/* c8 ignore start */ +const getUintArray = (max) => !isPosInt(max) ? null + : max <= Math.pow(2, 8) ? Uint8Array + : max <= Math.pow(2, 16) ? Uint16Array + : max <= Math.pow(2, 32) ? Uint32Array + : max <= Number.MAX_SAFE_INTEGER ? ZeroArray + : null; +/* c8 ignore stop */ +class ZeroArray extends Array { + constructor(size) { + super(size); + this.fill(0); + } +} +class Stack { + /* c8 ignore start - not sure why this is showing up uncovered?? */ + heap; + /* c8 ignore stop */ + length; + // private constructor + static #constructing = false; + static create(max) { + const HeapCls = getUintArray(max); + if (!HeapCls) + return []; + Stack.#constructing = true; + const s = new Stack(max, HeapCls); + Stack.#constructing = false; + return s; + } + constructor(max, HeapCls) { + /* c8 ignore start */ + if (!Stack.#constructing) { + throw new TypeError('instantiate Stack using Stack.create(n)'); + } + /* c8 ignore stop */ + this.heap = new HeapCls(max); + this.length = 0; + } + push(n) { + this.heap[this.length++] = n; + } + pop() { + return this.heap[--this.length]; + } +} +/** + * Default export, the thing you're using this module to get. + * + * The `K` and `V` types define the key and value types, respectively. The + * optional `FC` type defines the type of the `context` object passed to + * `cache.fetch()` and `cache.memo()`. + * + * Keys and values **must not** be `null` or `undefined`. + * + * All properties from the options object (with the exception of `max`, + * `maxSize`, `fetchMethod`, `memoMethod`, `dispose` and `disposeAfter`) are + * added as normal public members. (The listed options are read-only getters.) + * + * Changing any of these will alter the defaults for subsequent method calls. + */ +class LRUCache { + // options that cannot be changed without disaster + #max; + #maxSize; + #dispose; + #onInsert; + #disposeAfter; + #fetchMethod; + #memoMethod; + #perf; + /** + * {@link LRUCache.OptionsBase.perf} + */ + get perf() { + return this.#perf; + } + /** + * {@link LRUCache.OptionsBase.ttl} + */ + ttl; + /** + * {@link LRUCache.OptionsBase.ttlResolution} + */ + ttlResolution; + /** + * {@link LRUCache.OptionsBase.ttlAutopurge} + */ + ttlAutopurge; + /** + * {@link LRUCache.OptionsBase.updateAgeOnGet} + */ + updateAgeOnGet; + /** + * {@link LRUCache.OptionsBase.updateAgeOnHas} + */ + updateAgeOnHas; + /** + * {@link LRUCache.OptionsBase.allowStale} + */ + allowStale; + /** + * {@link LRUCache.OptionsBase.noDisposeOnSet} + */ + noDisposeOnSet; + /** + * {@link LRUCache.OptionsBase.noUpdateTTL} + */ + noUpdateTTL; + /** + * {@link LRUCache.OptionsBase.maxEntrySize} + */ + maxEntrySize; + /** + * {@link LRUCache.OptionsBase.sizeCalculation} + */ + sizeCalculation; + /** + * {@link LRUCache.OptionsBase.noDeleteOnFetchRejection} + */ + noDeleteOnFetchRejection; + /** + * {@link LRUCache.OptionsBase.noDeleteOnStaleGet} + */ + noDeleteOnStaleGet; + /** + * {@link LRUCache.OptionsBase.allowStaleOnFetchAbort} + */ + allowStaleOnFetchAbort; + /** + * {@link LRUCache.OptionsBase.allowStaleOnFetchRejection} + */ + allowStaleOnFetchRejection; + /** + * {@link LRUCache.OptionsBase.ignoreFetchAbort} + */ + ignoreFetchAbort; + /** {@link LRUCache.OptionsBase.backgroundFetchSize} */ + backgroundFetchSize; + // computed properties + #size; + #calculatedSize; + #keyMap; + #keyList; + #valList; + #next; + #prev; + #head; + #tail; + #free; + #disposed; + #sizes; + #starts; + #ttls; + #autopurgeTimers; + #hasDispose; + #hasFetchMethod; + #hasDisposeAfter; + #hasOnInsert; + /** + * Do not call this method unless you need to inspect the + * inner workings of the cache. If anything returned by this + * object is modified in any way, strange breakage may occur. + * + * These fields are private for a reason! + * + * @internal + */ + static unsafeExposeInternals(c) { + return { + // properties + starts: c.#starts, + ttls: c.#ttls, + autopurgeTimers: c.#autopurgeTimers, + sizes: c.#sizes, + keyMap: c.#keyMap, + keyList: c.#keyList, + valList: c.#valList, + next: c.#next, + prev: c.#prev, + get head() { + return c.#head; + }, + get tail() { + return c.#tail; + }, + free: c.#free, + // methods + isBackgroundFetch: (p) => c.#isBackgroundFetch(p), + backgroundFetch: (k, index, options, context) => c.#backgroundFetch(k, index, options, context), + moveToTail: (index) => c.#moveToTail(index), + indexes: (options) => c.#indexes(options), + rindexes: (options) => c.#rindexes(options), + isStale: (index) => c.#isStale(index), + }; + } + // Protected read-only members + /** + * {@link LRUCache.OptionsBase.max} (read-only) + */ + get max() { + return this.#max; + } + /** + * {@link LRUCache.OptionsBase.maxSize} (read-only) + */ + get maxSize() { + return this.#maxSize; + } + /** + * The total computed size of items in the cache (read-only) + */ + get calculatedSize() { + return this.#calculatedSize; + } + /** + * The number of items stored in the cache (read-only) + */ + get size() { + return this.#size; + } + /** + * {@link LRUCache.OptionsBase.fetchMethod} (read-only) + */ + get fetchMethod() { + return this.#fetchMethod; + } + get memoMethod() { + return this.#memoMethod; + } + /** + * {@link LRUCache.OptionsBase.dispose} (read-only) + */ + get dispose() { + return this.#dispose; + } + /** + * {@link LRUCache.OptionsBase.onInsert} (read-only) + */ + get onInsert() { + return this.#onInsert; + } + /** + * {@link LRUCache.OptionsBase.disposeAfter} (read-only) + */ + get disposeAfter() { + return this.#disposeAfter; + } + constructor(options) { + const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options; + this.backgroundFetchSize = backgroundFetchSize; + if (perf !== undefined) { + if (typeof perf?.now !== 'function') { + throw new TypeError('perf option must have a now() method if specified'); + } + } + this.#perf = perf ?? perf_js_1.defaultPerf; + if (max !== 0 && !isPosInt(max)) { + throw new TypeError('max option must be a nonnegative integer'); + } + const UintArray = max ? getUintArray(max) : Array; + if (!UintArray) { + throw new Error('invalid max value: ' + max); + } + this.#max = max; + this.#maxSize = maxSize; + this.maxEntrySize = maxEntrySize || this.#maxSize; + this.sizeCalculation = sizeCalculation; + if (this.sizeCalculation) { + if (!this.#maxSize && !this.maxEntrySize) { + throw new TypeError('cannot set sizeCalculation without setting maxSize or maxEntrySize'); + } + if (typeof this.sizeCalculation !== 'function') { + throw new TypeError('sizeCalculation set to non-function'); + } + } + if (memoMethod !== undefined && typeof memoMethod !== 'function') { + throw new TypeError('memoMethod must be a function if defined'); + } + this.#memoMethod = memoMethod; + if (fetchMethod !== undefined && typeof fetchMethod !== 'function') { + throw new TypeError('fetchMethod must be a function if specified'); + } + this.#fetchMethod = fetchMethod; + this.#hasFetchMethod = !!fetchMethod; + this.#keyMap = new Map(); + this.#keyList = Array.from({ length: max }).fill(undefined); + this.#valList = Array.from({ length: max }).fill(undefined); + this.#next = new UintArray(max); + this.#prev = new UintArray(max); + this.#head = 0; + this.#tail = 0; + this.#free = Stack.create(max); + this.#size = 0; + this.#calculatedSize = 0; + if (typeof dispose === 'function') { + this.#dispose = dispose; + } + if (typeof onInsert === 'function') { + this.#onInsert = onInsert; + } + if (typeof disposeAfter === 'function') { + this.#disposeAfter = disposeAfter; + this.#disposed = []; + } + else { + this.#disposeAfter = undefined; + this.#disposed = undefined; + } + this.#hasDispose = !!this.#dispose; + this.#hasOnInsert = !!this.#onInsert; + this.#hasDisposeAfter = !!this.#disposeAfter; + this.noDisposeOnSet = !!noDisposeOnSet; + this.noUpdateTTL = !!noUpdateTTL; + this.noDeleteOnFetchRejection = !!noDeleteOnFetchRejection; + this.allowStaleOnFetchRejection = !!allowStaleOnFetchRejection; + this.allowStaleOnFetchAbort = !!allowStaleOnFetchAbort; + this.ignoreFetchAbort = !!ignoreFetchAbort; + // NB: maxEntrySize is set to maxSize if it's set + if (this.maxEntrySize !== 0) { + if (this.#maxSize !== 0) { + if (!isPosInt(this.#maxSize)) { + throw new TypeError('maxSize must be a positive integer if specified'); + } + } + if (!isPosInt(this.maxEntrySize)) { + throw new TypeError('maxEntrySize must be a positive integer if specified'); + } + this.#initializeSizeTracking(); + } + this.allowStale = !!allowStale; + this.noDeleteOnStaleGet = !!noDeleteOnStaleGet; + this.updateAgeOnGet = !!updateAgeOnGet; + this.updateAgeOnHas = !!updateAgeOnHas; + this.ttlResolution = + isPosInt(ttlResolution) || ttlResolution === 0 ? ttlResolution : 1; + this.ttlAutopurge = !!ttlAutopurge; + this.ttl = ttl || 0; + if (this.ttl) { + if (!isPosInt(this.ttl)) { + throw new TypeError('ttl must be a positive integer if specified'); + } + this.#initializeTTLTracking(); + } + // do not allow completely unbounded caches + if (this.#max === 0 && this.ttl === 0 && this.#maxSize === 0) { + throw new TypeError('At least one of max, maxSize, or ttl is required'); + } + if (!this.ttlAutopurge && !this.#max && !this.#maxSize) { + const code = 'LRU_CACHE_UNBOUNDED'; + if (shouldWarn(code)) { + warned.add(code); + const msg = 'TTL caching without ttlAutopurge, max, or maxSize can ' + + 'result in unbounded memory consumption.'; + emitWarning(msg, 'UnboundedCacheWarning', code, LRUCache); + } + } + } + /** + * Return the number of ms left in the item's TTL. If item is not in cache, + * returns `0`. Returns `Infinity` if item is in cache without a defined TTL. + */ + getRemainingTTL(key) { + return this.#keyMap.has(key) ? Infinity : 0; + } + #initializeTTLTracking() { + const ttls = new ZeroArray(this.#max); + const starts = new ZeroArray(this.#max); + this.#ttls = ttls; + this.#starts = starts; + const purgeTimers = this.ttlAutopurge ? + Array.from({ + length: this.#max, + }) + : undefined; + this.#autopurgeTimers = purgeTimers; + this.#setItemTTL = (index, ttl, start = this.#perf.now()) => { + starts[index] = ttl !== 0 ? start : 0; + ttls[index] = ttl; + setPurgetTimer(index, ttl); + }; + this.#updateItemAge = index => { + starts[index] = ttls[index] !== 0 ? this.#perf.now() : 0; + setPurgetTimer(index, ttls[index]); + }; + // clear out the purge timer if we're setting TTL to 0, and + // previously had a ttl purge timer running, so it doesn't + // fire unnecessarily. Don't need to do this if we're not doing + // autopurge. + const setPurgetTimer = !this.ttlAutopurge ? + () => { } + : (index, ttl) => { + if (purgeTimers?.[index]) { + clearTimeout(purgeTimers[index]); + purgeTimers[index] = undefined; + } + if (ttl && ttl !== 0 && purgeTimers) { + const t = setTimeout(() => { + if (this.#isStale(index)) { + this.#delete(this.#keyList[index], 'expire'); + } + }, ttl + 1); + // unref() not supported on all platforms + /* c8 ignore start */ + if (t.unref) { + t.unref(); + } + /* c8 ignore stop */ + purgeTimers[index] = t; + } + }; + this.#statusTTL = (status, index) => { + if (ttls[index]) { + const ttl = ttls[index]; + const start = starts[index]; + /* c8 ignore start */ + if (!ttl || !start) { + return; + } + /* c8 ignore stop */ + status.ttl = ttl; + status.start = start; + status.now = cachedNow || getNow(); + const age = status.now - start; + status.remainingTTL = ttl - age; + } + }; + // debounce calls to perf.now() to 1s so we're not hitting + // that costly call repeatedly. + let cachedNow = 0; + const getNow = () => { + const n = this.#perf.now(); + if (this.ttlResolution > 0) { + cachedNow = n; + const t = setTimeout(() => (cachedNow = 0), this.ttlResolution); + // not available on all platforms + /* c8 ignore start */ + if (t.unref) { + t.unref(); + } + /* c8 ignore stop */ + } + return n; + }; + this.getRemainingTTL = key => { + const index = this.#keyMap.get(key); + if (index === undefined) { + return 0; + } + const ttl = ttls[index]; + const start = starts[index]; + if (!ttl || !start) { + return Infinity; + } + const age = (cachedNow || getNow()) - start; + return ttl - age; + }; + this.#isStale = index => { + const s = starts[index]; + const t = ttls[index]; + return !!t && !!s && (cachedNow || getNow()) - s > t; + }; + } + // conditionally set private methods related to TTL + #updateItemAge = () => { }; + #statusTTL = () => { }; + #setItemTTL = () => { }; + /* c8 ignore stop */ + #isStale = () => false; + #initializeSizeTracking() { + const sizes = new ZeroArray(this.#max); + this.#calculatedSize = 0; + this.#sizes = sizes; + this.#removeItemSize = index => { + this.#calculatedSize -= sizes[index]; + sizes[index] = 0; + }; + this.#requireSize = (k, v, size, sizeCalculation) => { + if (!isPosInt(size)) { + // provisionally accept background fetches. + // actual value size will be checked when they return. + if (this.#isBackgroundFetch(v)) { + // NB: this cannot occur if v.__staleWhileFetching is set, + // because in that case, it would take on the size of the + // existing entry that it temporarily replaces. + return this.backgroundFetchSize; + } + if (sizeCalculation) { + if (typeof sizeCalculation !== 'function') { + throw new TypeError('sizeCalculation must be a function'); + } + size = sizeCalculation(v, k); + if (!isPosInt(size)) { + throw new TypeError('sizeCalculation return invalid (expect positive integer)'); + } + } + else { + throw new TypeError('invalid size value (must be positive integer). ' + + 'When maxSize or maxEntrySize is used, sizeCalculation ' + + 'or size must be set.'); + } + } + return size; + }; + this.#addItemSize = (index, size, status) => { + sizes[index] = size; + if (this.#maxSize) { + const maxSize = this.#maxSize - sizes[index]; + while (this.#calculatedSize > maxSize) { + this.#evict(true); + } + } + this.#calculatedSize += sizes[index]; + if (status) { + status.entrySize = size; + status.totalCalculatedSize = this.#calculatedSize; + } + }; + } + #removeItemSize = _i => { }; + #addItemSize = (_i, _s, _st) => { }; + #requireSize = (_k, _v, size, sizeCalculation) => { + if (size || sizeCalculation) { + throw new TypeError('cannot set size without setting maxSize or maxEntrySize on cache'); + } + return 0; + }; + *#indexes({ allowStale = this.allowStale } = {}) { + if (this.#size) { + for (let i = this.#tail; this.#isValidIndex(i);) { + if (allowStale || !this.#isStale(i)) { + yield i; + } + if (i === this.#head) { + break; + } + else { + i = this.#prev[i]; + } + } + } + } + *#rindexes({ allowStale = this.allowStale } = {}) { + if (this.#size) { + for (let i = this.#head; this.#isValidIndex(i);) { + if (allowStale || !this.#isStale(i)) { + yield i; + } + if (i === this.#tail) { + break; + } + else { + i = this.#next[i]; + } + } + } + } + #isValidIndex(index) { + return (index !== undefined && + this.#keyMap.get(this.#keyList[index]) === index); + } + /** + * Return a generator yielding `[key, value]` pairs, + * in order from most recently used to least recently used. + */ + *entries() { + for (const i of this.#indexes()) { + if (this.#valList[i] !== undefined && + this.#keyList[i] !== undefined && + !this.#isBackgroundFetch(this.#valList[i])) { + yield [this.#keyList[i], this.#valList[i]]; + } + } + } + /** + * Inverse order version of {@link LRUCache.entries} + * + * Return a generator yielding `[key, value]` pairs, + * in order from least recently used to most recently used. + */ + *rentries() { + for (const i of this.#rindexes()) { + if (this.#valList[i] !== undefined && + this.#keyList[i] !== undefined && + !this.#isBackgroundFetch(this.#valList[i])) { + yield [this.#keyList[i], this.#valList[i]]; + } + } + } + /** + * Return a generator yielding the keys in the cache, + * in order from most recently used to least recently used. + */ + *keys() { + for (const i of this.#indexes()) { + const k = this.#keyList[i]; + if (k !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield k; + } + } + } + /** + * Inverse order version of {@link LRUCache.keys} + * + * Return a generator yielding the keys in the cache, + * in order from least recently used to most recently used. + */ + *rkeys() { + for (const i of this.#rindexes()) { + const k = this.#keyList[i]; + if (k !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield k; + } + } + } + /** + * Return a generator yielding the values in the cache, + * in order from most recently used to least recently used. + */ + *values() { + for (const i of this.#indexes()) { + const v = this.#valList[i]; + if (v !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield this.#valList[i]; + } + } + } + /** + * Inverse order version of {@link LRUCache.values} + * + * Return a generator yielding the values in the cache, + * in order from least recently used to most recently used. + */ + *rvalues() { + for (const i of this.#rindexes()) { + const v = this.#valList[i]; + if (v !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield this.#valList[i]; + } + } + } + /** + * Iterating over the cache itself yields the same results as + * {@link LRUCache.entries} + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * A String value that is used in the creation of the default string + * description of an object. Called by the built-in method + * `Object.prototype.toString`. + */ + [Symbol.toStringTag] = 'LRUCache'; + /** + * Find a value for which the supplied fn method returns a truthy value, + * similar to `Array.find()`. fn is called as `fn(value, key, cache)`. + */ + find(fn, getOptions = {}) { + for (const i of this.#indexes()) { + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + continue; + if (fn(value, this.#keyList[i], this)) { + return this.#get(this.#keyList[i], getOptions); + } + } + } + /** + * Call the supplied function on each item in the cache, in order from most + * recently used to least recently used. + * + * `fn` is called as `fn(value, key, cache)`. + * + * If `thisp` is provided, function will be called in the `this`-context of + * the provided object, or the cache if no `thisp` object is provided. + * + * Does not update age or recenty of use, or iterate over stale values. + */ + forEach(fn, thisp = this) { + for (const i of this.#indexes()) { + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + continue; + fn.call(thisp, value, this.#keyList[i], this); + } + } + /** + * The same as {@link LRUCache.forEach} but items are iterated over in + * reverse order. (ie, less recently used items are iterated over first.) + */ + rforEach(fn, thisp = this) { + for (const i of this.#rindexes()) { + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + continue; + fn.call(thisp, value, this.#keyList[i], this); + } + } + /** + * Delete any stale entries. Returns true if anything was removed, + * false otherwise. + */ + purgeStale() { + let deleted = false; + for (const i of this.#rindexes({ allowStale: true })) { + if (this.#isStale(i)) { + this.#delete(this.#keyList[i], 'expire'); + deleted = true; + } + } + return deleted; + } + /** + * Get the extended info about a given entry, to get its value, size, and + * TTL info simultaneously. Returns `undefined` if the key is not present. + * + * Unlike {@link LRUCache#dump}, which is designed to be portable and survive + * serialization, the `start` value is always the current timestamp, and the + * `ttl` is a calculated remaining time to live (negative if expired). + * + * Always returns stale values, if their info is found in the cache, so be + * sure to check for expirations (ie, a negative {@link LRUCache.Entry#ttl}) + * if relevant. + */ + info(key) { + const i = this.#keyMap.get(key); + if (i === undefined) + return undefined; + const v = this.#valList[i]; + /* c8 ignore start - this isn't tested for the info function, + * but it's the same logic as found in other places. */ + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + return undefined; + /* c8 ignore stop */ + const entry = { value }; + if (this.#ttls && this.#starts) { + const ttl = this.#ttls[i]; + const start = this.#starts[i]; + if (ttl && start) { + const remain = ttl - (this.#perf.now() - start); + entry.ttl = remain; + entry.start = Date.now(); + } + } + if (this.#sizes) { + entry.size = this.#sizes[i]; + } + return entry; + } + /** + * Return an array of [key, {@link LRUCache.Entry}] tuples which can be + * passed to {@link LRUCache#load}. + * + * The `start` fields are calculated relative to a portable `Date.now()` + * timestamp, even if `performance.now()` is available. + * + * Stale entries are always included in the `dump`, even if + * {@link LRUCache.OptionsBase.allowStale} is false. + * + * Note: this returns an actual array, not a generator, so it can be more + * easily passed around. + */ + dump() { + const arr = []; + for (const i of this.#indexes({ allowStale: true })) { + const key = this.#keyList[i]; + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined || key === undefined) + continue; + const entry = { value }; + if (this.#ttls && this.#starts) { + entry.ttl = this.#ttls[i]; + // always dump the start relative to a portable timestamp + // it's ok for this to be a bit slow, it's a rare operation. + const age = this.#perf.now() - this.#starts[i]; + entry.start = Math.floor(Date.now() - age); + } + if (this.#sizes) { + entry.size = this.#sizes[i]; + } + arr.unshift([key, entry]); + } + return arr; + } + /** + * Reset the cache and load in the items in entries in the order listed. + * + * The shape of the resulting cache may be different if the same options are + * not used in both caches. + * + * The `start` fields are assumed to be calculated relative to a portable + * `Date.now()` timestamp, even if `performance.now()` is available. + */ + load(arr) { + this.clear(); + for (const [key, entry] of arr) { + if (entry.start) { + // entry.start is a portable timestamp, but we may be using + // node's performance.now(), so calculate the offset, so that + // we get the intended remaining TTL, no matter how long it's + // been on ice. + // + // it's ok for this to be a bit slow, it's a rare operation. + const age = Date.now() - entry.start; + entry.start = this.#perf.now() - age; + } + this.#set(key, entry.value, entry); + } + } + /** + * Add a value to the cache. + * + * Note: if `undefined` is specified as a value, this is an alias for + * {@link LRUCache#delete} + * + * Fields on the {@link LRUCache.SetOptions} options param will override + * their corresponding values in the constructor options for the scope + * of this single `set()` operation. + * + * If `start` is provided, then that will set the effective start + * time for the TTL calculation. Note that this must be a previous + * value of `performance.now()` if supported, or a previous value of + * `Date.now()` if not. + * + * Options object may also include `size`, which will prevent + * calling the `sizeCalculation` function and just use the specified + * number if it is a positive integer, and `noDisposeOnSet` which + * will prevent calling a `dispose` function in the case of + * overwrites. + * + * If the `size` (or return value of `sizeCalculation`) for a given + * entry is greater than `maxEntrySize`, then the item will not be + * added to the cache. + * + * Will update the recency of the entry. + * + * If the value is `undefined`, then this is an alias for + * `cache.delete(key)`. `undefined` is never stored in the cache. + */ + set(k, v, setOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = setOptions; + setOptions.status = status; + if (status) { + status.op = 'set'; + status.key = k; + if (v !== undefined) + status.value = v; + status.cache = this; + } + const result = this.#set(k, v, setOptions); + if (status && diagnostics_channel_js_1.metrics.hasSubscribers) { + diagnostics_channel_js_1.metrics.publish(status); + } + return result; + } + #set(k, v, setOptions, bf) { + const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions; + const isBF = this.#isBackgroundFetch(v); + if (v === undefined) { + if (status) + status.set = 'deleted'; + this.delete(k); + return this; + } + let { noUpdateTTL = this.noUpdateTTL } = setOptions; + if (status && !isBF) + status.value = v; + const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status); + // if the item doesn't fit, don't do anything + // NB: maxEntrySize set to maxSize by default + if (this.maxEntrySize && size > this.maxEntrySize) { + // have to delete, in case something is there already. + this.#delete(k, 'set'); + if (status) { + status.set = 'miss'; + status.maxEntrySizeExceeded = true; + } + return this; + } + let index = this.#size === 0 ? undefined : this.#keyMap.get(k); + if (index === undefined) { + // addition + index = (this.#size === 0 ? this.#tail + : this.#free.length !== 0 ? this.#free.pop() + : this.#size === this.#max ? this.#evict(false) + : this.#size); + this.#keyList[index] = k; + this.#valList[index] = v; + this.#keyMap.set(k, index); + this.#next[this.#tail] = index; + this.#prev[index] = this.#tail; + this.#tail = index; + this.#size++; + this.#addItemSize(index, size, status); + if (status) + status.set = 'add'; + noUpdateTTL = false; + if (this.#hasOnInsert && !isBF) { + this.#onInsert?.(v, k, 'add'); + } + } + else { + // update + // might be updating a background fetch! + this.#moveToTail(index); + const oldVal = this.#valList[index]; + if (v !== oldVal) { + if (!noDisposeOnSet) { + if (this.#isBackgroundFetch(oldVal)) { + if (oldVal !== bf) { + // setting over a background fetch, not merely resolving it. + oldVal.__abortController.abort(new Error('replaced')); + } + const { __staleWhileFetching: s } = oldVal; + if (s !== undefined && s !== v) { + if (this.#hasDispose) { + this.#dispose?.(s, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([s, k, 'set']); + } + } + } + else { + if (this.#hasDispose) { + this.#dispose?.(oldVal, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([oldVal, k, 'set']); + } + } + } + this.#removeItemSize(index); + this.#addItemSize(index, size, status); + this.#valList[index] = v; + if (!isBF) { + const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ? + oldVal.__staleWhileFetching + : oldVal; + const setType = oldValue === undefined ? 'add' + : v !== oldValue ? 'replace' + : 'update'; + if (status) { + status.set = setType; + if (oldValue !== undefined) + status.oldValue = oldValue; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, setType); + } + } + } + else if (!isBF) { + if (status) { + status.set = 'update'; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, 'update'); + } + } + } + if (ttl !== 0 && !this.#ttls) { + this.#initializeTTLTracking(); + } + if (this.#ttls) { + if (!noUpdateTTL) { + this.#setItemTTL(index, ttl, start); + } + if (status) + this.#statusTTL(status, index); + } + if (!noDisposeOnSet && this.#hasDisposeAfter && this.#disposed) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + return this; + } + /** + * Evict the least recently used item, returning its value or + * `undefined` if cache is empty. + */ + pop() { + try { + while (this.#size) { + const val = this.#valList[this.#head]; + this.#evict(true); + if (this.#isBackgroundFetch(val)) { + if (val.__staleWhileFetching) { + return val.__staleWhileFetching; + } + } + else if (val !== undefined) { + return val; + } + } + } + finally { + if (this.#hasDisposeAfter && this.#disposed) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + } + } + #evict(free) { + const head = this.#head; + const k = this.#keyList[head]; + const v = this.#valList[head]; + const isBF = this.#isBackgroundFetch(v); + if (isBF) { + v.__abortController.abort(new Error('evicted')); + } + const oldValue = isBF ? v.__staleWhileFetching : v; + if ((this.#hasDispose || this.#hasDisposeAfter) && + oldValue !== undefined) { + if (this.#hasDispose) { + this.#dispose?.(oldValue, k, 'evict'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([oldValue, k, 'evict']); + } + } + this.#removeItemSize(head); + if (this.#autopurgeTimers?.[head]) { + clearTimeout(this.#autopurgeTimers[head]); + this.#autopurgeTimers[head] = undefined; + } + // if we aren't about to use the index, then null these out + if (free) { + this.#keyList[head] = undefined; + this.#valList[head] = undefined; + this.#free.push(head); + } + if (this.#size === 1) { + this.#head = this.#tail = 0; + this.#free.length = 0; + } + else { + this.#head = this.#next[head]; + } + this.#keyMap.delete(k); + this.#size--; + return head; + } + /** + * Check if a key is in the cache, without updating the recency of use. + * Will return false if the item is stale, even though it is technically + * in the cache. + * + * Check if a key is in the cache, without updating the recency of + * use. Age is updated if {@link LRUCache.OptionsBase.updateAgeOnHas} is set + * to `true` in either the options or the constructor. + * + * Will return `false` if the item is stale, even though it is technically in + * the cache. The difference can be determined (if it matters) by using a + * `status` argument, and inspecting the `has` field. + * + * Will not update item age unless + * {@link LRUCache.OptionsBase.updateAgeOnHas} is set. + */ + has(k, hasOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = hasOptions; + hasOptions.status = status; + if (status) { + status.op = 'has'; + status.key = k; + status.cache = this; + } + const result = this.#has(k, hasOptions); + if (diagnostics_channel_js_1.metrics.hasSubscribers) + diagnostics_channel_js_1.metrics.publish(status); + return result; + } + #has(k, hasOptions = {}) { + const { updateAgeOnHas = this.updateAgeOnHas, status } = hasOptions; + const index = this.#keyMap.get(k); + if (index !== undefined) { + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v) && + v.__staleWhileFetching === undefined) { + return false; + } + if (!this.#isStale(index)) { + if (updateAgeOnHas) { + this.#updateItemAge(index); + } + if (status) { + status.has = 'hit'; + this.#statusTTL(status, index); + } + return true; + } + else if (status) { + status.has = 'stale'; + this.#statusTTL(status, index); + } + } + else if (status) { + status.has = 'miss'; + } + return false; + } + /** + * Like {@link LRUCache#get} but doesn't update recency or delete stale + * items. + * + * Returns `undefined` if the item is stale, unless + * {@link LRUCache.OptionsBase.allowStale} is set. + */ + peek(k, peekOptions = {}) { + const { status = hasSubscribers() ? {} : undefined } = peekOptions; + if (status) { + status.op = 'peek'; + status.key = k; + status.cache = this; + } + peekOptions.status = status; + const result = this.#peek(k, peekOptions); + if (diagnostics_channel_js_1.metrics.hasSubscribers) { + diagnostics_channel_js_1.metrics.publish(status); + } + return result; + } + #peek(k, peekOptions) { + const { status, allowStale = this.allowStale } = peekOptions; + const index = this.#keyMap.get(k); + if (index === undefined || (!allowStale && this.#isStale(index))) { + if (status) + status.peek = index === undefined ? 'miss' : 'stale'; + return undefined; + } + const v = this.#valList[index]; + const val = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (status) { + if (val !== undefined) { + status.peek = 'hit'; + status.value = val; + } + else { + status.peek = 'miss'; + } + } + return val; + } + #backgroundFetch(k, index, options, context) { + const v = index === undefined ? undefined : this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + return v; + } + const ac = new AbortController(); + const { signal } = options; + // when/if our AC signals, then stop listening to theirs. + signal?.addEventListener('abort', () => ac.abort(signal.reason), { + signal: ac.signal, + }); + const fetchOpts = { + signal: ac.signal, + options, + context, + }; + const cb = (v, updateCache = false) => { + const { aborted } = ac.signal; + const ignoreAbort = options.ignoreFetchAbort && v !== undefined; + const proceed = options.ignoreFetchAbort || + !!(options.allowStaleOnFetchAbort && v !== undefined); + if (options.status) { + if (aborted && !updateCache) { + options.status.fetchAborted = true; + options.status.fetchError = ac.signal.reason; + if (ignoreAbort) + options.status.fetchAbortIgnored = true; + } + else { + options.status.fetchResolved = true; + } + } + if (aborted && !ignoreAbort && !updateCache) { + return fetchFail(ac.signal.reason, proceed); + } + // either we didn't abort, and are still here, or we did, and ignored + const bf = p; + // if nothing else has been written there but we're set to update the + // cache and ignore the abort, or if it's still pending on this specific + // background request, then write it to the cache. + const vl = this.#valList[index]; + if (vl === p || (vl === undefined && ignoreAbort && updateCache)) { + if (v === undefined) { + if (bf.__staleWhileFetching !== undefined) { + this.#valList[index] = bf.__staleWhileFetching; + } + else { + this.#delete(k, 'fetch'); + } + } + else { + if (options.status) + options.status.fetchUpdated = true; + this.#set(k, v, fetchOpts.options, bf); + } + } + return v; + }; + const eb = (er) => { + if (options.status) { + options.status.fetchRejected = true; + options.status.fetchError = er; + } + // do not pass go, do not collect $200 + return fetchFail(er, false); + }; + const fetchFail = (er, proceed) => { + const { aborted } = ac.signal; + const allowStaleAborted = aborted && options.allowStaleOnFetchAbort; + const allowStale = allowStaleAborted || options.allowStaleOnFetchRejection; + const noDelete = allowStale || options.noDeleteOnFetchRejection; + const bf = p; + if (this.#valList[index] === p) { + // if we allow stale on fetch rejections, then we need to ensure that + // the stale value is not removed from the cache when the fetch fails. + const del = !noDelete || (!proceed && bf.__staleWhileFetching === undefined); + if (del) { + this.#delete(k, 'fetch'); + } + else if (!allowStaleAborted) { + // still replace the *promise* with the stale value, + // since we are done with the promise at this point. + // leave it untouched if we're still waiting for an + // aborted background fetch that hasn't yet returned. + this.#valList[index] = bf.__staleWhileFetching; + } + } + if (allowStale) { + if (options.status && bf.__staleWhileFetching !== undefined) { + options.status.returnedStale = true; + } + return bf.__staleWhileFetching; + } + else if (bf.__returned === bf) { + throw er; + } + }; + const pcall = (res, rej) => { + const fmp = this.#fetchMethod?.(k, v, fetchOpts); + // ignored, we go until we finish, regardless. + // defer check until we are actually aborting, + // so fetchMethod can override. + ac.signal.addEventListener('abort', () => { + if (!options.ignoreFetchAbort || options.allowStaleOnFetchAbort) { + res(undefined); + // when it eventually resolves, update the cache. + if (options.allowStaleOnFetchAbort) { + res = v => cb(v, true); + } + } + }); + if (fmp && fmp instanceof Promise) { + fmp.then(v => res(v === undefined ? undefined : v), rej); + } + else if (fmp !== undefined) { + res(fmp); + } + }; + if (options.status) + options.status.fetchDispatched = true; + const p = new Promise(pcall).then(cb, eb); + const bf = Object.assign(p, { + __abortController: ac, + __staleWhileFetching: v, + __returned: undefined, + }); + if (index === undefined) { + // internal, don't expose status. + this.#set(k, bf, { ...fetchOpts.options, status: undefined }); + index = this.#keyMap.get(k); + } + else { + // do not call #set, because we do not want to adjust its place + // in the lru queue, as it has not yet been "used". Also, we don't + // need to worry about evicting for size, because a background fetch + // over a stale value is treated as the same size as its stale value. + this.#valList[index] = bf; + } + return bf; + } + #isBackgroundFetch(p) { + if (!this.#hasFetchMethod) + return false; + const b = p; + return (!!b && + b instanceof Promise && + b.hasOwnProperty('__staleWhileFetching') && + b.__abortController instanceof AbortController); + } + fetch(k, fetchOptions = {}) { + const ths = diagnostics_channel_js_1.tracing.hasSubscribers; + const { status = hasSubscribers() ? {} : undefined } = fetchOptions; + fetchOptions.status = status; + if (status && fetchOptions.context) { + status.context = fetchOptions.context; + } + const p = this.#fetch(k, fetchOptions); + if (status && ths) { + status.trace = true; + diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); + } + return p; + } + async #fetch(k, fetchOptions = {}) { + const { + // get options + allowStale = this.allowStale, updateAgeOnGet = this.updateAgeOnGet, noDeleteOnStaleGet = this.noDeleteOnStaleGet, + // set options + ttl = this.ttl, noDisposeOnSet = this.noDisposeOnSet, size = 0, sizeCalculation = this.sizeCalculation, noUpdateTTL = this.noUpdateTTL, + // fetch exclusive options + noDeleteOnFetchRejection = this.noDeleteOnFetchRejection, allowStaleOnFetchRejection = this.allowStaleOnFetchRejection, ignoreFetchAbort = this.ignoreFetchAbort, allowStaleOnFetchAbort = this.allowStaleOnFetchAbort, context, forceRefresh = false, status, signal, } = fetchOptions; + if (status) { + status.op = 'fetch'; + status.key = k; + if (forceRefresh) + status.forceRefresh = true; + status.cache = this; + } + if (!this.#hasFetchMethod) { + if (status) + status.fetch = 'get'; + return this.#get(k, { + allowStale, + updateAgeOnGet, + noDeleteOnStaleGet, + status, + }); + } + const options = { + allowStale, + updateAgeOnGet, + noDeleteOnStaleGet, + ttl, + noDisposeOnSet, + size, + sizeCalculation, + noUpdateTTL, + noDeleteOnFetchRejection, + allowStaleOnFetchRejection, + allowStaleOnFetchAbort, + ignoreFetchAbort, + status, + signal, + }; + let index = this.#keyMap.get(k); + if (index === undefined) { + if (status) + status.fetch = 'miss'; + const p = this.#backgroundFetch(k, index, options, context); + return (p.__returned = p); + } + else { + // in cache, maybe already fetching + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + const stale = allowStale && v.__staleWhileFetching !== undefined; + if (status) { + status.fetch = 'inflight'; + if (stale) + status.returnedStale = true; + } + return stale ? v.__staleWhileFetching : (v.__returned = v); + } + // if we force a refresh, that means do NOT serve the cached value, + // unless we are already in the process of refreshing the cache. + const isStale = this.#isStale(index); + if (!forceRefresh && !isStale) { + if (status) + status.fetch = 'hit'; + this.#moveToTail(index); + if (updateAgeOnGet) { + this.#updateItemAge(index); + } + if (status) + this.#statusTTL(status, index); + return v; + } + // ok, it is stale or a forced refresh, and not already fetching. + // refresh the cache. + const p = this.#backgroundFetch(k, index, options, context); + const hasStale = p.__staleWhileFetching !== undefined; + const staleVal = hasStale && allowStale; + if (status) { + status.fetch = isStale ? 'stale' : 'refresh'; + if (staleVal && isStale) + status.returnedStale = true; + } + return staleVal ? p.__staleWhileFetching : (p.__returned = p); + } + } + forceFetch(k, fetchOptions = {}) { + const ths = diagnostics_channel_js_1.tracing.hasSubscribers; + const { status = hasSubscribers() ? {} : undefined } = fetchOptions; + fetchOptions.status = status; + if (status && fetchOptions.context) { + status.context = fetchOptions.context; + } + const p = this.#forceFetch(k, fetchOptions); + if (status && ths) { + status.trace = true; + diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); + } + return p; + } + async #forceFetch(k, fetchOptions = {}) { + const v = await this.#fetch(k, fetchOptions); + if (v === undefined) + throw new Error('fetch() returned undefined'); + return v; + } + memo(k, memoOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = memoOptions; + memoOptions.status = status; + if (status) { + status.op = 'memo'; + status.key = k; + if (memoOptions.context) { + status.context = memoOptions.context; + } + status.cache = this; + } + const result = this.#memo(k, memoOptions); + if (status) + status.value = result; + if (diagnostics_channel_js_1.metrics.hasSubscribers) + diagnostics_channel_js_1.metrics.publish(status); + return result; + } + #memo(k, memoOptions = {}) { + const memoMethod = this.#memoMethod; + if (!memoMethod) { + throw new Error('no memoMethod provided to constructor'); + } + const { context, status, forceRefresh, ...options } = memoOptions; + if (status && forceRefresh) + status.forceRefresh = true; + const v = this.#get(k, options); + const refresh = forceRefresh || v === undefined; + if (status) { + status.memo = refresh ? 'miss' : 'hit'; + if (!refresh) + status.value = v; + } + if (!refresh) + return v; + const vv = memoMethod(k, v, { + options, + context, + }); + if (status) + status.value = vv; + this.#set(k, vv, options); + return vv; + } + /** + * Return a value from the cache. Will update the recency of the cache + * entry found. + * + * If the key is not found, get() will return `undefined`. + */ + get(k, getOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = getOptions; + getOptions.status = status; + if (status) { + status.op = 'get'; + status.key = k; + status.cache = this; + } + const result = this.#get(k, getOptions); + if (status) { + if (result !== undefined) + status.value = result; + if (diagnostics_channel_js_1.metrics.hasSubscribers) + diagnostics_channel_js_1.metrics.publish(status); + } + return result; + } + #get(k, getOptions = {}) { + const { allowStale = this.allowStale, updateAgeOnGet = this.updateAgeOnGet, noDeleteOnStaleGet = this.noDeleteOnStaleGet, status, } = getOptions; + const index = this.#keyMap.get(k); + if (index === undefined) { + if (status) + status.get = 'miss'; + return undefined; + } + const value = this.#valList[index]; + const fetching = this.#isBackgroundFetch(value); + if (status) + this.#statusTTL(status, index); + if (this.#isStale(index)) { + // delete only if not an in-flight background fetch + if (!fetching) { + if (!noDeleteOnStaleGet) { + this.#delete(k, 'expire'); + } + if (status) + status.get = 'stale'; + if (allowStale) { + if (status) + status.returnedStale = true; + return value; + } + return undefined; + } + if (status) + status.get = 'stale-fetching'; + if (allowStale && value.__staleWhileFetching !== undefined) { + if (status) + status.returnedStale = true; + return value.__staleWhileFetching; + } + return undefined; + } + // not stale + if (status) + status.get = fetching ? 'fetching' : 'hit'; + // if we're currently fetching it, we don't actually have it yet + // it's not stale, which means this isn't a staleWhileRefetching. + // If it's not stale, and fetching, AND has a __staleWhileFetching + // value, then that means the user fetched with {forceRefresh:true}, + // so it's safe to return that value. + this.#moveToTail(index); + if (updateAgeOnGet) { + this.#updateItemAge(index); + } + return fetching ? value.__staleWhileFetching : value; + } + #connect(p, n) { + this.#prev[n] = p; + this.#next[p] = n; + } + #moveToTail(index) { + // if tail already, nothing to do + // if head, move head to next[index] + // else + // move next[prev[index]] to next[index] (head has no prev) + // move prev[next[index]] to prev[index] + // prev[index] = tail + // next[tail] = index + // tail = index + if (index !== this.#tail) { + if (index === this.#head) { + this.#head = this.#next[index]; + } + else { + this.#connect(this.#prev[index], this.#next[index]); + } + this.#connect(this.#tail, index); + this.#tail = index; + } + } + /** + * Deletes a key out of the cache. + * + * Returns true if the key was deleted, false otherwise. + */ + delete(k) { + return this.#delete(k, 'delete'); + } + #delete(k, reason) { + if (diagnostics_channel_js_1.metrics.hasSubscribers) { + diagnostics_channel_js_1.metrics.publish({ + op: 'delete', + delete: reason, + key: k, + cache: this, + }); + } + let deleted = false; + if (this.#size !== 0) { + const index = this.#keyMap.get(k); + if (index !== undefined) { + if (this.#autopurgeTimers?.[index]) { + clearTimeout(this.#autopurgeTimers?.[index]); + this.#autopurgeTimers[index] = undefined; + } + deleted = true; + if (this.#size === 1) { + this.#clear(reason); + } + else { + this.#removeItemSize(index); + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + v.__abortController.abort(new Error('deleted')); + } + else if (this.#hasDispose || this.#hasDisposeAfter) { + if (this.#hasDispose) { + this.#dispose?.(v, k, reason); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([v, k, reason]); + } + } + this.#keyMap.delete(k); + this.#keyList[index] = undefined; + this.#valList[index] = undefined; + if (index === this.#tail) { + this.#tail = this.#prev[index]; + } + else if (index === this.#head) { + this.#head = this.#next[index]; + } + else { + const pi = this.#prev[index]; + this.#next[pi] = this.#next[index]; + const ni = this.#next[index]; + this.#prev[ni] = this.#prev[index]; + } + this.#size--; + this.#free.push(index); + } + } + } + if (this.#hasDisposeAfter && this.#disposed?.length) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + return deleted; + } + /** + * Clear the cache entirely, throwing away all values. + */ + clear() { + return this.#clear('delete'); + } + #clear(reason) { + for (const index of this.#rindexes({ allowStale: true })) { + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + v.__abortController.abort(new Error('deleted')); + } + else { + const k = this.#keyList[index]; + if (this.#hasDispose) { + this.#dispose?.(v, k, reason); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([v, k, reason]); + } + } + } + this.#keyMap.clear(); + void this.#valList.fill(undefined); + this.#keyList.fill(undefined); + if (this.#ttls && this.#starts) { + this.#ttls.fill(0); + this.#starts.fill(0); + for (const t of this.#autopurgeTimers ?? []) { + if (t !== undefined) + clearTimeout(t); + } + this.#autopurgeTimers?.fill(undefined); + } + if (this.#sizes) { + this.#sizes.fill(0); + } + this.#head = 0; + this.#tail = 0; + this.#free.length = 0; + this.#calculatedSize = 0; + this.#size = 0; + if (this.#hasDisposeAfter && this.#disposed) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + } +} +exports.LRUCache = LRUCache; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/browser/index.min.js b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/index.min.js new file mode 100644 index 00000000000000..8e42461074475a --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/index.min.js @@ -0,0 +1,2 @@ +"use strict";var j=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports);var I=j(O=>{"use strict";Object.defineProperty(O,"__esModule",{value:!0});O.tracing=O.metrics=void 0;var U={hasSubscribers:!1};O.metrics=U;O.tracing=U});var P=j(D=>{"use strict";Object.defineProperty(D,"__esModule",{value:!0});D.defaultPerf=void 0;D.defaultPerf=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date});Object.defineProperty(exports,"__esModule",{value:!0});exports.LRUCache=void 0;var g=I(),N=P(),x=()=>g.metrics.hasSubscribers||g.tracing.hasSubscribers,k=new Set,G=typeof process=="object"&&process?process:{},V=(c,t,e,i)=>{typeof G.emitWarning=="function"?G.emitWarning(c,t,e,i):console.error(`[${e}] ${t}: ${c}`)},B=c=>!k.has(c);var T=c=>!!c&&c===Math.floor(c)&&c>0&&isFinite(c),H=c=>T(c)?c<=Math.pow(2,8)?Uint8Array:c<=Math.pow(2,16)?Uint16Array:c<=Math.pow(2,32)?Uint32Array:c<=Number.MAX_SAFE_INTEGER?W:null:null,W=class extends Array{constructor(t){super(t),this.fill(0)}},C=class c{heap;length;static#o=!1;static create(t){let e=H(t);if(!e)return[];c.#o=!0;let i=new c(t,e);return c.#o=!1,i}constructor(t,e){if(!c.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new e(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},L=class c{#o;#c;#m;#W;#S;#M;#j;#w;get perf(){return this.#w}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;backgroundFetchSize;#n;#b;#s;#i;#t;#l;#u;#a;#h;#y;#r;#_;#F;#d;#g;#T;#U;#f;#D;static unsafeExposeInternals(t){return{starts:t.#F,ttls:t.#d,autopurgeTimers:t.#g,sizes:t.#_,keyMap:t.#s,keyList:t.#i,valList:t.#t,next:t.#l,prev:t.#u,get head(){return t.#a},get tail(){return t.#h},free:t.#y,isBackgroundFetch:e=>t.#e(e),backgroundFetch:(e,i,s,n)=>t.#G(e,i,s,n),moveToTail:e=>t.#L(e),indexes:e=>t.#A(e),rindexes:e=>t.#z(e),isStale:e=>t.#p(e)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#j}get dispose(){return this.#m}get onInsert(){return this.#W}get disposeAfter(){return this.#S}constructor(t){let{max:e=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:r,updateAgeOnHas:h,allowStale:a,dispose:o,onInsert:d,disposeAfter:y,noDisposeOnSet:_,noUpdateTTL:u,maxSize:p=0,maxEntrySize:f=0,sizeCalculation:b,fetchMethod:l,memoMethod:S,noDeleteOnFetchRejection:F,noDeleteOnStaleGet:w,allowStaleOnFetchRejection:m,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,backgroundFetchSize:M=1,perf:v}=t;if(this.backgroundFetchSize=M,v!==void 0&&typeof v?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#w=v??N.defaultPerf,e!==0&&!T(e))throw new TypeError("max option must be a nonnegative integer");let E=e?H(e):Array;if(!E)throw new Error("invalid max value: "+e);if(this.#o=e,this.#c=p,this.maxEntrySize=f||this.#c,this.sizeCalculation=b,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(S!==void 0&&typeof S!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#j=S,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#U=!!l,this.#s=new Map,this.#i=Array.from({length:e}).fill(void 0),this.#t=Array.from({length:e}).fill(void 0),this.#l=new E(e),this.#u=new E(e),this.#a=0,this.#h=0,this.#y=C.create(e),this.#n=0,this.#b=0,typeof o=="function"&&(this.#m=o),typeof d=="function"&&(this.#W=d),typeof y=="function"?(this.#S=y,this.#r=[]):(this.#S=void 0,this.#r=void 0),this.#T=!!this.#m,this.#D=!!this.#W,this.#f=!!this.#S,this.noDisposeOnSet=!!_,this.noUpdateTTL=!!u,this.noDeleteOnFetchRejection=!!F,this.allowStaleOnFetchRejection=!!m,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#c!==0&&!T(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!T(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!a,this.noDeleteOnStaleGet=!!w,this.updateAgeOnGet=!!r,this.updateAgeOnHas=!!h,this.ttlResolution=T(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!T(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let R="LRU_CACHE_UNBOUNDED";B(R)&&(k.add(R),V("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",R,c))}}getRemainingTTL(t){return this.#s.has(t)?1/0:0}#k(){let t=new W(this.#o),e=new W(this.#o);this.#d=t,this.#F=e;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#H=(h,a,o=this.#w.now())=>{e[h]=a!==0?o:0,t[h]=a,s(h,a)},this.#R=h=>{e[h]=t[h]!==0?this.#w.now():0,s(h,t[h])};let s=this.ttlAutopurge?(h,a)=>{if(i?.[h]&&(clearTimeout(i[h]),i[h]=void 0),a&&a!==0&&i){let o=setTimeout(()=>{this.#p(h)&&this.#v(this.#i[h],"expire")},a+1);o.unref&&o.unref(),i[h]=o}}:()=>{};this.#E=(h,a)=>{if(t[a]){let o=t[a],d=e[a];if(!o||!d)return;h.ttl=o,h.start=d,h.now=n||r();let y=h.now-d;h.remainingTTL=o-y}};let n=0,r=()=>{let h=this.#w.now();if(this.ttlResolution>0){n=h;let a=setTimeout(()=>n=0,this.ttlResolution);a.unref&&a.unref()}return h};this.getRemainingTTL=h=>{let a=this.#s.get(h);if(a===void 0)return 0;let o=t[a],d=e[a];if(!o||!d)return 1/0;let y=(n||r())-d;return o-y},this.#p=h=>{let a=e[h],o=t[h];return!!o&&!!a&&(n||r())-a>o}}#R=()=>{};#E=()=>{};#H=()=>{};#p=()=>!1;#X(){let t=new W(this.#o);this.#b=0,this.#_=t,this.#x=e=>{this.#b-=t[e],t[e]=0},this.#N=(e,i,s,n)=>{if(!T(s)){if(this.#e(i))return this.backgroundFetchSize;if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,e),!T(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.")}return s},this.#I=(e,i,s)=>{if(t[e]=i,this.#c){let n=this.#c-t[e];for(;this.#b>n;)this.#P(!0)}this.#b+=t[e],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#x=t=>{};#I=(t,e,i)=>{};#N=(t,e,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#h;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#a);)e=this.#u[e]}*#z({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#a;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#h);)e=this.#l[e]}#V(t){return t!==void 0&&this.#s.get(this.#i[t])===t}*entries(){for(let t of this.#A())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*rentries(){for(let t of this.#z())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*keys(){for(let t of this.#A()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*rkeys(){for(let t of this.#z()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*values(){for(let t of this.#A())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}*rvalues(){for(let t of this.#z())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(t,e={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&t(n,this.#i[i],this))return this.#C(this.#i[i],e)}}forEach(t,e=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}rforEach(t,e=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}purgeStale(){let t=!1;for(let e of this.#z({allowStale:!0}))this.#p(e)&&(this.#v(this.#i[e],"expire"),t=!0);return t}info(t){let e=this.#s.get(t);if(e===void 0)return;let i=this.#t[e],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let r=this.#d[e],h=this.#F[e];if(r&&h){let a=r-(this.#w.now()-h);n.ttl=a,n.start=Date.now()}}return this.#_&&(n.size=this.#_[e]),n}dump(){let t=[];for(let e of this.#A({allowStale:!0})){let i=this.#i[e],s=this.#t[e],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let r={value:n};if(this.#d&&this.#F){r.ttl=this.#d[e];let h=this.#w.now()-this.#F[e];r.start=Math.floor(Date.now()-h)}this.#_&&(r.size=this.#_[e]),t.unshift([i,r])}return t}load(t){this.clear();for(let[e,i]of t){if(i.start){let s=Date.now()-i.start;i.start=this.#w.now()-s}this.#O(e,i.value,i)}}set(t,e,i={}){let{status:s=g.metrics.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=t,e!==void 0&&(s.value=e),s.cache=this);let n=this.#O(t,e,i);return s&&g.metrics.hasSubscribers&&g.metrics.publish(s),n}#O(t,e,i,s){let{ttl:n=this.ttl,start:r,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:o}=i,d=this.#e(e);if(e===void 0)return o&&(o.set="deleted"),this.delete(t),this;let{noUpdateTTL:y=this.noUpdateTTL}=i;o&&!d&&(o.value=e);let _=this.#N(t,e,i.size||0,a,o);if(this.maxEntrySize&&_>this.maxEntrySize)return this.#v(t,"set"),o&&(o.set="miss",o.maxEntrySizeExceeded=!0),this;let u=this.#n===0?void 0:this.#s.get(t);if(u===void 0)u=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#P(!1):this.#n,this.#i[u]=t,this.#t[u]=e,this.#s.set(t,u),this.#l[this.#h]=u,this.#u[u]=this.#h,this.#h=u,this.#n++,this.#I(u,_,o),o&&(o.set="add"),y=!1,this.#D&&!d&&this.#W?.(e,t,"add");else{this.#L(u);let p=this.#t[u];if(e!==p){if(!h)if(this.#e(p)){p!==s&&p.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:f}=p;f!==void 0&&f!==e&&(this.#T&&this.#m?.(f,t,"set"),this.#f&&this.#r?.push([f,t,"set"]))}else this.#T&&this.#m?.(p,t,"set"),this.#f&&this.#r?.push([p,t,"set"]);if(this.#x(u),this.#I(u,_,o),this.#t[u]=e,!d){let f=p&&this.#e(p)?p.__staleWhileFetching:p,b=f===void 0?"add":e!==f?"replace":"update";o&&(o.set=b,f!==void 0&&(o.oldValue=f)),this.#D&&this.onInsert?.(e,t,b)}}else d||(o&&(o.set="update"),this.#D&&this.onInsert?.(e,t,"update"))}if(n!==0&&!this.#d&&this.#k(),this.#d&&(y||this.#H(u,n,r),o&&this.#E(o,u)),!h&&this.#f&&this.#r){let p=this.#r,f;for(;f=p?.shift();)this.#S?.(...f)}return this}pop(){try{for(;this.#n;){let t=this.#t[this.#a];if(this.#P(!0),this.#e(t)){if(t.__staleWhileFetching)return t.__staleWhileFetching}else if(t!==void 0)return t}}finally{if(this.#f&&this.#r){let t=this.#r,e;for(;e=t?.shift();)this.#S?.(...e)}}}#P(t){let e=this.#a,i=this.#i[e],s=this.#t[e],n=this.#e(s);n&&s.__abortController.abort(new Error("evicted"));let r=n?s.__staleWhileFetching:s;return(this.#T||this.#f)&&r!==void 0&&(this.#T&&this.#m?.(r,i,"evict"),this.#f&&this.#r?.push([r,i,"evict"])),this.#x(e),this.#g?.[e]&&(clearTimeout(this.#g[e]),this.#g[e]=void 0),t&&(this.#i[e]=void 0,this.#t[e]=void 0,this.#y.push(e)),this.#n===1?(this.#a=this.#h=0,this.#y.length=0):this.#a=this.#l[e],this.#s.delete(i),this.#n--,e}has(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="has",i.key=t,i.cache=this);let s=this.#Y(t,e);return g.metrics.hasSubscribers&&g.metrics.publish(i),s}#Y(t,e={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=e,n=this.#s.get(t);if(n!==void 0){let r=this.#t[n];if(this.#e(r)&&r.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#R(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(t,e={}){let{status:i=x()?{}:void 0}=e;i&&(i.op="peek",i.key=t,i.cache=this),e.status=i;let s=this.#J(t,e);return g.metrics.hasSubscribers&&g.metrics.publish(i),s}#J(t,e){let{status:i,allowStale:s=this.allowStale}=e,n=this.#s.get(t);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let r=this.#t[n],h=this.#e(r)?r.__staleWhileFetching:r;return i&&(h!==void 0?(i.peek="hit",i.value=h):i.peek="miss"),h}#G(t,e,i,s){let n=e===void 0?void 0:this.#t[e];if(this.#e(n))return n;let r=new AbortController,{signal:h}=i;h?.addEventListener("abort",()=>r.abort(h.reason),{signal:r.signal});let a={signal:r.signal,options:i,context:s},o=(f,b=!1)=>{let{aborted:l}=r.signal,S=i.ignoreFetchAbort&&f!==void 0,F=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&f!==void 0);if(i.status&&(l&&!b?(i.status.fetchAborted=!0,i.status.fetchError=r.signal.reason,S&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!S&&!b)return y(r.signal.reason,F);let w=u,m=this.#t[e];return(m===u||m===void 0&&S&&b)&&(f===void 0?w.__staleWhileFetching!==void 0?this.#t[e]=w.__staleWhileFetching:this.#v(t,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#O(t,f,a.options,w))),f},d=f=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=f),y(f,!1)),y=(f,b)=>{let{aborted:l}=r.signal,S=l&&i.allowStaleOnFetchAbort,F=S||i.allowStaleOnFetchRejection,w=F||i.noDeleteOnFetchRejection,m=u;if(this.#t[e]===u&&(!w||!b&&m.__staleWhileFetching===void 0?this.#v(t,"fetch"):S||(this.#t[e]=m.__staleWhileFetching)),F)return i.status&&m.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),m.__staleWhileFetching;if(m.__returned===m)throw f},_=(f,b)=>{let l=this.#M?.(t,n,a);r.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(f(void 0),i.allowStaleOnFetchAbort&&(f=S=>o(S,!0)))}),l&&l instanceof Promise?l.then(S=>f(S===void 0?void 0:S),b):l!==void 0&&f(l)};i.status&&(i.status.fetchDispatched=!0);let u=new Promise(_).then(o,d),p=Object.assign(u,{__abortController:r,__staleWhileFetching:n,__returned:void 0});return e===void 0?(this.#O(t,p,{...a.options,status:void 0}),e=this.#s.get(t)):this.#t[e]=p,p}#e(t){if(!this.#U)return!1;let e=t;return!!e&&e instanceof Promise&&e.hasOwnProperty("__staleWhileFetching")&&e.__abortController instanceof AbortController}fetch(t,e={}){let i=g.tracing.hasSubscribers,{status:s=x()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#B(t,e);return s&&i&&(s.trace=!0,g.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#B(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:r=this.ttl,noDisposeOnSet:h=this.noDisposeOnSet,size:a=0,sizeCalculation:o=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:y=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:_=this.allowStaleOnFetchRejection,ignoreFetchAbort:u=this.ignoreFetchAbort,allowStaleOnFetchAbort:p=this.allowStaleOnFetchAbort,context:f,forceRefresh:b=!1,status:l,signal:S}=e;if(l&&(l.op="fetch",l.key=t,b&&(l.forceRefresh=!0),l.cache=this),!this.#U)return l&&(l.fetch="get"),this.#C(t,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let F={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:r,noDisposeOnSet:h,size:a,sizeCalculation:o,noUpdateTTL:d,noDeleteOnFetchRejection:y,allowStaleOnFetchRejection:_,allowStaleOnFetchAbort:p,ignoreFetchAbort:u,status:l,signal:S},w=this.#s.get(t);if(w===void 0){l&&(l.fetch="miss");let m=this.#G(t,w,F,f);return m.__returned=m}else{let m=this.#t[w];if(this.#e(m)){let E=i&&m.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",E&&(l.returnedStale=!0)),E?m.__staleWhileFetching:m.__returned=m}let A=this.#p(w);if(!b&&!A)return l&&(l.fetch="hit"),this.#L(w),s&&this.#R(w),l&&this.#E(l,w),m;let z=this.#G(t,w,F,f),v=z.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=A?"stale":"refresh",v&&A&&(l.returnedStale=!0)),v?z.__staleWhileFetching:z.__returned=z}}forceFetch(t,e={}){let i=g.tracing.hasSubscribers,{status:s=x()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#K(t,e);return s&&i&&(s.trace=!0,g.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#K(t,e={}){let i=await this.#B(t,e);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="memo",i.key=t,e.context&&(i.context=e.context),i.cache=this);let s=this.#Q(t,e);return i&&(i.value=s),g.metrics.hasSubscribers&&g.metrics.publish(i),s}#Q(t,e={}){let i=this.#j;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:r,...h}=e;n&&r&&(n.forceRefresh=!0);let a=this.#C(t,h),o=r||a===void 0;if(n&&(n.memo=o?"miss":"hit",o||(n.value=a)),!o)return a;let d=i(t,a,{options:h,context:s});return n&&(n.value=d),this.#O(t,d,h),d}get(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="get",i.key=t,i.cache=this);let s=this.#C(t,e);return i&&(s!==void 0&&(i.value=s),g.metrics.hasSubscribers&&g.metrics.publish(i)),s}#C(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:r}=e,h=this.#s.get(t);if(h===void 0){r&&(r.get="miss");return}let a=this.#t[h],o=this.#e(a);return r&&this.#E(r,h),this.#p(h)?o?(r&&(r.get="stale-fetching"),i&&a.__staleWhileFetching!==void 0?(r&&(r.returnedStale=!0),a.__staleWhileFetching):void 0):(n||this.#v(t,"expire"),r&&(r.get="stale"),i?(r&&(r.returnedStale=!0),a):void 0):(r&&(r.get=o?"fetching":"hit"),this.#L(h),s&&this.#R(h),o?a.__staleWhileFetching:a)}#q(t,e){this.#u[e]=t,this.#l[t]=e}#L(t){t!==this.#h&&(t===this.#a?this.#a=this.#l[t]:this.#q(this.#u[t],this.#l[t]),this.#q(this.#h,t),this.#h=t)}delete(t){return this.#v(t,"delete")}#v(t,e){g.metrics.hasSubscribers&&g.metrics.publish({op:"delete",delete:e,key:t,cache:this});let i=!1;if(this.#n!==0){let s=this.#s.get(t);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#$(e);else{this.#x(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#m?.(n,t,e),this.#f&&this.#r?.push([n,t,e])),this.#s.delete(t),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let r=this.#u[s];this.#l[r]=this.#l[s];let h=this.#l[s];this.#u[h]=this.#u[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#S?.(...n)}return i}clear(){return this.#$("delete")}#$(t){for(let e of this.#z({allowStale:!0})){let i=this.#t[e];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[e];this.#T&&this.#m?.(i,s,t),this.#f&&this.#r?.push([i,s,t])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let e of this.#g??[])e!==void 0&&clearTimeout(e);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#a=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let e=this.#r,i;for(;i=e?.shift();)this.#S?.(...i)}}};exports.LRUCache=L; +//# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/browser/perf.js b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/perf.js new file mode 100644 index 00000000000000..bd4c80f461d6c1 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/browser/perf.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.defaultPerf = void 0; +exports.defaultPerf = (typeof performance === 'object' && + performance && + typeof performance.now === 'function') ? + /* c8 ignore start - this gets covered, but c8 gets confused */ + performance + : /* c8 ignore stop */ Date; +//# sourceMappingURL=perf.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js b/deps/npm/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js index 3a3c4e1be38b28..bdd4f41f903666 100644 --- a/deps/npm/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js @@ -1,10 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.tracing = exports.metrics = void 0; -// simple node version that imports from node builtin -// this gets compiled to a require() commonjs-style override, -// not using top level await on a conditional dynamic import -const node_diagnostics_channel_1 = require("node:diagnostics_channel"); -exports.metrics = (0, node_diagnostics_channel_1.channel)('lru-cache:metrics'); -exports.tracing = (0, node_diagnostics_channel_1.tracingChannel)('lru-cache'); -//# sourceMappingURL=diagnostics-channel.js.map \ No newline at end of file +const dummy = { hasSubscribers: false }; +exports.metrics = dummy; +exports.tracing = dummy; +//# sourceMappingURL=diagnostics-channel-cjs.cjs.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/index.js b/deps/npm/node_modules/lru-cache/dist/commonjs/index.js index bb8256253be5f8..6b8268b0ea9123 100644 --- a/deps/npm/node_modules/lru-cache/dist/commonjs/index.js +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/index.js @@ -5,12 +5,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.LRUCache = void 0; const diagnostics_channel_js_1 = require("./diagnostics-channel.js"); +const perf_js_1 = require("./perf.js"); const hasSubscribers = () => diagnostics_channel_js_1.metrics.hasSubscribers || diagnostics_channel_js_1.tracing.hasSubscribers; -const defaultPerf = (typeof performance === 'object' && - performance && - typeof performance.now === 'function') ? - performance - : Date; const warned = new Set(); /* c8 ignore start */ const PROCESS = (typeof process === 'object' && !!process ? @@ -52,7 +48,9 @@ class ZeroArray extends Array { } } class Stack { + /* c8 ignore start - not sure why this is showing up uncovered?? */ heap; + /* c8 ignore stop */ length; // private constructor static #constructing = false; @@ -172,6 +170,8 @@ class LRUCache { * {@link LRUCache.OptionsBase.ignoreFetchAbort} */ ignoreFetchAbort; + /** {@link LRUCache.OptionsBase.backgroundFetchSize} */ + backgroundFetchSize; // computed properties #size; #calculatedSize; @@ -282,13 +282,14 @@ class LRUCache { return this.#disposeAfter; } constructor(options) { - const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, perf, } = options; + const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options; + this.backgroundFetchSize = backgroundFetchSize; if (perf !== undefined) { if (typeof perf?.now !== 'function') { throw new TypeError('perf option must have a now() method if specified'); } } - this.#perf = perf ?? defaultPerf; + this.#perf = perf ?? perf_js_1.defaultPerf; if (max !== 0 && !isPosInt(max)) { throw new TypeError('max option must be a nonnegative integer'); } @@ -510,12 +511,15 @@ class LRUCache { sizes[index] = 0; }; this.#requireSize = (k, v, size, sizeCalculation) => { - // provisionally accept background fetches. - // actual value size will be checked when they return. - if (this.#isBackgroundFetch(v)) { - return 0; - } if (!isPosInt(size)) { + // provisionally accept background fetches. + // actual value size will be checked when they return. + if (this.#isBackgroundFetch(v)) { + // NB: this cannot occur if v.__staleWhileFetching is set, + // because in that case, it would take on the size of the + // existing entry that it temporarily replaces. + return this.backgroundFetchSize; + } if (sizeCalculation) { if (typeof sizeCalculation !== 'function') { throw new TypeError('sizeCalculation must be a function'); @@ -882,6 +886,7 @@ class LRUCache { status.key = k; if (v !== undefined) status.value = v; + status.cache = this; } const result = this.#set(k, v, setOptions); if (status && diagnostics_channel_js_1.metrics.hasSubscribers) { @@ -889,8 +894,9 @@ class LRUCache { } return result; } - #set(k, v, setOptions = {}) { + #set(k, v, setOptions, bf) { const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions; + const isBF = this.#isBackgroundFetch(v); if (v === undefined) { if (status) status.set = 'deleted'; @@ -898,7 +904,7 @@ class LRUCache { return this; } let { noUpdateTTL = this.noUpdateTTL } = setOptions; - if (status && !this.#isBackgroundFetch(v)) + if (status && !isBF) status.value = v; const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status); // if the item doesn't fit, don't do anything @@ -930,52 +936,68 @@ class LRUCache { if (status) status.set = 'add'; noUpdateTTL = false; - if (this.#hasOnInsert) { + if (this.#hasOnInsert && !isBF) { this.#onInsert?.(v, k, 'add'); } } else { // update + // might be updating a background fetch! this.#moveToTail(index); const oldVal = this.#valList[index]; if (v !== oldVal) { - if (this.#hasFetchMethod && this.#isBackgroundFetch(oldVal)) { - oldVal.__abortController.abort(new Error('replaced')); - const { __staleWhileFetching: s } = oldVal; - if (s !== undefined && !noDisposeOnSet) { + if (!noDisposeOnSet) { + if (this.#isBackgroundFetch(oldVal)) { + if (oldVal !== bf) { + // setting over a background fetch, not merely resolving it. + oldVal.__abortController.abort(new Error('replaced')); + } + const { __staleWhileFetching: s } = oldVal; + if (s !== undefined && s !== v) { + if (this.#hasDispose) { + this.#dispose?.(s, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([s, k, 'set']); + } + } + } + else { if (this.#hasDispose) { - this.#dispose?.(s, k, 'set'); + this.#dispose?.(oldVal, k, 'set'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([s, k, 'set']); + this.#disposed?.push([oldVal, k, 'set']); } } } - else if (!noDisposeOnSet) { - if (this.#hasDispose) { - this.#dispose?.(oldVal, k, 'set'); - } - if (this.#hasDisposeAfter) { - this.#disposed?.push([oldVal, k, 'set']); - } - } this.#removeItemSize(index); this.#addItemSize(index, size, status); this.#valList[index] = v; - if (status) { - status.set = 'replace'; + if (!isBF) { const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ? oldVal.__staleWhileFetching : oldVal; - if (oldValue !== undefined) - status.oldValue = oldValue; + const setType = oldValue === undefined ? 'add' + : v !== oldValue ? 'replace' + : 'update'; + if (status) { + status.set = setType; + if (oldValue !== undefined) + status.oldValue = oldValue; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, setType); + } } } - else if (status) { - status.set = 'update'; - } - if (this.#hasOnInsert) { - this.onInsert?.(v, k, v === oldVal ? 'update' : 'replace'); + else if (!isBF) { + if (status) { + status.set = 'update'; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, 'update'); + } } } if (ttl !== 0 && !this.#ttls) { @@ -1030,15 +1052,18 @@ class LRUCache { const head = this.#head; const k = this.#keyList[head]; const v = this.#valList[head]; - if (this.#hasFetchMethod && this.#isBackgroundFetch(v)) { + const isBF = this.#isBackgroundFetch(v); + if (isBF) { v.__abortController.abort(new Error('evicted')); } - else if (this.#hasDispose || this.#hasDisposeAfter) { + const oldValue = isBF ? v.__staleWhileFetching : v; + if ((this.#hasDispose || this.#hasDisposeAfter) && + oldValue !== undefined) { if (this.#hasDispose) { - this.#dispose?.(v, k, 'evict'); + this.#dispose?.(oldValue, k, 'evict'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([v, k, 'evict']); + this.#disposed?.push([oldValue, k, 'evict']); } } this.#removeItemSize(head); @@ -1085,6 +1110,7 @@ class LRUCache { if (status) { status.op = 'has'; status.key = k; + status.cache = this; } const result = this.#has(k, hasOptions); if (diagnostics_channel_js_1.metrics.hasSubscribers) @@ -1132,6 +1158,7 @@ class LRUCache { if (status) { status.op = 'peek'; status.key = k; + status.cache = this; } peekOptions.status = status; const result = this.#peek(k, peekOptions); @@ -1214,7 +1241,7 @@ class LRUCache { else { if (options.status) options.status.fetchUpdated = true; - this.#set(k, v, fetchOpts.options); + this.#set(k, v, fetchOpts.options, bf); } } return v; @@ -1260,9 +1287,6 @@ class LRUCache { }; const pcall = (res, rej) => { const fmp = this.#fetchMethod?.(k, v, fetchOpts); - if (fmp && fmp instanceof Promise) { - fmp.then(v => res(v === undefined ? undefined : v), rej); - } // ignored, we go until we finish, regardless. // defer check until we are actually aborting, // so fetchMethod can override. @@ -1275,6 +1299,12 @@ class LRUCache { } } }); + if (fmp && fmp instanceof Promise) { + fmp.then(v => res(v === undefined ? undefined : v), rej); + } + else if (fmp !== undefined) { + res(fmp); + } }; if (options.status) options.status.fetchDispatched = true; @@ -1290,6 +1320,10 @@ class LRUCache { index = this.#keyMap.get(k); } else { + // do not call #set, because we do not want to adjust its place + // in the lru queue, as it has not yet been "used". Also, we don't + // need to worry about evicting for size, because a background fetch + // over a stale value is treated as the same size as its stale value. this.#valList[index] = bf; } return bf; @@ -1311,11 +1345,9 @@ class LRUCache { status.context = fetchOptions.context; } const p = this.#fetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1332,6 +1364,7 @@ class LRUCache { status.key = k; if (forceRefresh) status.forceRefresh = true; + status.cache = this; } if (!this.#hasFetchMethod) { if (status) @@ -1413,11 +1446,9 @@ class LRUCache { status.context = fetchOptions.context; } const p = this.#forceFetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1436,6 +1467,7 @@ class LRUCache { if (memoOptions.context) { status.context = memoOptions.context; } + status.cache = this; } const result = this.#memo(k, memoOptions); if (status) @@ -1482,6 +1514,7 @@ class LRUCache { if (status) { status.op = 'get'; status.key = k; + status.cache = this; } const result = this.#get(k, getOptions); if (status) { @@ -1580,6 +1613,7 @@ class LRUCache { op: 'delete', delete: reason, key: k, + cache: this, }); } let deleted = false; @@ -1660,7 +1694,7 @@ class LRUCache { } } this.#keyMap.clear(); - this.#valList.fill(undefined); + void this.#valList.fill(undefined); this.#keyList.fill(undefined); if (this.#ttls && this.#starts) { this.#ttls.fill(0); diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/index.min.js b/deps/npm/node_modules/lru-cache/dist/commonjs/index.min.js index 383a09d043d362..8e42461074475a 100644 --- a/deps/npm/node_modules/lru-cache/dist/commonjs/index.min.js +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/index.min.js @@ -1,2 +1,2 @@ -"use strict";var G=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports);var M=G(O=>{"use strict";Object.defineProperty(O,"__esModule",{value:!0});O.tracing=O.metrics=void 0;var L=require("node:diagnostics_channel");O.metrics=(0,L.channel)("lru-cache:metrics");O.tracing=(0,L.tracingChannel)("lru-cache")});Object.defineProperty(exports,"__esModule",{value:!0});exports.LRUCache=void 0;var u=M(),D=()=>u.metrics.hasSubscribers||u.tracing.hasSubscribers,P=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,U=new Set,j=typeof process=="object"&&process?process:{},H=(c,t,e,i)=>{typeof j.emitWarning=="function"?j.emitWarning(c,t,e,i):console.error(`[${e}] ${t}: ${c}`)},N=c=>!U.has(c),B=Symbol("type"),F=c=>!!c&&c===Math.floor(c)&&c>0&&isFinite(c),I=c=>F(c)?c<=Math.pow(2,8)?Uint8Array:c<=Math.pow(2,16)?Uint16Array:c<=Math.pow(2,32)?Uint32Array:c<=Number.MAX_SAFE_INTEGER?W:null:null,W=class extends Array{constructor(t){super(t),this.fill(0)}},C=class c{heap;length;static#o=!1;static create(t){let e=I(t);if(!e)return[];c.#o=!0;let i=new c(t,e);return c.#o=!1,i}constructor(t,e){if(!c.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new e(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},x=class c{#o;#c;#m;#D;#w;#M;#j;#S;get perf(){return this.#S}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#n;#b;#s;#i;#t;#l;#u;#a;#h;#y;#r;#_;#F;#d;#g;#T;#O;#f;#U;static unsafeExposeInternals(t){return{starts:t.#F,ttls:t.#d,autopurgeTimers:t.#g,sizes:t.#_,keyMap:t.#s,keyList:t.#i,valList:t.#t,next:t.#l,prev:t.#u,get head(){return t.#a},get tail(){return t.#h},free:t.#y,isBackgroundFetch:e=>t.#e(e),backgroundFetch:(e,i,s,n)=>t.#P(e,i,s,n),moveToTail:e=>t.#L(e),indexes:e=>t.#A(e),rindexes:e=>t.#v(e),isStale:e=>t.#p(e)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#j}get dispose(){return this.#m}get onInsert(){return this.#D}get disposeAfter(){return this.#w}constructor(t){let{max:e=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:o,updateAgeOnHas:r,allowStale:h,dispose:a,onInsert:d,disposeAfter:f,noDisposeOnSet:p,noUpdateTTL:m,maxSize:T=0,maxEntrySize:w=0,sizeCalculation:y,fetchMethod:l,memoMethod:S,noDeleteOnFetchRejection:_,noDeleteOnStaleGet:b,allowStaleOnFetchRejection:g,allowStaleOnFetchAbort:A,ignoreFetchAbort:v,perf:R}=t;if(R!==void 0&&typeof R?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#S=R??P,e!==0&&!F(e))throw new TypeError("max option must be a nonnegative integer");let z=e?I(e):Array;if(!z)throw new Error("invalid max value: "+e);if(this.#o=e,this.#c=T,this.maxEntrySize=w||this.#c,this.sizeCalculation=y,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(S!==void 0&&typeof S!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#j=S,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#O=!!l,this.#s=new Map,this.#i=Array.from({length:e}).fill(void 0),this.#t=Array.from({length:e}).fill(void 0),this.#l=new z(e),this.#u=new z(e),this.#a=0,this.#h=0,this.#y=C.create(e),this.#n=0,this.#b=0,typeof a=="function"&&(this.#m=a),typeof d=="function"&&(this.#D=d),typeof f=="function"?(this.#w=f,this.#r=[]):(this.#w=void 0,this.#r=void 0),this.#T=!!this.#m,this.#U=!!this.#D,this.#f=!!this.#w,this.noDisposeOnSet=!!p,this.noUpdateTTL=!!m,this.noDeleteOnFetchRejection=!!_,this.allowStaleOnFetchRejection=!!g,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!v,this.maxEntrySize!==0){if(this.#c!==0&&!F(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!F(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!h,this.noDeleteOnStaleGet=!!b,this.updateAgeOnGet=!!o,this.updateAgeOnHas=!!r,this.ttlResolution=F(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!F(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#H()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let E="LRU_CACHE_UNBOUNDED";N(E)&&(U.add(E),H("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",E,c))}}getRemainingTTL(t){return this.#s.has(t)?1/0:0}#H(){let t=new W(this.#o),e=new W(this.#o);this.#d=t,this.#F=e;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#N=(r,h,a=this.#S.now())=>{e[r]=h!==0?a:0,t[r]=h,s(r,h)},this.#R=r=>{e[r]=t[r]!==0?this.#S.now():0,s(r,t[r])};let s=this.ttlAutopurge?(r,h)=>{if(i?.[r]&&(clearTimeout(i[r]),i[r]=void 0),h&&h!==0&&i){let a=setTimeout(()=>{this.#p(r)&&this.#z(this.#i[r],"expire")},h+1);a.unref&&a.unref(),i[r]=a}}:()=>{};this.#E=(r,h)=>{if(t[h]){let a=t[h],d=e[h];if(!a||!d)return;r.ttl=a,r.start=d,r.now=n||o();let f=r.now-d;r.remainingTTL=a-f}};let n=0,o=()=>{let r=this.#S.now();if(this.ttlResolution>0){n=r;let h=setTimeout(()=>n=0,this.ttlResolution);h.unref&&h.unref()}return r};this.getRemainingTTL=r=>{let h=this.#s.get(r);if(h===void 0)return 0;let a=t[h],d=e[h];if(!a||!d)return 1/0;let f=(n||o())-d;return a-f},this.#p=r=>{let h=e[r],a=t[r];return!!a&&!!h&&(n||o())-h>a}}#R=()=>{};#E=()=>{};#N=()=>{};#p=()=>!1;#X(){let t=new W(this.#o);this.#b=0,this.#_=t,this.#C=e=>{this.#b-=t[e],t[e]=0},this.#k=(e,i,s,n)=>{if(this.#e(i))return 0;if(!F(s))if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,e),!F(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");return s},this.#I=(e,i,s)=>{if(t[e]=i,this.#c){let n=this.#c-t[e];for(;this.#b>n;)this.#G(!0)}this.#b+=t[e],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#C=t=>{};#I=(t,e,i)=>{};#k=(t,e,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#h;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#a);)e=this.#u[e]}*#v({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#a;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#h);)e=this.#l[e]}#V(t){return t!==void 0&&this.#s.get(this.#i[t])===t}*entries(){for(let t of this.#A())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*rentries(){for(let t of this.#v())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*keys(){for(let t of this.#A()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*rkeys(){for(let t of this.#v()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*values(){for(let t of this.#A())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}*rvalues(){for(let t of this.#v())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(t,e={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&t(n,this.#i[i],this))return this.#x(this.#i[i],e)}}forEach(t,e=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}rforEach(t,e=this){for(let i of this.#v()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}purgeStale(){let t=!1;for(let e of this.#v({allowStale:!0}))this.#p(e)&&(this.#z(this.#i[e],"expire"),t=!0);return t}info(t){let e=this.#s.get(t);if(e===void 0)return;let i=this.#t[e],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let o=this.#d[e],r=this.#F[e];if(o&&r){let h=o-(this.#S.now()-r);n.ttl=h,n.start=Date.now()}}return this.#_&&(n.size=this.#_[e]),n}dump(){let t=[];for(let e of this.#A({allowStale:!0})){let i=this.#i[e],s=this.#t[e],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let o={value:n};if(this.#d&&this.#F){o.ttl=this.#d[e];let r=this.#S.now()-this.#F[e];o.start=Math.floor(Date.now()-r)}this.#_&&(o.size=this.#_[e]),t.unshift([i,o])}return t}load(t){this.clear();for(let[e,i]of t){if(i.start){let s=Date.now()-i.start;i.start=this.#S.now()-s}this.#W(e,i.value,i)}}set(t,e,i={}){let{status:s=u.metrics.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=t,e!==void 0&&(s.value=e));let n=this.#W(t,e,i);return s&&u.metrics.hasSubscribers&&u.metrics.publish(s),n}#W(t,e,i={}){let{ttl:s=this.ttl,start:n,noDisposeOnSet:o=this.noDisposeOnSet,sizeCalculation:r=this.sizeCalculation,status:h}=i;if(e===void 0)return h&&(h.set="deleted"),this.delete(t),this;let{noUpdateTTL:a=this.noUpdateTTL}=i;h&&!this.#e(e)&&(h.value=e);let d=this.#k(t,e,i.size||0,r,h);if(this.maxEntrySize&&d>this.maxEntrySize)return this.#z(t,"set"),h&&(h.set="miss",h.maxEntrySizeExceeded=!0),this;let f=this.#n===0?void 0:this.#s.get(t);if(f===void 0)f=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[f]=t,this.#t[f]=e,this.#s.set(t,f),this.#l[this.#h]=f,this.#u[f]=this.#h,this.#h=f,this.#n++,this.#I(f,d,h),h&&(h.set="add"),a=!1,this.#U&&this.#D?.(e,t,"add");else{this.#L(f);let p=this.#t[f];if(e!==p){if(this.#O&&this.#e(p)){p.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:m}=p;m!==void 0&&!o&&(this.#T&&this.#m?.(m,t,"set"),this.#f&&this.#r?.push([m,t,"set"]))}else o||(this.#T&&this.#m?.(p,t,"set"),this.#f&&this.#r?.push([p,t,"set"]));if(this.#C(f),this.#I(f,d,h),this.#t[f]=e,h){h.set="replace";let m=p&&this.#e(p)?p.__staleWhileFetching:p;m!==void 0&&(h.oldValue=m)}}else h&&(h.set="update");this.#U&&this.onInsert?.(e,t,e===p?"update":"replace")}if(s!==0&&!this.#d&&this.#H(),this.#d&&(a||this.#N(f,s,n),h&&this.#E(h,f)),!o&&this.#f&&this.#r){let p=this.#r,m;for(;m=p?.shift();)this.#w?.(...m)}return this}pop(){try{for(;this.#n;){let t=this.#t[this.#a];if(this.#G(!0),this.#e(t)){if(t.__staleWhileFetching)return t.__staleWhileFetching}else if(t!==void 0)return t}}finally{if(this.#f&&this.#r){let t=this.#r,e;for(;e=t?.shift();)this.#w?.(...e)}}}#G(t){let e=this.#a,i=this.#i[e],s=this.#t[e];return this.#O&&this.#e(s)?s.__abortController.abort(new Error("evicted")):(this.#T||this.#f)&&(this.#T&&this.#m?.(s,i,"evict"),this.#f&&this.#r?.push([s,i,"evict"])),this.#C(e),this.#g?.[e]&&(clearTimeout(this.#g[e]),this.#g[e]=void 0),t&&(this.#i[e]=void 0,this.#t[e]=void 0,this.#y.push(e)),this.#n===1?(this.#a=this.#h=0,this.#y.length=0):this.#a=this.#l[e],this.#s.delete(i),this.#n--,e}has(t,e={}){let{status:i=u.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="has",i.key=t);let s=this.#Y(t,e);return u.metrics.hasSubscribers&&u.metrics.publish(i),s}#Y(t,e={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=e,n=this.#s.get(t);if(n!==void 0){let o=this.#t[n];if(this.#e(o)&&o.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#R(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(t,e={}){let{status:i=D()?{}:void 0}=e;i&&(i.op="peek",i.key=t),e.status=i;let s=this.#J(t,e);return u.metrics.hasSubscribers&&u.metrics.publish(i),s}#J(t,e){let{status:i,allowStale:s=this.allowStale}=e,n=this.#s.get(t);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let o=this.#t[n],r=this.#e(o)?o.__staleWhileFetching:o;return i&&(r!==void 0?(i.peek="hit",i.value=r):i.peek="miss"),r}#P(t,e,i,s){let n=e===void 0?void 0:this.#t[e];if(this.#e(n))return n;let o=new AbortController,{signal:r}=i;r?.addEventListener("abort",()=>o.abort(r.reason),{signal:o.signal});let h={signal:o.signal,options:i,context:s},a=(w,y=!1)=>{let{aborted:l}=o.signal,S=i.ignoreFetchAbort&&w!==void 0,_=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&w!==void 0);if(i.status&&(l&&!y?(i.status.fetchAborted=!0,i.status.fetchError=o.signal.reason,S&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!S&&!y)return f(o.signal.reason,_);let b=m,g=this.#t[e];return(g===m||g===void 0&&S&&y)&&(w===void 0?b.__staleWhileFetching!==void 0?this.#t[e]=b.__staleWhileFetching:this.#z(t,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#W(t,w,h.options))),w},d=w=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=w),f(w,!1)),f=(w,y)=>{let{aborted:l}=o.signal,S=l&&i.allowStaleOnFetchAbort,_=S||i.allowStaleOnFetchRejection,b=_||i.noDeleteOnFetchRejection,g=m;if(this.#t[e]===m&&(!b||!y&&g.__staleWhileFetching===void 0?this.#z(t,"fetch"):S||(this.#t[e]=g.__staleWhileFetching)),_)return i.status&&g.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),g.__staleWhileFetching;if(g.__returned===g)throw w},p=(w,y)=>{let l=this.#M?.(t,n,h);l&&l instanceof Promise&&l.then(S=>w(S===void 0?void 0:S),y),o.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(w(void 0),i.allowStaleOnFetchAbort&&(w=S=>a(S,!0)))})};i.status&&(i.status.fetchDispatched=!0);let m=new Promise(p).then(a,d),T=Object.assign(m,{__abortController:o,__staleWhileFetching:n,__returned:void 0});return e===void 0?(this.#W(t,T,{...h.options,status:void 0}),e=this.#s.get(t)):this.#t[e]=T,T}#e(t){if(!this.#O)return!1;let e=t;return!!e&&e instanceof Promise&&e.hasOwnProperty("__staleWhileFetching")&&e.__abortController instanceof AbortController}fetch(t,e={}){let i=u.tracing.hasSubscribers,{status:s=D()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#q(t,e);return s&&D()&&i&&(s.trace=!0,u.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#q(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:o=this.ttl,noDisposeOnSet:r=this.noDisposeOnSet,size:h=0,sizeCalculation:a=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:f=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:p=this.allowStaleOnFetchRejection,ignoreFetchAbort:m=this.ignoreFetchAbort,allowStaleOnFetchAbort:T=this.allowStaleOnFetchAbort,context:w,forceRefresh:y=!1,status:l,signal:S}=e;if(l&&(l.op="fetch",l.key=t,y&&(l.forceRefresh=!0)),!this.#O)return l&&(l.fetch="get"),this.#x(t,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let _={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:o,noDisposeOnSet:r,size:h,sizeCalculation:a,noUpdateTTL:d,noDeleteOnFetchRejection:f,allowStaleOnFetchRejection:p,allowStaleOnFetchAbort:T,ignoreFetchAbort:m,status:l,signal:S},b=this.#s.get(t);if(b===void 0){l&&(l.fetch="miss");let g=this.#P(t,b,_,w);return g.__returned=g}else{let g=this.#t[b];if(this.#e(g)){let E=i&&g.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",E&&(l.returnedStale=!0)),E?g.__staleWhileFetching:g.__returned=g}let A=this.#p(b);if(!y&&!A)return l&&(l.fetch="hit"),this.#L(b),s&&this.#R(b),l&&this.#E(l,b),g;let v=this.#P(t,b,_,w),z=v.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=A?"stale":"refresh",z&&A&&(l.returnedStale=!0)),z?v.__staleWhileFetching:v.__returned=v}}forceFetch(t,e={}){let i=u.tracing.hasSubscribers,{status:s=D()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#K(t,e);return s&&D()&&i&&(s.trace=!0,u.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#K(t,e={}){let i=await this.#q(t,e);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(t,e={}){let{status:i=u.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="memo",i.key=t,e.context&&(i.context=e.context));let s=this.#Q(t,e);return i&&(i.value=s),u.metrics.hasSubscribers&&u.metrics.publish(i),s}#Q(t,e={}){let i=this.#j;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:o,...r}=e;n&&o&&(n.forceRefresh=!0);let h=this.#x(t,r),a=o||h===void 0;if(n&&(n.memo=a?"miss":"hit",a||(n.value=h)),!a)return h;let d=i(t,h,{options:r,context:s});return n&&(n.value=d),this.#W(t,d,r),d}get(t,e={}){let{status:i=u.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="get",i.key=t);let s=this.#x(t,e);return i&&(s!==void 0&&(i.value=s),u.metrics.hasSubscribers&&u.metrics.publish(i)),s}#x(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:o}=e,r=this.#s.get(t);if(r===void 0){o&&(o.get="miss");return}let h=this.#t[r],a=this.#e(h);return o&&this.#E(o,r),this.#p(r)?a?(o&&(o.get="stale-fetching"),i&&h.__staleWhileFetching!==void 0?(o&&(o.returnedStale=!0),h.__staleWhileFetching):void 0):(n||this.#z(t,"expire"),o&&(o.get="stale"),i?(o&&(o.returnedStale=!0),h):void 0):(o&&(o.get=a?"fetching":"hit"),this.#L(r),s&&this.#R(r),a?h.__staleWhileFetching:h)}#B(t,e){this.#u[e]=t,this.#l[t]=e}#L(t){t!==this.#h&&(t===this.#a?this.#a=this.#l[t]:this.#B(this.#u[t],this.#l[t]),this.#B(this.#h,t),this.#h=t)}delete(t){return this.#z(t,"delete")}#z(t,e){u.metrics.hasSubscribers&&u.metrics.publish({op:"delete",delete:e,key:t});let i=!1;if(this.#n!==0){let s=this.#s.get(t);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#$(e);else{this.#C(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#m?.(n,t,e),this.#f&&this.#r?.push([n,t,e])),this.#s.delete(t),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let o=this.#u[s];this.#l[o]=this.#l[s];let r=this.#l[s];this.#u[r]=this.#u[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#w?.(...n)}return i}clear(){return this.#$("delete")}#$(t){for(let e of this.#v({allowStale:!0})){let i=this.#t[e];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[e];this.#T&&this.#m?.(i,s,t),this.#f&&this.#r?.push([i,s,t])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let e of this.#g??[])e!==void 0&&clearTimeout(e);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#a=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let e=this.#r,i;for(;i=e?.shift();)this.#w?.(...i)}}};exports.LRUCache=x; +"use strict";var j=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports);var I=j(O=>{"use strict";Object.defineProperty(O,"__esModule",{value:!0});O.tracing=O.metrics=void 0;var U={hasSubscribers:!1};O.metrics=U;O.tracing=U});var P=j(D=>{"use strict";Object.defineProperty(D,"__esModule",{value:!0});D.defaultPerf=void 0;D.defaultPerf=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date});Object.defineProperty(exports,"__esModule",{value:!0});exports.LRUCache=void 0;var g=I(),N=P(),x=()=>g.metrics.hasSubscribers||g.tracing.hasSubscribers,k=new Set,G=typeof process=="object"&&process?process:{},V=(c,t,e,i)=>{typeof G.emitWarning=="function"?G.emitWarning(c,t,e,i):console.error(`[${e}] ${t}: ${c}`)},B=c=>!k.has(c);var T=c=>!!c&&c===Math.floor(c)&&c>0&&isFinite(c),H=c=>T(c)?c<=Math.pow(2,8)?Uint8Array:c<=Math.pow(2,16)?Uint16Array:c<=Math.pow(2,32)?Uint32Array:c<=Number.MAX_SAFE_INTEGER?W:null:null,W=class extends Array{constructor(t){super(t),this.fill(0)}},C=class c{heap;length;static#o=!1;static create(t){let e=H(t);if(!e)return[];c.#o=!0;let i=new c(t,e);return c.#o=!1,i}constructor(t,e){if(!c.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new e(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},L=class c{#o;#c;#m;#W;#S;#M;#j;#w;get perf(){return this.#w}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;backgroundFetchSize;#n;#b;#s;#i;#t;#l;#u;#a;#h;#y;#r;#_;#F;#d;#g;#T;#U;#f;#D;static unsafeExposeInternals(t){return{starts:t.#F,ttls:t.#d,autopurgeTimers:t.#g,sizes:t.#_,keyMap:t.#s,keyList:t.#i,valList:t.#t,next:t.#l,prev:t.#u,get head(){return t.#a},get tail(){return t.#h},free:t.#y,isBackgroundFetch:e=>t.#e(e),backgroundFetch:(e,i,s,n)=>t.#G(e,i,s,n),moveToTail:e=>t.#L(e),indexes:e=>t.#A(e),rindexes:e=>t.#z(e),isStale:e=>t.#p(e)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#j}get dispose(){return this.#m}get onInsert(){return this.#W}get disposeAfter(){return this.#S}constructor(t){let{max:e=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:r,updateAgeOnHas:h,allowStale:a,dispose:o,onInsert:d,disposeAfter:y,noDisposeOnSet:_,noUpdateTTL:u,maxSize:p=0,maxEntrySize:f=0,sizeCalculation:b,fetchMethod:l,memoMethod:S,noDeleteOnFetchRejection:F,noDeleteOnStaleGet:w,allowStaleOnFetchRejection:m,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,backgroundFetchSize:M=1,perf:v}=t;if(this.backgroundFetchSize=M,v!==void 0&&typeof v?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#w=v??N.defaultPerf,e!==0&&!T(e))throw new TypeError("max option must be a nonnegative integer");let E=e?H(e):Array;if(!E)throw new Error("invalid max value: "+e);if(this.#o=e,this.#c=p,this.maxEntrySize=f||this.#c,this.sizeCalculation=b,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(S!==void 0&&typeof S!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#j=S,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#U=!!l,this.#s=new Map,this.#i=Array.from({length:e}).fill(void 0),this.#t=Array.from({length:e}).fill(void 0),this.#l=new E(e),this.#u=new E(e),this.#a=0,this.#h=0,this.#y=C.create(e),this.#n=0,this.#b=0,typeof o=="function"&&(this.#m=o),typeof d=="function"&&(this.#W=d),typeof y=="function"?(this.#S=y,this.#r=[]):(this.#S=void 0,this.#r=void 0),this.#T=!!this.#m,this.#D=!!this.#W,this.#f=!!this.#S,this.noDisposeOnSet=!!_,this.noUpdateTTL=!!u,this.noDeleteOnFetchRejection=!!F,this.allowStaleOnFetchRejection=!!m,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#c!==0&&!T(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!T(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!a,this.noDeleteOnStaleGet=!!w,this.updateAgeOnGet=!!r,this.updateAgeOnHas=!!h,this.ttlResolution=T(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!T(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let R="LRU_CACHE_UNBOUNDED";B(R)&&(k.add(R),V("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",R,c))}}getRemainingTTL(t){return this.#s.has(t)?1/0:0}#k(){let t=new W(this.#o),e=new W(this.#o);this.#d=t,this.#F=e;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#H=(h,a,o=this.#w.now())=>{e[h]=a!==0?o:0,t[h]=a,s(h,a)},this.#R=h=>{e[h]=t[h]!==0?this.#w.now():0,s(h,t[h])};let s=this.ttlAutopurge?(h,a)=>{if(i?.[h]&&(clearTimeout(i[h]),i[h]=void 0),a&&a!==0&&i){let o=setTimeout(()=>{this.#p(h)&&this.#v(this.#i[h],"expire")},a+1);o.unref&&o.unref(),i[h]=o}}:()=>{};this.#E=(h,a)=>{if(t[a]){let o=t[a],d=e[a];if(!o||!d)return;h.ttl=o,h.start=d,h.now=n||r();let y=h.now-d;h.remainingTTL=o-y}};let n=0,r=()=>{let h=this.#w.now();if(this.ttlResolution>0){n=h;let a=setTimeout(()=>n=0,this.ttlResolution);a.unref&&a.unref()}return h};this.getRemainingTTL=h=>{let a=this.#s.get(h);if(a===void 0)return 0;let o=t[a],d=e[a];if(!o||!d)return 1/0;let y=(n||r())-d;return o-y},this.#p=h=>{let a=e[h],o=t[h];return!!o&&!!a&&(n||r())-a>o}}#R=()=>{};#E=()=>{};#H=()=>{};#p=()=>!1;#X(){let t=new W(this.#o);this.#b=0,this.#_=t,this.#x=e=>{this.#b-=t[e],t[e]=0},this.#N=(e,i,s,n)=>{if(!T(s)){if(this.#e(i))return this.backgroundFetchSize;if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,e),!T(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.")}return s},this.#I=(e,i,s)=>{if(t[e]=i,this.#c){let n=this.#c-t[e];for(;this.#b>n;)this.#P(!0)}this.#b+=t[e],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#x=t=>{};#I=(t,e,i)=>{};#N=(t,e,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#h;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#a);)e=this.#u[e]}*#z({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#a;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#h);)e=this.#l[e]}#V(t){return t!==void 0&&this.#s.get(this.#i[t])===t}*entries(){for(let t of this.#A())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*rentries(){for(let t of this.#z())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*keys(){for(let t of this.#A()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*rkeys(){for(let t of this.#z()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*values(){for(let t of this.#A())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}*rvalues(){for(let t of this.#z())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(t,e={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&t(n,this.#i[i],this))return this.#C(this.#i[i],e)}}forEach(t,e=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}rforEach(t,e=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}purgeStale(){let t=!1;for(let e of this.#z({allowStale:!0}))this.#p(e)&&(this.#v(this.#i[e],"expire"),t=!0);return t}info(t){let e=this.#s.get(t);if(e===void 0)return;let i=this.#t[e],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let r=this.#d[e],h=this.#F[e];if(r&&h){let a=r-(this.#w.now()-h);n.ttl=a,n.start=Date.now()}}return this.#_&&(n.size=this.#_[e]),n}dump(){let t=[];for(let e of this.#A({allowStale:!0})){let i=this.#i[e],s=this.#t[e],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let r={value:n};if(this.#d&&this.#F){r.ttl=this.#d[e];let h=this.#w.now()-this.#F[e];r.start=Math.floor(Date.now()-h)}this.#_&&(r.size=this.#_[e]),t.unshift([i,r])}return t}load(t){this.clear();for(let[e,i]of t){if(i.start){let s=Date.now()-i.start;i.start=this.#w.now()-s}this.#O(e,i.value,i)}}set(t,e,i={}){let{status:s=g.metrics.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=t,e!==void 0&&(s.value=e),s.cache=this);let n=this.#O(t,e,i);return s&&g.metrics.hasSubscribers&&g.metrics.publish(s),n}#O(t,e,i,s){let{ttl:n=this.ttl,start:r,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:o}=i,d=this.#e(e);if(e===void 0)return o&&(o.set="deleted"),this.delete(t),this;let{noUpdateTTL:y=this.noUpdateTTL}=i;o&&!d&&(o.value=e);let _=this.#N(t,e,i.size||0,a,o);if(this.maxEntrySize&&_>this.maxEntrySize)return this.#v(t,"set"),o&&(o.set="miss",o.maxEntrySizeExceeded=!0),this;let u=this.#n===0?void 0:this.#s.get(t);if(u===void 0)u=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#P(!1):this.#n,this.#i[u]=t,this.#t[u]=e,this.#s.set(t,u),this.#l[this.#h]=u,this.#u[u]=this.#h,this.#h=u,this.#n++,this.#I(u,_,o),o&&(o.set="add"),y=!1,this.#D&&!d&&this.#W?.(e,t,"add");else{this.#L(u);let p=this.#t[u];if(e!==p){if(!h)if(this.#e(p)){p!==s&&p.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:f}=p;f!==void 0&&f!==e&&(this.#T&&this.#m?.(f,t,"set"),this.#f&&this.#r?.push([f,t,"set"]))}else this.#T&&this.#m?.(p,t,"set"),this.#f&&this.#r?.push([p,t,"set"]);if(this.#x(u),this.#I(u,_,o),this.#t[u]=e,!d){let f=p&&this.#e(p)?p.__staleWhileFetching:p,b=f===void 0?"add":e!==f?"replace":"update";o&&(o.set=b,f!==void 0&&(o.oldValue=f)),this.#D&&this.onInsert?.(e,t,b)}}else d||(o&&(o.set="update"),this.#D&&this.onInsert?.(e,t,"update"))}if(n!==0&&!this.#d&&this.#k(),this.#d&&(y||this.#H(u,n,r),o&&this.#E(o,u)),!h&&this.#f&&this.#r){let p=this.#r,f;for(;f=p?.shift();)this.#S?.(...f)}return this}pop(){try{for(;this.#n;){let t=this.#t[this.#a];if(this.#P(!0),this.#e(t)){if(t.__staleWhileFetching)return t.__staleWhileFetching}else if(t!==void 0)return t}}finally{if(this.#f&&this.#r){let t=this.#r,e;for(;e=t?.shift();)this.#S?.(...e)}}}#P(t){let e=this.#a,i=this.#i[e],s=this.#t[e],n=this.#e(s);n&&s.__abortController.abort(new Error("evicted"));let r=n?s.__staleWhileFetching:s;return(this.#T||this.#f)&&r!==void 0&&(this.#T&&this.#m?.(r,i,"evict"),this.#f&&this.#r?.push([r,i,"evict"])),this.#x(e),this.#g?.[e]&&(clearTimeout(this.#g[e]),this.#g[e]=void 0),t&&(this.#i[e]=void 0,this.#t[e]=void 0,this.#y.push(e)),this.#n===1?(this.#a=this.#h=0,this.#y.length=0):this.#a=this.#l[e],this.#s.delete(i),this.#n--,e}has(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="has",i.key=t,i.cache=this);let s=this.#Y(t,e);return g.metrics.hasSubscribers&&g.metrics.publish(i),s}#Y(t,e={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=e,n=this.#s.get(t);if(n!==void 0){let r=this.#t[n];if(this.#e(r)&&r.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#R(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(t,e={}){let{status:i=x()?{}:void 0}=e;i&&(i.op="peek",i.key=t,i.cache=this),e.status=i;let s=this.#J(t,e);return g.metrics.hasSubscribers&&g.metrics.publish(i),s}#J(t,e){let{status:i,allowStale:s=this.allowStale}=e,n=this.#s.get(t);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let r=this.#t[n],h=this.#e(r)?r.__staleWhileFetching:r;return i&&(h!==void 0?(i.peek="hit",i.value=h):i.peek="miss"),h}#G(t,e,i,s){let n=e===void 0?void 0:this.#t[e];if(this.#e(n))return n;let r=new AbortController,{signal:h}=i;h?.addEventListener("abort",()=>r.abort(h.reason),{signal:r.signal});let a={signal:r.signal,options:i,context:s},o=(f,b=!1)=>{let{aborted:l}=r.signal,S=i.ignoreFetchAbort&&f!==void 0,F=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&f!==void 0);if(i.status&&(l&&!b?(i.status.fetchAborted=!0,i.status.fetchError=r.signal.reason,S&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!S&&!b)return y(r.signal.reason,F);let w=u,m=this.#t[e];return(m===u||m===void 0&&S&&b)&&(f===void 0?w.__staleWhileFetching!==void 0?this.#t[e]=w.__staleWhileFetching:this.#v(t,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#O(t,f,a.options,w))),f},d=f=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=f),y(f,!1)),y=(f,b)=>{let{aborted:l}=r.signal,S=l&&i.allowStaleOnFetchAbort,F=S||i.allowStaleOnFetchRejection,w=F||i.noDeleteOnFetchRejection,m=u;if(this.#t[e]===u&&(!w||!b&&m.__staleWhileFetching===void 0?this.#v(t,"fetch"):S||(this.#t[e]=m.__staleWhileFetching)),F)return i.status&&m.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),m.__staleWhileFetching;if(m.__returned===m)throw f},_=(f,b)=>{let l=this.#M?.(t,n,a);r.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(f(void 0),i.allowStaleOnFetchAbort&&(f=S=>o(S,!0)))}),l&&l instanceof Promise?l.then(S=>f(S===void 0?void 0:S),b):l!==void 0&&f(l)};i.status&&(i.status.fetchDispatched=!0);let u=new Promise(_).then(o,d),p=Object.assign(u,{__abortController:r,__staleWhileFetching:n,__returned:void 0});return e===void 0?(this.#O(t,p,{...a.options,status:void 0}),e=this.#s.get(t)):this.#t[e]=p,p}#e(t){if(!this.#U)return!1;let e=t;return!!e&&e instanceof Promise&&e.hasOwnProperty("__staleWhileFetching")&&e.__abortController instanceof AbortController}fetch(t,e={}){let i=g.tracing.hasSubscribers,{status:s=x()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#B(t,e);return s&&i&&(s.trace=!0,g.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#B(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:r=this.ttl,noDisposeOnSet:h=this.noDisposeOnSet,size:a=0,sizeCalculation:o=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:y=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:_=this.allowStaleOnFetchRejection,ignoreFetchAbort:u=this.ignoreFetchAbort,allowStaleOnFetchAbort:p=this.allowStaleOnFetchAbort,context:f,forceRefresh:b=!1,status:l,signal:S}=e;if(l&&(l.op="fetch",l.key=t,b&&(l.forceRefresh=!0),l.cache=this),!this.#U)return l&&(l.fetch="get"),this.#C(t,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let F={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:r,noDisposeOnSet:h,size:a,sizeCalculation:o,noUpdateTTL:d,noDeleteOnFetchRejection:y,allowStaleOnFetchRejection:_,allowStaleOnFetchAbort:p,ignoreFetchAbort:u,status:l,signal:S},w=this.#s.get(t);if(w===void 0){l&&(l.fetch="miss");let m=this.#G(t,w,F,f);return m.__returned=m}else{let m=this.#t[w];if(this.#e(m)){let E=i&&m.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",E&&(l.returnedStale=!0)),E?m.__staleWhileFetching:m.__returned=m}let A=this.#p(w);if(!b&&!A)return l&&(l.fetch="hit"),this.#L(w),s&&this.#R(w),l&&this.#E(l,w),m;let z=this.#G(t,w,F,f),v=z.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=A?"stale":"refresh",v&&A&&(l.returnedStale=!0)),v?z.__staleWhileFetching:z.__returned=z}}forceFetch(t,e={}){let i=g.tracing.hasSubscribers,{status:s=x()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#K(t,e);return s&&i&&(s.trace=!0,g.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#K(t,e={}){let i=await this.#B(t,e);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="memo",i.key=t,e.context&&(i.context=e.context),i.cache=this);let s=this.#Q(t,e);return i&&(i.value=s),g.metrics.hasSubscribers&&g.metrics.publish(i),s}#Q(t,e={}){let i=this.#j;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:r,...h}=e;n&&r&&(n.forceRefresh=!0);let a=this.#C(t,h),o=r||a===void 0;if(n&&(n.memo=o?"miss":"hit",o||(n.value=a)),!o)return a;let d=i(t,a,{options:h,context:s});return n&&(n.value=d),this.#O(t,d,h),d}get(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="get",i.key=t,i.cache=this);let s=this.#C(t,e);return i&&(s!==void 0&&(i.value=s),g.metrics.hasSubscribers&&g.metrics.publish(i)),s}#C(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:r}=e,h=this.#s.get(t);if(h===void 0){r&&(r.get="miss");return}let a=this.#t[h],o=this.#e(a);return r&&this.#E(r,h),this.#p(h)?o?(r&&(r.get="stale-fetching"),i&&a.__staleWhileFetching!==void 0?(r&&(r.returnedStale=!0),a.__staleWhileFetching):void 0):(n||this.#v(t,"expire"),r&&(r.get="stale"),i?(r&&(r.returnedStale=!0),a):void 0):(r&&(r.get=o?"fetching":"hit"),this.#L(h),s&&this.#R(h),o?a.__staleWhileFetching:a)}#q(t,e){this.#u[e]=t,this.#l[t]=e}#L(t){t!==this.#h&&(t===this.#a?this.#a=this.#l[t]:this.#q(this.#u[t],this.#l[t]),this.#q(this.#h,t),this.#h=t)}delete(t){return this.#v(t,"delete")}#v(t,e){g.metrics.hasSubscribers&&g.metrics.publish({op:"delete",delete:e,key:t,cache:this});let i=!1;if(this.#n!==0){let s=this.#s.get(t);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#$(e);else{this.#x(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#m?.(n,t,e),this.#f&&this.#r?.push([n,t,e])),this.#s.delete(t),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let r=this.#u[s];this.#l[r]=this.#l[s];let h=this.#l[s];this.#u[h]=this.#u[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#S?.(...n)}return i}clear(){return this.#$("delete")}#$(t){for(let e of this.#z({allowStale:!0})){let i=this.#t[e];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[e];this.#T&&this.#m?.(i,s,t),this.#f&&this.#r?.push([i,s,t])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let e of this.#g??[])e!==void 0&&clearTimeout(e);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#a=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let e=this.#r,i;for(;i=e?.shift();)this.#S?.(...i)}}};exports.LRUCache=L; //# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js b/deps/npm/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js new file mode 100644 index 00000000000000..30f39a05a5630c --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tracing = exports.metrics = void 0; +// simple node version that imports from node builtin +// this is built to both ESM and CommonJS on the 'node' import path +const node_diagnostics_channel_1 = require("node:diagnostics_channel"); +exports.metrics = (0, node_diagnostics_channel_1.channel)('lru-cache:metrics'); +exports.tracing = (0, node_diagnostics_channel_1.tracingChannel)('lru-cache'); +//# sourceMappingURL=diagnostics-channel-node.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/node/index.js b/deps/npm/node_modules/lru-cache/dist/commonjs/node/index.js new file mode 100644 index 00000000000000..6b8268b0ea9123 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/node/index.js @@ -0,0 +1,1726 @@ +"use strict"; +/** + * @module LRUCache + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LRUCache = void 0; +const diagnostics_channel_js_1 = require("./diagnostics-channel.js"); +const perf_js_1 = require("./perf.js"); +const hasSubscribers = () => diagnostics_channel_js_1.metrics.hasSubscribers || diagnostics_channel_js_1.tracing.hasSubscribers; +const warned = new Set(); +/* c8 ignore start */ +const PROCESS = (typeof process === 'object' && !!process ? + process + : {}); +/* c8 ignore stop */ +const emitWarning = (msg, type, code, fn) => { + if (typeof PROCESS.emitWarning === 'function') { + PROCESS.emitWarning(msg, type, code, fn); + } + else { + //oxlint-disable-next-line no-console + console.error(`[${code}] ${type}: ${msg}`); + } +}; +const shouldWarn = (code) => !warned.has(code); +const TYPE = Symbol('type'); +const isPosInt = (n) => !!n && n === Math.floor(n) && n > 0 && isFinite(n); +// This is a little bit ridiculous, tbh. +// The maximum array length is 2^32-1 or thereabouts on most JS impls. +// And well before that point, you're caching the entire world, I mean, +// that's ~32GB of just integers for the next/prev links, plus whatever +// else to hold that many keys and values. Just filling the memory with +// zeroes at init time is brutal when you get that big. +// But why not be complete? +// Maybe in the future, these limits will have expanded. +/* c8 ignore start */ +const getUintArray = (max) => !isPosInt(max) ? null + : max <= Math.pow(2, 8) ? Uint8Array + : max <= Math.pow(2, 16) ? Uint16Array + : max <= Math.pow(2, 32) ? Uint32Array + : max <= Number.MAX_SAFE_INTEGER ? ZeroArray + : null; +/* c8 ignore stop */ +class ZeroArray extends Array { + constructor(size) { + super(size); + this.fill(0); + } +} +class Stack { + /* c8 ignore start - not sure why this is showing up uncovered?? */ + heap; + /* c8 ignore stop */ + length; + // private constructor + static #constructing = false; + static create(max) { + const HeapCls = getUintArray(max); + if (!HeapCls) + return []; + Stack.#constructing = true; + const s = new Stack(max, HeapCls); + Stack.#constructing = false; + return s; + } + constructor(max, HeapCls) { + /* c8 ignore start */ + if (!Stack.#constructing) { + throw new TypeError('instantiate Stack using Stack.create(n)'); + } + /* c8 ignore stop */ + this.heap = new HeapCls(max); + this.length = 0; + } + push(n) { + this.heap[this.length++] = n; + } + pop() { + return this.heap[--this.length]; + } +} +/** + * Default export, the thing you're using this module to get. + * + * The `K` and `V` types define the key and value types, respectively. The + * optional `FC` type defines the type of the `context` object passed to + * `cache.fetch()` and `cache.memo()`. + * + * Keys and values **must not** be `null` or `undefined`. + * + * All properties from the options object (with the exception of `max`, + * `maxSize`, `fetchMethod`, `memoMethod`, `dispose` and `disposeAfter`) are + * added as normal public members. (The listed options are read-only getters.) + * + * Changing any of these will alter the defaults for subsequent method calls. + */ +class LRUCache { + // options that cannot be changed without disaster + #max; + #maxSize; + #dispose; + #onInsert; + #disposeAfter; + #fetchMethod; + #memoMethod; + #perf; + /** + * {@link LRUCache.OptionsBase.perf} + */ + get perf() { + return this.#perf; + } + /** + * {@link LRUCache.OptionsBase.ttl} + */ + ttl; + /** + * {@link LRUCache.OptionsBase.ttlResolution} + */ + ttlResolution; + /** + * {@link LRUCache.OptionsBase.ttlAutopurge} + */ + ttlAutopurge; + /** + * {@link LRUCache.OptionsBase.updateAgeOnGet} + */ + updateAgeOnGet; + /** + * {@link LRUCache.OptionsBase.updateAgeOnHas} + */ + updateAgeOnHas; + /** + * {@link LRUCache.OptionsBase.allowStale} + */ + allowStale; + /** + * {@link LRUCache.OptionsBase.noDisposeOnSet} + */ + noDisposeOnSet; + /** + * {@link LRUCache.OptionsBase.noUpdateTTL} + */ + noUpdateTTL; + /** + * {@link LRUCache.OptionsBase.maxEntrySize} + */ + maxEntrySize; + /** + * {@link LRUCache.OptionsBase.sizeCalculation} + */ + sizeCalculation; + /** + * {@link LRUCache.OptionsBase.noDeleteOnFetchRejection} + */ + noDeleteOnFetchRejection; + /** + * {@link LRUCache.OptionsBase.noDeleteOnStaleGet} + */ + noDeleteOnStaleGet; + /** + * {@link LRUCache.OptionsBase.allowStaleOnFetchAbort} + */ + allowStaleOnFetchAbort; + /** + * {@link LRUCache.OptionsBase.allowStaleOnFetchRejection} + */ + allowStaleOnFetchRejection; + /** + * {@link LRUCache.OptionsBase.ignoreFetchAbort} + */ + ignoreFetchAbort; + /** {@link LRUCache.OptionsBase.backgroundFetchSize} */ + backgroundFetchSize; + // computed properties + #size; + #calculatedSize; + #keyMap; + #keyList; + #valList; + #next; + #prev; + #head; + #tail; + #free; + #disposed; + #sizes; + #starts; + #ttls; + #autopurgeTimers; + #hasDispose; + #hasFetchMethod; + #hasDisposeAfter; + #hasOnInsert; + /** + * Do not call this method unless you need to inspect the + * inner workings of the cache. If anything returned by this + * object is modified in any way, strange breakage may occur. + * + * These fields are private for a reason! + * + * @internal + */ + static unsafeExposeInternals(c) { + return { + // properties + starts: c.#starts, + ttls: c.#ttls, + autopurgeTimers: c.#autopurgeTimers, + sizes: c.#sizes, + keyMap: c.#keyMap, + keyList: c.#keyList, + valList: c.#valList, + next: c.#next, + prev: c.#prev, + get head() { + return c.#head; + }, + get tail() { + return c.#tail; + }, + free: c.#free, + // methods + isBackgroundFetch: (p) => c.#isBackgroundFetch(p), + backgroundFetch: (k, index, options, context) => c.#backgroundFetch(k, index, options, context), + moveToTail: (index) => c.#moveToTail(index), + indexes: (options) => c.#indexes(options), + rindexes: (options) => c.#rindexes(options), + isStale: (index) => c.#isStale(index), + }; + } + // Protected read-only members + /** + * {@link LRUCache.OptionsBase.max} (read-only) + */ + get max() { + return this.#max; + } + /** + * {@link LRUCache.OptionsBase.maxSize} (read-only) + */ + get maxSize() { + return this.#maxSize; + } + /** + * The total computed size of items in the cache (read-only) + */ + get calculatedSize() { + return this.#calculatedSize; + } + /** + * The number of items stored in the cache (read-only) + */ + get size() { + return this.#size; + } + /** + * {@link LRUCache.OptionsBase.fetchMethod} (read-only) + */ + get fetchMethod() { + return this.#fetchMethod; + } + get memoMethod() { + return this.#memoMethod; + } + /** + * {@link LRUCache.OptionsBase.dispose} (read-only) + */ + get dispose() { + return this.#dispose; + } + /** + * {@link LRUCache.OptionsBase.onInsert} (read-only) + */ + get onInsert() { + return this.#onInsert; + } + /** + * {@link LRUCache.OptionsBase.disposeAfter} (read-only) + */ + get disposeAfter() { + return this.#disposeAfter; + } + constructor(options) { + const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options; + this.backgroundFetchSize = backgroundFetchSize; + if (perf !== undefined) { + if (typeof perf?.now !== 'function') { + throw new TypeError('perf option must have a now() method if specified'); + } + } + this.#perf = perf ?? perf_js_1.defaultPerf; + if (max !== 0 && !isPosInt(max)) { + throw new TypeError('max option must be a nonnegative integer'); + } + const UintArray = max ? getUintArray(max) : Array; + if (!UintArray) { + throw new Error('invalid max value: ' + max); + } + this.#max = max; + this.#maxSize = maxSize; + this.maxEntrySize = maxEntrySize || this.#maxSize; + this.sizeCalculation = sizeCalculation; + if (this.sizeCalculation) { + if (!this.#maxSize && !this.maxEntrySize) { + throw new TypeError('cannot set sizeCalculation without setting maxSize or maxEntrySize'); + } + if (typeof this.sizeCalculation !== 'function') { + throw new TypeError('sizeCalculation set to non-function'); + } + } + if (memoMethod !== undefined && typeof memoMethod !== 'function') { + throw new TypeError('memoMethod must be a function if defined'); + } + this.#memoMethod = memoMethod; + if (fetchMethod !== undefined && typeof fetchMethod !== 'function') { + throw new TypeError('fetchMethod must be a function if specified'); + } + this.#fetchMethod = fetchMethod; + this.#hasFetchMethod = !!fetchMethod; + this.#keyMap = new Map(); + this.#keyList = Array.from({ length: max }).fill(undefined); + this.#valList = Array.from({ length: max }).fill(undefined); + this.#next = new UintArray(max); + this.#prev = new UintArray(max); + this.#head = 0; + this.#tail = 0; + this.#free = Stack.create(max); + this.#size = 0; + this.#calculatedSize = 0; + if (typeof dispose === 'function') { + this.#dispose = dispose; + } + if (typeof onInsert === 'function') { + this.#onInsert = onInsert; + } + if (typeof disposeAfter === 'function') { + this.#disposeAfter = disposeAfter; + this.#disposed = []; + } + else { + this.#disposeAfter = undefined; + this.#disposed = undefined; + } + this.#hasDispose = !!this.#dispose; + this.#hasOnInsert = !!this.#onInsert; + this.#hasDisposeAfter = !!this.#disposeAfter; + this.noDisposeOnSet = !!noDisposeOnSet; + this.noUpdateTTL = !!noUpdateTTL; + this.noDeleteOnFetchRejection = !!noDeleteOnFetchRejection; + this.allowStaleOnFetchRejection = !!allowStaleOnFetchRejection; + this.allowStaleOnFetchAbort = !!allowStaleOnFetchAbort; + this.ignoreFetchAbort = !!ignoreFetchAbort; + // NB: maxEntrySize is set to maxSize if it's set + if (this.maxEntrySize !== 0) { + if (this.#maxSize !== 0) { + if (!isPosInt(this.#maxSize)) { + throw new TypeError('maxSize must be a positive integer if specified'); + } + } + if (!isPosInt(this.maxEntrySize)) { + throw new TypeError('maxEntrySize must be a positive integer if specified'); + } + this.#initializeSizeTracking(); + } + this.allowStale = !!allowStale; + this.noDeleteOnStaleGet = !!noDeleteOnStaleGet; + this.updateAgeOnGet = !!updateAgeOnGet; + this.updateAgeOnHas = !!updateAgeOnHas; + this.ttlResolution = + isPosInt(ttlResolution) || ttlResolution === 0 ? ttlResolution : 1; + this.ttlAutopurge = !!ttlAutopurge; + this.ttl = ttl || 0; + if (this.ttl) { + if (!isPosInt(this.ttl)) { + throw new TypeError('ttl must be a positive integer if specified'); + } + this.#initializeTTLTracking(); + } + // do not allow completely unbounded caches + if (this.#max === 0 && this.ttl === 0 && this.#maxSize === 0) { + throw new TypeError('At least one of max, maxSize, or ttl is required'); + } + if (!this.ttlAutopurge && !this.#max && !this.#maxSize) { + const code = 'LRU_CACHE_UNBOUNDED'; + if (shouldWarn(code)) { + warned.add(code); + const msg = 'TTL caching without ttlAutopurge, max, or maxSize can ' + + 'result in unbounded memory consumption.'; + emitWarning(msg, 'UnboundedCacheWarning', code, LRUCache); + } + } + } + /** + * Return the number of ms left in the item's TTL. If item is not in cache, + * returns `0`. Returns `Infinity` if item is in cache without a defined TTL. + */ + getRemainingTTL(key) { + return this.#keyMap.has(key) ? Infinity : 0; + } + #initializeTTLTracking() { + const ttls = new ZeroArray(this.#max); + const starts = new ZeroArray(this.#max); + this.#ttls = ttls; + this.#starts = starts; + const purgeTimers = this.ttlAutopurge ? + Array.from({ + length: this.#max, + }) + : undefined; + this.#autopurgeTimers = purgeTimers; + this.#setItemTTL = (index, ttl, start = this.#perf.now()) => { + starts[index] = ttl !== 0 ? start : 0; + ttls[index] = ttl; + setPurgetTimer(index, ttl); + }; + this.#updateItemAge = index => { + starts[index] = ttls[index] !== 0 ? this.#perf.now() : 0; + setPurgetTimer(index, ttls[index]); + }; + // clear out the purge timer if we're setting TTL to 0, and + // previously had a ttl purge timer running, so it doesn't + // fire unnecessarily. Don't need to do this if we're not doing + // autopurge. + const setPurgetTimer = !this.ttlAutopurge ? + () => { } + : (index, ttl) => { + if (purgeTimers?.[index]) { + clearTimeout(purgeTimers[index]); + purgeTimers[index] = undefined; + } + if (ttl && ttl !== 0 && purgeTimers) { + const t = setTimeout(() => { + if (this.#isStale(index)) { + this.#delete(this.#keyList[index], 'expire'); + } + }, ttl + 1); + // unref() not supported on all platforms + /* c8 ignore start */ + if (t.unref) { + t.unref(); + } + /* c8 ignore stop */ + purgeTimers[index] = t; + } + }; + this.#statusTTL = (status, index) => { + if (ttls[index]) { + const ttl = ttls[index]; + const start = starts[index]; + /* c8 ignore start */ + if (!ttl || !start) { + return; + } + /* c8 ignore stop */ + status.ttl = ttl; + status.start = start; + status.now = cachedNow || getNow(); + const age = status.now - start; + status.remainingTTL = ttl - age; + } + }; + // debounce calls to perf.now() to 1s so we're not hitting + // that costly call repeatedly. + let cachedNow = 0; + const getNow = () => { + const n = this.#perf.now(); + if (this.ttlResolution > 0) { + cachedNow = n; + const t = setTimeout(() => (cachedNow = 0), this.ttlResolution); + // not available on all platforms + /* c8 ignore start */ + if (t.unref) { + t.unref(); + } + /* c8 ignore stop */ + } + return n; + }; + this.getRemainingTTL = key => { + const index = this.#keyMap.get(key); + if (index === undefined) { + return 0; + } + const ttl = ttls[index]; + const start = starts[index]; + if (!ttl || !start) { + return Infinity; + } + const age = (cachedNow || getNow()) - start; + return ttl - age; + }; + this.#isStale = index => { + const s = starts[index]; + const t = ttls[index]; + return !!t && !!s && (cachedNow || getNow()) - s > t; + }; + } + // conditionally set private methods related to TTL + #updateItemAge = () => { }; + #statusTTL = () => { }; + #setItemTTL = () => { }; + /* c8 ignore stop */ + #isStale = () => false; + #initializeSizeTracking() { + const sizes = new ZeroArray(this.#max); + this.#calculatedSize = 0; + this.#sizes = sizes; + this.#removeItemSize = index => { + this.#calculatedSize -= sizes[index]; + sizes[index] = 0; + }; + this.#requireSize = (k, v, size, sizeCalculation) => { + if (!isPosInt(size)) { + // provisionally accept background fetches. + // actual value size will be checked when they return. + if (this.#isBackgroundFetch(v)) { + // NB: this cannot occur if v.__staleWhileFetching is set, + // because in that case, it would take on the size of the + // existing entry that it temporarily replaces. + return this.backgroundFetchSize; + } + if (sizeCalculation) { + if (typeof sizeCalculation !== 'function') { + throw new TypeError('sizeCalculation must be a function'); + } + size = sizeCalculation(v, k); + if (!isPosInt(size)) { + throw new TypeError('sizeCalculation return invalid (expect positive integer)'); + } + } + else { + throw new TypeError('invalid size value (must be positive integer). ' + + 'When maxSize or maxEntrySize is used, sizeCalculation ' + + 'or size must be set.'); + } + } + return size; + }; + this.#addItemSize = (index, size, status) => { + sizes[index] = size; + if (this.#maxSize) { + const maxSize = this.#maxSize - sizes[index]; + while (this.#calculatedSize > maxSize) { + this.#evict(true); + } + } + this.#calculatedSize += sizes[index]; + if (status) { + status.entrySize = size; + status.totalCalculatedSize = this.#calculatedSize; + } + }; + } + #removeItemSize = _i => { }; + #addItemSize = (_i, _s, _st) => { }; + #requireSize = (_k, _v, size, sizeCalculation) => { + if (size || sizeCalculation) { + throw new TypeError('cannot set size without setting maxSize or maxEntrySize on cache'); + } + return 0; + }; + *#indexes({ allowStale = this.allowStale } = {}) { + if (this.#size) { + for (let i = this.#tail; this.#isValidIndex(i);) { + if (allowStale || !this.#isStale(i)) { + yield i; + } + if (i === this.#head) { + break; + } + else { + i = this.#prev[i]; + } + } + } + } + *#rindexes({ allowStale = this.allowStale } = {}) { + if (this.#size) { + for (let i = this.#head; this.#isValidIndex(i);) { + if (allowStale || !this.#isStale(i)) { + yield i; + } + if (i === this.#tail) { + break; + } + else { + i = this.#next[i]; + } + } + } + } + #isValidIndex(index) { + return (index !== undefined && + this.#keyMap.get(this.#keyList[index]) === index); + } + /** + * Return a generator yielding `[key, value]` pairs, + * in order from most recently used to least recently used. + */ + *entries() { + for (const i of this.#indexes()) { + if (this.#valList[i] !== undefined && + this.#keyList[i] !== undefined && + !this.#isBackgroundFetch(this.#valList[i])) { + yield [this.#keyList[i], this.#valList[i]]; + } + } + } + /** + * Inverse order version of {@link LRUCache.entries} + * + * Return a generator yielding `[key, value]` pairs, + * in order from least recently used to most recently used. + */ + *rentries() { + for (const i of this.#rindexes()) { + if (this.#valList[i] !== undefined && + this.#keyList[i] !== undefined && + !this.#isBackgroundFetch(this.#valList[i])) { + yield [this.#keyList[i], this.#valList[i]]; + } + } + } + /** + * Return a generator yielding the keys in the cache, + * in order from most recently used to least recently used. + */ + *keys() { + for (const i of this.#indexes()) { + const k = this.#keyList[i]; + if (k !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield k; + } + } + } + /** + * Inverse order version of {@link LRUCache.keys} + * + * Return a generator yielding the keys in the cache, + * in order from least recently used to most recently used. + */ + *rkeys() { + for (const i of this.#rindexes()) { + const k = this.#keyList[i]; + if (k !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield k; + } + } + } + /** + * Return a generator yielding the values in the cache, + * in order from most recently used to least recently used. + */ + *values() { + for (const i of this.#indexes()) { + const v = this.#valList[i]; + if (v !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield this.#valList[i]; + } + } + } + /** + * Inverse order version of {@link LRUCache.values} + * + * Return a generator yielding the values in the cache, + * in order from least recently used to most recently used. + */ + *rvalues() { + for (const i of this.#rindexes()) { + const v = this.#valList[i]; + if (v !== undefined && !this.#isBackgroundFetch(this.#valList[i])) { + yield this.#valList[i]; + } + } + } + /** + * Iterating over the cache itself yields the same results as + * {@link LRUCache.entries} + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * A String value that is used in the creation of the default string + * description of an object. Called by the built-in method + * `Object.prototype.toString`. + */ + [Symbol.toStringTag] = 'LRUCache'; + /** + * Find a value for which the supplied fn method returns a truthy value, + * similar to `Array.find()`. fn is called as `fn(value, key, cache)`. + */ + find(fn, getOptions = {}) { + for (const i of this.#indexes()) { + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + continue; + if (fn(value, this.#keyList[i], this)) { + return this.#get(this.#keyList[i], getOptions); + } + } + } + /** + * Call the supplied function on each item in the cache, in order from most + * recently used to least recently used. + * + * `fn` is called as `fn(value, key, cache)`. + * + * If `thisp` is provided, function will be called in the `this`-context of + * the provided object, or the cache if no `thisp` object is provided. + * + * Does not update age or recenty of use, or iterate over stale values. + */ + forEach(fn, thisp = this) { + for (const i of this.#indexes()) { + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + continue; + fn.call(thisp, value, this.#keyList[i], this); + } + } + /** + * The same as {@link LRUCache.forEach} but items are iterated over in + * reverse order. (ie, less recently used items are iterated over first.) + */ + rforEach(fn, thisp = this) { + for (const i of this.#rindexes()) { + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + continue; + fn.call(thisp, value, this.#keyList[i], this); + } + } + /** + * Delete any stale entries. Returns true if anything was removed, + * false otherwise. + */ + purgeStale() { + let deleted = false; + for (const i of this.#rindexes({ allowStale: true })) { + if (this.#isStale(i)) { + this.#delete(this.#keyList[i], 'expire'); + deleted = true; + } + } + return deleted; + } + /** + * Get the extended info about a given entry, to get its value, size, and + * TTL info simultaneously. Returns `undefined` if the key is not present. + * + * Unlike {@link LRUCache#dump}, which is designed to be portable and survive + * serialization, the `start` value is always the current timestamp, and the + * `ttl` is a calculated remaining time to live (negative if expired). + * + * Always returns stale values, if their info is found in the cache, so be + * sure to check for expirations (ie, a negative {@link LRUCache.Entry#ttl}) + * if relevant. + */ + info(key) { + const i = this.#keyMap.get(key); + if (i === undefined) + return undefined; + const v = this.#valList[i]; + /* c8 ignore start - this isn't tested for the info function, + * but it's the same logic as found in other places. */ + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined) + return undefined; + /* c8 ignore stop */ + const entry = { value }; + if (this.#ttls && this.#starts) { + const ttl = this.#ttls[i]; + const start = this.#starts[i]; + if (ttl && start) { + const remain = ttl - (this.#perf.now() - start); + entry.ttl = remain; + entry.start = Date.now(); + } + } + if (this.#sizes) { + entry.size = this.#sizes[i]; + } + return entry; + } + /** + * Return an array of [key, {@link LRUCache.Entry}] tuples which can be + * passed to {@link LRUCache#load}. + * + * The `start` fields are calculated relative to a portable `Date.now()` + * timestamp, even if `performance.now()` is available. + * + * Stale entries are always included in the `dump`, even if + * {@link LRUCache.OptionsBase.allowStale} is false. + * + * Note: this returns an actual array, not a generator, so it can be more + * easily passed around. + */ + dump() { + const arr = []; + for (const i of this.#indexes({ allowStale: true })) { + const key = this.#keyList[i]; + const v = this.#valList[i]; + const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (value === undefined || key === undefined) + continue; + const entry = { value }; + if (this.#ttls && this.#starts) { + entry.ttl = this.#ttls[i]; + // always dump the start relative to a portable timestamp + // it's ok for this to be a bit slow, it's a rare operation. + const age = this.#perf.now() - this.#starts[i]; + entry.start = Math.floor(Date.now() - age); + } + if (this.#sizes) { + entry.size = this.#sizes[i]; + } + arr.unshift([key, entry]); + } + return arr; + } + /** + * Reset the cache and load in the items in entries in the order listed. + * + * The shape of the resulting cache may be different if the same options are + * not used in both caches. + * + * The `start` fields are assumed to be calculated relative to a portable + * `Date.now()` timestamp, even if `performance.now()` is available. + */ + load(arr) { + this.clear(); + for (const [key, entry] of arr) { + if (entry.start) { + // entry.start is a portable timestamp, but we may be using + // node's performance.now(), so calculate the offset, so that + // we get the intended remaining TTL, no matter how long it's + // been on ice. + // + // it's ok for this to be a bit slow, it's a rare operation. + const age = Date.now() - entry.start; + entry.start = this.#perf.now() - age; + } + this.#set(key, entry.value, entry); + } + } + /** + * Add a value to the cache. + * + * Note: if `undefined` is specified as a value, this is an alias for + * {@link LRUCache#delete} + * + * Fields on the {@link LRUCache.SetOptions} options param will override + * their corresponding values in the constructor options for the scope + * of this single `set()` operation. + * + * If `start` is provided, then that will set the effective start + * time for the TTL calculation. Note that this must be a previous + * value of `performance.now()` if supported, or a previous value of + * `Date.now()` if not. + * + * Options object may also include `size`, which will prevent + * calling the `sizeCalculation` function and just use the specified + * number if it is a positive integer, and `noDisposeOnSet` which + * will prevent calling a `dispose` function in the case of + * overwrites. + * + * If the `size` (or return value of `sizeCalculation`) for a given + * entry is greater than `maxEntrySize`, then the item will not be + * added to the cache. + * + * Will update the recency of the entry. + * + * If the value is `undefined`, then this is an alias for + * `cache.delete(key)`. `undefined` is never stored in the cache. + */ + set(k, v, setOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = setOptions; + setOptions.status = status; + if (status) { + status.op = 'set'; + status.key = k; + if (v !== undefined) + status.value = v; + status.cache = this; + } + const result = this.#set(k, v, setOptions); + if (status && diagnostics_channel_js_1.metrics.hasSubscribers) { + diagnostics_channel_js_1.metrics.publish(status); + } + return result; + } + #set(k, v, setOptions, bf) { + const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions; + const isBF = this.#isBackgroundFetch(v); + if (v === undefined) { + if (status) + status.set = 'deleted'; + this.delete(k); + return this; + } + let { noUpdateTTL = this.noUpdateTTL } = setOptions; + if (status && !isBF) + status.value = v; + const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status); + // if the item doesn't fit, don't do anything + // NB: maxEntrySize set to maxSize by default + if (this.maxEntrySize && size > this.maxEntrySize) { + // have to delete, in case something is there already. + this.#delete(k, 'set'); + if (status) { + status.set = 'miss'; + status.maxEntrySizeExceeded = true; + } + return this; + } + let index = this.#size === 0 ? undefined : this.#keyMap.get(k); + if (index === undefined) { + // addition + index = (this.#size === 0 ? this.#tail + : this.#free.length !== 0 ? this.#free.pop() + : this.#size === this.#max ? this.#evict(false) + : this.#size); + this.#keyList[index] = k; + this.#valList[index] = v; + this.#keyMap.set(k, index); + this.#next[this.#tail] = index; + this.#prev[index] = this.#tail; + this.#tail = index; + this.#size++; + this.#addItemSize(index, size, status); + if (status) + status.set = 'add'; + noUpdateTTL = false; + if (this.#hasOnInsert && !isBF) { + this.#onInsert?.(v, k, 'add'); + } + } + else { + // update + // might be updating a background fetch! + this.#moveToTail(index); + const oldVal = this.#valList[index]; + if (v !== oldVal) { + if (!noDisposeOnSet) { + if (this.#isBackgroundFetch(oldVal)) { + if (oldVal !== bf) { + // setting over a background fetch, not merely resolving it. + oldVal.__abortController.abort(new Error('replaced')); + } + const { __staleWhileFetching: s } = oldVal; + if (s !== undefined && s !== v) { + if (this.#hasDispose) { + this.#dispose?.(s, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([s, k, 'set']); + } + } + } + else { + if (this.#hasDispose) { + this.#dispose?.(oldVal, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([oldVal, k, 'set']); + } + } + } + this.#removeItemSize(index); + this.#addItemSize(index, size, status); + this.#valList[index] = v; + if (!isBF) { + const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ? + oldVal.__staleWhileFetching + : oldVal; + const setType = oldValue === undefined ? 'add' + : v !== oldValue ? 'replace' + : 'update'; + if (status) { + status.set = setType; + if (oldValue !== undefined) + status.oldValue = oldValue; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, setType); + } + } + } + else if (!isBF) { + if (status) { + status.set = 'update'; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, 'update'); + } + } + } + if (ttl !== 0 && !this.#ttls) { + this.#initializeTTLTracking(); + } + if (this.#ttls) { + if (!noUpdateTTL) { + this.#setItemTTL(index, ttl, start); + } + if (status) + this.#statusTTL(status, index); + } + if (!noDisposeOnSet && this.#hasDisposeAfter && this.#disposed) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + return this; + } + /** + * Evict the least recently used item, returning its value or + * `undefined` if cache is empty. + */ + pop() { + try { + while (this.#size) { + const val = this.#valList[this.#head]; + this.#evict(true); + if (this.#isBackgroundFetch(val)) { + if (val.__staleWhileFetching) { + return val.__staleWhileFetching; + } + } + else if (val !== undefined) { + return val; + } + } + } + finally { + if (this.#hasDisposeAfter && this.#disposed) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + } + } + #evict(free) { + const head = this.#head; + const k = this.#keyList[head]; + const v = this.#valList[head]; + const isBF = this.#isBackgroundFetch(v); + if (isBF) { + v.__abortController.abort(new Error('evicted')); + } + const oldValue = isBF ? v.__staleWhileFetching : v; + if ((this.#hasDispose || this.#hasDisposeAfter) && + oldValue !== undefined) { + if (this.#hasDispose) { + this.#dispose?.(oldValue, k, 'evict'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([oldValue, k, 'evict']); + } + } + this.#removeItemSize(head); + if (this.#autopurgeTimers?.[head]) { + clearTimeout(this.#autopurgeTimers[head]); + this.#autopurgeTimers[head] = undefined; + } + // if we aren't about to use the index, then null these out + if (free) { + this.#keyList[head] = undefined; + this.#valList[head] = undefined; + this.#free.push(head); + } + if (this.#size === 1) { + this.#head = this.#tail = 0; + this.#free.length = 0; + } + else { + this.#head = this.#next[head]; + } + this.#keyMap.delete(k); + this.#size--; + return head; + } + /** + * Check if a key is in the cache, without updating the recency of use. + * Will return false if the item is stale, even though it is technically + * in the cache. + * + * Check if a key is in the cache, without updating the recency of + * use. Age is updated if {@link LRUCache.OptionsBase.updateAgeOnHas} is set + * to `true` in either the options or the constructor. + * + * Will return `false` if the item is stale, even though it is technically in + * the cache. The difference can be determined (if it matters) by using a + * `status` argument, and inspecting the `has` field. + * + * Will not update item age unless + * {@link LRUCache.OptionsBase.updateAgeOnHas} is set. + */ + has(k, hasOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = hasOptions; + hasOptions.status = status; + if (status) { + status.op = 'has'; + status.key = k; + status.cache = this; + } + const result = this.#has(k, hasOptions); + if (diagnostics_channel_js_1.metrics.hasSubscribers) + diagnostics_channel_js_1.metrics.publish(status); + return result; + } + #has(k, hasOptions = {}) { + const { updateAgeOnHas = this.updateAgeOnHas, status } = hasOptions; + const index = this.#keyMap.get(k); + if (index !== undefined) { + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v) && + v.__staleWhileFetching === undefined) { + return false; + } + if (!this.#isStale(index)) { + if (updateAgeOnHas) { + this.#updateItemAge(index); + } + if (status) { + status.has = 'hit'; + this.#statusTTL(status, index); + } + return true; + } + else if (status) { + status.has = 'stale'; + this.#statusTTL(status, index); + } + } + else if (status) { + status.has = 'miss'; + } + return false; + } + /** + * Like {@link LRUCache#get} but doesn't update recency or delete stale + * items. + * + * Returns `undefined` if the item is stale, unless + * {@link LRUCache.OptionsBase.allowStale} is set. + */ + peek(k, peekOptions = {}) { + const { status = hasSubscribers() ? {} : undefined } = peekOptions; + if (status) { + status.op = 'peek'; + status.key = k; + status.cache = this; + } + peekOptions.status = status; + const result = this.#peek(k, peekOptions); + if (diagnostics_channel_js_1.metrics.hasSubscribers) { + diagnostics_channel_js_1.metrics.publish(status); + } + return result; + } + #peek(k, peekOptions) { + const { status, allowStale = this.allowStale } = peekOptions; + const index = this.#keyMap.get(k); + if (index === undefined || (!allowStale && this.#isStale(index))) { + if (status) + status.peek = index === undefined ? 'miss' : 'stale'; + return undefined; + } + const v = this.#valList[index]; + const val = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v; + if (status) { + if (val !== undefined) { + status.peek = 'hit'; + status.value = val; + } + else { + status.peek = 'miss'; + } + } + return val; + } + #backgroundFetch(k, index, options, context) { + const v = index === undefined ? undefined : this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + return v; + } + const ac = new AbortController(); + const { signal } = options; + // when/if our AC signals, then stop listening to theirs. + signal?.addEventListener('abort', () => ac.abort(signal.reason), { + signal: ac.signal, + }); + const fetchOpts = { + signal: ac.signal, + options, + context, + }; + const cb = (v, updateCache = false) => { + const { aborted } = ac.signal; + const ignoreAbort = options.ignoreFetchAbort && v !== undefined; + const proceed = options.ignoreFetchAbort || + !!(options.allowStaleOnFetchAbort && v !== undefined); + if (options.status) { + if (aborted && !updateCache) { + options.status.fetchAborted = true; + options.status.fetchError = ac.signal.reason; + if (ignoreAbort) + options.status.fetchAbortIgnored = true; + } + else { + options.status.fetchResolved = true; + } + } + if (aborted && !ignoreAbort && !updateCache) { + return fetchFail(ac.signal.reason, proceed); + } + // either we didn't abort, and are still here, or we did, and ignored + const bf = p; + // if nothing else has been written there but we're set to update the + // cache and ignore the abort, or if it's still pending on this specific + // background request, then write it to the cache. + const vl = this.#valList[index]; + if (vl === p || (vl === undefined && ignoreAbort && updateCache)) { + if (v === undefined) { + if (bf.__staleWhileFetching !== undefined) { + this.#valList[index] = bf.__staleWhileFetching; + } + else { + this.#delete(k, 'fetch'); + } + } + else { + if (options.status) + options.status.fetchUpdated = true; + this.#set(k, v, fetchOpts.options, bf); + } + } + return v; + }; + const eb = (er) => { + if (options.status) { + options.status.fetchRejected = true; + options.status.fetchError = er; + } + // do not pass go, do not collect $200 + return fetchFail(er, false); + }; + const fetchFail = (er, proceed) => { + const { aborted } = ac.signal; + const allowStaleAborted = aborted && options.allowStaleOnFetchAbort; + const allowStale = allowStaleAborted || options.allowStaleOnFetchRejection; + const noDelete = allowStale || options.noDeleteOnFetchRejection; + const bf = p; + if (this.#valList[index] === p) { + // if we allow stale on fetch rejections, then we need to ensure that + // the stale value is not removed from the cache when the fetch fails. + const del = !noDelete || (!proceed && bf.__staleWhileFetching === undefined); + if (del) { + this.#delete(k, 'fetch'); + } + else if (!allowStaleAborted) { + // still replace the *promise* with the stale value, + // since we are done with the promise at this point. + // leave it untouched if we're still waiting for an + // aborted background fetch that hasn't yet returned. + this.#valList[index] = bf.__staleWhileFetching; + } + } + if (allowStale) { + if (options.status && bf.__staleWhileFetching !== undefined) { + options.status.returnedStale = true; + } + return bf.__staleWhileFetching; + } + else if (bf.__returned === bf) { + throw er; + } + }; + const pcall = (res, rej) => { + const fmp = this.#fetchMethod?.(k, v, fetchOpts); + // ignored, we go until we finish, regardless. + // defer check until we are actually aborting, + // so fetchMethod can override. + ac.signal.addEventListener('abort', () => { + if (!options.ignoreFetchAbort || options.allowStaleOnFetchAbort) { + res(undefined); + // when it eventually resolves, update the cache. + if (options.allowStaleOnFetchAbort) { + res = v => cb(v, true); + } + } + }); + if (fmp && fmp instanceof Promise) { + fmp.then(v => res(v === undefined ? undefined : v), rej); + } + else if (fmp !== undefined) { + res(fmp); + } + }; + if (options.status) + options.status.fetchDispatched = true; + const p = new Promise(pcall).then(cb, eb); + const bf = Object.assign(p, { + __abortController: ac, + __staleWhileFetching: v, + __returned: undefined, + }); + if (index === undefined) { + // internal, don't expose status. + this.#set(k, bf, { ...fetchOpts.options, status: undefined }); + index = this.#keyMap.get(k); + } + else { + // do not call #set, because we do not want to adjust its place + // in the lru queue, as it has not yet been "used". Also, we don't + // need to worry about evicting for size, because a background fetch + // over a stale value is treated as the same size as its stale value. + this.#valList[index] = bf; + } + return bf; + } + #isBackgroundFetch(p) { + if (!this.#hasFetchMethod) + return false; + const b = p; + return (!!b && + b instanceof Promise && + b.hasOwnProperty('__staleWhileFetching') && + b.__abortController instanceof AbortController); + } + fetch(k, fetchOptions = {}) { + const ths = diagnostics_channel_js_1.tracing.hasSubscribers; + const { status = hasSubscribers() ? {} : undefined } = fetchOptions; + fetchOptions.status = status; + if (status && fetchOptions.context) { + status.context = fetchOptions.context; + } + const p = this.#fetch(k, fetchOptions); + if (status && ths) { + status.trace = true; + diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); + } + return p; + } + async #fetch(k, fetchOptions = {}) { + const { + // get options + allowStale = this.allowStale, updateAgeOnGet = this.updateAgeOnGet, noDeleteOnStaleGet = this.noDeleteOnStaleGet, + // set options + ttl = this.ttl, noDisposeOnSet = this.noDisposeOnSet, size = 0, sizeCalculation = this.sizeCalculation, noUpdateTTL = this.noUpdateTTL, + // fetch exclusive options + noDeleteOnFetchRejection = this.noDeleteOnFetchRejection, allowStaleOnFetchRejection = this.allowStaleOnFetchRejection, ignoreFetchAbort = this.ignoreFetchAbort, allowStaleOnFetchAbort = this.allowStaleOnFetchAbort, context, forceRefresh = false, status, signal, } = fetchOptions; + if (status) { + status.op = 'fetch'; + status.key = k; + if (forceRefresh) + status.forceRefresh = true; + status.cache = this; + } + if (!this.#hasFetchMethod) { + if (status) + status.fetch = 'get'; + return this.#get(k, { + allowStale, + updateAgeOnGet, + noDeleteOnStaleGet, + status, + }); + } + const options = { + allowStale, + updateAgeOnGet, + noDeleteOnStaleGet, + ttl, + noDisposeOnSet, + size, + sizeCalculation, + noUpdateTTL, + noDeleteOnFetchRejection, + allowStaleOnFetchRejection, + allowStaleOnFetchAbort, + ignoreFetchAbort, + status, + signal, + }; + let index = this.#keyMap.get(k); + if (index === undefined) { + if (status) + status.fetch = 'miss'; + const p = this.#backgroundFetch(k, index, options, context); + return (p.__returned = p); + } + else { + // in cache, maybe already fetching + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + const stale = allowStale && v.__staleWhileFetching !== undefined; + if (status) { + status.fetch = 'inflight'; + if (stale) + status.returnedStale = true; + } + return stale ? v.__staleWhileFetching : (v.__returned = v); + } + // if we force a refresh, that means do NOT serve the cached value, + // unless we are already in the process of refreshing the cache. + const isStale = this.#isStale(index); + if (!forceRefresh && !isStale) { + if (status) + status.fetch = 'hit'; + this.#moveToTail(index); + if (updateAgeOnGet) { + this.#updateItemAge(index); + } + if (status) + this.#statusTTL(status, index); + return v; + } + // ok, it is stale or a forced refresh, and not already fetching. + // refresh the cache. + const p = this.#backgroundFetch(k, index, options, context); + const hasStale = p.__staleWhileFetching !== undefined; + const staleVal = hasStale && allowStale; + if (status) { + status.fetch = isStale ? 'stale' : 'refresh'; + if (staleVal && isStale) + status.returnedStale = true; + } + return staleVal ? p.__staleWhileFetching : (p.__returned = p); + } + } + forceFetch(k, fetchOptions = {}) { + const ths = diagnostics_channel_js_1.tracing.hasSubscribers; + const { status = hasSubscribers() ? {} : undefined } = fetchOptions; + fetchOptions.status = status; + if (status && fetchOptions.context) { + status.context = fetchOptions.context; + } + const p = this.#forceFetch(k, fetchOptions); + if (status && ths) { + status.trace = true; + diagnostics_channel_js_1.tracing.tracePromise(() => p, status).catch(() => { }); + } + return p; + } + async #forceFetch(k, fetchOptions = {}) { + const v = await this.#fetch(k, fetchOptions); + if (v === undefined) + throw new Error('fetch() returned undefined'); + return v; + } + memo(k, memoOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = memoOptions; + memoOptions.status = status; + if (status) { + status.op = 'memo'; + status.key = k; + if (memoOptions.context) { + status.context = memoOptions.context; + } + status.cache = this; + } + const result = this.#memo(k, memoOptions); + if (status) + status.value = result; + if (diagnostics_channel_js_1.metrics.hasSubscribers) + diagnostics_channel_js_1.metrics.publish(status); + return result; + } + #memo(k, memoOptions = {}) { + const memoMethod = this.#memoMethod; + if (!memoMethod) { + throw new Error('no memoMethod provided to constructor'); + } + const { context, status, forceRefresh, ...options } = memoOptions; + if (status && forceRefresh) + status.forceRefresh = true; + const v = this.#get(k, options); + const refresh = forceRefresh || v === undefined; + if (status) { + status.memo = refresh ? 'miss' : 'hit'; + if (!refresh) + status.value = v; + } + if (!refresh) + return v; + const vv = memoMethod(k, v, { + options, + context, + }); + if (status) + status.value = vv; + this.#set(k, vv, options); + return vv; + } + /** + * Return a value from the cache. Will update the recency of the cache + * entry found. + * + * If the key is not found, get() will return `undefined`. + */ + get(k, getOptions = {}) { + const { status = diagnostics_channel_js_1.metrics.hasSubscribers ? {} : undefined } = getOptions; + getOptions.status = status; + if (status) { + status.op = 'get'; + status.key = k; + status.cache = this; + } + const result = this.#get(k, getOptions); + if (status) { + if (result !== undefined) + status.value = result; + if (diagnostics_channel_js_1.metrics.hasSubscribers) + diagnostics_channel_js_1.metrics.publish(status); + } + return result; + } + #get(k, getOptions = {}) { + const { allowStale = this.allowStale, updateAgeOnGet = this.updateAgeOnGet, noDeleteOnStaleGet = this.noDeleteOnStaleGet, status, } = getOptions; + const index = this.#keyMap.get(k); + if (index === undefined) { + if (status) + status.get = 'miss'; + return undefined; + } + const value = this.#valList[index]; + const fetching = this.#isBackgroundFetch(value); + if (status) + this.#statusTTL(status, index); + if (this.#isStale(index)) { + // delete only if not an in-flight background fetch + if (!fetching) { + if (!noDeleteOnStaleGet) { + this.#delete(k, 'expire'); + } + if (status) + status.get = 'stale'; + if (allowStale) { + if (status) + status.returnedStale = true; + return value; + } + return undefined; + } + if (status) + status.get = 'stale-fetching'; + if (allowStale && value.__staleWhileFetching !== undefined) { + if (status) + status.returnedStale = true; + return value.__staleWhileFetching; + } + return undefined; + } + // not stale + if (status) + status.get = fetching ? 'fetching' : 'hit'; + // if we're currently fetching it, we don't actually have it yet + // it's not stale, which means this isn't a staleWhileRefetching. + // If it's not stale, and fetching, AND has a __staleWhileFetching + // value, then that means the user fetched with {forceRefresh:true}, + // so it's safe to return that value. + this.#moveToTail(index); + if (updateAgeOnGet) { + this.#updateItemAge(index); + } + return fetching ? value.__staleWhileFetching : value; + } + #connect(p, n) { + this.#prev[n] = p; + this.#next[p] = n; + } + #moveToTail(index) { + // if tail already, nothing to do + // if head, move head to next[index] + // else + // move next[prev[index]] to next[index] (head has no prev) + // move prev[next[index]] to prev[index] + // prev[index] = tail + // next[tail] = index + // tail = index + if (index !== this.#tail) { + if (index === this.#head) { + this.#head = this.#next[index]; + } + else { + this.#connect(this.#prev[index], this.#next[index]); + } + this.#connect(this.#tail, index); + this.#tail = index; + } + } + /** + * Deletes a key out of the cache. + * + * Returns true if the key was deleted, false otherwise. + */ + delete(k) { + return this.#delete(k, 'delete'); + } + #delete(k, reason) { + if (diagnostics_channel_js_1.metrics.hasSubscribers) { + diagnostics_channel_js_1.metrics.publish({ + op: 'delete', + delete: reason, + key: k, + cache: this, + }); + } + let deleted = false; + if (this.#size !== 0) { + const index = this.#keyMap.get(k); + if (index !== undefined) { + if (this.#autopurgeTimers?.[index]) { + clearTimeout(this.#autopurgeTimers?.[index]); + this.#autopurgeTimers[index] = undefined; + } + deleted = true; + if (this.#size === 1) { + this.#clear(reason); + } + else { + this.#removeItemSize(index); + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + v.__abortController.abort(new Error('deleted')); + } + else if (this.#hasDispose || this.#hasDisposeAfter) { + if (this.#hasDispose) { + this.#dispose?.(v, k, reason); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([v, k, reason]); + } + } + this.#keyMap.delete(k); + this.#keyList[index] = undefined; + this.#valList[index] = undefined; + if (index === this.#tail) { + this.#tail = this.#prev[index]; + } + else if (index === this.#head) { + this.#head = this.#next[index]; + } + else { + const pi = this.#prev[index]; + this.#next[pi] = this.#next[index]; + const ni = this.#next[index]; + this.#prev[ni] = this.#prev[index]; + } + this.#size--; + this.#free.push(index); + } + } + } + if (this.#hasDisposeAfter && this.#disposed?.length) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + return deleted; + } + /** + * Clear the cache entirely, throwing away all values. + */ + clear() { + return this.#clear('delete'); + } + #clear(reason) { + for (const index of this.#rindexes({ allowStale: true })) { + const v = this.#valList[index]; + if (this.#isBackgroundFetch(v)) { + v.__abortController.abort(new Error('deleted')); + } + else { + const k = this.#keyList[index]; + if (this.#hasDispose) { + this.#dispose?.(v, k, reason); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([v, k, reason]); + } + } + } + this.#keyMap.clear(); + void this.#valList.fill(undefined); + this.#keyList.fill(undefined); + if (this.#ttls && this.#starts) { + this.#ttls.fill(0); + this.#starts.fill(0); + for (const t of this.#autopurgeTimers ?? []) { + if (t !== undefined) + clearTimeout(t); + } + this.#autopurgeTimers?.fill(undefined); + } + if (this.#sizes) { + this.#sizes.fill(0); + } + this.#head = 0; + this.#tail = 0; + this.#free.length = 0; + this.#calculatedSize = 0; + this.#size = 0; + if (this.#hasDisposeAfter && this.#disposed) { + const dt = this.#disposed; + let task; + while ((task = dt?.shift())) { + this.#disposeAfter?.(...task); + } + } + } +} +exports.LRUCache = LRUCache; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/node/index.min.js b/deps/npm/node_modules/lru-cache/dist/commonjs/node/index.min.js new file mode 100644 index 00000000000000..a03fc771913873 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/node/index.min.js @@ -0,0 +1,2 @@ +"use strict";var j=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports);var I=j(O=>{"use strict";Object.defineProperty(O,"__esModule",{value:!0});O.tracing=O.metrics=void 0;var U=require("node:diagnostics_channel");O.metrics=(0,U.channel)("lru-cache:metrics");O.tracing=(0,U.tracingChannel)("lru-cache")});var P=j(D=>{"use strict";Object.defineProperty(D,"__esModule",{value:!0});D.defaultPerf=void 0;D.defaultPerf=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date});Object.defineProperty(exports,"__esModule",{value:!0});exports.LRUCache=void 0;var g=I(),N=P(),C=()=>g.metrics.hasSubscribers||g.tracing.hasSubscribers,k=new Set,G=typeof process=="object"&&process?process:{},V=(c,t,e,i)=>{typeof G.emitWarning=="function"?G.emitWarning(c,t,e,i):console.error(`[${e}] ${t}: ${c}`)},q=c=>!k.has(c);var T=c=>!!c&&c===Math.floor(c)&&c>0&&isFinite(c),H=c=>T(c)?c<=Math.pow(2,8)?Uint8Array:c<=Math.pow(2,16)?Uint16Array:c<=Math.pow(2,32)?Uint32Array:c<=Number.MAX_SAFE_INTEGER?W:null:null,W=class extends Array{constructor(t){super(t),this.fill(0)}},x=class c{heap;length;static#o=!1;static create(t){let e=H(t);if(!e)return[];c.#o=!0;let i=new c(t,e);return c.#o=!1,i}constructor(t,e){if(!c.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new e(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},L=class c{#o;#c;#m;#W;#S;#M;#j;#w;get perf(){return this.#w}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;backgroundFetchSize;#n;#b;#s;#i;#t;#l;#u;#a;#h;#_;#r;#y;#F;#d;#g;#T;#U;#f;#D;static unsafeExposeInternals(t){return{starts:t.#F,ttls:t.#d,autopurgeTimers:t.#g,sizes:t.#y,keyMap:t.#s,keyList:t.#i,valList:t.#t,next:t.#l,prev:t.#u,get head(){return t.#a},get tail(){return t.#h},free:t.#_,isBackgroundFetch:e=>t.#e(e),backgroundFetch:(e,i,s,n)=>t.#G(e,i,s,n),moveToTail:e=>t.#L(e),indexes:e=>t.#A(e),rindexes:e=>t.#z(e),isStale:e=>t.#p(e)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#j}get dispose(){return this.#m}get onInsert(){return this.#W}get disposeAfter(){return this.#S}constructor(t){let{max:e=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:r,updateAgeOnHas:h,allowStale:a,dispose:o,onInsert:d,disposeAfter:_,noDisposeOnSet:y,noUpdateTTL:u,maxSize:p=0,maxEntrySize:f=0,sizeCalculation:b,fetchMethod:l,memoMethod:S,noDeleteOnFetchRejection:F,noDeleteOnStaleGet:w,allowStaleOnFetchRejection:m,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,backgroundFetchSize:M=1,perf:v}=t;if(this.backgroundFetchSize=M,v!==void 0&&typeof v?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#w=v??N.defaultPerf,e!==0&&!T(e))throw new TypeError("max option must be a nonnegative integer");let E=e?H(e):Array;if(!E)throw new Error("invalid max value: "+e);if(this.#o=e,this.#c=p,this.maxEntrySize=f||this.#c,this.sizeCalculation=b,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(S!==void 0&&typeof S!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#j=S,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#U=!!l,this.#s=new Map,this.#i=Array.from({length:e}).fill(void 0),this.#t=Array.from({length:e}).fill(void 0),this.#l=new E(e),this.#u=new E(e),this.#a=0,this.#h=0,this.#_=x.create(e),this.#n=0,this.#b=0,typeof o=="function"&&(this.#m=o),typeof d=="function"&&(this.#W=d),typeof _=="function"?(this.#S=_,this.#r=[]):(this.#S=void 0,this.#r=void 0),this.#T=!!this.#m,this.#D=!!this.#W,this.#f=!!this.#S,this.noDisposeOnSet=!!y,this.noUpdateTTL=!!u,this.noDeleteOnFetchRejection=!!F,this.allowStaleOnFetchRejection=!!m,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#c!==0&&!T(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!T(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!a,this.noDeleteOnStaleGet=!!w,this.updateAgeOnGet=!!r,this.updateAgeOnHas=!!h,this.ttlResolution=T(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!T(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let R="LRU_CACHE_UNBOUNDED";q(R)&&(k.add(R),V("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",R,c))}}getRemainingTTL(t){return this.#s.has(t)?1/0:0}#k(){let t=new W(this.#o),e=new W(this.#o);this.#d=t,this.#F=e;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#H=(h,a,o=this.#w.now())=>{e[h]=a!==0?o:0,t[h]=a,s(h,a)},this.#R=h=>{e[h]=t[h]!==0?this.#w.now():0,s(h,t[h])};let s=this.ttlAutopurge?(h,a)=>{if(i?.[h]&&(clearTimeout(i[h]),i[h]=void 0),a&&a!==0&&i){let o=setTimeout(()=>{this.#p(h)&&this.#v(this.#i[h],"expire")},a+1);o.unref&&o.unref(),i[h]=o}}:()=>{};this.#E=(h,a)=>{if(t[a]){let o=t[a],d=e[a];if(!o||!d)return;h.ttl=o,h.start=d,h.now=n||r();let _=h.now-d;h.remainingTTL=o-_}};let n=0,r=()=>{let h=this.#w.now();if(this.ttlResolution>0){n=h;let a=setTimeout(()=>n=0,this.ttlResolution);a.unref&&a.unref()}return h};this.getRemainingTTL=h=>{let a=this.#s.get(h);if(a===void 0)return 0;let o=t[a],d=e[a];if(!o||!d)return 1/0;let _=(n||r())-d;return o-_},this.#p=h=>{let a=e[h],o=t[h];return!!o&&!!a&&(n||r())-a>o}}#R=()=>{};#E=()=>{};#H=()=>{};#p=()=>!1;#X(){let t=new W(this.#o);this.#b=0,this.#y=t,this.#C=e=>{this.#b-=t[e],t[e]=0},this.#N=(e,i,s,n)=>{if(!T(s)){if(this.#e(i))return this.backgroundFetchSize;if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,e),!T(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.")}return s},this.#I=(e,i,s)=>{if(t[e]=i,this.#c){let n=this.#c-t[e];for(;this.#b>n;)this.#P(!0)}this.#b+=t[e],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#C=t=>{};#I=(t,e,i)=>{};#N=(t,e,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#h;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#a);)e=this.#u[e]}*#z({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#a;this.#V(e)&&((t||!this.#p(e))&&(yield e),e!==this.#h);)e=this.#l[e]}#V(t){return t!==void 0&&this.#s.get(this.#i[t])===t}*entries(){for(let t of this.#A())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*rentries(){for(let t of this.#z())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*keys(){for(let t of this.#A()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*rkeys(){for(let t of this.#z()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*values(){for(let t of this.#A())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}*rvalues(){for(let t of this.#z())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(t,e={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&t(n,this.#i[i],this))return this.#x(this.#i[i],e)}}forEach(t,e=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}rforEach(t,e=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}purgeStale(){let t=!1;for(let e of this.#z({allowStale:!0}))this.#p(e)&&(this.#v(this.#i[e],"expire"),t=!0);return t}info(t){let e=this.#s.get(t);if(e===void 0)return;let i=this.#t[e],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let r=this.#d[e],h=this.#F[e];if(r&&h){let a=r-(this.#w.now()-h);n.ttl=a,n.start=Date.now()}}return this.#y&&(n.size=this.#y[e]),n}dump(){let t=[];for(let e of this.#A({allowStale:!0})){let i=this.#i[e],s=this.#t[e],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let r={value:n};if(this.#d&&this.#F){r.ttl=this.#d[e];let h=this.#w.now()-this.#F[e];r.start=Math.floor(Date.now()-h)}this.#y&&(r.size=this.#y[e]),t.unshift([i,r])}return t}load(t){this.clear();for(let[e,i]of t){if(i.start){let s=Date.now()-i.start;i.start=this.#w.now()-s}this.#O(e,i.value,i)}}set(t,e,i={}){let{status:s=g.metrics.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=t,e!==void 0&&(s.value=e),s.cache=this);let n=this.#O(t,e,i);return s&&g.metrics.hasSubscribers&&g.metrics.publish(s),n}#O(t,e,i,s){let{ttl:n=this.ttl,start:r,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:o}=i,d=this.#e(e);if(e===void 0)return o&&(o.set="deleted"),this.delete(t),this;let{noUpdateTTL:_=this.noUpdateTTL}=i;o&&!d&&(o.value=e);let y=this.#N(t,e,i.size||0,a,o);if(this.maxEntrySize&&y>this.maxEntrySize)return this.#v(t,"set"),o&&(o.set="miss",o.maxEntrySizeExceeded=!0),this;let u=this.#n===0?void 0:this.#s.get(t);if(u===void 0)u=this.#n===0?this.#h:this.#_.length!==0?this.#_.pop():this.#n===this.#o?this.#P(!1):this.#n,this.#i[u]=t,this.#t[u]=e,this.#s.set(t,u),this.#l[this.#h]=u,this.#u[u]=this.#h,this.#h=u,this.#n++,this.#I(u,y,o),o&&(o.set="add"),_=!1,this.#D&&!d&&this.#W?.(e,t,"add");else{this.#L(u);let p=this.#t[u];if(e!==p){if(!h)if(this.#e(p)){p!==s&&p.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:f}=p;f!==void 0&&f!==e&&(this.#T&&this.#m?.(f,t,"set"),this.#f&&this.#r?.push([f,t,"set"]))}else this.#T&&this.#m?.(p,t,"set"),this.#f&&this.#r?.push([p,t,"set"]);if(this.#C(u),this.#I(u,y,o),this.#t[u]=e,!d){let f=p&&this.#e(p)?p.__staleWhileFetching:p,b=f===void 0?"add":e!==f?"replace":"update";o&&(o.set=b,f!==void 0&&(o.oldValue=f)),this.#D&&this.onInsert?.(e,t,b)}}else d||(o&&(o.set="update"),this.#D&&this.onInsert?.(e,t,"update"))}if(n!==0&&!this.#d&&this.#k(),this.#d&&(_||this.#H(u,n,r),o&&this.#E(o,u)),!h&&this.#f&&this.#r){let p=this.#r,f;for(;f=p?.shift();)this.#S?.(...f)}return this}pop(){try{for(;this.#n;){let t=this.#t[this.#a];if(this.#P(!0),this.#e(t)){if(t.__staleWhileFetching)return t.__staleWhileFetching}else if(t!==void 0)return t}}finally{if(this.#f&&this.#r){let t=this.#r,e;for(;e=t?.shift();)this.#S?.(...e)}}}#P(t){let e=this.#a,i=this.#i[e],s=this.#t[e],n=this.#e(s);n&&s.__abortController.abort(new Error("evicted"));let r=n?s.__staleWhileFetching:s;return(this.#T||this.#f)&&r!==void 0&&(this.#T&&this.#m?.(r,i,"evict"),this.#f&&this.#r?.push([r,i,"evict"])),this.#C(e),this.#g?.[e]&&(clearTimeout(this.#g[e]),this.#g[e]=void 0),t&&(this.#i[e]=void 0,this.#t[e]=void 0,this.#_.push(e)),this.#n===1?(this.#a=this.#h=0,this.#_.length=0):this.#a=this.#l[e],this.#s.delete(i),this.#n--,e}has(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="has",i.key=t,i.cache=this);let s=this.#Y(t,e);return g.metrics.hasSubscribers&&g.metrics.publish(i),s}#Y(t,e={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=e,n=this.#s.get(t);if(n!==void 0){let r=this.#t[n];if(this.#e(r)&&r.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#R(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(t,e={}){let{status:i=C()?{}:void 0}=e;i&&(i.op="peek",i.key=t,i.cache=this),e.status=i;let s=this.#J(t,e);return g.metrics.hasSubscribers&&g.metrics.publish(i),s}#J(t,e){let{status:i,allowStale:s=this.allowStale}=e,n=this.#s.get(t);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let r=this.#t[n],h=this.#e(r)?r.__staleWhileFetching:r;return i&&(h!==void 0?(i.peek="hit",i.value=h):i.peek="miss"),h}#G(t,e,i,s){let n=e===void 0?void 0:this.#t[e];if(this.#e(n))return n;let r=new AbortController,{signal:h}=i;h?.addEventListener("abort",()=>r.abort(h.reason),{signal:r.signal});let a={signal:r.signal,options:i,context:s},o=(f,b=!1)=>{let{aborted:l}=r.signal,S=i.ignoreFetchAbort&&f!==void 0,F=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&f!==void 0);if(i.status&&(l&&!b?(i.status.fetchAborted=!0,i.status.fetchError=r.signal.reason,S&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!S&&!b)return _(r.signal.reason,F);let w=u,m=this.#t[e];return(m===u||m===void 0&&S&&b)&&(f===void 0?w.__staleWhileFetching!==void 0?this.#t[e]=w.__staleWhileFetching:this.#v(t,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#O(t,f,a.options,w))),f},d=f=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=f),_(f,!1)),_=(f,b)=>{let{aborted:l}=r.signal,S=l&&i.allowStaleOnFetchAbort,F=S||i.allowStaleOnFetchRejection,w=F||i.noDeleteOnFetchRejection,m=u;if(this.#t[e]===u&&(!w||!b&&m.__staleWhileFetching===void 0?this.#v(t,"fetch"):S||(this.#t[e]=m.__staleWhileFetching)),F)return i.status&&m.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),m.__staleWhileFetching;if(m.__returned===m)throw f},y=(f,b)=>{let l=this.#M?.(t,n,a);r.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(f(void 0),i.allowStaleOnFetchAbort&&(f=S=>o(S,!0)))}),l&&l instanceof Promise?l.then(S=>f(S===void 0?void 0:S),b):l!==void 0&&f(l)};i.status&&(i.status.fetchDispatched=!0);let u=new Promise(y).then(o,d),p=Object.assign(u,{__abortController:r,__staleWhileFetching:n,__returned:void 0});return e===void 0?(this.#O(t,p,{...a.options,status:void 0}),e=this.#s.get(t)):this.#t[e]=p,p}#e(t){if(!this.#U)return!1;let e=t;return!!e&&e instanceof Promise&&e.hasOwnProperty("__staleWhileFetching")&&e.__abortController instanceof AbortController}fetch(t,e={}){let i=g.tracing.hasSubscribers,{status:s=C()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#q(t,e);return s&&i&&(s.trace=!0,g.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#q(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:r=this.ttl,noDisposeOnSet:h=this.noDisposeOnSet,size:a=0,sizeCalculation:o=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:_=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:y=this.allowStaleOnFetchRejection,ignoreFetchAbort:u=this.ignoreFetchAbort,allowStaleOnFetchAbort:p=this.allowStaleOnFetchAbort,context:f,forceRefresh:b=!1,status:l,signal:S}=e;if(l&&(l.op="fetch",l.key=t,b&&(l.forceRefresh=!0),l.cache=this),!this.#U)return l&&(l.fetch="get"),this.#x(t,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let F={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:r,noDisposeOnSet:h,size:a,sizeCalculation:o,noUpdateTTL:d,noDeleteOnFetchRejection:_,allowStaleOnFetchRejection:y,allowStaleOnFetchAbort:p,ignoreFetchAbort:u,status:l,signal:S},w=this.#s.get(t);if(w===void 0){l&&(l.fetch="miss");let m=this.#G(t,w,F,f);return m.__returned=m}else{let m=this.#t[w];if(this.#e(m)){let E=i&&m.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",E&&(l.returnedStale=!0)),E?m.__staleWhileFetching:m.__returned=m}let A=this.#p(w);if(!b&&!A)return l&&(l.fetch="hit"),this.#L(w),s&&this.#R(w),l&&this.#E(l,w),m;let z=this.#G(t,w,F,f),v=z.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=A?"stale":"refresh",v&&A&&(l.returnedStale=!0)),v?z.__staleWhileFetching:z.__returned=z}}forceFetch(t,e={}){let i=g.tracing.hasSubscribers,{status:s=C()?{}:void 0}=e;e.status=s,s&&e.context&&(s.context=e.context);let n=this.#K(t,e);return s&&i&&(s.trace=!0,g.tracing.tracePromise(()=>n,s).catch(()=>{})),n}async#K(t,e={}){let i=await this.#q(t,e);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="memo",i.key=t,e.context&&(i.context=e.context),i.cache=this);let s=this.#Q(t,e);return i&&(i.value=s),g.metrics.hasSubscribers&&g.metrics.publish(i),s}#Q(t,e={}){let i=this.#j;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:r,...h}=e;n&&r&&(n.forceRefresh=!0);let a=this.#x(t,h),o=r||a===void 0;if(n&&(n.memo=o?"miss":"hit",o||(n.value=a)),!o)return a;let d=i(t,a,{options:h,context:s});return n&&(n.value=d),this.#O(t,d,h),d}get(t,e={}){let{status:i=g.metrics.hasSubscribers?{}:void 0}=e;e.status=i,i&&(i.op="get",i.key=t,i.cache=this);let s=this.#x(t,e);return i&&(s!==void 0&&(i.value=s),g.metrics.hasSubscribers&&g.metrics.publish(i)),s}#x(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:r}=e,h=this.#s.get(t);if(h===void 0){r&&(r.get="miss");return}let a=this.#t[h],o=this.#e(a);return r&&this.#E(r,h),this.#p(h)?o?(r&&(r.get="stale-fetching"),i&&a.__staleWhileFetching!==void 0?(r&&(r.returnedStale=!0),a.__staleWhileFetching):void 0):(n||this.#v(t,"expire"),r&&(r.get="stale"),i?(r&&(r.returnedStale=!0),a):void 0):(r&&(r.get=o?"fetching":"hit"),this.#L(h),s&&this.#R(h),o?a.__staleWhileFetching:a)}#B(t,e){this.#u[e]=t,this.#l[t]=e}#L(t){t!==this.#h&&(t===this.#a?this.#a=this.#l[t]:this.#B(this.#u[t],this.#l[t]),this.#B(this.#h,t),this.#h=t)}delete(t){return this.#v(t,"delete")}#v(t,e){g.metrics.hasSubscribers&&g.metrics.publish({op:"delete",delete:e,key:t,cache:this});let i=!1;if(this.#n!==0){let s=this.#s.get(t);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#$(e);else{this.#C(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#m?.(n,t,e),this.#f&&this.#r?.push([n,t,e])),this.#s.delete(t),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let r=this.#u[s];this.#l[r]=this.#l[s];let h=this.#l[s];this.#u[h]=this.#u[s]}this.#n--,this.#_.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#S?.(...n)}return i}clear(){return this.#$("delete")}#$(t){for(let e of this.#z({allowStale:!0})){let i=this.#t[e];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[e];this.#T&&this.#m?.(i,s,t),this.#f&&this.#r?.push([i,s,t])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let e of this.#g??[])e!==void 0&&clearTimeout(e);this.#g?.fill(void 0)}if(this.#y&&this.#y.fill(0),this.#a=0,this.#h=0,this.#_.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let e=this.#r,i;for(;i=e?.shift();)this.#S?.(...i)}}};exports.LRUCache=L; +//# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/node/perf.js b/deps/npm/node_modules/lru-cache/dist/commonjs/node/perf.js new file mode 100644 index 00000000000000..bd4c80f461d6c1 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/node/perf.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.defaultPerf = void 0; +exports.defaultPerf = (typeof performance === 'object' && + performance && + typeof performance.now === 'function') ? + /* c8 ignore start - this gets covered, but c8 gets confused */ + performance + : /* c8 ignore stop */ Date; +//# sourceMappingURL=perf.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/commonjs/perf.js b/deps/npm/node_modules/lru-cache/dist/commonjs/perf.js new file mode 100644 index 00000000000000..bd4c80f461d6c1 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/commonjs/perf.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.defaultPerf = void 0; +exports.defaultPerf = (typeof performance === 'object' && + performance && + typeof performance.now === 'function') ? + /* c8 ignore start - this gets covered, but c8 gets confused */ + performance + : /* c8 ignore stop */ Date; +//# sourceMappingURL=perf.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js b/deps/npm/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js index 08ec4d4d484b44..37524f355f050c 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js @@ -1,4 +1,4 @@ const dummy = { hasSubscribers: false }; export const metrics = dummy; export const tracing = dummy; -//# sourceMappingURL=diagnostics-channel-browser.mjs.map \ No newline at end of file +//# sourceMappingURL=diagnostics-channel-browser.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/esm/browser/index.js b/deps/npm/node_modules/lru-cache/dist/esm/browser/index.js index 11c8cd8dfbf5cf..114a4e9908390f 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/browser/index.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/browser/index.js @@ -2,12 +2,8 @@ * @module LRUCache */ import { metrics, tracing } from './diagnostics-channel.js'; +import { defaultPerf } from './perf.js'; const hasSubscribers = () => metrics.hasSubscribers || tracing.hasSubscribers; -const defaultPerf = (typeof performance === 'object' && - performance && - typeof performance.now === 'function') ? - performance - : Date; const warned = new Set(); /* c8 ignore start */ const PROCESS = (typeof process === 'object' && !!process ? @@ -49,7 +45,9 @@ class ZeroArray extends Array { } } class Stack { + /* c8 ignore start - not sure why this is showing up uncovered?? */ heap; + /* c8 ignore stop */ length; // private constructor static #constructing = false; @@ -169,6 +167,8 @@ export class LRUCache { * {@link LRUCache.OptionsBase.ignoreFetchAbort} */ ignoreFetchAbort; + /** {@link LRUCache.OptionsBase.backgroundFetchSize} */ + backgroundFetchSize; // computed properties #size; #calculatedSize; @@ -279,7 +279,8 @@ export class LRUCache { return this.#disposeAfter; } constructor(options) { - const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, perf, } = options; + const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options; + this.backgroundFetchSize = backgroundFetchSize; if (perf !== undefined) { if (typeof perf?.now !== 'function') { throw new TypeError('perf option must have a now() method if specified'); @@ -507,12 +508,15 @@ export class LRUCache { sizes[index] = 0; }; this.#requireSize = (k, v, size, sizeCalculation) => { - // provisionally accept background fetches. - // actual value size will be checked when they return. - if (this.#isBackgroundFetch(v)) { - return 0; - } if (!isPosInt(size)) { + // provisionally accept background fetches. + // actual value size will be checked when they return. + if (this.#isBackgroundFetch(v)) { + // NB: this cannot occur if v.__staleWhileFetching is set, + // because in that case, it would take on the size of the + // existing entry that it temporarily replaces. + return this.backgroundFetchSize; + } if (sizeCalculation) { if (typeof sizeCalculation !== 'function') { throw new TypeError('sizeCalculation must be a function'); @@ -879,6 +883,7 @@ export class LRUCache { status.key = k; if (v !== undefined) status.value = v; + status.cache = this; } const result = this.#set(k, v, setOptions); if (status && metrics.hasSubscribers) { @@ -886,8 +891,9 @@ export class LRUCache { } return result; } - #set(k, v, setOptions = {}) { + #set(k, v, setOptions, bf) { const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions; + const isBF = this.#isBackgroundFetch(v); if (v === undefined) { if (status) status.set = 'deleted'; @@ -895,7 +901,7 @@ export class LRUCache { return this; } let { noUpdateTTL = this.noUpdateTTL } = setOptions; - if (status && !this.#isBackgroundFetch(v)) + if (status && !isBF) status.value = v; const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status); // if the item doesn't fit, don't do anything @@ -927,52 +933,68 @@ export class LRUCache { if (status) status.set = 'add'; noUpdateTTL = false; - if (this.#hasOnInsert) { + if (this.#hasOnInsert && !isBF) { this.#onInsert?.(v, k, 'add'); } } else { // update + // might be updating a background fetch! this.#moveToTail(index); const oldVal = this.#valList[index]; if (v !== oldVal) { - if (this.#hasFetchMethod && this.#isBackgroundFetch(oldVal)) { - oldVal.__abortController.abort(new Error('replaced')); - const { __staleWhileFetching: s } = oldVal; - if (s !== undefined && !noDisposeOnSet) { + if (!noDisposeOnSet) { + if (this.#isBackgroundFetch(oldVal)) { + if (oldVal !== bf) { + // setting over a background fetch, not merely resolving it. + oldVal.__abortController.abort(new Error('replaced')); + } + const { __staleWhileFetching: s } = oldVal; + if (s !== undefined && s !== v) { + if (this.#hasDispose) { + this.#dispose?.(s, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([s, k, 'set']); + } + } + } + else { if (this.#hasDispose) { - this.#dispose?.(s, k, 'set'); + this.#dispose?.(oldVal, k, 'set'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([s, k, 'set']); + this.#disposed?.push([oldVal, k, 'set']); } } } - else if (!noDisposeOnSet) { - if (this.#hasDispose) { - this.#dispose?.(oldVal, k, 'set'); - } - if (this.#hasDisposeAfter) { - this.#disposed?.push([oldVal, k, 'set']); - } - } this.#removeItemSize(index); this.#addItemSize(index, size, status); this.#valList[index] = v; - if (status) { - status.set = 'replace'; + if (!isBF) { const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ? oldVal.__staleWhileFetching : oldVal; - if (oldValue !== undefined) - status.oldValue = oldValue; + const setType = oldValue === undefined ? 'add' + : v !== oldValue ? 'replace' + : 'update'; + if (status) { + status.set = setType; + if (oldValue !== undefined) + status.oldValue = oldValue; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, setType); + } } } - else if (status) { - status.set = 'update'; - } - if (this.#hasOnInsert) { - this.onInsert?.(v, k, v === oldVal ? 'update' : 'replace'); + else if (!isBF) { + if (status) { + status.set = 'update'; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, 'update'); + } } } if (ttl !== 0 && !this.#ttls) { @@ -1027,15 +1049,18 @@ export class LRUCache { const head = this.#head; const k = this.#keyList[head]; const v = this.#valList[head]; - if (this.#hasFetchMethod && this.#isBackgroundFetch(v)) { + const isBF = this.#isBackgroundFetch(v); + if (isBF) { v.__abortController.abort(new Error('evicted')); } - else if (this.#hasDispose || this.#hasDisposeAfter) { + const oldValue = isBF ? v.__staleWhileFetching : v; + if ((this.#hasDispose || this.#hasDisposeAfter) && + oldValue !== undefined) { if (this.#hasDispose) { - this.#dispose?.(v, k, 'evict'); + this.#dispose?.(oldValue, k, 'evict'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([v, k, 'evict']); + this.#disposed?.push([oldValue, k, 'evict']); } } this.#removeItemSize(head); @@ -1082,6 +1107,7 @@ export class LRUCache { if (status) { status.op = 'has'; status.key = k; + status.cache = this; } const result = this.#has(k, hasOptions); if (metrics.hasSubscribers) @@ -1129,6 +1155,7 @@ export class LRUCache { if (status) { status.op = 'peek'; status.key = k; + status.cache = this; } peekOptions.status = status; const result = this.#peek(k, peekOptions); @@ -1211,7 +1238,7 @@ export class LRUCache { else { if (options.status) options.status.fetchUpdated = true; - this.#set(k, v, fetchOpts.options); + this.#set(k, v, fetchOpts.options, bf); } } return v; @@ -1257,9 +1284,6 @@ export class LRUCache { }; const pcall = (res, rej) => { const fmp = this.#fetchMethod?.(k, v, fetchOpts); - if (fmp && fmp instanceof Promise) { - fmp.then(v => res(v === undefined ? undefined : v), rej); - } // ignored, we go until we finish, regardless. // defer check until we are actually aborting, // so fetchMethod can override. @@ -1272,6 +1296,12 @@ export class LRUCache { } } }); + if (fmp && fmp instanceof Promise) { + fmp.then(v => res(v === undefined ? undefined : v), rej); + } + else if (fmp !== undefined) { + res(fmp); + } }; if (options.status) options.status.fetchDispatched = true; @@ -1287,6 +1317,10 @@ export class LRUCache { index = this.#keyMap.get(k); } else { + // do not call #set, because we do not want to adjust its place + // in the lru queue, as it has not yet been "used". Also, we don't + // need to worry about evicting for size, because a background fetch + // over a stale value is treated as the same size as its stale value. this.#valList[index] = bf; } return bf; @@ -1308,11 +1342,9 @@ export class LRUCache { status.context = fetchOptions.context; } const p = this.#fetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1329,6 +1361,7 @@ export class LRUCache { status.key = k; if (forceRefresh) status.forceRefresh = true; + status.cache = this; } if (!this.#hasFetchMethod) { if (status) @@ -1410,11 +1443,9 @@ export class LRUCache { status.context = fetchOptions.context; } const p = this.#forceFetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1433,6 +1464,7 @@ export class LRUCache { if (memoOptions.context) { status.context = memoOptions.context; } + status.cache = this; } const result = this.#memo(k, memoOptions); if (status) @@ -1479,6 +1511,7 @@ export class LRUCache { if (status) { status.op = 'get'; status.key = k; + status.cache = this; } const result = this.#get(k, getOptions); if (status) { @@ -1577,6 +1610,7 @@ export class LRUCache { op: 'delete', delete: reason, key: k, + cache: this, }); } let deleted = false; @@ -1657,7 +1691,7 @@ export class LRUCache { } } this.#keyMap.clear(); - this.#valList.fill(undefined); + void this.#valList.fill(undefined); this.#keyList.fill(undefined); if (this.#ttls && this.#starts) { this.#ttls.fill(0); diff --git a/deps/npm/node_modules/lru-cache/dist/esm/browser/index.min.js b/deps/npm/node_modules/lru-cache/dist/esm/browser/index.min.js index 50dd21a570e15f..86618ad212c13e 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/browser/index.min.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/browser/index.min.js @@ -1,2 +1,2 @@ -var C={hasSubscribers:!1},S=C,W=C;var D=()=>S.hasSubscribers||W.hasSubscribers,I=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,U=new Set,L=typeof process=="object"&&process?process:{},G=(u,e,t,i)=>{typeof L.emitWarning=="function"?L.emitWarning(u,e,t,i):console.error(`[${t}] ${e}: ${u}`)},P=u=>!U.has(u),V=Symbol("type"),F=u=>!!u&&u===Math.floor(u)&&u>0&&isFinite(u),j=u=>F(u)?u<=Math.pow(2,8)?Uint8Array:u<=Math.pow(2,16)?Uint16Array:u<=Math.pow(2,32)?Uint32Array:u<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(e){super(e),this.fill(0)}},R=class u{heap;length;static#o=!1;static create(e){let t=j(e);if(!t)return[];u.#o=!0;let i=new u(e,t);return u.#o=!1,i}constructor(e,t){if(!u.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new t(e),this.length=0}push(e){this.heap[this.length++]=e}pop(){return this.heap[--this.length]}},M=class u{#o;#u;#w;#D;#S;#M;#U;#m;get perf(){return this.#m}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#n;#b;#s;#i;#t;#a;#c;#l;#h;#y;#r;#_;#F;#d;#g;#T;#W;#f;#j;static unsafeExposeInternals(e){return{starts:e.#F,ttls:e.#d,autopurgeTimers:e.#g,sizes:e.#_,keyMap:e.#s,keyList:e.#i,valList:e.#t,next:e.#a,prev:e.#c,get head(){return e.#l},get tail(){return e.#h},free:e.#y,isBackgroundFetch:t=>e.#e(t),backgroundFetch:(t,i,s,n)=>e.#P(t,i,s,n),moveToTail:t=>e.#L(t),indexes:t=>e.#A(t),rindexes:t=>e.#z(t),isStale:t=>e.#p(t)}}get max(){return this.#o}get maxSize(){return this.#u}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#U}get dispose(){return this.#w}get onInsert(){return this.#D}get disposeAfter(){return this.#S}constructor(e){let{max:t=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:o,updateAgeOnHas:r,allowStale:h,dispose:l,onInsert:c,disposeAfter:f,noDisposeOnSet:g,noUpdateTTL:p,maxSize:T=0,maxEntrySize:w=0,sizeCalculation:y,fetchMethod:a,memoMethod:m,noDeleteOnFetchRejection:_,noDeleteOnStaleGet:b,allowStaleOnFetchRejection:d,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,perf:x}=e;if(x!==void 0&&typeof x?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#m=x??I,t!==0&&!F(t))throw new TypeError("max option must be a nonnegative integer");let v=t?j(t):Array;if(!v)throw new Error("invalid max value: "+t);if(this.#o=t,this.#u=T,this.maxEntrySize=w||this.#u,this.sizeCalculation=y,this.sizeCalculation){if(!this.#u&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(m!==void 0&&typeof m!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#U=m,a!==void 0&&typeof a!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=a,this.#W=!!a,this.#s=new Map,this.#i=Array.from({length:t}).fill(void 0),this.#t=Array.from({length:t}).fill(void 0),this.#a=new v(t),this.#c=new v(t),this.#l=0,this.#h=0,this.#y=R.create(t),this.#n=0,this.#b=0,typeof l=="function"&&(this.#w=l),typeof c=="function"&&(this.#D=c),typeof f=="function"?(this.#S=f,this.#r=[]):(this.#S=void 0,this.#r=void 0),this.#T=!!this.#w,this.#j=!!this.#D,this.#f=!!this.#S,this.noDisposeOnSet=!!g,this.noUpdateTTL=!!p,this.noDeleteOnFetchRejection=!!_,this.allowStaleOnFetchRejection=!!d,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#u!==0&&!F(this.#u))throw new TypeError("maxSize must be a positive integer if specified");if(!F(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!h,this.noDeleteOnStaleGet=!!b,this.updateAgeOnGet=!!o,this.updateAgeOnHas=!!r,this.ttlResolution=F(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!F(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#H()}if(this.#o===0&&this.ttl===0&&this.#u===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#u){let E="LRU_CACHE_UNBOUNDED";P(E)&&(U.add(E),G("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",E,u))}}getRemainingTTL(e){return this.#s.has(e)?1/0:0}#H(){let e=new O(this.#o),t=new O(this.#o);this.#d=e,this.#F=t;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#N=(r,h,l=this.#m.now())=>{t[r]=h!==0?l:0,e[r]=h,s(r,h)},this.#x=r=>{t[r]=e[r]!==0?this.#m.now():0,s(r,e[r])};let s=this.ttlAutopurge?(r,h)=>{if(i?.[r]&&(clearTimeout(i[r]),i[r]=void 0),h&&h!==0&&i){let l=setTimeout(()=>{this.#p(r)&&this.#v(this.#i[r],"expire")},h+1);l.unref&&l.unref(),i[r]=l}}:()=>{};this.#E=(r,h)=>{if(e[h]){let l=e[h],c=t[h];if(!l||!c)return;r.ttl=l,r.start=c,r.now=n||o();let f=r.now-c;r.remainingTTL=l-f}};let n=0,o=()=>{let r=this.#m.now();if(this.ttlResolution>0){n=r;let h=setTimeout(()=>n=0,this.ttlResolution);h.unref&&h.unref()}return r};this.getRemainingTTL=r=>{let h=this.#s.get(r);if(h===void 0)return 0;let l=e[h],c=t[h];if(!l||!c)return 1/0;let f=(n||o())-c;return l-f},this.#p=r=>{let h=t[r],l=e[r];return!!l&&!!h&&(n||o())-h>l}}#x=()=>{};#E=()=>{};#N=()=>{};#p=()=>!1;#X(){let e=new O(this.#o);this.#b=0,this.#_=e,this.#R=t=>{this.#b-=e[t],e[t]=0},this.#k=(t,i,s,n)=>{if(this.#e(i))return 0;if(!F(s))if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,t),!F(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");return s},this.#I=(t,i,s)=>{if(e[t]=i,this.#u){let n=this.#u-e[t];for(;this.#b>n;)this.#G(!0)}this.#b+=e[t],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#R=e=>{};#I=(e,t,i)=>{};#k=(e,t,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#l);)t=this.#c[t]}*#z({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#l;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#h);)t=this.#a[t]}#V(e){return e!==void 0&&this.#s.get(this.#i[e])===e}*entries(){for(let e of this.#A())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*rentries(){for(let e of this.#z())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*keys(){for(let e of this.#A()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*rkeys(){for(let e of this.#z()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*values(){for(let e of this.#A())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}*rvalues(){for(let e of this.#z())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(e,t={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&e(n,this.#i[i],this))return this.#C(this.#i[i],t)}}forEach(e,t=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}rforEach(e,t=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}purgeStale(){let e=!1;for(let t of this.#z({allowStale:!0}))this.#p(t)&&(this.#v(this.#i[t],"expire"),e=!0);return e}info(e){let t=this.#s.get(e);if(t===void 0)return;let i=this.#t[t],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let o=this.#d[t],r=this.#F[t];if(o&&r){let h=o-(this.#m.now()-r);n.ttl=h,n.start=Date.now()}}return this.#_&&(n.size=this.#_[t]),n}dump(){let e=[];for(let t of this.#A({allowStale:!0})){let i=this.#i[t],s=this.#t[t],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let o={value:n};if(this.#d&&this.#F){o.ttl=this.#d[t];let r=this.#m.now()-this.#F[t];o.start=Math.floor(Date.now()-r)}this.#_&&(o.size=this.#_[t]),e.unshift([i,o])}return e}load(e){this.clear();for(let[t,i]of e){if(i.start){let s=Date.now()-i.start;i.start=this.#m.now()-s}this.#O(t,i.value,i)}}set(e,t,i={}){let{status:s=S.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=e,t!==void 0&&(s.value=t));let n=this.#O(e,t,i);return s&&S.hasSubscribers&&S.publish(s),n}#O(e,t,i={}){let{ttl:s=this.ttl,start:n,noDisposeOnSet:o=this.noDisposeOnSet,sizeCalculation:r=this.sizeCalculation,status:h}=i;if(t===void 0)return h&&(h.set="deleted"),this.delete(e),this;let{noUpdateTTL:l=this.noUpdateTTL}=i;h&&!this.#e(t)&&(h.value=t);let c=this.#k(e,t,i.size||0,r,h);if(this.maxEntrySize&&c>this.maxEntrySize)return this.#v(e,"set"),h&&(h.set="miss",h.maxEntrySizeExceeded=!0),this;let f=this.#n===0?void 0:this.#s.get(e);if(f===void 0)f=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[f]=e,this.#t[f]=t,this.#s.set(e,f),this.#a[this.#h]=f,this.#c[f]=this.#h,this.#h=f,this.#n++,this.#I(f,c,h),h&&(h.set="add"),l=!1,this.#j&&this.#D?.(t,e,"add");else{this.#L(f);let g=this.#t[f];if(t!==g){if(this.#W&&this.#e(g)){g.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:p}=g;p!==void 0&&!o&&(this.#T&&this.#w?.(p,e,"set"),this.#f&&this.#r?.push([p,e,"set"]))}else o||(this.#T&&this.#w?.(g,e,"set"),this.#f&&this.#r?.push([g,e,"set"]));if(this.#R(f),this.#I(f,c,h),this.#t[f]=t,h){h.set="replace";let p=g&&this.#e(g)?g.__staleWhileFetching:g;p!==void 0&&(h.oldValue=p)}}else h&&(h.set="update");this.#j&&this.onInsert?.(t,e,t===g?"update":"replace")}if(s!==0&&!this.#d&&this.#H(),this.#d&&(l||this.#N(f,s,n),h&&this.#E(h,f)),!o&&this.#f&&this.#r){let g=this.#r,p;for(;p=g?.shift();)this.#S?.(...p)}return this}pop(){try{for(;this.#n;){let e=this.#t[this.#l];if(this.#G(!0),this.#e(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#f&&this.#r){let e=this.#r,t;for(;t=e?.shift();)this.#S?.(...t)}}}#G(e){let t=this.#l,i=this.#i[t],s=this.#t[t];return this.#W&&this.#e(s)?s.__abortController.abort(new Error("evicted")):(this.#T||this.#f)&&(this.#T&&this.#w?.(s,i,"evict"),this.#f&&this.#r?.push([s,i,"evict"])),this.#R(t),this.#g?.[t]&&(clearTimeout(this.#g[t]),this.#g[t]=void 0),e&&(this.#i[t]=void 0,this.#t[t]=void 0,this.#y.push(t)),this.#n===1?(this.#l=this.#h=0,this.#y.length=0):this.#l=this.#a[t],this.#s.delete(i),this.#n--,t}has(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="has",i.key=e);let s=this.#Y(e,t);return S.hasSubscribers&&S.publish(i),s}#Y(e,t={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=t,n=this.#s.get(e);if(n!==void 0){let o=this.#t[n];if(this.#e(o)&&o.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#x(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(e,t={}){let{status:i=D()?{}:void 0}=t;i&&(i.op="peek",i.key=e),t.status=i;let s=this.#J(e,t);return S.hasSubscribers&&S.publish(i),s}#J(e,t){let{status:i,allowStale:s=this.allowStale}=t,n=this.#s.get(e);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let o=this.#t[n],r=this.#e(o)?o.__staleWhileFetching:o;return i&&(r!==void 0?(i.peek="hit",i.value=r):i.peek="miss"),r}#P(e,t,i,s){let n=t===void 0?void 0:this.#t[t];if(this.#e(n))return n;let o=new AbortController,{signal:r}=i;r?.addEventListener("abort",()=>o.abort(r.reason),{signal:o.signal});let h={signal:o.signal,options:i,context:s},l=(w,y=!1)=>{let{aborted:a}=o.signal,m=i.ignoreFetchAbort&&w!==void 0,_=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&w!==void 0);if(i.status&&(a&&!y?(i.status.fetchAborted=!0,i.status.fetchError=o.signal.reason,m&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),a&&!m&&!y)return f(o.signal.reason,_);let b=p,d=this.#t[t];return(d===p||d===void 0&&m&&y)&&(w===void 0?b.__staleWhileFetching!==void 0?this.#t[t]=b.__staleWhileFetching:this.#v(e,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#O(e,w,h.options))),w},c=w=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=w),f(w,!1)),f=(w,y)=>{let{aborted:a}=o.signal,m=a&&i.allowStaleOnFetchAbort,_=m||i.allowStaleOnFetchRejection,b=_||i.noDeleteOnFetchRejection,d=p;if(this.#t[t]===p&&(!b||!y&&d.__staleWhileFetching===void 0?this.#v(e,"fetch"):m||(this.#t[t]=d.__staleWhileFetching)),_)return i.status&&d.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),d.__staleWhileFetching;if(d.__returned===d)throw w},g=(w,y)=>{let a=this.#M?.(e,n,h);a&&a instanceof Promise&&a.then(m=>w(m===void 0?void 0:m),y),o.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(w(void 0),i.allowStaleOnFetchAbort&&(w=m=>l(m,!0)))})};i.status&&(i.status.fetchDispatched=!0);let p=new Promise(g).then(l,c),T=Object.assign(p,{__abortController:o,__staleWhileFetching:n,__returned:void 0});return t===void 0?(this.#O(e,T,{...h.options,status:void 0}),t=this.#s.get(e)):this.#t[t]=T,T}#e(e){if(!this.#W)return!1;let t=e;return!!t&&t instanceof Promise&&t.hasOwnProperty("__staleWhileFetching")&&t.__abortController instanceof AbortController}fetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#B(e,t);return s&&D()&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#B(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:o=this.ttl,noDisposeOnSet:r=this.noDisposeOnSet,size:h=0,sizeCalculation:l=this.sizeCalculation,noUpdateTTL:c=this.noUpdateTTL,noDeleteOnFetchRejection:f=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:g=this.allowStaleOnFetchRejection,ignoreFetchAbort:p=this.ignoreFetchAbort,allowStaleOnFetchAbort:T=this.allowStaleOnFetchAbort,context:w,forceRefresh:y=!1,status:a,signal:m}=t;if(a&&(a.op="fetch",a.key=e,y&&(a.forceRefresh=!0)),!this.#W)return a&&(a.fetch="get"),this.#C(e,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:a});let _={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:o,noDisposeOnSet:r,size:h,sizeCalculation:l,noUpdateTTL:c,noDeleteOnFetchRejection:f,allowStaleOnFetchRejection:g,allowStaleOnFetchAbort:T,ignoreFetchAbort:p,status:a,signal:m},b=this.#s.get(e);if(b===void 0){a&&(a.fetch="miss");let d=this.#P(e,b,_,w);return d.__returned=d}else{let d=this.#t[b];if(this.#e(d)){let E=i&&d.__staleWhileFetching!==void 0;return a&&(a.fetch="inflight",E&&(a.returnedStale=!0)),E?d.__staleWhileFetching:d.__returned=d}let A=this.#p(b);if(!y&&!A)return a&&(a.fetch="hit"),this.#L(b),s&&this.#x(b),a&&this.#E(a,b),d;let z=this.#P(e,b,_,w),v=z.__staleWhileFetching!==void 0&&i;return a&&(a.fetch=A?"stale":"refresh",v&&A&&(a.returnedStale=!0)),v?z.__staleWhileFetching:z.__returned=z}}forceFetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#K(e,t);return s&&D()&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#K(e,t={}){let i=await this.#B(e,t);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="memo",i.key=e,t.context&&(i.context=t.context));let s=this.#Q(e,t);return i&&(i.value=s),S.hasSubscribers&&S.publish(i),s}#Q(e,t={}){let i=this.#U;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:o,...r}=t;n&&o&&(n.forceRefresh=!0);let h=this.#C(e,r),l=o||h===void 0;if(n&&(n.memo=l?"miss":"hit",l||(n.value=h)),!l)return h;let c=i(e,h,{options:r,context:s});return n&&(n.value=c),this.#O(e,c,r),c}get(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="get",i.key=e);let s=this.#C(e,t);return i&&(s!==void 0&&(i.value=s),S.hasSubscribers&&S.publish(i)),s}#C(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:o}=t,r=this.#s.get(e);if(r===void 0){o&&(o.get="miss");return}let h=this.#t[r],l=this.#e(h);return o&&this.#E(o,r),this.#p(r)?l?(o&&(o.get="stale-fetching"),i&&h.__staleWhileFetching!==void 0?(o&&(o.returnedStale=!0),h.__staleWhileFetching):void 0):(n||this.#v(e,"expire"),o&&(o.get="stale"),i?(o&&(o.returnedStale=!0),h):void 0):(o&&(o.get=l?"fetching":"hit"),this.#L(r),s&&this.#x(r),l?h.__staleWhileFetching:h)}#$(e,t){this.#c[t]=e,this.#a[e]=t}#L(e){e!==this.#h&&(e===this.#l?this.#l=this.#a[e]:this.#$(this.#c[e],this.#a[e]),this.#$(this.#h,e),this.#h=e)}delete(e){return this.#v(e,"delete")}#v(e,t){S.hasSubscribers&&S.publish({op:"delete",delete:t,key:e});let i=!1;if(this.#n!==0){let s=this.#s.get(e);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#q(t);else{this.#R(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#w?.(n,e,t),this.#f&&this.#r?.push([n,e,t])),this.#s.delete(e),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#c[s];else if(s===this.#l)this.#l=this.#a[s];else{let o=this.#c[s];this.#a[o]=this.#a[s];let r=this.#a[s];this.#c[r]=this.#c[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#S?.(...n)}return i}clear(){return this.#q("delete")}#q(e){for(let t of this.#z({allowStale:!0})){let i=this.#t[t];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[t];this.#T&&this.#w?.(i,s,e),this.#f&&this.#r?.push([i,s,e])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let t of this.#g??[])t!==void 0&&clearTimeout(t);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#l=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let t=this.#r,i;for(;i=t?.shift();)this.#S?.(...i)}}};export{M as LRUCache}; +var L={hasSubscribers:!1},S=L,W=L;var M=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date;var D=()=>S.hasSubscribers||W.hasSubscribers,j=new Set,I=typeof process=="object"&&process?process:{},P=(u,e,t,i)=>{typeof I.emitWarning=="function"?I.emitWarning(u,e,t,i):console.error(`[${t}] ${e}: ${u}`)},k=u=>!j.has(u);var T=u=>!!u&&u===Math.floor(u)&&u>0&&isFinite(u),G=u=>T(u)?u<=Math.pow(2,8)?Uint8Array:u<=Math.pow(2,16)?Uint16Array:u<=Math.pow(2,32)?Uint32Array:u<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(e){super(e),this.fill(0)}},R=class u{heap;length;static#o=!1;static create(e){let t=G(e);if(!t)return[];u.#o=!0;let i=new u(e,t);return u.#o=!1,i}constructor(e,t){if(!u.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new t(e),this.length=0}push(e){this.heap[this.length++]=e}pop(){return this.heap[--this.length]}},U=class u{#o;#c;#S;#O;#w;#M;#I;#m;get perf(){return this.#m}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;backgroundFetchSize;#n;#b;#s;#i;#t;#l;#u;#a;#h;#y;#r;#_;#F;#d;#g;#T;#U;#f;#x;static unsafeExposeInternals(e){return{starts:e.#F,ttls:e.#d,autopurgeTimers:e.#g,sizes:e.#_,keyMap:e.#s,keyList:e.#i,valList:e.#t,next:e.#l,prev:e.#u,get head(){return e.#a},get tail(){return e.#h},free:e.#y,isBackgroundFetch:t=>e.#e(t),backgroundFetch:(t,i,s,n)=>e.#P(t,i,s,n),moveToTail:t=>e.#L(t),indexes:t=>e.#A(t),rindexes:t=>e.#z(t),isStale:t=>e.#p(t)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#I}get dispose(){return this.#S}get onInsert(){return this.#O}get disposeAfter(){return this.#w}constructor(e){let{max:t=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:r,updateAgeOnHas:h,allowStale:a,dispose:o,onInsert:d,disposeAfter:y,noDisposeOnSet:_,noUpdateTTL:c,maxSize:g=0,maxEntrySize:f=0,sizeCalculation:b,fetchMethod:l,memoMethod:w,noDeleteOnFetchRejection:F,noDeleteOnStaleGet:m,allowStaleOnFetchRejection:p,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,backgroundFetchSize:C=1,perf:E}=e;if(this.backgroundFetchSize=C,E!==void 0&&typeof E?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#m=E??M,t!==0&&!T(t))throw new TypeError("max option must be a nonnegative integer");let v=t?G(t):Array;if(!v)throw new Error("invalid max value: "+t);if(this.#o=t,this.#c=g,this.maxEntrySize=f||this.#c,this.sizeCalculation=b,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(w!==void 0&&typeof w!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#I=w,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#U=!!l,this.#s=new Map,this.#i=Array.from({length:t}).fill(void 0),this.#t=Array.from({length:t}).fill(void 0),this.#l=new v(t),this.#u=new v(t),this.#a=0,this.#h=0,this.#y=R.create(t),this.#n=0,this.#b=0,typeof o=="function"&&(this.#S=o),typeof d=="function"&&(this.#O=d),typeof y=="function"?(this.#w=y,this.#r=[]):(this.#w=void 0,this.#r=void 0),this.#T=!!this.#S,this.#x=!!this.#O,this.#f=!!this.#w,this.noDisposeOnSet=!!_,this.noUpdateTTL=!!c,this.noDeleteOnFetchRejection=!!F,this.allowStaleOnFetchRejection=!!p,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#c!==0&&!T(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!T(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!a,this.noDeleteOnStaleGet=!!m,this.updateAgeOnGet=!!r,this.updateAgeOnHas=!!h,this.ttlResolution=T(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!T(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let x="LRU_CACHE_UNBOUNDED";k(x)&&(j.add(x),P("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",x,u))}}getRemainingTTL(e){return this.#s.has(e)?1/0:0}#k(){let e=new O(this.#o),t=new O(this.#o);this.#d=e,this.#F=t;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#H=(h,a,o=this.#m.now())=>{t[h]=a!==0?o:0,e[h]=a,s(h,a)},this.#D=h=>{t[h]=e[h]!==0?this.#m.now():0,s(h,e[h])};let s=this.ttlAutopurge?(h,a)=>{if(i?.[h]&&(clearTimeout(i[h]),i[h]=void 0),a&&a!==0&&i){let o=setTimeout(()=>{this.#p(h)&&this.#E(this.#i[h],"expire")},a+1);o.unref&&o.unref(),i[h]=o}}:()=>{};this.#v=(h,a)=>{if(e[a]){let o=e[a],d=t[a];if(!o||!d)return;h.ttl=o,h.start=d,h.now=n||r();let y=h.now-d;h.remainingTTL=o-y}};let n=0,r=()=>{let h=this.#m.now();if(this.ttlResolution>0){n=h;let a=setTimeout(()=>n=0,this.ttlResolution);a.unref&&a.unref()}return h};this.getRemainingTTL=h=>{let a=this.#s.get(h);if(a===void 0)return 0;let o=e[a],d=t[a];if(!o||!d)return 1/0;let y=(n||r())-d;return o-y},this.#p=h=>{let a=t[h],o=e[h];return!!o&&!!a&&(n||r())-a>o}}#D=()=>{};#v=()=>{};#H=()=>{};#p=()=>!1;#X(){let e=new O(this.#o);this.#b=0,this.#_=e,this.#R=t=>{this.#b-=e[t],e[t]=0},this.#N=(t,i,s,n)=>{if(!T(s)){if(this.#e(i))return this.backgroundFetchSize;if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,t),!T(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.")}return s},this.#j=(t,i,s)=>{if(e[t]=i,this.#c){let n=this.#c-e[t];for(;this.#b>n;)this.#G(!0)}this.#b+=e[t],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#R=e=>{};#j=(e,t,i)=>{};#N=(e,t,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#a);)t=this.#u[t]}*#z({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#a;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#h);)t=this.#l[t]}#V(e){return e!==void 0&&this.#s.get(this.#i[e])===e}*entries(){for(let e of this.#A())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*rentries(){for(let e of this.#z())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*keys(){for(let e of this.#A()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*rkeys(){for(let e of this.#z()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*values(){for(let e of this.#A())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}*rvalues(){for(let e of this.#z())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(e,t={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&e(n,this.#i[i],this))return this.#C(this.#i[i],t)}}forEach(e,t=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}rforEach(e,t=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}purgeStale(){let e=!1;for(let t of this.#z({allowStale:!0}))this.#p(t)&&(this.#E(this.#i[t],"expire"),e=!0);return e}info(e){let t=this.#s.get(e);if(t===void 0)return;let i=this.#t[t],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let r=this.#d[t],h=this.#F[t];if(r&&h){let a=r-(this.#m.now()-h);n.ttl=a,n.start=Date.now()}}return this.#_&&(n.size=this.#_[t]),n}dump(){let e=[];for(let t of this.#A({allowStale:!0})){let i=this.#i[t],s=this.#t[t],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let r={value:n};if(this.#d&&this.#F){r.ttl=this.#d[t];let h=this.#m.now()-this.#F[t];r.start=Math.floor(Date.now()-h)}this.#_&&(r.size=this.#_[t]),e.unshift([i,r])}return e}load(e){this.clear();for(let[t,i]of e){if(i.start){let s=Date.now()-i.start;i.start=this.#m.now()-s}this.#W(t,i.value,i)}}set(e,t,i={}){let{status:s=S.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=e,t!==void 0&&(s.value=t),s.cache=this);let n=this.#W(e,t,i);return s&&S.hasSubscribers&&S.publish(s),n}#W(e,t,i,s){let{ttl:n=this.ttl,start:r,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:o}=i,d=this.#e(t);if(t===void 0)return o&&(o.set="deleted"),this.delete(e),this;let{noUpdateTTL:y=this.noUpdateTTL}=i;o&&!d&&(o.value=t);let _=this.#N(e,t,i.size||0,a,o);if(this.maxEntrySize&&_>this.maxEntrySize)return this.#E(e,"set"),o&&(o.set="miss",o.maxEntrySizeExceeded=!0),this;let c=this.#n===0?void 0:this.#s.get(e);if(c===void 0)c=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[c]=e,this.#t[c]=t,this.#s.set(e,c),this.#l[this.#h]=c,this.#u[c]=this.#h,this.#h=c,this.#n++,this.#j(c,_,o),o&&(o.set="add"),y=!1,this.#x&&!d&&this.#O?.(t,e,"add");else{this.#L(c);let g=this.#t[c];if(t!==g){if(!h)if(this.#e(g)){g!==s&&g.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:f}=g;f!==void 0&&f!==t&&(this.#T&&this.#S?.(f,e,"set"),this.#f&&this.#r?.push([f,e,"set"]))}else this.#T&&this.#S?.(g,e,"set"),this.#f&&this.#r?.push([g,e,"set"]);if(this.#R(c),this.#j(c,_,o),this.#t[c]=t,!d){let f=g&&this.#e(g)?g.__staleWhileFetching:g,b=f===void 0?"add":t!==f?"replace":"update";o&&(o.set=b,f!==void 0&&(o.oldValue=f)),this.#x&&this.onInsert?.(t,e,b)}}else d||(o&&(o.set="update"),this.#x&&this.onInsert?.(t,e,"update"))}if(n!==0&&!this.#d&&this.#k(),this.#d&&(y||this.#H(c,n,r),o&&this.#v(o,c)),!h&&this.#f&&this.#r){let g=this.#r,f;for(;f=g?.shift();)this.#w?.(...f)}return this}pop(){try{for(;this.#n;){let e=this.#t[this.#a];if(this.#G(!0),this.#e(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#f&&this.#r){let e=this.#r,t;for(;t=e?.shift();)this.#w?.(...t)}}}#G(e){let t=this.#a,i=this.#i[t],s=this.#t[t],n=this.#e(s);n&&s.__abortController.abort(new Error("evicted"));let r=n?s.__staleWhileFetching:s;return(this.#T||this.#f)&&r!==void 0&&(this.#T&&this.#S?.(r,i,"evict"),this.#f&&this.#r?.push([r,i,"evict"])),this.#R(t),this.#g?.[t]&&(clearTimeout(this.#g[t]),this.#g[t]=void 0),e&&(this.#i[t]=void 0,this.#t[t]=void 0,this.#y.push(t)),this.#n===1?(this.#a=this.#h=0,this.#y.length=0):this.#a=this.#l[t],this.#s.delete(i),this.#n--,t}has(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="has",i.key=e,i.cache=this);let s=this.#Y(e,t);return S.hasSubscribers&&S.publish(i),s}#Y(e,t={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=t,n=this.#s.get(e);if(n!==void 0){let r=this.#t[n];if(this.#e(r)&&r.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#v(s,n));else return i&&this.#D(n),s&&(s.has="hit",this.#v(s,n)),!0}else s&&(s.has="miss");return!1}peek(e,t={}){let{status:i=D()?{}:void 0}=t;i&&(i.op="peek",i.key=e,i.cache=this),t.status=i;let s=this.#J(e,t);return S.hasSubscribers&&S.publish(i),s}#J(e,t){let{status:i,allowStale:s=this.allowStale}=t,n=this.#s.get(e);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let r=this.#t[n],h=this.#e(r)?r.__staleWhileFetching:r;return i&&(h!==void 0?(i.peek="hit",i.value=h):i.peek="miss"),h}#P(e,t,i,s){let n=t===void 0?void 0:this.#t[t];if(this.#e(n))return n;let r=new AbortController,{signal:h}=i;h?.addEventListener("abort",()=>r.abort(h.reason),{signal:r.signal});let a={signal:r.signal,options:i,context:s},o=(f,b=!1)=>{let{aborted:l}=r.signal,w=i.ignoreFetchAbort&&f!==void 0,F=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&f!==void 0);if(i.status&&(l&&!b?(i.status.fetchAborted=!0,i.status.fetchError=r.signal.reason,w&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!w&&!b)return y(r.signal.reason,F);let m=c,p=this.#t[t];return(p===c||p===void 0&&w&&b)&&(f===void 0?m.__staleWhileFetching!==void 0?this.#t[t]=m.__staleWhileFetching:this.#E(e,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#W(e,f,a.options,m))),f},d=f=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=f),y(f,!1)),y=(f,b)=>{let{aborted:l}=r.signal,w=l&&i.allowStaleOnFetchAbort,F=w||i.allowStaleOnFetchRejection,m=F||i.noDeleteOnFetchRejection,p=c;if(this.#t[t]===c&&(!m||!b&&p.__staleWhileFetching===void 0?this.#E(e,"fetch"):w||(this.#t[t]=p.__staleWhileFetching)),F)return i.status&&p.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),p.__staleWhileFetching;if(p.__returned===p)throw f},_=(f,b)=>{let l=this.#M?.(e,n,a);r.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(f(void 0),i.allowStaleOnFetchAbort&&(f=w=>o(w,!0)))}),l&&l instanceof Promise?l.then(w=>f(w===void 0?void 0:w),b):l!==void 0&&f(l)};i.status&&(i.status.fetchDispatched=!0);let c=new Promise(_).then(o,d),g=Object.assign(c,{__abortController:r,__staleWhileFetching:n,__returned:void 0});return t===void 0?(this.#W(e,g,{...a.options,status:void 0}),t=this.#s.get(e)):this.#t[t]=g,g}#e(e){if(!this.#U)return!1;let t=e;return!!t&&t instanceof Promise&&t.hasOwnProperty("__staleWhileFetching")&&t.__abortController instanceof AbortController}fetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#B(e,t);return s&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#B(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:r=this.ttl,noDisposeOnSet:h=this.noDisposeOnSet,size:a=0,sizeCalculation:o=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:y=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:_=this.allowStaleOnFetchRejection,ignoreFetchAbort:c=this.ignoreFetchAbort,allowStaleOnFetchAbort:g=this.allowStaleOnFetchAbort,context:f,forceRefresh:b=!1,status:l,signal:w}=t;if(l&&(l.op="fetch",l.key=e,b&&(l.forceRefresh=!0),l.cache=this),!this.#U)return l&&(l.fetch="get"),this.#C(e,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let F={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:r,noDisposeOnSet:h,size:a,sizeCalculation:o,noUpdateTTL:d,noDeleteOnFetchRejection:y,allowStaleOnFetchRejection:_,allowStaleOnFetchAbort:g,ignoreFetchAbort:c,status:l,signal:w},m=this.#s.get(e);if(m===void 0){l&&(l.fetch="miss");let p=this.#P(e,m,F,f);return p.__returned=p}else{let p=this.#t[m];if(this.#e(p)){let v=i&&p.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",v&&(l.returnedStale=!0)),v?p.__staleWhileFetching:p.__returned=p}let A=this.#p(m);if(!b&&!A)return l&&(l.fetch="hit"),this.#L(m),s&&this.#D(m),l&&this.#v(l,m),p;let z=this.#P(e,m,F,f),E=z.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=A?"stale":"refresh",E&&A&&(l.returnedStale=!0)),E?z.__staleWhileFetching:z.__returned=z}}forceFetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#K(e,t);return s&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#K(e,t={}){let i=await this.#B(e,t);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="memo",i.key=e,t.context&&(i.context=t.context),i.cache=this);let s=this.#Q(e,t);return i&&(i.value=s),S.hasSubscribers&&S.publish(i),s}#Q(e,t={}){let i=this.#I;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:r,...h}=t;n&&r&&(n.forceRefresh=!0);let a=this.#C(e,h),o=r||a===void 0;if(n&&(n.memo=o?"miss":"hit",o||(n.value=a)),!o)return a;let d=i(e,a,{options:h,context:s});return n&&(n.value=d),this.#W(e,d,h),d}get(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="get",i.key=e,i.cache=this);let s=this.#C(e,t);return i&&(s!==void 0&&(i.value=s),S.hasSubscribers&&S.publish(i)),s}#C(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:r}=t,h=this.#s.get(e);if(h===void 0){r&&(r.get="miss");return}let a=this.#t[h],o=this.#e(a);return r&&this.#v(r,h),this.#p(h)?o?(r&&(r.get="stale-fetching"),i&&a.__staleWhileFetching!==void 0?(r&&(r.returnedStale=!0),a.__staleWhileFetching):void 0):(n||this.#E(e,"expire"),r&&(r.get="stale"),i?(r&&(r.returnedStale=!0),a):void 0):(r&&(r.get=o?"fetching":"hit"),this.#L(h),s&&this.#D(h),o?a.__staleWhileFetching:a)}#$(e,t){this.#u[t]=e,this.#l[e]=t}#L(e){e!==this.#h&&(e===this.#a?this.#a=this.#l[e]:this.#$(this.#u[e],this.#l[e]),this.#$(this.#h,e),this.#h=e)}delete(e){return this.#E(e,"delete")}#E(e,t){S.hasSubscribers&&S.publish({op:"delete",delete:t,key:e,cache:this});let i=!1;if(this.#n!==0){let s=this.#s.get(e);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#q(t);else{this.#R(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#S?.(n,e,t),this.#f&&this.#r?.push([n,e,t])),this.#s.delete(e),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let r=this.#u[s];this.#l[r]=this.#l[s];let h=this.#l[s];this.#u[h]=this.#u[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#w?.(...n)}return i}clear(){return this.#q("delete")}#q(e){for(let t of this.#z({allowStale:!0})){let i=this.#t[t];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[t];this.#T&&this.#S?.(i,s,e),this.#f&&this.#r?.push([i,s,e])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let t of this.#g??[])t!==void 0&&clearTimeout(t);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#a=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let t=this.#r,i;for(;i=t?.shift();)this.#w?.(...i)}}};export{U as LRUCache}; //# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/lru-cache/dist/esm/browser/perf.js b/deps/npm/node_modules/lru-cache/dist/esm/browser/perf.js new file mode 100644 index 00000000000000..f21cd88c8692d7 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/esm/browser/perf.js @@ -0,0 +1,7 @@ +export const defaultPerf = (typeof performance === 'object' && + performance && + typeof performance.now === 'function') ? + /* c8 ignore start - this gets covered, but c8 gets confused */ + performance + : /* c8 ignore stop */ Date; +//# sourceMappingURL=perf.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/esm/index.js b/deps/npm/node_modules/lru-cache/dist/esm/index.js index 11c8cd8dfbf5cf..114a4e9908390f 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/index.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/index.js @@ -2,12 +2,8 @@ * @module LRUCache */ import { metrics, tracing } from './diagnostics-channel.js'; +import { defaultPerf } from './perf.js'; const hasSubscribers = () => metrics.hasSubscribers || tracing.hasSubscribers; -const defaultPerf = (typeof performance === 'object' && - performance && - typeof performance.now === 'function') ? - performance - : Date; const warned = new Set(); /* c8 ignore start */ const PROCESS = (typeof process === 'object' && !!process ? @@ -49,7 +45,9 @@ class ZeroArray extends Array { } } class Stack { + /* c8 ignore start - not sure why this is showing up uncovered?? */ heap; + /* c8 ignore stop */ length; // private constructor static #constructing = false; @@ -169,6 +167,8 @@ export class LRUCache { * {@link LRUCache.OptionsBase.ignoreFetchAbort} */ ignoreFetchAbort; + /** {@link LRUCache.OptionsBase.backgroundFetchSize} */ + backgroundFetchSize; // computed properties #size; #calculatedSize; @@ -279,7 +279,8 @@ export class LRUCache { return this.#disposeAfter; } constructor(options) { - const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, perf, } = options; + const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options; + this.backgroundFetchSize = backgroundFetchSize; if (perf !== undefined) { if (typeof perf?.now !== 'function') { throw new TypeError('perf option must have a now() method if specified'); @@ -507,12 +508,15 @@ export class LRUCache { sizes[index] = 0; }; this.#requireSize = (k, v, size, sizeCalculation) => { - // provisionally accept background fetches. - // actual value size will be checked when they return. - if (this.#isBackgroundFetch(v)) { - return 0; - } if (!isPosInt(size)) { + // provisionally accept background fetches. + // actual value size will be checked when they return. + if (this.#isBackgroundFetch(v)) { + // NB: this cannot occur if v.__staleWhileFetching is set, + // because in that case, it would take on the size of the + // existing entry that it temporarily replaces. + return this.backgroundFetchSize; + } if (sizeCalculation) { if (typeof sizeCalculation !== 'function') { throw new TypeError('sizeCalculation must be a function'); @@ -879,6 +883,7 @@ export class LRUCache { status.key = k; if (v !== undefined) status.value = v; + status.cache = this; } const result = this.#set(k, v, setOptions); if (status && metrics.hasSubscribers) { @@ -886,8 +891,9 @@ export class LRUCache { } return result; } - #set(k, v, setOptions = {}) { + #set(k, v, setOptions, bf) { const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions; + const isBF = this.#isBackgroundFetch(v); if (v === undefined) { if (status) status.set = 'deleted'; @@ -895,7 +901,7 @@ export class LRUCache { return this; } let { noUpdateTTL = this.noUpdateTTL } = setOptions; - if (status && !this.#isBackgroundFetch(v)) + if (status && !isBF) status.value = v; const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status); // if the item doesn't fit, don't do anything @@ -927,52 +933,68 @@ export class LRUCache { if (status) status.set = 'add'; noUpdateTTL = false; - if (this.#hasOnInsert) { + if (this.#hasOnInsert && !isBF) { this.#onInsert?.(v, k, 'add'); } } else { // update + // might be updating a background fetch! this.#moveToTail(index); const oldVal = this.#valList[index]; if (v !== oldVal) { - if (this.#hasFetchMethod && this.#isBackgroundFetch(oldVal)) { - oldVal.__abortController.abort(new Error('replaced')); - const { __staleWhileFetching: s } = oldVal; - if (s !== undefined && !noDisposeOnSet) { + if (!noDisposeOnSet) { + if (this.#isBackgroundFetch(oldVal)) { + if (oldVal !== bf) { + // setting over a background fetch, not merely resolving it. + oldVal.__abortController.abort(new Error('replaced')); + } + const { __staleWhileFetching: s } = oldVal; + if (s !== undefined && s !== v) { + if (this.#hasDispose) { + this.#dispose?.(s, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([s, k, 'set']); + } + } + } + else { if (this.#hasDispose) { - this.#dispose?.(s, k, 'set'); + this.#dispose?.(oldVal, k, 'set'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([s, k, 'set']); + this.#disposed?.push([oldVal, k, 'set']); } } } - else if (!noDisposeOnSet) { - if (this.#hasDispose) { - this.#dispose?.(oldVal, k, 'set'); - } - if (this.#hasDisposeAfter) { - this.#disposed?.push([oldVal, k, 'set']); - } - } this.#removeItemSize(index); this.#addItemSize(index, size, status); this.#valList[index] = v; - if (status) { - status.set = 'replace'; + if (!isBF) { const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ? oldVal.__staleWhileFetching : oldVal; - if (oldValue !== undefined) - status.oldValue = oldValue; + const setType = oldValue === undefined ? 'add' + : v !== oldValue ? 'replace' + : 'update'; + if (status) { + status.set = setType; + if (oldValue !== undefined) + status.oldValue = oldValue; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, setType); + } } } - else if (status) { - status.set = 'update'; - } - if (this.#hasOnInsert) { - this.onInsert?.(v, k, v === oldVal ? 'update' : 'replace'); + else if (!isBF) { + if (status) { + status.set = 'update'; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, 'update'); + } } } if (ttl !== 0 && !this.#ttls) { @@ -1027,15 +1049,18 @@ export class LRUCache { const head = this.#head; const k = this.#keyList[head]; const v = this.#valList[head]; - if (this.#hasFetchMethod && this.#isBackgroundFetch(v)) { + const isBF = this.#isBackgroundFetch(v); + if (isBF) { v.__abortController.abort(new Error('evicted')); } - else if (this.#hasDispose || this.#hasDisposeAfter) { + const oldValue = isBF ? v.__staleWhileFetching : v; + if ((this.#hasDispose || this.#hasDisposeAfter) && + oldValue !== undefined) { if (this.#hasDispose) { - this.#dispose?.(v, k, 'evict'); + this.#dispose?.(oldValue, k, 'evict'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([v, k, 'evict']); + this.#disposed?.push([oldValue, k, 'evict']); } } this.#removeItemSize(head); @@ -1082,6 +1107,7 @@ export class LRUCache { if (status) { status.op = 'has'; status.key = k; + status.cache = this; } const result = this.#has(k, hasOptions); if (metrics.hasSubscribers) @@ -1129,6 +1155,7 @@ export class LRUCache { if (status) { status.op = 'peek'; status.key = k; + status.cache = this; } peekOptions.status = status; const result = this.#peek(k, peekOptions); @@ -1211,7 +1238,7 @@ export class LRUCache { else { if (options.status) options.status.fetchUpdated = true; - this.#set(k, v, fetchOpts.options); + this.#set(k, v, fetchOpts.options, bf); } } return v; @@ -1257,9 +1284,6 @@ export class LRUCache { }; const pcall = (res, rej) => { const fmp = this.#fetchMethod?.(k, v, fetchOpts); - if (fmp && fmp instanceof Promise) { - fmp.then(v => res(v === undefined ? undefined : v), rej); - } // ignored, we go until we finish, regardless. // defer check until we are actually aborting, // so fetchMethod can override. @@ -1272,6 +1296,12 @@ export class LRUCache { } } }); + if (fmp && fmp instanceof Promise) { + fmp.then(v => res(v === undefined ? undefined : v), rej); + } + else if (fmp !== undefined) { + res(fmp); + } }; if (options.status) options.status.fetchDispatched = true; @@ -1287,6 +1317,10 @@ export class LRUCache { index = this.#keyMap.get(k); } else { + // do not call #set, because we do not want to adjust its place + // in the lru queue, as it has not yet been "used". Also, we don't + // need to worry about evicting for size, because a background fetch + // over a stale value is treated as the same size as its stale value. this.#valList[index] = bf; } return bf; @@ -1308,11 +1342,9 @@ export class LRUCache { status.context = fetchOptions.context; } const p = this.#fetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1329,6 +1361,7 @@ export class LRUCache { status.key = k; if (forceRefresh) status.forceRefresh = true; + status.cache = this; } if (!this.#hasFetchMethod) { if (status) @@ -1410,11 +1443,9 @@ export class LRUCache { status.context = fetchOptions.context; } const p = this.#forceFetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1433,6 +1464,7 @@ export class LRUCache { if (memoOptions.context) { status.context = memoOptions.context; } + status.cache = this; } const result = this.#memo(k, memoOptions); if (status) @@ -1479,6 +1511,7 @@ export class LRUCache { if (status) { status.op = 'get'; status.key = k; + status.cache = this; } const result = this.#get(k, getOptions); if (status) { @@ -1577,6 +1610,7 @@ export class LRUCache { op: 'delete', delete: reason, key: k, + cache: this, }); } let deleted = false; @@ -1657,7 +1691,7 @@ export class LRUCache { } } this.#keyMap.clear(); - this.#valList.fill(undefined); + void this.#valList.fill(undefined); this.#keyList.fill(undefined); if (this.#ttls && this.#starts) { this.#ttls.fill(0); diff --git a/deps/npm/node_modules/lru-cache/dist/esm/index.min.js b/deps/npm/node_modules/lru-cache/dist/esm/index.min.js index fab73dbf022822..5715ef55079b71 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/index.min.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/index.min.js @@ -1,2 +1,2 @@ -var C={hasSubscribers:!1},S=C,A=C;import("node:diagnostics_channel").then(u=>{S=u.channel("lru-cache:metrics"),A=u.tracingChannel("lru-cache")}).catch(()=>{});var D=()=>S.hasSubscribers||A.hasSubscribers,I=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,U=new Set,L=typeof process=="object"&&process?process:{},G=(u,e,t,i)=>{typeof L.emitWarning=="function"?L.emitWarning(u,e,t,i):console.error(`[${t}] ${e}: ${u}`)},P=u=>!U.has(u),V=Symbol("type"),F=u=>!!u&&u===Math.floor(u)&&u>0&&isFinite(u),j=u=>F(u)?u<=Math.pow(2,8)?Uint8Array:u<=Math.pow(2,16)?Uint16Array:u<=Math.pow(2,32)?Uint32Array:u<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(e){super(e),this.fill(0)}},R=class u{heap;length;static#o=!1;static create(e){let t=j(e);if(!t)return[];u.#o=!0;let i=new u(e,t);return u.#o=!1,i}constructor(e,t){if(!u.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new t(e),this.length=0}push(e){this.heap[this.length++]=e}pop(){return this.heap[--this.length]}},M=class u{#o;#u;#w;#D;#S;#M;#U;#m;get perf(){return this.#m}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#n;#b;#s;#i;#t;#a;#c;#l;#h;#y;#r;#_;#F;#d;#g;#T;#W;#f;#j;static unsafeExposeInternals(e){return{starts:e.#F,ttls:e.#d,autopurgeTimers:e.#g,sizes:e.#_,keyMap:e.#s,keyList:e.#i,valList:e.#t,next:e.#a,prev:e.#c,get head(){return e.#l},get tail(){return e.#h},free:e.#y,isBackgroundFetch:t=>e.#e(t),backgroundFetch:(t,i,s,n)=>e.#P(t,i,s,n),moveToTail:t=>e.#L(t),indexes:t=>e.#A(t),rindexes:t=>e.#z(t),isStale:t=>e.#p(t)}}get max(){return this.#o}get maxSize(){return this.#u}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#U}get dispose(){return this.#w}get onInsert(){return this.#D}get disposeAfter(){return this.#S}constructor(e){let{max:t=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:o,updateAgeOnHas:r,allowStale:h,dispose:l,onInsert:c,disposeAfter:f,noDisposeOnSet:g,noUpdateTTL:p,maxSize:T=0,maxEntrySize:w=0,sizeCalculation:y,fetchMethod:a,memoMethod:m,noDeleteOnFetchRejection:_,noDeleteOnStaleGet:b,allowStaleOnFetchRejection:d,allowStaleOnFetchAbort:z,ignoreFetchAbort:v,perf:x}=e;if(x!==void 0&&typeof x?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#m=x??I,t!==0&&!F(t))throw new TypeError("max option must be a nonnegative integer");let E=t?j(t):Array;if(!E)throw new Error("invalid max value: "+t);if(this.#o=t,this.#u=T,this.maxEntrySize=w||this.#u,this.sizeCalculation=y,this.sizeCalculation){if(!this.#u&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(m!==void 0&&typeof m!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#U=m,a!==void 0&&typeof a!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=a,this.#W=!!a,this.#s=new Map,this.#i=Array.from({length:t}).fill(void 0),this.#t=Array.from({length:t}).fill(void 0),this.#a=new E(t),this.#c=new E(t),this.#l=0,this.#h=0,this.#y=R.create(t),this.#n=0,this.#b=0,typeof l=="function"&&(this.#w=l),typeof c=="function"&&(this.#D=c),typeof f=="function"?(this.#S=f,this.#r=[]):(this.#S=void 0,this.#r=void 0),this.#T=!!this.#w,this.#j=!!this.#D,this.#f=!!this.#S,this.noDisposeOnSet=!!g,this.noUpdateTTL=!!p,this.noDeleteOnFetchRejection=!!_,this.allowStaleOnFetchRejection=!!d,this.allowStaleOnFetchAbort=!!z,this.ignoreFetchAbort=!!v,this.maxEntrySize!==0){if(this.#u!==0&&!F(this.#u))throw new TypeError("maxSize must be a positive integer if specified");if(!F(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!h,this.noDeleteOnStaleGet=!!b,this.updateAgeOnGet=!!o,this.updateAgeOnHas=!!r,this.ttlResolution=F(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!F(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#H()}if(this.#o===0&&this.ttl===0&&this.#u===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#u){let W="LRU_CACHE_UNBOUNDED";P(W)&&(U.add(W),G("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",W,u))}}getRemainingTTL(e){return this.#s.has(e)?1/0:0}#H(){let e=new O(this.#o),t=new O(this.#o);this.#d=e,this.#F=t;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#N=(r,h,l=this.#m.now())=>{t[r]=h!==0?l:0,e[r]=h,s(r,h)},this.#x=r=>{t[r]=e[r]!==0?this.#m.now():0,s(r,e[r])};let s=this.ttlAutopurge?(r,h)=>{if(i?.[r]&&(clearTimeout(i[r]),i[r]=void 0),h&&h!==0&&i){let l=setTimeout(()=>{this.#p(r)&&this.#v(this.#i[r],"expire")},h+1);l.unref&&l.unref(),i[r]=l}}:()=>{};this.#E=(r,h)=>{if(e[h]){let l=e[h],c=t[h];if(!l||!c)return;r.ttl=l,r.start=c,r.now=n||o();let f=r.now-c;r.remainingTTL=l-f}};let n=0,o=()=>{let r=this.#m.now();if(this.ttlResolution>0){n=r;let h=setTimeout(()=>n=0,this.ttlResolution);h.unref&&h.unref()}return r};this.getRemainingTTL=r=>{let h=this.#s.get(r);if(h===void 0)return 0;let l=e[h],c=t[h];if(!l||!c)return 1/0;let f=(n||o())-c;return l-f},this.#p=r=>{let h=t[r],l=e[r];return!!l&&!!h&&(n||o())-h>l}}#x=()=>{};#E=()=>{};#N=()=>{};#p=()=>!1;#X(){let e=new O(this.#o);this.#b=0,this.#_=e,this.#R=t=>{this.#b-=e[t],e[t]=0},this.#k=(t,i,s,n)=>{if(this.#e(i))return 0;if(!F(s))if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,t),!F(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");return s},this.#I=(t,i,s)=>{if(e[t]=i,this.#u){let n=this.#u-e[t];for(;this.#b>n;)this.#G(!0)}this.#b+=e[t],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#R=e=>{};#I=(e,t,i)=>{};#k=(e,t,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#l);)t=this.#c[t]}*#z({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#l;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#h);)t=this.#a[t]}#V(e){return e!==void 0&&this.#s.get(this.#i[e])===e}*entries(){for(let e of this.#A())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*rentries(){for(let e of this.#z())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*keys(){for(let e of this.#A()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*rkeys(){for(let e of this.#z()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*values(){for(let e of this.#A())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}*rvalues(){for(let e of this.#z())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(e,t={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&e(n,this.#i[i],this))return this.#C(this.#i[i],t)}}forEach(e,t=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}rforEach(e,t=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}purgeStale(){let e=!1;for(let t of this.#z({allowStale:!0}))this.#p(t)&&(this.#v(this.#i[t],"expire"),e=!0);return e}info(e){let t=this.#s.get(e);if(t===void 0)return;let i=this.#t[t],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let o=this.#d[t],r=this.#F[t];if(o&&r){let h=o-(this.#m.now()-r);n.ttl=h,n.start=Date.now()}}return this.#_&&(n.size=this.#_[t]),n}dump(){let e=[];for(let t of this.#A({allowStale:!0})){let i=this.#i[t],s=this.#t[t],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let o={value:n};if(this.#d&&this.#F){o.ttl=this.#d[t];let r=this.#m.now()-this.#F[t];o.start=Math.floor(Date.now()-r)}this.#_&&(o.size=this.#_[t]),e.unshift([i,o])}return e}load(e){this.clear();for(let[t,i]of e){if(i.start){let s=Date.now()-i.start;i.start=this.#m.now()-s}this.#O(t,i.value,i)}}set(e,t,i={}){let{status:s=S.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=e,t!==void 0&&(s.value=t));let n=this.#O(e,t,i);return s&&S.hasSubscribers&&S.publish(s),n}#O(e,t,i={}){let{ttl:s=this.ttl,start:n,noDisposeOnSet:o=this.noDisposeOnSet,sizeCalculation:r=this.sizeCalculation,status:h}=i;if(t===void 0)return h&&(h.set="deleted"),this.delete(e),this;let{noUpdateTTL:l=this.noUpdateTTL}=i;h&&!this.#e(t)&&(h.value=t);let c=this.#k(e,t,i.size||0,r,h);if(this.maxEntrySize&&c>this.maxEntrySize)return this.#v(e,"set"),h&&(h.set="miss",h.maxEntrySizeExceeded=!0),this;let f=this.#n===0?void 0:this.#s.get(e);if(f===void 0)f=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[f]=e,this.#t[f]=t,this.#s.set(e,f),this.#a[this.#h]=f,this.#c[f]=this.#h,this.#h=f,this.#n++,this.#I(f,c,h),h&&(h.set="add"),l=!1,this.#j&&this.#D?.(t,e,"add");else{this.#L(f);let g=this.#t[f];if(t!==g){if(this.#W&&this.#e(g)){g.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:p}=g;p!==void 0&&!o&&(this.#T&&this.#w?.(p,e,"set"),this.#f&&this.#r?.push([p,e,"set"]))}else o||(this.#T&&this.#w?.(g,e,"set"),this.#f&&this.#r?.push([g,e,"set"]));if(this.#R(f),this.#I(f,c,h),this.#t[f]=t,h){h.set="replace";let p=g&&this.#e(g)?g.__staleWhileFetching:g;p!==void 0&&(h.oldValue=p)}}else h&&(h.set="update");this.#j&&this.onInsert?.(t,e,t===g?"update":"replace")}if(s!==0&&!this.#d&&this.#H(),this.#d&&(l||this.#N(f,s,n),h&&this.#E(h,f)),!o&&this.#f&&this.#r){let g=this.#r,p;for(;p=g?.shift();)this.#S?.(...p)}return this}pop(){try{for(;this.#n;){let e=this.#t[this.#l];if(this.#G(!0),this.#e(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#f&&this.#r){let e=this.#r,t;for(;t=e?.shift();)this.#S?.(...t)}}}#G(e){let t=this.#l,i=this.#i[t],s=this.#t[t];return this.#W&&this.#e(s)?s.__abortController.abort(new Error("evicted")):(this.#T||this.#f)&&(this.#T&&this.#w?.(s,i,"evict"),this.#f&&this.#r?.push([s,i,"evict"])),this.#R(t),this.#g?.[t]&&(clearTimeout(this.#g[t]),this.#g[t]=void 0),e&&(this.#i[t]=void 0,this.#t[t]=void 0,this.#y.push(t)),this.#n===1?(this.#l=this.#h=0,this.#y.length=0):this.#l=this.#a[t],this.#s.delete(i),this.#n--,t}has(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="has",i.key=e);let s=this.#Y(e,t);return S.hasSubscribers&&S.publish(i),s}#Y(e,t={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=t,n=this.#s.get(e);if(n!==void 0){let o=this.#t[n];if(this.#e(o)&&o.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#x(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(e,t={}){let{status:i=D()?{}:void 0}=t;i&&(i.op="peek",i.key=e),t.status=i;let s=this.#J(e,t);return S.hasSubscribers&&S.publish(i),s}#J(e,t){let{status:i,allowStale:s=this.allowStale}=t,n=this.#s.get(e);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let o=this.#t[n],r=this.#e(o)?o.__staleWhileFetching:o;return i&&(r!==void 0?(i.peek="hit",i.value=r):i.peek="miss"),r}#P(e,t,i,s){let n=t===void 0?void 0:this.#t[t];if(this.#e(n))return n;let o=new AbortController,{signal:r}=i;r?.addEventListener("abort",()=>o.abort(r.reason),{signal:o.signal});let h={signal:o.signal,options:i,context:s},l=(w,y=!1)=>{let{aborted:a}=o.signal,m=i.ignoreFetchAbort&&w!==void 0,_=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&w!==void 0);if(i.status&&(a&&!y?(i.status.fetchAborted=!0,i.status.fetchError=o.signal.reason,m&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),a&&!m&&!y)return f(o.signal.reason,_);let b=p,d=this.#t[t];return(d===p||d===void 0&&m&&y)&&(w===void 0?b.__staleWhileFetching!==void 0?this.#t[t]=b.__staleWhileFetching:this.#v(e,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#O(e,w,h.options))),w},c=w=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=w),f(w,!1)),f=(w,y)=>{let{aborted:a}=o.signal,m=a&&i.allowStaleOnFetchAbort,_=m||i.allowStaleOnFetchRejection,b=_||i.noDeleteOnFetchRejection,d=p;if(this.#t[t]===p&&(!b||!y&&d.__staleWhileFetching===void 0?this.#v(e,"fetch"):m||(this.#t[t]=d.__staleWhileFetching)),_)return i.status&&d.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),d.__staleWhileFetching;if(d.__returned===d)throw w},g=(w,y)=>{let a=this.#M?.(e,n,h);a&&a instanceof Promise&&a.then(m=>w(m===void 0?void 0:m),y),o.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(w(void 0),i.allowStaleOnFetchAbort&&(w=m=>l(m,!0)))})};i.status&&(i.status.fetchDispatched=!0);let p=new Promise(g).then(l,c),T=Object.assign(p,{__abortController:o,__staleWhileFetching:n,__returned:void 0});return t===void 0?(this.#O(e,T,{...h.options,status:void 0}),t=this.#s.get(e)):this.#t[t]=T,T}#e(e){if(!this.#W)return!1;let t=e;return!!t&&t instanceof Promise&&t.hasOwnProperty("__staleWhileFetching")&&t.__abortController instanceof AbortController}fetch(e,t={}){let i=A.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#B(e,t);return s&&D()&&i&&(s.trace=!0,A.tracePromise(()=>n,s).catch(()=>{})),n}async#B(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:o=this.ttl,noDisposeOnSet:r=this.noDisposeOnSet,size:h=0,sizeCalculation:l=this.sizeCalculation,noUpdateTTL:c=this.noUpdateTTL,noDeleteOnFetchRejection:f=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:g=this.allowStaleOnFetchRejection,ignoreFetchAbort:p=this.ignoreFetchAbort,allowStaleOnFetchAbort:T=this.allowStaleOnFetchAbort,context:w,forceRefresh:y=!1,status:a,signal:m}=t;if(a&&(a.op="fetch",a.key=e,y&&(a.forceRefresh=!0)),!this.#W)return a&&(a.fetch="get"),this.#C(e,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:a});let _={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:o,noDisposeOnSet:r,size:h,sizeCalculation:l,noUpdateTTL:c,noDeleteOnFetchRejection:f,allowStaleOnFetchRejection:g,allowStaleOnFetchAbort:T,ignoreFetchAbort:p,status:a,signal:m},b=this.#s.get(e);if(b===void 0){a&&(a.fetch="miss");let d=this.#P(e,b,_,w);return d.__returned=d}else{let d=this.#t[b];if(this.#e(d)){let W=i&&d.__staleWhileFetching!==void 0;return a&&(a.fetch="inflight",W&&(a.returnedStale=!0)),W?d.__staleWhileFetching:d.__returned=d}let z=this.#p(b);if(!y&&!z)return a&&(a.fetch="hit"),this.#L(b),s&&this.#x(b),a&&this.#E(a,b),d;let v=this.#P(e,b,_,w),E=v.__staleWhileFetching!==void 0&&i;return a&&(a.fetch=z?"stale":"refresh",E&&z&&(a.returnedStale=!0)),E?v.__staleWhileFetching:v.__returned=v}}forceFetch(e,t={}){let i=A.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#K(e,t);return s&&D()&&i&&(s.trace=!0,A.tracePromise(()=>n,s).catch(()=>{})),n}async#K(e,t={}){let i=await this.#B(e,t);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="memo",i.key=e,t.context&&(i.context=t.context));let s=this.#Q(e,t);return i&&(i.value=s),S.hasSubscribers&&S.publish(i),s}#Q(e,t={}){let i=this.#U;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:o,...r}=t;n&&o&&(n.forceRefresh=!0);let h=this.#C(e,r),l=o||h===void 0;if(n&&(n.memo=l?"miss":"hit",l||(n.value=h)),!l)return h;let c=i(e,h,{options:r,context:s});return n&&(n.value=c),this.#O(e,c,r),c}get(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="get",i.key=e);let s=this.#C(e,t);return i&&(s!==void 0&&(i.value=s),S.hasSubscribers&&S.publish(i)),s}#C(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:o}=t,r=this.#s.get(e);if(r===void 0){o&&(o.get="miss");return}let h=this.#t[r],l=this.#e(h);return o&&this.#E(o,r),this.#p(r)?l?(o&&(o.get="stale-fetching"),i&&h.__staleWhileFetching!==void 0?(o&&(o.returnedStale=!0),h.__staleWhileFetching):void 0):(n||this.#v(e,"expire"),o&&(o.get="stale"),i?(o&&(o.returnedStale=!0),h):void 0):(o&&(o.get=l?"fetching":"hit"),this.#L(r),s&&this.#x(r),l?h.__staleWhileFetching:h)}#$(e,t){this.#c[t]=e,this.#a[e]=t}#L(e){e!==this.#h&&(e===this.#l?this.#l=this.#a[e]:this.#$(this.#c[e],this.#a[e]),this.#$(this.#h,e),this.#h=e)}delete(e){return this.#v(e,"delete")}#v(e,t){S.hasSubscribers&&S.publish({op:"delete",delete:t,key:e});let i=!1;if(this.#n!==0){let s=this.#s.get(e);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#q(t);else{this.#R(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#w?.(n,e,t),this.#f&&this.#r?.push([n,e,t])),this.#s.delete(e),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#c[s];else if(s===this.#l)this.#l=this.#a[s];else{let o=this.#c[s];this.#a[o]=this.#a[s];let r=this.#a[s];this.#c[r]=this.#c[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#S?.(...n)}return i}clear(){return this.#q("delete")}#q(e){for(let t of this.#z({allowStale:!0})){let i=this.#t[t];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[t];this.#T&&this.#w?.(i,s,e),this.#f&&this.#r?.push([i,s,e])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let t of this.#g??[])t!==void 0&&clearTimeout(t);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#l=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let t=this.#r,i;for(;i=t?.shift();)this.#S?.(...i)}}};export{M as LRUCache}; +var L={hasSubscribers:!1},S=L,A=L;import("node:diagnostics_channel").then(c=>{S=c.channel("lru-cache:metrics"),A=c.tracingChannel("lru-cache")}).catch(()=>{});var M=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date;var D=()=>S.hasSubscribers||A.hasSubscribers,j=new Set,I=typeof process=="object"&&process?process:{},P=(c,e,t,i)=>{typeof I.emitWarning=="function"?I.emitWarning(c,e,t,i):console.error(`[${t}] ${e}: ${c}`)},k=c=>!j.has(c);var T=c=>!!c&&c===Math.floor(c)&&c>0&&isFinite(c),G=c=>T(c)?c<=Math.pow(2,8)?Uint8Array:c<=Math.pow(2,16)?Uint16Array:c<=Math.pow(2,32)?Uint32Array:c<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(e){super(e),this.fill(0)}},R=class c{heap;length;static#o=!1;static create(e){let t=G(e);if(!t)return[];c.#o=!0;let i=new c(e,t);return c.#o=!1,i}constructor(e,t){if(!c.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new t(e),this.length=0}push(e){this.heap[this.length++]=e}pop(){return this.heap[--this.length]}},U=class c{#o;#c;#S;#O;#w;#M;#I;#m;get perf(){return this.#m}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;backgroundFetchSize;#n;#b;#s;#i;#t;#l;#u;#a;#h;#y;#r;#_;#F;#d;#g;#T;#U;#f;#x;static unsafeExposeInternals(e){return{starts:e.#F,ttls:e.#d,autopurgeTimers:e.#g,sizes:e.#_,keyMap:e.#s,keyList:e.#i,valList:e.#t,next:e.#l,prev:e.#u,get head(){return e.#a},get tail(){return e.#h},free:e.#y,isBackgroundFetch:t=>e.#e(t),backgroundFetch:(t,i,s,n)=>e.#P(t,i,s,n),moveToTail:t=>e.#L(t),indexes:t=>e.#A(t),rindexes:t=>e.#z(t),isStale:t=>e.#p(t)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#I}get dispose(){return this.#S}get onInsert(){return this.#O}get disposeAfter(){return this.#w}constructor(e){let{max:t=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:r,updateAgeOnHas:h,allowStale:a,dispose:o,onInsert:d,disposeAfter:y,noDisposeOnSet:_,noUpdateTTL:u,maxSize:g=0,maxEntrySize:f=0,sizeCalculation:b,fetchMethod:l,memoMethod:w,noDeleteOnFetchRejection:F,noDeleteOnStaleGet:m,allowStaleOnFetchRejection:p,allowStaleOnFetchAbort:z,ignoreFetchAbort:E,backgroundFetchSize:C=1,perf:v}=e;if(this.backgroundFetchSize=C,v!==void 0&&typeof v?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#m=v??M,t!==0&&!T(t))throw new TypeError("max option must be a nonnegative integer");let W=t?G(t):Array;if(!W)throw new Error("invalid max value: "+t);if(this.#o=t,this.#c=g,this.maxEntrySize=f||this.#c,this.sizeCalculation=b,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(w!==void 0&&typeof w!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#I=w,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#U=!!l,this.#s=new Map,this.#i=Array.from({length:t}).fill(void 0),this.#t=Array.from({length:t}).fill(void 0),this.#l=new W(t),this.#u=new W(t),this.#a=0,this.#h=0,this.#y=R.create(t),this.#n=0,this.#b=0,typeof o=="function"&&(this.#S=o),typeof d=="function"&&(this.#O=d),typeof y=="function"?(this.#w=y,this.#r=[]):(this.#w=void 0,this.#r=void 0),this.#T=!!this.#S,this.#x=!!this.#O,this.#f=!!this.#w,this.noDisposeOnSet=!!_,this.noUpdateTTL=!!u,this.noDeleteOnFetchRejection=!!F,this.allowStaleOnFetchRejection=!!p,this.allowStaleOnFetchAbort=!!z,this.ignoreFetchAbort=!!E,this.maxEntrySize!==0){if(this.#c!==0&&!T(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!T(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!a,this.noDeleteOnStaleGet=!!m,this.updateAgeOnGet=!!r,this.updateAgeOnHas=!!h,this.ttlResolution=T(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!T(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let x="LRU_CACHE_UNBOUNDED";k(x)&&(j.add(x),P("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",x,c))}}getRemainingTTL(e){return this.#s.has(e)?1/0:0}#k(){let e=new O(this.#o),t=new O(this.#o);this.#d=e,this.#F=t;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#H=(h,a,o=this.#m.now())=>{t[h]=a!==0?o:0,e[h]=a,s(h,a)},this.#D=h=>{t[h]=e[h]!==0?this.#m.now():0,s(h,e[h])};let s=this.ttlAutopurge?(h,a)=>{if(i?.[h]&&(clearTimeout(i[h]),i[h]=void 0),a&&a!==0&&i){let o=setTimeout(()=>{this.#p(h)&&this.#E(this.#i[h],"expire")},a+1);o.unref&&o.unref(),i[h]=o}}:()=>{};this.#v=(h,a)=>{if(e[a]){let o=e[a],d=t[a];if(!o||!d)return;h.ttl=o,h.start=d,h.now=n||r();let y=h.now-d;h.remainingTTL=o-y}};let n=0,r=()=>{let h=this.#m.now();if(this.ttlResolution>0){n=h;let a=setTimeout(()=>n=0,this.ttlResolution);a.unref&&a.unref()}return h};this.getRemainingTTL=h=>{let a=this.#s.get(h);if(a===void 0)return 0;let o=e[a],d=t[a];if(!o||!d)return 1/0;let y=(n||r())-d;return o-y},this.#p=h=>{let a=t[h],o=e[h];return!!o&&!!a&&(n||r())-a>o}}#D=()=>{};#v=()=>{};#H=()=>{};#p=()=>!1;#X(){let e=new O(this.#o);this.#b=0,this.#_=e,this.#R=t=>{this.#b-=e[t],e[t]=0},this.#N=(t,i,s,n)=>{if(!T(s)){if(this.#e(i))return this.backgroundFetchSize;if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,t),!T(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.")}return s},this.#j=(t,i,s)=>{if(e[t]=i,this.#c){let n=this.#c-e[t];for(;this.#b>n;)this.#G(!0)}this.#b+=e[t],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#R=e=>{};#j=(e,t,i)=>{};#N=(e,t,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#a);)t=this.#u[t]}*#z({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#a;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#h);)t=this.#l[t]}#V(e){return e!==void 0&&this.#s.get(this.#i[e])===e}*entries(){for(let e of this.#A())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*rentries(){for(let e of this.#z())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*keys(){for(let e of this.#A()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*rkeys(){for(let e of this.#z()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*values(){for(let e of this.#A())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}*rvalues(){for(let e of this.#z())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(e,t={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&e(n,this.#i[i],this))return this.#C(this.#i[i],t)}}forEach(e,t=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}rforEach(e,t=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}purgeStale(){let e=!1;for(let t of this.#z({allowStale:!0}))this.#p(t)&&(this.#E(this.#i[t],"expire"),e=!0);return e}info(e){let t=this.#s.get(e);if(t===void 0)return;let i=this.#t[t],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let r=this.#d[t],h=this.#F[t];if(r&&h){let a=r-(this.#m.now()-h);n.ttl=a,n.start=Date.now()}}return this.#_&&(n.size=this.#_[t]),n}dump(){let e=[];for(let t of this.#A({allowStale:!0})){let i=this.#i[t],s=this.#t[t],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let r={value:n};if(this.#d&&this.#F){r.ttl=this.#d[t];let h=this.#m.now()-this.#F[t];r.start=Math.floor(Date.now()-h)}this.#_&&(r.size=this.#_[t]),e.unshift([i,r])}return e}load(e){this.clear();for(let[t,i]of e){if(i.start){let s=Date.now()-i.start;i.start=this.#m.now()-s}this.#W(t,i.value,i)}}set(e,t,i={}){let{status:s=S.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=e,t!==void 0&&(s.value=t),s.cache=this);let n=this.#W(e,t,i);return s&&S.hasSubscribers&&S.publish(s),n}#W(e,t,i,s){let{ttl:n=this.ttl,start:r,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:o}=i,d=this.#e(t);if(t===void 0)return o&&(o.set="deleted"),this.delete(e),this;let{noUpdateTTL:y=this.noUpdateTTL}=i;o&&!d&&(o.value=t);let _=this.#N(e,t,i.size||0,a,o);if(this.maxEntrySize&&_>this.maxEntrySize)return this.#E(e,"set"),o&&(o.set="miss",o.maxEntrySizeExceeded=!0),this;let u=this.#n===0?void 0:this.#s.get(e);if(u===void 0)u=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[u]=e,this.#t[u]=t,this.#s.set(e,u),this.#l[this.#h]=u,this.#u[u]=this.#h,this.#h=u,this.#n++,this.#j(u,_,o),o&&(o.set="add"),y=!1,this.#x&&!d&&this.#O?.(t,e,"add");else{this.#L(u);let g=this.#t[u];if(t!==g){if(!h)if(this.#e(g)){g!==s&&g.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:f}=g;f!==void 0&&f!==t&&(this.#T&&this.#S?.(f,e,"set"),this.#f&&this.#r?.push([f,e,"set"]))}else this.#T&&this.#S?.(g,e,"set"),this.#f&&this.#r?.push([g,e,"set"]);if(this.#R(u),this.#j(u,_,o),this.#t[u]=t,!d){let f=g&&this.#e(g)?g.__staleWhileFetching:g,b=f===void 0?"add":t!==f?"replace":"update";o&&(o.set=b,f!==void 0&&(o.oldValue=f)),this.#x&&this.onInsert?.(t,e,b)}}else d||(o&&(o.set="update"),this.#x&&this.onInsert?.(t,e,"update"))}if(n!==0&&!this.#d&&this.#k(),this.#d&&(y||this.#H(u,n,r),o&&this.#v(o,u)),!h&&this.#f&&this.#r){let g=this.#r,f;for(;f=g?.shift();)this.#w?.(...f)}return this}pop(){try{for(;this.#n;){let e=this.#t[this.#a];if(this.#G(!0),this.#e(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#f&&this.#r){let e=this.#r,t;for(;t=e?.shift();)this.#w?.(...t)}}}#G(e){let t=this.#a,i=this.#i[t],s=this.#t[t],n=this.#e(s);n&&s.__abortController.abort(new Error("evicted"));let r=n?s.__staleWhileFetching:s;return(this.#T||this.#f)&&r!==void 0&&(this.#T&&this.#S?.(r,i,"evict"),this.#f&&this.#r?.push([r,i,"evict"])),this.#R(t),this.#g?.[t]&&(clearTimeout(this.#g[t]),this.#g[t]=void 0),e&&(this.#i[t]=void 0,this.#t[t]=void 0,this.#y.push(t)),this.#n===1?(this.#a=this.#h=0,this.#y.length=0):this.#a=this.#l[t],this.#s.delete(i),this.#n--,t}has(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="has",i.key=e,i.cache=this);let s=this.#Y(e,t);return S.hasSubscribers&&S.publish(i),s}#Y(e,t={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=t,n=this.#s.get(e);if(n!==void 0){let r=this.#t[n];if(this.#e(r)&&r.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#v(s,n));else return i&&this.#D(n),s&&(s.has="hit",this.#v(s,n)),!0}else s&&(s.has="miss");return!1}peek(e,t={}){let{status:i=D()?{}:void 0}=t;i&&(i.op="peek",i.key=e,i.cache=this),t.status=i;let s=this.#J(e,t);return S.hasSubscribers&&S.publish(i),s}#J(e,t){let{status:i,allowStale:s=this.allowStale}=t,n=this.#s.get(e);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let r=this.#t[n],h=this.#e(r)?r.__staleWhileFetching:r;return i&&(h!==void 0?(i.peek="hit",i.value=h):i.peek="miss"),h}#P(e,t,i,s){let n=t===void 0?void 0:this.#t[t];if(this.#e(n))return n;let r=new AbortController,{signal:h}=i;h?.addEventListener("abort",()=>r.abort(h.reason),{signal:r.signal});let a={signal:r.signal,options:i,context:s},o=(f,b=!1)=>{let{aborted:l}=r.signal,w=i.ignoreFetchAbort&&f!==void 0,F=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&f!==void 0);if(i.status&&(l&&!b?(i.status.fetchAborted=!0,i.status.fetchError=r.signal.reason,w&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!w&&!b)return y(r.signal.reason,F);let m=u,p=this.#t[t];return(p===u||p===void 0&&w&&b)&&(f===void 0?m.__staleWhileFetching!==void 0?this.#t[t]=m.__staleWhileFetching:this.#E(e,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#W(e,f,a.options,m))),f},d=f=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=f),y(f,!1)),y=(f,b)=>{let{aborted:l}=r.signal,w=l&&i.allowStaleOnFetchAbort,F=w||i.allowStaleOnFetchRejection,m=F||i.noDeleteOnFetchRejection,p=u;if(this.#t[t]===u&&(!m||!b&&p.__staleWhileFetching===void 0?this.#E(e,"fetch"):w||(this.#t[t]=p.__staleWhileFetching)),F)return i.status&&p.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),p.__staleWhileFetching;if(p.__returned===p)throw f},_=(f,b)=>{let l=this.#M?.(e,n,a);r.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(f(void 0),i.allowStaleOnFetchAbort&&(f=w=>o(w,!0)))}),l&&l instanceof Promise?l.then(w=>f(w===void 0?void 0:w),b):l!==void 0&&f(l)};i.status&&(i.status.fetchDispatched=!0);let u=new Promise(_).then(o,d),g=Object.assign(u,{__abortController:r,__staleWhileFetching:n,__returned:void 0});return t===void 0?(this.#W(e,g,{...a.options,status:void 0}),t=this.#s.get(e)):this.#t[t]=g,g}#e(e){if(!this.#U)return!1;let t=e;return!!t&&t instanceof Promise&&t.hasOwnProperty("__staleWhileFetching")&&t.__abortController instanceof AbortController}fetch(e,t={}){let i=A.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#B(e,t);return s&&i&&(s.trace=!0,A.tracePromise(()=>n,s).catch(()=>{})),n}async#B(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:r=this.ttl,noDisposeOnSet:h=this.noDisposeOnSet,size:a=0,sizeCalculation:o=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:y=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:_=this.allowStaleOnFetchRejection,ignoreFetchAbort:u=this.ignoreFetchAbort,allowStaleOnFetchAbort:g=this.allowStaleOnFetchAbort,context:f,forceRefresh:b=!1,status:l,signal:w}=t;if(l&&(l.op="fetch",l.key=e,b&&(l.forceRefresh=!0),l.cache=this),!this.#U)return l&&(l.fetch="get"),this.#C(e,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let F={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:r,noDisposeOnSet:h,size:a,sizeCalculation:o,noUpdateTTL:d,noDeleteOnFetchRejection:y,allowStaleOnFetchRejection:_,allowStaleOnFetchAbort:g,ignoreFetchAbort:u,status:l,signal:w},m=this.#s.get(e);if(m===void 0){l&&(l.fetch="miss");let p=this.#P(e,m,F,f);return p.__returned=p}else{let p=this.#t[m];if(this.#e(p)){let W=i&&p.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",W&&(l.returnedStale=!0)),W?p.__staleWhileFetching:p.__returned=p}let z=this.#p(m);if(!b&&!z)return l&&(l.fetch="hit"),this.#L(m),s&&this.#D(m),l&&this.#v(l,m),p;let E=this.#P(e,m,F,f),v=E.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=z?"stale":"refresh",v&&z&&(l.returnedStale=!0)),v?E.__staleWhileFetching:E.__returned=E}}forceFetch(e,t={}){let i=A.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#K(e,t);return s&&i&&(s.trace=!0,A.tracePromise(()=>n,s).catch(()=>{})),n}async#K(e,t={}){let i=await this.#B(e,t);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="memo",i.key=e,t.context&&(i.context=t.context),i.cache=this);let s=this.#Q(e,t);return i&&(i.value=s),S.hasSubscribers&&S.publish(i),s}#Q(e,t={}){let i=this.#I;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:r,...h}=t;n&&r&&(n.forceRefresh=!0);let a=this.#C(e,h),o=r||a===void 0;if(n&&(n.memo=o?"miss":"hit",o||(n.value=a)),!o)return a;let d=i(e,a,{options:h,context:s});return n&&(n.value=d),this.#W(e,d,h),d}get(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="get",i.key=e,i.cache=this);let s=this.#C(e,t);return i&&(s!==void 0&&(i.value=s),S.hasSubscribers&&S.publish(i)),s}#C(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:r}=t,h=this.#s.get(e);if(h===void 0){r&&(r.get="miss");return}let a=this.#t[h],o=this.#e(a);return r&&this.#v(r,h),this.#p(h)?o?(r&&(r.get="stale-fetching"),i&&a.__staleWhileFetching!==void 0?(r&&(r.returnedStale=!0),a.__staleWhileFetching):void 0):(n||this.#E(e,"expire"),r&&(r.get="stale"),i?(r&&(r.returnedStale=!0),a):void 0):(r&&(r.get=o?"fetching":"hit"),this.#L(h),s&&this.#D(h),o?a.__staleWhileFetching:a)}#$(e,t){this.#u[t]=e,this.#l[e]=t}#L(e){e!==this.#h&&(e===this.#a?this.#a=this.#l[e]:this.#$(this.#u[e],this.#l[e]),this.#$(this.#h,e),this.#h=e)}delete(e){return this.#E(e,"delete")}#E(e,t){S.hasSubscribers&&S.publish({op:"delete",delete:t,key:e,cache:this});let i=!1;if(this.#n!==0){let s=this.#s.get(e);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#q(t);else{this.#R(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#S?.(n,e,t),this.#f&&this.#r?.push([n,e,t])),this.#s.delete(e),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let r=this.#u[s];this.#l[r]=this.#l[s];let h=this.#l[s];this.#u[h]=this.#u[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#w?.(...n)}return i}clear(){return this.#q("delete")}#q(e){for(let t of this.#z({allowStale:!0})){let i=this.#t[t];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[t];this.#T&&this.#S?.(i,s,e),this.#f&&this.#r?.push([i,s,e])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let t of this.#g??[])t!==void 0&&clearTimeout(t);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#a=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let t=this.#r,i;for(;i=t?.shift();)this.#w?.(...i)}}};export{U as LRUCache}; //# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js b/deps/npm/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js index d6d8c48f2be7db..f37e0baac5977d 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js @@ -1,7 +1,6 @@ // simple node version that imports from node builtin -// this gets compiled to a require() commonjs-style override, -// not using top level await on a conditional dynamic import +// this is built to both ESM and CommonJS on the 'node' import path import { tracingChannel, channel } from 'node:diagnostics_channel'; export const metrics = channel('lru-cache:metrics'); export const tracing = tracingChannel('lru-cache'); -//# sourceMappingURL=diagnostics-channel-node.mjs.map \ No newline at end of file +//# sourceMappingURL=diagnostics-channel-node.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/esm/node/index.js b/deps/npm/node_modules/lru-cache/dist/esm/node/index.js index 11c8cd8dfbf5cf..114a4e9908390f 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/node/index.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/node/index.js @@ -2,12 +2,8 @@ * @module LRUCache */ import { metrics, tracing } from './diagnostics-channel.js'; +import { defaultPerf } from './perf.js'; const hasSubscribers = () => metrics.hasSubscribers || tracing.hasSubscribers; -const defaultPerf = (typeof performance === 'object' && - performance && - typeof performance.now === 'function') ? - performance - : Date; const warned = new Set(); /* c8 ignore start */ const PROCESS = (typeof process === 'object' && !!process ? @@ -49,7 +45,9 @@ class ZeroArray extends Array { } } class Stack { + /* c8 ignore start - not sure why this is showing up uncovered?? */ heap; + /* c8 ignore stop */ length; // private constructor static #constructing = false; @@ -169,6 +167,8 @@ export class LRUCache { * {@link LRUCache.OptionsBase.ignoreFetchAbort} */ ignoreFetchAbort; + /** {@link LRUCache.OptionsBase.backgroundFetchSize} */ + backgroundFetchSize; // computed properties #size; #calculatedSize; @@ -279,7 +279,8 @@ export class LRUCache { return this.#disposeAfter; } constructor(options) { - const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, perf, } = options; + const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options; + this.backgroundFetchSize = backgroundFetchSize; if (perf !== undefined) { if (typeof perf?.now !== 'function') { throw new TypeError('perf option must have a now() method if specified'); @@ -507,12 +508,15 @@ export class LRUCache { sizes[index] = 0; }; this.#requireSize = (k, v, size, sizeCalculation) => { - // provisionally accept background fetches. - // actual value size will be checked when they return. - if (this.#isBackgroundFetch(v)) { - return 0; - } if (!isPosInt(size)) { + // provisionally accept background fetches. + // actual value size will be checked when they return. + if (this.#isBackgroundFetch(v)) { + // NB: this cannot occur if v.__staleWhileFetching is set, + // because in that case, it would take on the size of the + // existing entry that it temporarily replaces. + return this.backgroundFetchSize; + } if (sizeCalculation) { if (typeof sizeCalculation !== 'function') { throw new TypeError('sizeCalculation must be a function'); @@ -879,6 +883,7 @@ export class LRUCache { status.key = k; if (v !== undefined) status.value = v; + status.cache = this; } const result = this.#set(k, v, setOptions); if (status && metrics.hasSubscribers) { @@ -886,8 +891,9 @@ export class LRUCache { } return result; } - #set(k, v, setOptions = {}) { + #set(k, v, setOptions, bf) { const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions; + const isBF = this.#isBackgroundFetch(v); if (v === undefined) { if (status) status.set = 'deleted'; @@ -895,7 +901,7 @@ export class LRUCache { return this; } let { noUpdateTTL = this.noUpdateTTL } = setOptions; - if (status && !this.#isBackgroundFetch(v)) + if (status && !isBF) status.value = v; const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status); // if the item doesn't fit, don't do anything @@ -927,52 +933,68 @@ export class LRUCache { if (status) status.set = 'add'; noUpdateTTL = false; - if (this.#hasOnInsert) { + if (this.#hasOnInsert && !isBF) { this.#onInsert?.(v, k, 'add'); } } else { // update + // might be updating a background fetch! this.#moveToTail(index); const oldVal = this.#valList[index]; if (v !== oldVal) { - if (this.#hasFetchMethod && this.#isBackgroundFetch(oldVal)) { - oldVal.__abortController.abort(new Error('replaced')); - const { __staleWhileFetching: s } = oldVal; - if (s !== undefined && !noDisposeOnSet) { + if (!noDisposeOnSet) { + if (this.#isBackgroundFetch(oldVal)) { + if (oldVal !== bf) { + // setting over a background fetch, not merely resolving it. + oldVal.__abortController.abort(new Error('replaced')); + } + const { __staleWhileFetching: s } = oldVal; + if (s !== undefined && s !== v) { + if (this.#hasDispose) { + this.#dispose?.(s, k, 'set'); + } + if (this.#hasDisposeAfter) { + this.#disposed?.push([s, k, 'set']); + } + } + } + else { if (this.#hasDispose) { - this.#dispose?.(s, k, 'set'); + this.#dispose?.(oldVal, k, 'set'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([s, k, 'set']); + this.#disposed?.push([oldVal, k, 'set']); } } } - else if (!noDisposeOnSet) { - if (this.#hasDispose) { - this.#dispose?.(oldVal, k, 'set'); - } - if (this.#hasDisposeAfter) { - this.#disposed?.push([oldVal, k, 'set']); - } - } this.#removeItemSize(index); this.#addItemSize(index, size, status); this.#valList[index] = v; - if (status) { - status.set = 'replace'; + if (!isBF) { const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ? oldVal.__staleWhileFetching : oldVal; - if (oldValue !== undefined) - status.oldValue = oldValue; + const setType = oldValue === undefined ? 'add' + : v !== oldValue ? 'replace' + : 'update'; + if (status) { + status.set = setType; + if (oldValue !== undefined) + status.oldValue = oldValue; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, setType); + } } } - else if (status) { - status.set = 'update'; - } - if (this.#hasOnInsert) { - this.onInsert?.(v, k, v === oldVal ? 'update' : 'replace'); + else if (!isBF) { + if (status) { + status.set = 'update'; + } + if (this.#hasOnInsert) { + this.onInsert?.(v, k, 'update'); + } } } if (ttl !== 0 && !this.#ttls) { @@ -1027,15 +1049,18 @@ export class LRUCache { const head = this.#head; const k = this.#keyList[head]; const v = this.#valList[head]; - if (this.#hasFetchMethod && this.#isBackgroundFetch(v)) { + const isBF = this.#isBackgroundFetch(v); + if (isBF) { v.__abortController.abort(new Error('evicted')); } - else if (this.#hasDispose || this.#hasDisposeAfter) { + const oldValue = isBF ? v.__staleWhileFetching : v; + if ((this.#hasDispose || this.#hasDisposeAfter) && + oldValue !== undefined) { if (this.#hasDispose) { - this.#dispose?.(v, k, 'evict'); + this.#dispose?.(oldValue, k, 'evict'); } if (this.#hasDisposeAfter) { - this.#disposed?.push([v, k, 'evict']); + this.#disposed?.push([oldValue, k, 'evict']); } } this.#removeItemSize(head); @@ -1082,6 +1107,7 @@ export class LRUCache { if (status) { status.op = 'has'; status.key = k; + status.cache = this; } const result = this.#has(k, hasOptions); if (metrics.hasSubscribers) @@ -1129,6 +1155,7 @@ export class LRUCache { if (status) { status.op = 'peek'; status.key = k; + status.cache = this; } peekOptions.status = status; const result = this.#peek(k, peekOptions); @@ -1211,7 +1238,7 @@ export class LRUCache { else { if (options.status) options.status.fetchUpdated = true; - this.#set(k, v, fetchOpts.options); + this.#set(k, v, fetchOpts.options, bf); } } return v; @@ -1257,9 +1284,6 @@ export class LRUCache { }; const pcall = (res, rej) => { const fmp = this.#fetchMethod?.(k, v, fetchOpts); - if (fmp && fmp instanceof Promise) { - fmp.then(v => res(v === undefined ? undefined : v), rej); - } // ignored, we go until we finish, regardless. // defer check until we are actually aborting, // so fetchMethod can override. @@ -1272,6 +1296,12 @@ export class LRUCache { } } }); + if (fmp && fmp instanceof Promise) { + fmp.then(v => res(v === undefined ? undefined : v), rej); + } + else if (fmp !== undefined) { + res(fmp); + } }; if (options.status) options.status.fetchDispatched = true; @@ -1287,6 +1317,10 @@ export class LRUCache { index = this.#keyMap.get(k); } else { + // do not call #set, because we do not want to adjust its place + // in the lru queue, as it has not yet been "used". Also, we don't + // need to worry about evicting for size, because a background fetch + // over a stale value is treated as the same size as its stale value. this.#valList[index] = bf; } return bf; @@ -1308,11 +1342,9 @@ export class LRUCache { status.context = fetchOptions.context; } const p = this.#fetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1329,6 +1361,7 @@ export class LRUCache { status.key = k; if (forceRefresh) status.forceRefresh = true; + status.cache = this; } if (!this.#hasFetchMethod) { if (status) @@ -1410,11 +1443,9 @@ export class LRUCache { status.context = fetchOptions.context; } const p = this.#forceFetch(k, fetchOptions); - if (status && hasSubscribers()) { - if (ths) { - status.trace = true; - tracing.tracePromise(() => p, status).catch(() => { }); - } + if (status && ths) { + status.trace = true; + tracing.tracePromise(() => p, status).catch(() => { }); } return p; } @@ -1433,6 +1464,7 @@ export class LRUCache { if (memoOptions.context) { status.context = memoOptions.context; } + status.cache = this; } const result = this.#memo(k, memoOptions); if (status) @@ -1479,6 +1511,7 @@ export class LRUCache { if (status) { status.op = 'get'; status.key = k; + status.cache = this; } const result = this.#get(k, getOptions); if (status) { @@ -1577,6 +1610,7 @@ export class LRUCache { op: 'delete', delete: reason, key: k, + cache: this, }); } let deleted = false; @@ -1657,7 +1691,7 @@ export class LRUCache { } } this.#keyMap.clear(); - this.#valList.fill(undefined); + void this.#valList.fill(undefined); this.#keyList.fill(undefined); if (this.#ttls && this.#starts) { this.#ttls.fill(0); diff --git a/deps/npm/node_modules/lru-cache/dist/esm/node/index.min.js b/deps/npm/node_modules/lru-cache/dist/esm/node/index.min.js index bd92365ef37894..84c4c3ec14ffa3 100644 --- a/deps/npm/node_modules/lru-cache/dist/esm/node/index.min.js +++ b/deps/npm/node_modules/lru-cache/dist/esm/node/index.min.js @@ -1,2 +1,2 @@ -import{tracingChannel as j,channel as I}from"node:diagnostics_channel";var S=I("lru-cache:metrics"),W=j("lru-cache");var D=()=>S.hasSubscribers||W.hasSubscribers,G=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,M=new Set,C=typeof process=="object"&&process?process:{},P=(u,e,t,i)=>{typeof C.emitWarning=="function"?C.emitWarning(u,e,t,i):console.error(`[${t}] ${e}: ${u}`)},H=u=>!M.has(u),$=Symbol("type"),F=u=>!!u&&u===Math.floor(u)&&u>0&&isFinite(u),U=u=>F(u)?u<=Math.pow(2,8)?Uint8Array:u<=Math.pow(2,16)?Uint16Array:u<=Math.pow(2,32)?Uint32Array:u<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(e){super(e),this.fill(0)}},R=class u{heap;length;static#o=!1;static create(e){let t=U(e);if(!t)return[];u.#o=!0;let i=new u(e,t);return u.#o=!1,i}constructor(e,t){if(!u.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new t(e),this.length=0}push(e){this.heap[this.length++]=e}pop(){return this.heap[--this.length]}},L=class u{#o;#u;#w;#D;#S;#M;#U;#m;get perf(){return this.#m}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#n;#b;#s;#i;#t;#a;#c;#l;#h;#y;#r;#_;#F;#d;#g;#T;#W;#f;#j;static unsafeExposeInternals(e){return{starts:e.#F,ttls:e.#d,autopurgeTimers:e.#g,sizes:e.#_,keyMap:e.#s,keyList:e.#i,valList:e.#t,next:e.#a,prev:e.#c,get head(){return e.#l},get tail(){return e.#h},free:e.#y,isBackgroundFetch:t=>e.#e(t),backgroundFetch:(t,i,s,n)=>e.#P(t,i,s,n),moveToTail:t=>e.#L(t),indexes:t=>e.#A(t),rindexes:t=>e.#z(t),isStale:t=>e.#p(t)}}get max(){return this.#o}get maxSize(){return this.#u}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#U}get dispose(){return this.#w}get onInsert(){return this.#D}get disposeAfter(){return this.#S}constructor(e){let{max:t=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:o,updateAgeOnHas:r,allowStale:h,dispose:l,onInsert:c,disposeAfter:f,noDisposeOnSet:g,noUpdateTTL:p,maxSize:T=0,maxEntrySize:w=0,sizeCalculation:y,fetchMethod:a,memoMethod:m,noDeleteOnFetchRejection:_,noDeleteOnStaleGet:b,allowStaleOnFetchRejection:d,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,perf:x}=e;if(x!==void 0&&typeof x?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#m=x??G,t!==0&&!F(t))throw new TypeError("max option must be a nonnegative integer");let v=t?U(t):Array;if(!v)throw new Error("invalid max value: "+t);if(this.#o=t,this.#u=T,this.maxEntrySize=w||this.#u,this.sizeCalculation=y,this.sizeCalculation){if(!this.#u&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(m!==void 0&&typeof m!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#U=m,a!==void 0&&typeof a!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=a,this.#W=!!a,this.#s=new Map,this.#i=Array.from({length:t}).fill(void 0),this.#t=Array.from({length:t}).fill(void 0),this.#a=new v(t),this.#c=new v(t),this.#l=0,this.#h=0,this.#y=R.create(t),this.#n=0,this.#b=0,typeof l=="function"&&(this.#w=l),typeof c=="function"&&(this.#D=c),typeof f=="function"?(this.#S=f,this.#r=[]):(this.#S=void 0,this.#r=void 0),this.#T=!!this.#w,this.#j=!!this.#D,this.#f=!!this.#S,this.noDisposeOnSet=!!g,this.noUpdateTTL=!!p,this.noDeleteOnFetchRejection=!!_,this.allowStaleOnFetchRejection=!!d,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#u!==0&&!F(this.#u))throw new TypeError("maxSize must be a positive integer if specified");if(!F(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!h,this.noDeleteOnStaleGet=!!b,this.updateAgeOnGet=!!o,this.updateAgeOnHas=!!r,this.ttlResolution=F(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!F(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#H()}if(this.#o===0&&this.ttl===0&&this.#u===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#u){let E="LRU_CACHE_UNBOUNDED";H(E)&&(M.add(E),P("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",E,u))}}getRemainingTTL(e){return this.#s.has(e)?1/0:0}#H(){let e=new O(this.#o),t=new O(this.#o);this.#d=e,this.#F=t;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#N=(r,h,l=this.#m.now())=>{t[r]=h!==0?l:0,e[r]=h,s(r,h)},this.#x=r=>{t[r]=e[r]!==0?this.#m.now():0,s(r,e[r])};let s=this.ttlAutopurge?(r,h)=>{if(i?.[r]&&(clearTimeout(i[r]),i[r]=void 0),h&&h!==0&&i){let l=setTimeout(()=>{this.#p(r)&&this.#v(this.#i[r],"expire")},h+1);l.unref&&l.unref(),i[r]=l}}:()=>{};this.#E=(r,h)=>{if(e[h]){let l=e[h],c=t[h];if(!l||!c)return;r.ttl=l,r.start=c,r.now=n||o();let f=r.now-c;r.remainingTTL=l-f}};let n=0,o=()=>{let r=this.#m.now();if(this.ttlResolution>0){n=r;let h=setTimeout(()=>n=0,this.ttlResolution);h.unref&&h.unref()}return r};this.getRemainingTTL=r=>{let h=this.#s.get(r);if(h===void 0)return 0;let l=e[h],c=t[h];if(!l||!c)return 1/0;let f=(n||o())-c;return l-f},this.#p=r=>{let h=t[r],l=e[r];return!!l&&!!h&&(n||o())-h>l}}#x=()=>{};#E=()=>{};#N=()=>{};#p=()=>!1;#X(){let e=new O(this.#o);this.#b=0,this.#_=e,this.#R=t=>{this.#b-=e[t],e[t]=0},this.#k=(t,i,s,n)=>{if(this.#e(i))return 0;if(!F(s))if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,t),!F(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");return s},this.#I=(t,i,s)=>{if(e[t]=i,this.#u){let n=this.#u-e[t];for(;this.#b>n;)this.#G(!0)}this.#b+=e[t],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#R=e=>{};#I=(e,t,i)=>{};#k=(e,t,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#l);)t=this.#c[t]}*#z({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#l;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#h);)t=this.#a[t]}#V(e){return e!==void 0&&this.#s.get(this.#i[e])===e}*entries(){for(let e of this.#A())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*rentries(){for(let e of this.#z())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*keys(){for(let e of this.#A()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*rkeys(){for(let e of this.#z()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*values(){for(let e of this.#A())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}*rvalues(){for(let e of this.#z())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(e,t={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&e(n,this.#i[i],this))return this.#C(this.#i[i],t)}}forEach(e,t=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}rforEach(e,t=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}purgeStale(){let e=!1;for(let t of this.#z({allowStale:!0}))this.#p(t)&&(this.#v(this.#i[t],"expire"),e=!0);return e}info(e){let t=this.#s.get(e);if(t===void 0)return;let i=this.#t[t],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let o=this.#d[t],r=this.#F[t];if(o&&r){let h=o-(this.#m.now()-r);n.ttl=h,n.start=Date.now()}}return this.#_&&(n.size=this.#_[t]),n}dump(){let e=[];for(let t of this.#A({allowStale:!0})){let i=this.#i[t],s=this.#t[t],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let o={value:n};if(this.#d&&this.#F){o.ttl=this.#d[t];let r=this.#m.now()-this.#F[t];o.start=Math.floor(Date.now()-r)}this.#_&&(o.size=this.#_[t]),e.unshift([i,o])}return e}load(e){this.clear();for(let[t,i]of e){if(i.start){let s=Date.now()-i.start;i.start=this.#m.now()-s}this.#O(t,i.value,i)}}set(e,t,i={}){let{status:s=S.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=e,t!==void 0&&(s.value=t));let n=this.#O(e,t,i);return s&&S.hasSubscribers&&S.publish(s),n}#O(e,t,i={}){let{ttl:s=this.ttl,start:n,noDisposeOnSet:o=this.noDisposeOnSet,sizeCalculation:r=this.sizeCalculation,status:h}=i;if(t===void 0)return h&&(h.set="deleted"),this.delete(e),this;let{noUpdateTTL:l=this.noUpdateTTL}=i;h&&!this.#e(t)&&(h.value=t);let c=this.#k(e,t,i.size||0,r,h);if(this.maxEntrySize&&c>this.maxEntrySize)return this.#v(e,"set"),h&&(h.set="miss",h.maxEntrySizeExceeded=!0),this;let f=this.#n===0?void 0:this.#s.get(e);if(f===void 0)f=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[f]=e,this.#t[f]=t,this.#s.set(e,f),this.#a[this.#h]=f,this.#c[f]=this.#h,this.#h=f,this.#n++,this.#I(f,c,h),h&&(h.set="add"),l=!1,this.#j&&this.#D?.(t,e,"add");else{this.#L(f);let g=this.#t[f];if(t!==g){if(this.#W&&this.#e(g)){g.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:p}=g;p!==void 0&&!o&&(this.#T&&this.#w?.(p,e,"set"),this.#f&&this.#r?.push([p,e,"set"]))}else o||(this.#T&&this.#w?.(g,e,"set"),this.#f&&this.#r?.push([g,e,"set"]));if(this.#R(f),this.#I(f,c,h),this.#t[f]=t,h){h.set="replace";let p=g&&this.#e(g)?g.__staleWhileFetching:g;p!==void 0&&(h.oldValue=p)}}else h&&(h.set="update");this.#j&&this.onInsert?.(t,e,t===g?"update":"replace")}if(s!==0&&!this.#d&&this.#H(),this.#d&&(l||this.#N(f,s,n),h&&this.#E(h,f)),!o&&this.#f&&this.#r){let g=this.#r,p;for(;p=g?.shift();)this.#S?.(...p)}return this}pop(){try{for(;this.#n;){let e=this.#t[this.#l];if(this.#G(!0),this.#e(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#f&&this.#r){let e=this.#r,t;for(;t=e?.shift();)this.#S?.(...t)}}}#G(e){let t=this.#l,i=this.#i[t],s=this.#t[t];return this.#W&&this.#e(s)?s.__abortController.abort(new Error("evicted")):(this.#T||this.#f)&&(this.#T&&this.#w?.(s,i,"evict"),this.#f&&this.#r?.push([s,i,"evict"])),this.#R(t),this.#g?.[t]&&(clearTimeout(this.#g[t]),this.#g[t]=void 0),e&&(this.#i[t]=void 0,this.#t[t]=void 0,this.#y.push(t)),this.#n===1?(this.#l=this.#h=0,this.#y.length=0):this.#l=this.#a[t],this.#s.delete(i),this.#n--,t}has(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="has",i.key=e);let s=this.#Y(e,t);return S.hasSubscribers&&S.publish(i),s}#Y(e,t={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=t,n=this.#s.get(e);if(n!==void 0){let o=this.#t[n];if(this.#e(o)&&o.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#E(s,n));else return i&&this.#x(n),s&&(s.has="hit",this.#E(s,n)),!0}else s&&(s.has="miss");return!1}peek(e,t={}){let{status:i=D()?{}:void 0}=t;i&&(i.op="peek",i.key=e),t.status=i;let s=this.#J(e,t);return S.hasSubscribers&&S.publish(i),s}#J(e,t){let{status:i,allowStale:s=this.allowStale}=t,n=this.#s.get(e);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let o=this.#t[n],r=this.#e(o)?o.__staleWhileFetching:o;return i&&(r!==void 0?(i.peek="hit",i.value=r):i.peek="miss"),r}#P(e,t,i,s){let n=t===void 0?void 0:this.#t[t];if(this.#e(n))return n;let o=new AbortController,{signal:r}=i;r?.addEventListener("abort",()=>o.abort(r.reason),{signal:o.signal});let h={signal:o.signal,options:i,context:s},l=(w,y=!1)=>{let{aborted:a}=o.signal,m=i.ignoreFetchAbort&&w!==void 0,_=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&w!==void 0);if(i.status&&(a&&!y?(i.status.fetchAborted=!0,i.status.fetchError=o.signal.reason,m&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),a&&!m&&!y)return f(o.signal.reason,_);let b=p,d=this.#t[t];return(d===p||d===void 0&&m&&y)&&(w===void 0?b.__staleWhileFetching!==void 0?this.#t[t]=b.__staleWhileFetching:this.#v(e,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#O(e,w,h.options))),w},c=w=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=w),f(w,!1)),f=(w,y)=>{let{aborted:a}=o.signal,m=a&&i.allowStaleOnFetchAbort,_=m||i.allowStaleOnFetchRejection,b=_||i.noDeleteOnFetchRejection,d=p;if(this.#t[t]===p&&(!b||!y&&d.__staleWhileFetching===void 0?this.#v(e,"fetch"):m||(this.#t[t]=d.__staleWhileFetching)),_)return i.status&&d.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),d.__staleWhileFetching;if(d.__returned===d)throw w},g=(w,y)=>{let a=this.#M?.(e,n,h);a&&a instanceof Promise&&a.then(m=>w(m===void 0?void 0:m),y),o.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(w(void 0),i.allowStaleOnFetchAbort&&(w=m=>l(m,!0)))})};i.status&&(i.status.fetchDispatched=!0);let p=new Promise(g).then(l,c),T=Object.assign(p,{__abortController:o,__staleWhileFetching:n,__returned:void 0});return t===void 0?(this.#O(e,T,{...h.options,status:void 0}),t=this.#s.get(e)):this.#t[t]=T,T}#e(e){if(!this.#W)return!1;let t=e;return!!t&&t instanceof Promise&&t.hasOwnProperty("__staleWhileFetching")&&t.__abortController instanceof AbortController}fetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#B(e,t);return s&&D()&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#B(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:o=this.ttl,noDisposeOnSet:r=this.noDisposeOnSet,size:h=0,sizeCalculation:l=this.sizeCalculation,noUpdateTTL:c=this.noUpdateTTL,noDeleteOnFetchRejection:f=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:g=this.allowStaleOnFetchRejection,ignoreFetchAbort:p=this.ignoreFetchAbort,allowStaleOnFetchAbort:T=this.allowStaleOnFetchAbort,context:w,forceRefresh:y=!1,status:a,signal:m}=t;if(a&&(a.op="fetch",a.key=e,y&&(a.forceRefresh=!0)),!this.#W)return a&&(a.fetch="get"),this.#C(e,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:a});let _={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:o,noDisposeOnSet:r,size:h,sizeCalculation:l,noUpdateTTL:c,noDeleteOnFetchRejection:f,allowStaleOnFetchRejection:g,allowStaleOnFetchAbort:T,ignoreFetchAbort:p,status:a,signal:m},b=this.#s.get(e);if(b===void 0){a&&(a.fetch="miss");let d=this.#P(e,b,_,w);return d.__returned=d}else{let d=this.#t[b];if(this.#e(d)){let E=i&&d.__staleWhileFetching!==void 0;return a&&(a.fetch="inflight",E&&(a.returnedStale=!0)),E?d.__staleWhileFetching:d.__returned=d}let A=this.#p(b);if(!y&&!A)return a&&(a.fetch="hit"),this.#L(b),s&&this.#x(b),a&&this.#E(a,b),d;let z=this.#P(e,b,_,w),v=z.__staleWhileFetching!==void 0&&i;return a&&(a.fetch=A?"stale":"refresh",v&&A&&(a.returnedStale=!0)),v?z.__staleWhileFetching:z.__returned=z}}forceFetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#K(e,t);return s&&D()&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#K(e,t={}){let i=await this.#B(e,t);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="memo",i.key=e,t.context&&(i.context=t.context));let s=this.#Q(e,t);return i&&(i.value=s),S.hasSubscribers&&S.publish(i),s}#Q(e,t={}){let i=this.#U;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:o,...r}=t;n&&o&&(n.forceRefresh=!0);let h=this.#C(e,r),l=o||h===void 0;if(n&&(n.memo=l?"miss":"hit",l||(n.value=h)),!l)return h;let c=i(e,h,{options:r,context:s});return n&&(n.value=c),this.#O(e,c,r),c}get(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="get",i.key=e);let s=this.#C(e,t);return i&&(s!==void 0&&(i.value=s),S.hasSubscribers&&S.publish(i)),s}#C(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:o}=t,r=this.#s.get(e);if(r===void 0){o&&(o.get="miss");return}let h=this.#t[r],l=this.#e(h);return o&&this.#E(o,r),this.#p(r)?l?(o&&(o.get="stale-fetching"),i&&h.__staleWhileFetching!==void 0?(o&&(o.returnedStale=!0),h.__staleWhileFetching):void 0):(n||this.#v(e,"expire"),o&&(o.get="stale"),i?(o&&(o.returnedStale=!0),h):void 0):(o&&(o.get=l?"fetching":"hit"),this.#L(r),s&&this.#x(r),l?h.__staleWhileFetching:h)}#$(e,t){this.#c[t]=e,this.#a[e]=t}#L(e){e!==this.#h&&(e===this.#l?this.#l=this.#a[e]:this.#$(this.#c[e],this.#a[e]),this.#$(this.#h,e),this.#h=e)}delete(e){return this.#v(e,"delete")}#v(e,t){S.hasSubscribers&&S.publish({op:"delete",delete:t,key:e});let i=!1;if(this.#n!==0){let s=this.#s.get(e);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#q(t);else{this.#R(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#w?.(n,e,t),this.#f&&this.#r?.push([n,e,t])),this.#s.delete(e),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#c[s];else if(s===this.#l)this.#l=this.#a[s];else{let o=this.#c[s];this.#a[o]=this.#a[s];let r=this.#a[s];this.#c[r]=this.#c[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#S?.(...n)}return i}clear(){return this.#q("delete")}#q(e){for(let t of this.#z({allowStale:!0})){let i=this.#t[t];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[t];this.#T&&this.#w?.(i,s,e),this.#f&&this.#r?.push([i,s,e])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let t of this.#g??[])t!==void 0&&clearTimeout(t);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#l=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let t=this.#r,i;for(;i=t?.shift();)this.#S?.(...i)}}};export{L as LRUCache}; +import{tracingChannel as G,channel as P}from"node:diagnostics_channel";var S=P("lru-cache:metrics"),W=G("lru-cache");var L=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date;var D=()=>S.hasSubscribers||W.hasSubscribers,U=new Set,M=typeof process=="object"&&process?process:{},k=(u,e,t,i)=>{typeof M.emitWarning=="function"?M.emitWarning(u,e,t,i):console.error(`[${t}] ${e}: ${u}`)},H=u=>!U.has(u);var T=u=>!!u&&u===Math.floor(u)&&u>0&&isFinite(u),j=u=>T(u)?u<=Math.pow(2,8)?Uint8Array:u<=Math.pow(2,16)?Uint16Array:u<=Math.pow(2,32)?Uint32Array:u<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(e){super(e),this.fill(0)}},R=class u{heap;length;static#o=!1;static create(e){let t=j(e);if(!t)return[];u.#o=!0;let i=new u(e,t);return u.#o=!1,i}constructor(e,t){if(!u.#o)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new t(e),this.length=0}push(e){this.heap[this.length++]=e}pop(){return this.heap[--this.length]}},I=class u{#o;#c;#S;#O;#w;#M;#I;#m;get perf(){return this.#m}ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;backgroundFetchSize;#n;#b;#s;#i;#t;#l;#u;#a;#h;#y;#r;#_;#F;#d;#g;#T;#U;#f;#x;static unsafeExposeInternals(e){return{starts:e.#F,ttls:e.#d,autopurgeTimers:e.#g,sizes:e.#_,keyMap:e.#s,keyList:e.#i,valList:e.#t,next:e.#l,prev:e.#u,get head(){return e.#a},get tail(){return e.#h},free:e.#y,isBackgroundFetch:t=>e.#e(t),backgroundFetch:(t,i,s,n)=>e.#P(t,i,s,n),moveToTail:t=>e.#L(t),indexes:t=>e.#A(t),rindexes:t=>e.#z(t),isStale:t=>e.#p(t)}}get max(){return this.#o}get maxSize(){return this.#c}get calculatedSize(){return this.#b}get size(){return this.#n}get fetchMethod(){return this.#M}get memoMethod(){return this.#I}get dispose(){return this.#S}get onInsert(){return this.#O}get disposeAfter(){return this.#w}constructor(e){let{max:t=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:r,updateAgeOnHas:h,allowStale:a,dispose:o,onInsert:d,disposeAfter:y,noDisposeOnSet:_,noUpdateTTL:c,maxSize:g=0,maxEntrySize:f=0,sizeCalculation:b,fetchMethod:l,memoMethod:w,noDeleteOnFetchRejection:F,noDeleteOnStaleGet:m,allowStaleOnFetchRejection:p,allowStaleOnFetchAbort:A,ignoreFetchAbort:z,backgroundFetchSize:C=1,perf:E}=e;if(this.backgroundFetchSize=C,E!==void 0&&typeof E?.now!="function")throw new TypeError("perf option must have a now() method if specified");if(this.#m=E??L,t!==0&&!T(t))throw new TypeError("max option must be a nonnegative integer");let v=t?j(t):Array;if(!v)throw new Error("invalid max value: "+t);if(this.#o=t,this.#c=g,this.maxEntrySize=f||this.#c,this.sizeCalculation=b,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(w!==void 0&&typeof w!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#I=w,l!==void 0&&typeof l!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#M=l,this.#U=!!l,this.#s=new Map,this.#i=Array.from({length:t}).fill(void 0),this.#t=Array.from({length:t}).fill(void 0),this.#l=new v(t),this.#u=new v(t),this.#a=0,this.#h=0,this.#y=R.create(t),this.#n=0,this.#b=0,typeof o=="function"&&(this.#S=o),typeof d=="function"&&(this.#O=d),typeof y=="function"?(this.#w=y,this.#r=[]):(this.#w=void 0,this.#r=void 0),this.#T=!!this.#S,this.#x=!!this.#O,this.#f=!!this.#w,this.noDisposeOnSet=!!_,this.noUpdateTTL=!!c,this.noDeleteOnFetchRejection=!!F,this.allowStaleOnFetchRejection=!!p,this.allowStaleOnFetchAbort=!!A,this.ignoreFetchAbort=!!z,this.maxEntrySize!==0){if(this.#c!==0&&!T(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!T(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#X()}if(this.allowStale=!!a,this.noDeleteOnStaleGet=!!m,this.updateAgeOnGet=!!r,this.updateAgeOnHas=!!h,this.ttlResolution=T(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!T(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k()}if(this.#o===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#o&&!this.#c){let x="LRU_CACHE_UNBOUNDED";H(x)&&(U.add(x),k("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",x,u))}}getRemainingTTL(e){return this.#s.has(e)?1/0:0}#k(){let e=new O(this.#o),t=new O(this.#o);this.#d=e,this.#F=t;let i=this.ttlAutopurge?Array.from({length:this.#o}):void 0;this.#g=i,this.#H=(h,a,o=this.#m.now())=>{t[h]=a!==0?o:0,e[h]=a,s(h,a)},this.#D=h=>{t[h]=e[h]!==0?this.#m.now():0,s(h,e[h])};let s=this.ttlAutopurge?(h,a)=>{if(i?.[h]&&(clearTimeout(i[h]),i[h]=void 0),a&&a!==0&&i){let o=setTimeout(()=>{this.#p(h)&&this.#E(this.#i[h],"expire")},a+1);o.unref&&o.unref(),i[h]=o}}:()=>{};this.#v=(h,a)=>{if(e[a]){let o=e[a],d=t[a];if(!o||!d)return;h.ttl=o,h.start=d,h.now=n||r();let y=h.now-d;h.remainingTTL=o-y}};let n=0,r=()=>{let h=this.#m.now();if(this.ttlResolution>0){n=h;let a=setTimeout(()=>n=0,this.ttlResolution);a.unref&&a.unref()}return h};this.getRemainingTTL=h=>{let a=this.#s.get(h);if(a===void 0)return 0;let o=e[a],d=t[a];if(!o||!d)return 1/0;let y=(n||r())-d;return o-y},this.#p=h=>{let a=t[h],o=e[h];return!!o&&!!a&&(n||r())-a>o}}#D=()=>{};#v=()=>{};#H=()=>{};#p=()=>!1;#X(){let e=new O(this.#o);this.#b=0,this.#_=e,this.#R=t=>{this.#b-=e[t],e[t]=0},this.#N=(t,i,s,n)=>{if(!T(s)){if(this.#e(i))return this.backgroundFetchSize;if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,t),!T(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.")}return s},this.#j=(t,i,s)=>{if(e[t]=i,this.#c){let n=this.#c-e[t];for(;this.#b>n;)this.#G(!0)}this.#b+=e[t],s&&(s.entrySize=i,s.totalCalculatedSize=this.#b)}}#R=e=>{};#j=(e,t,i)=>{};#N=(e,t,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#h;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#a);)t=this.#u[t]}*#z({allowStale:e=this.allowStale}={}){if(this.#n)for(let t=this.#a;this.#V(t)&&((e||!this.#p(t))&&(yield t),t!==this.#h);)t=this.#l[t]}#V(e){return e!==void 0&&this.#s.get(this.#i[e])===e}*entries(){for(let e of this.#A())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*rentries(){for(let e of this.#z())this.#t[e]!==void 0&&this.#i[e]!==void 0&&!this.#e(this.#t[e])&&(yield[this.#i[e],this.#t[e]])}*keys(){for(let e of this.#A()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*rkeys(){for(let e of this.#z()){let t=this.#i[e];t!==void 0&&!this.#e(this.#t[e])&&(yield t)}}*values(){for(let e of this.#A())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}*rvalues(){for(let e of this.#z())this.#t[e]!==void 0&&!this.#e(this.#t[e])&&(yield this.#t[e])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(e,t={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&e(n,this.#i[i],this))return this.#C(this.#i[i],t)}}forEach(e,t=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}rforEach(e,t=this){for(let i of this.#z()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&e.call(t,n,this.#i[i],this)}}purgeStale(){let e=!1;for(let t of this.#z({allowStale:!0}))this.#p(t)&&(this.#E(this.#i[t],"expire"),e=!0);return e}info(e){let t=this.#s.get(e);if(t===void 0)return;let i=this.#t[t],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#F){let r=this.#d[t],h=this.#F[t];if(r&&h){let a=r-(this.#m.now()-h);n.ttl=a,n.start=Date.now()}}return this.#_&&(n.size=this.#_[t]),n}dump(){let e=[];for(let t of this.#A({allowStale:!0})){let i=this.#i[t],s=this.#t[t],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let r={value:n};if(this.#d&&this.#F){r.ttl=this.#d[t];let h=this.#m.now()-this.#F[t];r.start=Math.floor(Date.now()-h)}this.#_&&(r.size=this.#_[t]),e.unshift([i,r])}return e}load(e){this.clear();for(let[t,i]of e){if(i.start){let s=Date.now()-i.start;i.start=this.#m.now()-s}this.#W(t,i.value,i)}}set(e,t,i={}){let{status:s=S.hasSubscribers?{}:void 0}=i;i.status=s,s&&(s.op="set",s.key=e,t!==void 0&&(s.value=t),s.cache=this);let n=this.#W(e,t,i);return s&&S.hasSubscribers&&S.publish(s),n}#W(e,t,i,s){let{ttl:n=this.ttl,start:r,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:o}=i,d=this.#e(t);if(t===void 0)return o&&(o.set="deleted"),this.delete(e),this;let{noUpdateTTL:y=this.noUpdateTTL}=i;o&&!d&&(o.value=t);let _=this.#N(e,t,i.size||0,a,o);if(this.maxEntrySize&&_>this.maxEntrySize)return this.#E(e,"set"),o&&(o.set="miss",o.maxEntrySizeExceeded=!0),this;let c=this.#n===0?void 0:this.#s.get(e);if(c===void 0)c=this.#n===0?this.#h:this.#y.length!==0?this.#y.pop():this.#n===this.#o?this.#G(!1):this.#n,this.#i[c]=e,this.#t[c]=t,this.#s.set(e,c),this.#l[this.#h]=c,this.#u[c]=this.#h,this.#h=c,this.#n++,this.#j(c,_,o),o&&(o.set="add"),y=!1,this.#x&&!d&&this.#O?.(t,e,"add");else{this.#L(c);let g=this.#t[c];if(t!==g){if(!h)if(this.#e(g)){g!==s&&g.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:f}=g;f!==void 0&&f!==t&&(this.#T&&this.#S?.(f,e,"set"),this.#f&&this.#r?.push([f,e,"set"]))}else this.#T&&this.#S?.(g,e,"set"),this.#f&&this.#r?.push([g,e,"set"]);if(this.#R(c),this.#j(c,_,o),this.#t[c]=t,!d){let f=g&&this.#e(g)?g.__staleWhileFetching:g,b=f===void 0?"add":t!==f?"replace":"update";o&&(o.set=b,f!==void 0&&(o.oldValue=f)),this.#x&&this.onInsert?.(t,e,b)}}else d||(o&&(o.set="update"),this.#x&&this.onInsert?.(t,e,"update"))}if(n!==0&&!this.#d&&this.#k(),this.#d&&(y||this.#H(c,n,r),o&&this.#v(o,c)),!h&&this.#f&&this.#r){let g=this.#r,f;for(;f=g?.shift();)this.#w?.(...f)}return this}pop(){try{for(;this.#n;){let e=this.#t[this.#a];if(this.#G(!0),this.#e(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#f&&this.#r){let e=this.#r,t;for(;t=e?.shift();)this.#w?.(...t)}}}#G(e){let t=this.#a,i=this.#i[t],s=this.#t[t],n=this.#e(s);n&&s.__abortController.abort(new Error("evicted"));let r=n?s.__staleWhileFetching:s;return(this.#T||this.#f)&&r!==void 0&&(this.#T&&this.#S?.(r,i,"evict"),this.#f&&this.#r?.push([r,i,"evict"])),this.#R(t),this.#g?.[t]&&(clearTimeout(this.#g[t]),this.#g[t]=void 0),e&&(this.#i[t]=void 0,this.#t[t]=void 0,this.#y.push(t)),this.#n===1?(this.#a=this.#h=0,this.#y.length=0):this.#a=this.#l[t],this.#s.delete(i),this.#n--,t}has(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="has",i.key=e,i.cache=this);let s=this.#Y(e,t);return S.hasSubscribers&&S.publish(i),s}#Y(e,t={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=t,n=this.#s.get(e);if(n!==void 0){let r=this.#t[n];if(this.#e(r)&&r.__staleWhileFetching===void 0)return!1;if(this.#p(n))s&&(s.has="stale",this.#v(s,n));else return i&&this.#D(n),s&&(s.has="hit",this.#v(s,n)),!0}else s&&(s.has="miss");return!1}peek(e,t={}){let{status:i=D()?{}:void 0}=t;i&&(i.op="peek",i.key=e,i.cache=this),t.status=i;let s=this.#J(e,t);return S.hasSubscribers&&S.publish(i),s}#J(e,t){let{status:i,allowStale:s=this.allowStale}=t,n=this.#s.get(e);if(n===void 0||!s&&this.#p(n)){i&&(i.peek=n===void 0?"miss":"stale");return}let r=this.#t[n],h=this.#e(r)?r.__staleWhileFetching:r;return i&&(h!==void 0?(i.peek="hit",i.value=h):i.peek="miss"),h}#P(e,t,i,s){let n=t===void 0?void 0:this.#t[t];if(this.#e(n))return n;let r=new AbortController,{signal:h}=i;h?.addEventListener("abort",()=>r.abort(h.reason),{signal:r.signal});let a={signal:r.signal,options:i,context:s},o=(f,b=!1)=>{let{aborted:l}=r.signal,w=i.ignoreFetchAbort&&f!==void 0,F=i.ignoreFetchAbort||!!(i.allowStaleOnFetchAbort&&f!==void 0);if(i.status&&(l&&!b?(i.status.fetchAborted=!0,i.status.fetchError=r.signal.reason,w&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),l&&!w&&!b)return y(r.signal.reason,F);let m=c,p=this.#t[t];return(p===c||p===void 0&&w&&b)&&(f===void 0?m.__staleWhileFetching!==void 0?this.#t[t]=m.__staleWhileFetching:this.#E(e,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.#W(e,f,a.options,m))),f},d=f=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=f),y(f,!1)),y=(f,b)=>{let{aborted:l}=r.signal,w=l&&i.allowStaleOnFetchAbort,F=w||i.allowStaleOnFetchRejection,m=F||i.noDeleteOnFetchRejection,p=c;if(this.#t[t]===c&&(!m||!b&&p.__staleWhileFetching===void 0?this.#E(e,"fetch"):w||(this.#t[t]=p.__staleWhileFetching)),F)return i.status&&p.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),p.__staleWhileFetching;if(p.__returned===p)throw f},_=(f,b)=>{let l=this.#M?.(e,n,a);r.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(f(void 0),i.allowStaleOnFetchAbort&&(f=w=>o(w,!0)))}),l&&l instanceof Promise?l.then(w=>f(w===void 0?void 0:w),b):l!==void 0&&f(l)};i.status&&(i.status.fetchDispatched=!0);let c=new Promise(_).then(o,d),g=Object.assign(c,{__abortController:r,__staleWhileFetching:n,__returned:void 0});return t===void 0?(this.#W(e,g,{...a.options,status:void 0}),t=this.#s.get(e)):this.#t[t]=g,g}#e(e){if(!this.#U)return!1;let t=e;return!!t&&t instanceof Promise&&t.hasOwnProperty("__staleWhileFetching")&&t.__abortController instanceof AbortController}fetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#B(e,t);return s&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#B(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:r=this.ttl,noDisposeOnSet:h=this.noDisposeOnSet,size:a=0,sizeCalculation:o=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL,noDeleteOnFetchRejection:y=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:_=this.allowStaleOnFetchRejection,ignoreFetchAbort:c=this.ignoreFetchAbort,allowStaleOnFetchAbort:g=this.allowStaleOnFetchAbort,context:f,forceRefresh:b=!1,status:l,signal:w}=t;if(l&&(l.op="fetch",l.key=e,b&&(l.forceRefresh=!0),l.cache=this),!this.#U)return l&&(l.fetch="get"),this.#C(e,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:l});let F={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:r,noDisposeOnSet:h,size:a,sizeCalculation:o,noUpdateTTL:d,noDeleteOnFetchRejection:y,allowStaleOnFetchRejection:_,allowStaleOnFetchAbort:g,ignoreFetchAbort:c,status:l,signal:w},m=this.#s.get(e);if(m===void 0){l&&(l.fetch="miss");let p=this.#P(e,m,F,f);return p.__returned=p}else{let p=this.#t[m];if(this.#e(p)){let v=i&&p.__staleWhileFetching!==void 0;return l&&(l.fetch="inflight",v&&(l.returnedStale=!0)),v?p.__staleWhileFetching:p.__returned=p}let A=this.#p(m);if(!b&&!A)return l&&(l.fetch="hit"),this.#L(m),s&&this.#D(m),l&&this.#v(l,m),p;let z=this.#P(e,m,F,f),E=z.__staleWhileFetching!==void 0&&i;return l&&(l.fetch=A?"stale":"refresh",E&&A&&(l.returnedStale=!0)),E?z.__staleWhileFetching:z.__returned=z}}forceFetch(e,t={}){let i=W.hasSubscribers,{status:s=D()?{}:void 0}=t;t.status=s,s&&t.context&&(s.context=t.context);let n=this.#K(e,t);return s&&i&&(s.trace=!0,W.tracePromise(()=>n,s).catch(()=>{})),n}async#K(e,t={}){let i=await this.#B(e,t);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="memo",i.key=e,t.context&&(i.context=t.context),i.cache=this);let s=this.#Q(e,t);return i&&(i.value=s),S.hasSubscribers&&S.publish(i),s}#Q(e,t={}){let i=this.#I;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,status:n,forceRefresh:r,...h}=t;n&&r&&(n.forceRefresh=!0);let a=this.#C(e,h),o=r||a===void 0;if(n&&(n.memo=o?"miss":"hit",o||(n.value=a)),!o)return a;let d=i(e,a,{options:h,context:s});return n&&(n.value=d),this.#W(e,d,h),d}get(e,t={}){let{status:i=S.hasSubscribers?{}:void 0}=t;t.status=i,i&&(i.op="get",i.key=e,i.cache=this);let s=this.#C(e,t);return i&&(s!==void 0&&(i.value=s),S.hasSubscribers&&S.publish(i)),s}#C(e,t={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:r}=t,h=this.#s.get(e);if(h===void 0){r&&(r.get="miss");return}let a=this.#t[h],o=this.#e(a);return r&&this.#v(r,h),this.#p(h)?o?(r&&(r.get="stale-fetching"),i&&a.__staleWhileFetching!==void 0?(r&&(r.returnedStale=!0),a.__staleWhileFetching):void 0):(n||this.#E(e,"expire"),r&&(r.get="stale"),i?(r&&(r.returnedStale=!0),a):void 0):(r&&(r.get=o?"fetching":"hit"),this.#L(h),s&&this.#D(h),o?a.__staleWhileFetching:a)}#$(e,t){this.#u[t]=e,this.#l[e]=t}#L(e){e!==this.#h&&(e===this.#a?this.#a=this.#l[e]:this.#$(this.#u[e],this.#l[e]),this.#$(this.#h,e),this.#h=e)}delete(e){return this.#E(e,"delete")}#E(e,t){S.hasSubscribers&&S.publish({op:"delete",delete:t,key:e,cache:this});let i=!1;if(this.#n!==0){let s=this.#s.get(e);if(s!==void 0)if(this.#g?.[s]&&(clearTimeout(this.#g?.[s]),this.#g[s]=void 0),i=!0,this.#n===1)this.#q(t);else{this.#R(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#T||this.#f)&&(this.#T&&this.#S?.(n,e,t),this.#f&&this.#r?.push([n,e,t])),this.#s.delete(e),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#a)this.#a=this.#l[s];else{let r=this.#u[s];this.#l[r]=this.#l[s];let h=this.#l[s];this.#u[h]=this.#u[s]}this.#n--,this.#y.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#w?.(...n)}return i}clear(){return this.#q("delete")}#q(e){for(let t of this.#z({allowStale:!0})){let i=this.#t[t];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[t];this.#T&&this.#S?.(i,s,e),this.#f&&this.#r?.push([i,s,e])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#F){this.#d.fill(0),this.#F.fill(0);for(let t of this.#g??[])t!==void 0&&clearTimeout(t);this.#g?.fill(void 0)}if(this.#_&&this.#_.fill(0),this.#a=0,this.#h=0,this.#y.length=0,this.#b=0,this.#n=0,this.#f&&this.#r){let t=this.#r,i;for(;i=t?.shift();)this.#w?.(...i)}}};export{I as LRUCache}; //# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/lru-cache/dist/esm/node/perf.js b/deps/npm/node_modules/lru-cache/dist/esm/node/perf.js new file mode 100644 index 00000000000000..f21cd88c8692d7 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/esm/node/perf.js @@ -0,0 +1,7 @@ +export const defaultPerf = (typeof performance === 'object' && + performance && + typeof performance.now === 'function') ? + /* c8 ignore start - this gets covered, but c8 gets confused */ + performance + : /* c8 ignore stop */ Date; +//# sourceMappingURL=perf.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/dist/esm/perf.js b/deps/npm/node_modules/lru-cache/dist/esm/perf.js new file mode 100644 index 00000000000000..f21cd88c8692d7 --- /dev/null +++ b/deps/npm/node_modules/lru-cache/dist/esm/perf.js @@ -0,0 +1,7 @@ +export const defaultPerf = (typeof performance === 'object' && + performance && + typeof performance.now === 'function') ? + /* c8 ignore start - this gets covered, but c8 gets confused */ + performance + : /* c8 ignore stop */ Date; +//# sourceMappingURL=perf.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/lru-cache/package.json b/deps/npm/node_modules/lru-cache/package.json index b571016553e727..6ada2c211f2d6c 100644 --- a/deps/npm/node_modules/lru-cache/package.json +++ b/deps/npm/node_modules/lru-cache/package.json @@ -1,7 +1,7 @@ { "name": "lru-cache", "description": "A cache object that deletes the least-recently-used items.", - "version": "11.3.5", + "version": "11.5.1", "author": "Isaac Z. Schlueter ", "keywords": [ "mru", @@ -34,25 +34,37 @@ "types": "./dist/commonjs/index.d.ts", "tshy": { "esmDialects": [ - "node", - "browser" + "browser", + "node" + ], + "commonjsDialects": [ + "browser", + "node" ], "exports": { "./raw": "./src/index.ts", ".": { "import": { - "node": { - "types": "./dist/esm/node/index.d.ts", - "default": "./dist/esm/node/index.min.js" - }, "browser": { "types": "./dist/esm/browser/index.d.ts", "default": "./dist/esm/browser/index.min.js" }, + "node": { + "types": "./dist/esm/node/index.d.ts", + "default": "./dist/esm/node/index.min.js" + }, "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.min.js" }, "require": { + "browser": { + "types": "./dist/commonjs/browser/index.d.ts", + "default": "./dist/commonjs/browser/index.min.js" + }, + "node": { + "types": "./dist/commonjs/node/index.d.ts", + "default": "./dist/commonjs/node/index.min.js" + }, "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.min.js" } @@ -66,15 +78,15 @@ }, "devDependencies": { "benchmark": "^2.1.4", - "esbuild": "^0.25.9", + "esbuild": "^0.28.0", "marked": "^4.2.12", "mkdirp": "^3.0.1", - "oxlint": "^1.58.0", - "oxlint-tsgolint": "^0.19.0", - "prettier": "^3.8.1", - "tap": "^21.6.3", - "tshy": "^4.1.1", - "typedoc": "^0.28.18" + "oxlint": "^1.65.0", + "oxlint-tsgolint": "^0.22.1", + "prettier": "^3.8.3", + "tap": "^21.7.4", + "tshy": "^4.1.2", + "typedoc": "^0.28.19" }, "license": "BlueOak-1.0.0", "files": [ @@ -86,36 +98,52 @@ "exports": { "./raw": { "import": { - "node": { - "types": "./dist/esm/node/index.d.ts", - "default": "./dist/esm/node/index.js" - }, "browser": { "types": "./dist/esm/browser/index.d.ts", "default": "./dist/esm/browser/index.js" }, + "node": { + "types": "./dist/esm/node/index.d.ts", + "default": "./dist/esm/node/index.js" + }, "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { + "browser": { + "types": "./dist/commonjs/browser/index.d.ts", + "default": "./dist/commonjs/browser/index.js" + }, + "node": { + "types": "./dist/commonjs/node/index.d.ts", + "default": "./dist/commonjs/node/index.js" + }, "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" } }, ".": { "import": { - "node": { - "types": "./dist/esm/node/index.d.ts", - "default": "./dist/esm/node/index.min.js" - }, "browser": { "types": "./dist/esm/browser/index.d.ts", "default": "./dist/esm/browser/index.min.js" }, + "node": { + "types": "./dist/esm/node/index.d.ts", + "default": "./dist/esm/node/index.min.js" + }, "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.min.js" }, "require": { + "browser": { + "types": "./dist/commonjs/browser/index.d.ts", + "default": "./dist/commonjs/browser/index.min.js" + }, + "node": { + "types": "./dist/commonjs/node/index.d.ts", + "default": "./dist/commonjs/node/index.min.js" + }, "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.min.js" } diff --git a/deps/npm/node_modules/make-fetch-happen/package.json b/deps/npm/node_modules/make-fetch-happen/package.json index 1d06ac4889c3e3..92c48b45871586 100644 --- a/deps/npm/node_modules/make-fetch-happen/package.json +++ b/deps/npm/node_modules/make-fetch-happen/package.json @@ -1,6 +1,6 @@ { "name": "make-fetch-happen", - "version": "15.0.5", + "version": "15.0.6", "description": "Opinionated, caching, retrying fetch client", "main": "lib/index.js", "files": [ diff --git a/deps/npm/node_modules/semver/README.md b/deps/npm/node_modules/semver/README.md index f6503bfefd4640..c307e82d46ae44 100644 --- a/deps/npm/node_modules/semver/README.md +++ b/deps/npm/node_modules/semver/README.md @@ -56,6 +56,7 @@ const semverCompareLoose = require('semver/functions/compare-loose') const semverCompareBuild = require('semver/functions/compare-build') const semverSort = require('semver/functions/sort') const semverRsort = require('semver/functions/rsort') +const semverTruncate = require('semver/functions/truncate') // low-level comparators between versions const semverGt = require('semver/functions/gt') @@ -399,12 +400,19 @@ nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * tilde ::= '~' partial caret ::= '^' partial qualifier ::= ( '-' pre )? ( '+' build )? -pre ::= parts -build ::= parts -parts ::= part ( '.' part ) * -part ::= nr | [-0-9A-Za-z]+ +pre ::= prepart ( '.' prepart ) * +prepart ::= nr | alphanumid +build ::= buildid ( '.' buildid ) * +alphanumid ::= ( ['0'-'9'] ) * [-A-Za-z] [-0-9A-Za-z] * +buildid ::= [-0-9A-Za-z]+ ``` +Note: Prerelease identifiers (`pre`) use `nr` for numeric parts, which +disallows leading zeros (e.g., `1.2.3-00` is invalid). Build metadata +identifiers (`build`) allow any alphanumeric string including leading +zeros (e.g., `1.2.3+00` is valid). This matches the +[SemVer 2.0.0 specification](https://semver.org/#spec-item-9). + ## Functions All methods and classes take a final `options` object argument. All @@ -449,6 +457,12 @@ strings that they parse. or comparators intersect. * `parse(v)`: Attempt to parse a string as a semantic version, returning either a `SemVer` object or `null`. +* `truncate(v, releaseType)`: Return the version with components _lower_ + than `releaseType` dropped off, e.g.: + * `major` removes build & prerelease info and sets minor & patch to 0. + * `minor` removes build & prerelease info, and sets patch to 0 + * `patch` removes build & prerelease info + * All prerelease types remove build info only ### Comparison @@ -650,6 +664,7 @@ The following modules are available: * `require('semver/functions/rsort')` * `require('semver/functions/satisfies')` * `require('semver/functions/sort')` +* `require('semver/functions/truncate')` * `require('semver/functions/valid')` * `require('semver/ranges/gtr')` * `require('semver/ranges/intersects')` diff --git a/deps/npm/node_modules/semver/bin/semver.js b/deps/npm/node_modules/semver/bin/semver.js index d62bfc0ecd5216..9ae8aadb95fbd9 100755 --- a/deps/npm/node_modules/semver/bin/semver.js +++ b/deps/npm/node_modules/semver/bin/semver.js @@ -46,6 +46,7 @@ const main = () => { a = a.slice(0, indexOfEqualSign) argv.unshift(value) } + switch (a) { case '-rv': case '-rev': case '--rev': case '--reverse': reverse = true @@ -60,15 +61,10 @@ const main = () => { versions.push(argv.shift()) break case '-i': case '--inc': case '--increment': - switch (argv[0]) { - case 'major': case 'minor': case 'patch': case 'prerelease': - case 'premajor': case 'preminor': case 'prepatch': - case 'release': - inc = argv.shift() - break - default: - inc = 'patch' - break + if (semver.RELEASE_TYPES.includes(argv[0]) || (argv[0] === 'release')) { + inc = { value: argv.shift(), maybeErrantValue: null, option: a } + } else { + inc = { value: 'patch', maybeErrantValue: argv[0], option: a } } break case '--preid': @@ -102,6 +98,14 @@ const main = () => { options = parseOptions({ loose, includePrerelease, rtl }) + if ( + inc && + versions.includes(inc.maybeErrantValue) && + !semver.valid(inc.maybeErrantValue, options) + ) { + console.warn(`Invalid value for ${inc.option}; defaulting to 'patch'. This may become a failure in future major versions.`) + } + versions = versions.map((v) => { return coerce ? (semver.coerce(v, options) || { version: v }).version : v }).filter((v) => { @@ -125,7 +129,7 @@ const main = () => { versions .sort((a, b) => semver[reverse ? 'rcompare' : 'compare'](a, b, options)) .map(v => semver.clean(v, options)) - .map(v => inc ? semver.inc(v, inc, options, identifier, identifierBase) : v) + .map(v => inc ? semver.inc(v, inc.value, options, identifier, identifierBase) : v) .forEach(v => console.log(v)) } diff --git a/deps/npm/node_modules/semver/classes/range.js b/deps/npm/node_modules/semver/classes/range.js index 94629ce6f5df60..c2e605e5173601 100644 --- a/deps/npm/node_modules/semver/classes/range.js +++ b/deps/npm/node_modules/semver/classes/range.js @@ -98,6 +98,9 @@ class Range { } parseRange (range) { + // strip build metadata so it can't bleed into the version + range = range.replace(BUILDSTRIPRE, '') + // memoize range parsing for performance. // this is a very hot path, and fully deterministic. const memoOpts = @@ -223,6 +226,7 @@ const debug = require('../internal/debug') const SemVer = require('./semver') const { safeRe: re, + src, t, comparatorTrimReplace, tildeTrimReplace, @@ -230,6 +234,9 @@ const { } = require('../internal/re') const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants') +// unbounded global build-metadata stripper used by parseRange +const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g') + const isNullSet = c => c.value === '<0.0.0-0' const isAny = c => c.value === '' diff --git a/deps/npm/node_modules/semver/functions/truncate.js b/deps/npm/node_modules/semver/functions/truncate.js new file mode 100644 index 00000000000000..8314e4e922a418 --- /dev/null +++ b/deps/npm/node_modules/semver/functions/truncate.js @@ -0,0 +1,48 @@ +'use strict' + +const parse = require('./parse') +const constants = require('../internal/constants') +const SemVer = require('../classes/semver') + +const truncate = (version, truncation, options) => { + if (!constants.RELEASE_TYPES.includes(truncation)) { + return null + } + + const clonedVersion = cloneInputVersion(version, options) + return clonedVersion && doTruncation(clonedVersion, truncation) +} + +const cloneInputVersion = (version, options) => { + const versionStringToParse = ( + version instanceof SemVer ? version.version : version + ) + + return parse(versionStringToParse, options) +} + +const doTruncation = (version, truncation) => { + if (isPrerelease(truncation)) { + return version.version + } + + version.prerelease = [] + + switch (truncation) { + case 'major': + version.minor = 0 + version.patch = 0 + break + case 'minor': + version.patch = 0 + break + } + + return version.format() +} + +const isPrerelease = (type) => { + return type.startsWith('pre') +} + +module.exports = truncate diff --git a/deps/npm/node_modules/semver/index.js b/deps/npm/node_modules/semver/index.js index 285662acb32892..bc1f608ceaad3d 100644 --- a/deps/npm/node_modules/semver/index.js +++ b/deps/npm/node_modules/semver/index.js @@ -28,6 +28,7 @@ const gte = require('./functions/gte') const lte = require('./functions/lte') const cmp = require('./functions/cmp') const coerce = require('./functions/coerce') +const truncate = require('./functions/truncate') const Comparator = require('./classes/comparator') const Range = require('./classes/range') const satisfies = require('./functions/satisfies') @@ -66,6 +67,7 @@ module.exports = { lte, cmp, coerce, + truncate, Comparator, Range, satisfies, diff --git a/deps/npm/node_modules/semver/internal/re.js b/deps/npm/node_modules/semver/internal/re.js index 639fca89de8e63..2ec2c22c972d32 100644 --- a/deps/npm/node_modules/semver/internal/re.js +++ b/deps/npm/node_modules/semver/internal/re.js @@ -136,7 +136,7 @@ createToken('LOOSE', `^${src[t.LOOSEPLAIN]}$`) createToken('GTLT', '((?:<|>)?=?)') // Something like "2.*" or "1.2.x". -// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Note that "x.x" is a valid xRange identifier, meaning "any version" // Only the first item is strictly required. createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`) createToken('XRANGEIDENTIFIER', `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`) diff --git a/deps/npm/node_modules/semver/package.json b/deps/npm/node_modules/semver/package.json index a84de916085998..6edb9ab49d9774 100644 --- a/deps/npm/node_modules/semver/package.json +++ b/deps/npm/node_modules/semver/package.json @@ -1,6 +1,6 @@ { "name": "semver", - "version": "7.7.4", + "version": "7.8.1", "description": "The semantic version parser used by npm.", "main": "index.js", "scripts": { @@ -15,7 +15,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^6.0.0", - "@npmcli/template-oss": "4.29.0", + "@npmcli/template-oss": "5.0.0", "benchmark": "^2.1.4", "tap": "^16.0.0" }, @@ -52,7 +52,7 @@ "author": "GitHub Inc.", "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.29.0", + "version": "5.0.0", "engines": ">=10", "distPaths": [ "classes/", diff --git a/deps/npm/node_modules/semver/range.bnf b/deps/npm/node_modules/semver/range.bnf index d4c6ae0d76c9ac..a7a4bc37831127 100644 --- a/deps/npm/node_modules/semver/range.bnf +++ b/deps/npm/node_modules/semver/range.bnf @@ -10,7 +10,8 @@ nr ::= '0' | [1-9] ( [0-9] ) * tilde ::= '~' partial caret ::= '^' partial qualifier ::= ( '-' pre )? ( '+' build )? -pre ::= parts -build ::= parts -parts ::= part ( '.' part ) * -part ::= nr | [-0-9A-Za-z]+ +pre ::= prepart ( '.' prepart ) * +prepart ::= nr | alphanumid +build ::= buildid ( '.' buildid ) * +alphanumid ::= ( [0-9] ) * [A-Za-z-] [-0-9A-Za-z] * +buildid ::= [-0-9A-Za-z]+ diff --git a/deps/npm/node_modules/semver/ranges/subset.js b/deps/npm/node_modules/semver/ranges/subset.js index 99f43218075c86..a949832329003b 100644 --- a/deps/npm/node_modules/semver/ranges/subset.js +++ b/deps/npm/node_modules/semver/ranges/subset.js @@ -174,7 +174,7 @@ const simpleSubset = (sub, dom, options) => { if (higher === c && higher !== gt) { return false } - } else if (gt.operator === '>=' && !satisfies(gt.semver, String(c), options)) { + } else if (gt.operator === '>=' && !c.test(gt.semver)) { return false } } @@ -192,7 +192,7 @@ const simpleSubset = (sub, dom, options) => { if (lower === c && lower !== lt) { return false } - } else if (lt.operator === '<=' && !satisfies(lt.semver, String(c), options)) { + } else if (lt.operator === '<=' && !c.test(lt.semver)) { return false } } diff --git a/deps/npm/node_modules/sigstore/dist/config.js b/deps/npm/node_modules/sigstore/dist/config.js index e8b2392f97f236..373149fe22fb75 100644 --- a/deps/npm/node_modules/sigstore/dist/config.js +++ b/deps/npm/node_modules/sigstore/dist/config.js @@ -65,6 +65,12 @@ function createVerificationPolicy(options) { if (options.certificateIssuer) { policy.extensions = { issuer: options.certificateIssuer }; } + if (options.certificateOIDs) { + policy.oids = Object.entries(options.certificateOIDs).map(([oid, value]) => ({ + oid: { id: oid.split('.').map(Number) }, + value: Buffer.from(value), + })); + } return policy; } // Instantiate the FulcioSigner based on the supplied options. diff --git a/deps/npm/node_modules/sigstore/package.json b/deps/npm/node_modules/sigstore/package.json index 5965f0889ca7db..e0acea6d96287e 100644 --- a/deps/npm/node_modules/sigstore/package.json +++ b/deps/npm/node_modules/sigstore/package.json @@ -1,6 +1,6 @@ { "name": "sigstore", - "version": "4.1.0", + "version": "4.1.1", "description": "code-signing for npm packages", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -29,17 +29,17 @@ "devDependencies": { "@sigstore/rekor-types": "^4.0.0", "@sigstore/jest": "^0.0.0", - "@sigstore/mock": "^0.11.0", - "@tufjs/repo-mock": "^4.0.0", + "@sigstore/mock": "^0.12.1", + "@tufjs/repo-mock": "^4.0.1", "@types/make-fetch-happen": "^10.0.4" }, "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/deps/npm/node_modules/socks/package.json b/deps/npm/node_modules/socks/package.json index a7a2a20190ad3a..078e21216b782e 100644 --- a/deps/npm/node_modules/socks/package.json +++ b/deps/npm/node_modules/socks/package.json @@ -1,14 +1,14 @@ { "name": "socks", "private": false, - "version": "2.8.7", + "version": "2.8.9", "description": "Fully featured SOCKS proxy client supporting SOCKSv4, SOCKSv4a, and SOCKSv5. Includes Bind and Associate functionality.", "main": "build/index.js", "typings": "typings/index.d.ts", "homepage": "https://github.com/JoshGlazebrook/socks/", "repository": { "type": "git", - "url": "https://github.com/JoshGlazebrook/socks.git" + "url": "git+https://github.com/JoshGlazebrook/socks.git" }, "bugs": { "url": "https://github.com/JoshGlazebrook/socks/issues" @@ -44,7 +44,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "scripts": { diff --git a/deps/npm/node_modules/tar/dist/commonjs/index.min.js b/deps/npm/node_modules/tar/dist/commonjs/index.min.js index 6563e9e669054d..5bdc937545e805 100644 --- a/deps/npm/node_modules/tar/dist/commonjs/index.min.js +++ b/deps/npm/node_modules/tar/dist/commonjs/index.min.js @@ -1,4 +1,4 @@ -"use strict";var d=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var We=d(F=>{"use strict";var Oo=F&&F.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(F,"__esModule",{value:!0});F.Minipass=F.isWritable=F.isReadable=F.isStream=void 0;var Cr=typeof process=="object"&&process?process:{stdout:null,stderr:null},ts=require("node:events"),jr=Oo(require("node:stream")),Ro=require("node:string_decoder"),vo=s=>!!s&&typeof s=="object"&&(s instanceof Ht||s instanceof jr.default||(0,F.isReadable)(s)||(0,F.isWritable)(s));F.isStream=vo;var To=s=>!!s&&typeof s=="object"&&s instanceof ts.EventEmitter&&typeof s.pipe=="function"&&s.pipe!==jr.default.Writable.prototype.pipe;F.isReadable=To;var Do=s=>!!s&&typeof s=="object"&&s instanceof ts.EventEmitter&&typeof s.write=="function"&&typeof s.end=="function";F.isWritable=Do;var le=Symbol("EOF"),ue=Symbol("maybeEmitEnd"),_e=Symbol("emittedEnd"),kt=Symbol("emittingEnd"),ft=Symbol("emittedError"),jt=Symbol("closed"),Br=Symbol("read"),xt=Symbol("flush"),zr=Symbol("flushChunk"),K=Symbol("encoding"),Ue=Symbol("decoder"),O=Symbol("flowing"),dt=Symbol("paused"),qe=Symbol("resume"),R=Symbol("buffer"),I=Symbol("pipes"),v=Symbol("bufferLength"),Vi=Symbol("bufferPush"),Ut=Symbol("bufferShift"),N=Symbol("objectMode"),y=Symbol("destroyed"),$i=Symbol("error"),Xi=Symbol("emitData"),kr=Symbol("emitEnd"),Qi=Symbol("emitEnd2"),J=Symbol("async"),Ji=Symbol("abort"),qt=Symbol("aborted"),mt=Symbol("signal"),Pe=Symbol("dataListeners"),k=Symbol("discarded"),pt=s=>Promise.resolve().then(s),Po=s=>s(),No=s=>s==="end"||s==="finish"||s==="prefinish",Mo=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Lo=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Wt=class{src;dest;opts;ondrain;constructor(e,t,i){this.src=e,this.dest=t,this.opts=i,this.ondrain=()=>e[qe](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},es=class extends Wt{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,t,i){super(e,t,i),this.proxyErrors=r=>this.dest.emit("error",r),e.on("error",this.proxyErrors)}},Ao=s=>!!s.objectMode,Io=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",Ht=class extends ts.EventEmitter{[O]=!1;[dt]=!1;[I]=[];[R]=[];[N];[K];[J];[Ue];[le]=!1;[_e]=!1;[kt]=!1;[jt]=!1;[ft]=null;[v]=0;[y]=!1;[mt];[qt]=!1;[Pe]=0;[k]=!1;writable=!0;readable=!0;constructor(...e){let t=e[0]||{};if(super(),t.objectMode&&typeof t.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Ao(t)?(this[N]=!0,this[K]=null):Io(t)?(this[K]=t.encoding,this[N]=!1):(this[N]=!1,this[K]=null),this[J]=!!t.async,this[Ue]=this[K]?new Ro.StringDecoder(this[K]):null,t&&t.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[R]}),t&&t.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[I]});let{signal:i}=t;i&&(this[mt]=i,i.aborted?this[Ji]():i.addEventListener("abort",()=>this[Ji]()))}get bufferLength(){return this[v]}get encoding(){return this[K]}set encoding(e){throw new Error("Encoding must be set at instantiation time")}setEncoding(e){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[N]}set objectMode(e){throw new Error("objectMode must be set at instantiation time")}get async(){return this[J]}set async(e){this[J]=this[J]||!!e}[Ji](){this[qt]=!0,this.emit("abort",this[mt]?.reason),this.destroy(this[mt]?.reason)}get aborted(){return this[qt]}set aborted(e){}write(e,t,i){if(this[qt])return!1;if(this[le])throw new Error("write after end");if(this[y])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof t=="function"&&(i=t,t="utf8"),t||(t="utf8");let r=this[J]?pt:Po;if(!this[N]&&!Buffer.isBuffer(e)){if(Lo(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(Mo(e))e=Buffer.from(e);else if(typeof e!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[N]?(this[O]&&this[v]!==0&&this[xt](!0),this[O]?this.emit("data",e):this[Vi](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[O]):e.length?(typeof e=="string"&&!(t===this[K]&&!this[Ue]?.lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[K]&&(e=this[Ue].write(e)),this[O]&&this[v]!==0&&this[xt](!0),this[O]?this.emit("data",e):this[Vi](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[O]):(this[v]!==0&&this.emit("readable"),i&&r(i),this[O])}read(e){if(this[y])return null;if(this[k]=!1,this[v]===0||e===0||e&&e>this[v])return this[ue](),null;this[N]&&(e=null),this[R].length>1&&!this[N]&&(this[R]=[this[K]?this[R].join(""):Buffer.concat(this[R],this[v])]);let t=this[Br](e||null,this[R][0]);return this[ue](),t}[Br](e,t){if(this[N])this[Ut]();else{let i=t;e===i.length||e===null?this[Ut]():typeof i=="string"?(this[R][0]=i.slice(e),t=i.slice(0,e),this[v]-=e):(this[R][0]=i.subarray(e),t=i.subarray(0,e),this[v]-=e)}return this.emit("data",t),!this[R].length&&!this[le]&&this.emit("drain"),t}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t="utf8"),e!==void 0&&this.write(e,t),i&&this.once("end",i),this[le]=!0,this.writable=!1,(this[O]||!this[dt])&&this[ue](),this}[qe](){this[y]||(!this[Pe]&&!this[I].length&&(this[k]=!0),this[dt]=!1,this[O]=!0,this.emit("resume"),this[R].length?this[xt]():this[le]?this[ue]():this.emit("drain"))}resume(){return this[qe]()}pause(){this[O]=!1,this[dt]=!0,this[k]=!1}get destroyed(){return this[y]}get flowing(){return this[O]}get paused(){return this[dt]}[Vi](e){this[N]?this[v]+=1:this[v]+=e.length,this[R].push(e)}[Ut](){return this[N]?this[v]-=1:this[v]-=this[R][0].length,this[R].shift()}[xt](e=!1){do;while(this[zr](this[Ut]())&&this[R].length);!e&&!this[R].length&&!this[le]&&this.emit("drain")}[zr](e){return this.emit("data",e),this[O]}pipe(e,t){if(this[y])return e;this[k]=!1;let i=this[_e];return t=t||{},e===Cr.stdout||e===Cr.stderr?t.end=!1:t.end=t.end!==!1,t.proxyErrors=!!t.proxyErrors,i?t.end&&e.end():(this[I].push(t.proxyErrors?new es(this,e,t):new Wt(this,e,t)),this[J]?pt(()=>this[qe]()):this[qe]()),e}unpipe(e){let t=this[I].find(i=>i.dest===e);t&&(this[I].length===1?(this[O]&&this[Pe]===0&&(this[O]=!1),this[I]=[]):this[I].splice(this[I].indexOf(t),1),t.unpipe())}addListener(e,t){return this.on(e,t)}on(e,t){let i=super.on(e,t);if(e==="data")this[k]=!1,this[Pe]++,!this[I].length&&!this[O]&&this[qe]();else if(e==="readable"&&this[v]!==0)super.emit("readable");else if(No(e)&&this[_e])super.emit(e),this.removeAllListeners(e);else if(e==="error"&&this[ft]){let r=t;this[J]?pt(()=>r.call(this,this[ft])):r.call(this,this[ft])}return i}removeListener(e,t){return this.off(e,t)}off(e,t){let i=super.off(e,t);return e==="data"&&(this[Pe]=this.listeners("data").length,this[Pe]===0&&!this[k]&&!this[I].length&&(this[O]=!1)),i}removeAllListeners(e){let t=super.removeAllListeners(e);return(e==="data"||e===void 0)&&(this[Pe]=0,!this[k]&&!this[I].length&&(this[O]=!1)),t}get emittedEnd(){return this[_e]}[ue](){!this[kt]&&!this[_e]&&!this[y]&&this[R].length===0&&this[le]&&(this[kt]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[jt]&&this.emit("close"),this[kt]=!1)}emit(e,...t){let i=t[0];if(e!=="error"&&e!=="close"&&e!==y&&this[y])return!1;if(e==="data")return!this[N]&&!i?!1:this[J]?(pt(()=>this[Xi](i)),!0):this[Xi](i);if(e==="end")return this[kr]();if(e==="close"){if(this[jt]=!0,!this[_e]&&!this[y])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(e==="error"){this[ft]=i,super.emit($i,i);let n=!this[mt]||this.listeners("error").length?super.emit("error",i):!1;return this[ue](),n}else if(e==="resume"){let n=super.emit("resume");return this[ue](),n}else if(e==="finish"||e==="prefinish"){let n=super.emit(e);return this.removeAllListeners(e),n}let r=super.emit(e,...t);return this[ue](),r}[Xi](e){for(let i of this[I])i.dest.write(e)===!1&&this.pause();let t=this[k]?!1:super.emit("data",e);return this[ue](),t}[kr](){return this[_e]?!1:(this[_e]=!0,this.readable=!1,this[J]?(pt(()=>this[Qi]()),!0):this[Qi]())}[Qi](){if(this[Ue]){let t=this[Ue].end();if(t){for(let i of this[I])i.dest.write(t);this[k]||super.emit("data",t)}}for(let t of this[I])t.end();let e=super.emit("end");return this.removeAllListeners("end"),e}async collect(){let e=Object.assign([],{dataLength:0});this[N]||(e.dataLength=0);let t=this.promise();return this.on("data",i=>{e.push(i),this[N]||(e.dataLength+=i.length)}),await t,e}async concat(){if(this[N])throw new Error("cannot concat in objectMode");let e=await this.collect();return this[K]?e.join(""):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,t)=>{this.on(y,()=>t(new Error("stream destroyed"))),this.on("error",i=>t(i)),this.on("end",()=>e())})}[Symbol.asyncIterator](){this[k]=!1;let e=!1,t=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return t();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[le])return t();let n,o,a=c=>{this.off("data",h),this.off("end",l),this.off(y,u),t(),o(c)},h=c=>{this.off("error",a),this.off("end",l),this.off(y,u),this.pause(),n({value:c,done:!!this[le]})},l=()=>{this.off("error",a),this.off("data",h),this.off(y,u),t(),n({done:!0,value:void 0})},u=()=>a(new Error("stream destroyed"));return new Promise((c,E)=>{o=E,n=c,this.once(y,u),this.once("error",a),this.once("end",l),this.once("data",h)})},throw:t,return:t,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[k]=!1;let e=!1,t=()=>(this.pause(),this.off($i,t),this.off(y,t),this.off("end",t),e=!0,{done:!0,value:void 0}),i=()=>{if(e)return t();let r=this.read();return r===null?t():{done:!1,value:r}};return this.once("end",t),this.once($i,t),this.once(y,t),{next:i,throw:t,return:t,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(e){if(this[y])return e?this.emit("error",e):this.emit(y),this;this[y]=!0,this[k]=!0,this[R].length=0,this[v]=0;let t=this;return typeof t.close=="function"&&!this[jt]&&t.close(),e?this.emit("error",e):this.emit(y),this}static get isStream(){return F.isStream}};F.Minipass=Ht});var Ke=d(W=>{"use strict";var xr=W&&W.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(W,"__esModule",{value:!0});W.WriteStreamSync=W.WriteStream=W.ReadStreamSync=W.ReadStream=void 0;var Fo=xr(require("events")),B=xr(require("fs")),Co=We(),Bo=B.default.writev,ye=Symbol("_autoClose"),$=Symbol("_close"),_t=Symbol("_ended"),p=Symbol("_fd"),is=Symbol("_finished"),fe=Symbol("_flags"),ss=Symbol("_flush"),as=Symbol("_handleChunk"),hs=Symbol("_makeBuf"),yt=Symbol("_mode"),Zt=Symbol("_needDrain"),Ge=Symbol("_onerror"),Ye=Symbol("_onopen"),rs=Symbol("_onread"),He=Symbol("_onwrite"),Ee=Symbol("_open"),V=Symbol("_path"),we=Symbol("_pos"),ee=Symbol("_queue"),Ze=Symbol("_read"),ns=Symbol("_readSize"),ce=Symbol("_reading"),wt=Symbol("_remain"),os=Symbol("_size"),Gt=Symbol("_write"),Ne=Symbol("_writing"),Yt=Symbol("_defaultFlag"),Me=Symbol("_errored"),Kt=class extends Co.Minipass{[Me]=!1;[p];[V];[ns];[ce]=!1;[os];[wt];[ye];constructor(e,t){if(t=t||{},super(t),this.readable=!0,this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[Me]=!1,this[p]=typeof t.fd=="number"?t.fd:void 0,this[V]=e,this[ns]=t.readSize||16*1024*1024,this[ce]=!1,this[os]=typeof t.size=="number"?t.size:1/0,this[wt]=this[os],this[ye]=typeof t.autoClose=="boolean"?t.autoClose:!0,typeof this[p]=="number"?this[Ze]():this[Ee]()}get fd(){return this[p]}get path(){return this[V]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[Ee](){B.default.open(this[V],"r",(e,t)=>this[Ye](e,t))}[Ye](e,t){e?this[Ge](e):(this[p]=t,this.emit("open",t),this[Ze]())}[hs](){return Buffer.allocUnsafe(Math.min(this[ns],this[wt]))}[Ze](){if(!this[ce]){this[ce]=!0;let e=this[hs]();if(e.length===0)return process.nextTick(()=>this[rs](null,0,e));B.default.read(this[p],e,0,e.length,null,(t,i,r)=>this[rs](t,i,r))}}[rs](e,t,i){this[ce]=!1,e?this[Ge](e):this[as](t,i)&&this[Ze]()}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,B.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}[Ge](e){this[ce]=!0,this[$](),this.emit("error",e)}[as](e,t){let i=!1;return this[wt]-=e,e>0&&(i=super.write(ethis[Ye](e,t))}[Ye](e,t){this[Yt]&&this[fe]==="r+"&&e&&e.code==="ENOENT"?(this[fe]="w",this[Ee]()):e?this[Ge](e):(this[p]=t,this.emit("open",t),this[Ne]||this[ss]())}end(e,t){return e&&this.write(e,t),this[_t]=!0,!this[Ne]&&!this[ee].length&&typeof this[p]=="number"&&this[He](null,0),this}write(e,t){return typeof e=="string"&&(e=Buffer.from(e,t)),this[_t]?(this.emit("error",new Error("write() after end()")),!1):this[p]===void 0||this[Ne]||this[ee].length?(this[ee].push(e),this[Zt]=!0,!1):(this[Ne]=!0,this[Gt](e),!0)}[Gt](e){B.default.write(this[p],e,0,e.length,this[we],(t,i)=>this[He](t,i))}[He](e,t){e?this[Ge](e):(this[we]!==void 0&&typeof t=="number"&&(this[we]+=t),this[ee].length?this[ss]():(this[Ne]=!1,this[_t]&&!this[is]?(this[is]=!0,this[$](),this.emit("finish")):this[Zt]&&(this[Zt]=!1,this.emit("drain"))))}[ss](){if(this[ee].length===0)this[_t]&&this[He](null,0);else if(this[ee].length===1)this[Gt](this[ee].pop());else{let e=this[ee];this[ee]=[],Bo(this[p],e,this[we],(t,i)=>this[He](t,i))}}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,B.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}};W.WriteStream=Vt;var us=class extends Vt{[Ee](){let e;if(this[Yt]&&this[fe]==="r+")try{e=B.default.openSync(this[V],this[fe],this[yt])}catch(t){if(t?.code==="ENOENT")return this[fe]="w",this[Ee]();throw t}else e=B.default.openSync(this[V],this[fe],this[yt]);this[Ye](null,e)}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,B.default.closeSync(e),this.emit("close")}}[Gt](e){let t=!0;try{this[He](null,B.default.writeSync(this[p],e,0,e.length,this[we])),t=!1}finally{if(t)try{this[$]()}catch{}}}};W.WriteStreamSync=us});var $t=d(b=>{"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.dealias=b.isNoFile=b.isFile=b.isAsync=b.isSync=b.isAsyncNoFile=b.isSyncNoFile=b.isAsyncFile=b.isSyncFile=void 0;var zo=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),ko=s=>!!s.sync&&!!s.file;b.isSyncFile=ko;var jo=s=>!s.sync&&!!s.file;b.isAsyncFile=jo;var xo=s=>!!s.sync&&!s.file;b.isSyncNoFile=xo;var Uo=s=>!s.sync&&!s.file;b.isAsyncNoFile=Uo;var qo=s=>!!s.sync;b.isSync=qo;var Wo=s=>!s.sync;b.isAsync=Wo;var Ho=s=>!!s.file;b.isFile=Ho;var Zo=s=>!s.file;b.isNoFile=Zo;var Go=s=>{let e=zo.get(s);return e||s},Yo=(s={})=>{if(!s)return{};let e={};for(let[t,i]of Object.entries(s)){let r=Go(t);e[r]=i}return e.chmod===void 0&&e.noChmod===!1&&(e.chmod=!0),delete e.noChmod,e};b.dealias=Yo});var Ve=d(Xt=>{"use strict";Object.defineProperty(Xt,"__esModule",{value:!0});Xt.makeCommand=void 0;var Et=$t(),Ko=(s,e,t,i,r)=>Object.assign((n=[],o,a)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(a=o,o=void 0),o=o?Array.from(o):[];let h=(0,Et.dealias)(n);if(r?.(h,o),(0,Et.isSyncFile)(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return s(h,o)}else if((0,Et.isAsyncFile)(h)){let l=e(h,o);return a?l.then(()=>a(),a):l}else if((0,Et.isSyncNoFile)(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return t(h,o)}else if((0,Et.isAsyncNoFile)(h)){if(typeof a=="function")throw new TypeError("callback only supported with file option");return i(h,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:e,syncNoFile:t,asyncNoFile:i,validate:r});Xt.makeCommand=Ko});var cs=d($e=>{"use strict";var Vo=$e&&$e.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty($e,"__esModule",{value:!0});$e.constants=void 0;var $o=Vo(require("zlib")),Xo=$o.default.constants||{ZLIB_VERNUM:4736};$e.constants=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},Xo))});var Ts=d(f=>{"use strict";var Qo=f&&f.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Jo=f&&f.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ea=f&&f.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;rs,fs=qr?.writable===!0||qr?.set!==void 0?s=>{Le.Buffer.concat=s?na:ra}:s=>{},Ae=Symbol("_superWrite"),Ie=class extends Error{code;errno;constructor(e,t){super("zlib: "+e.message,{cause:e}),this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,t??this.constructor)}get name(){return"ZlibError"}};f.ZlibError=Ie;var ds=Symbol("flushFlag"),bt=class extends ia.Minipass{#e=!1;#i=!1;#s;#n;#r;#t;#o;get sawError(){return this.#e}get handle(){return this.#t}get flushFlag(){return this.#s}constructor(e,t){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(e),this.#s=e.flush??0,this.#n=e.finishFlush??0,this.#r=e.fullFlushFlag??0,typeof Ur[t]!="function")throw new TypeError("Compression method not supported: "+t);try{this.#t=new Ur[t](e)}catch(i){throw new Ie(i,this.constructor)}this.#o=i=>{this.#e||(this.#e=!0,this.close(),this.emit("error",i))},this.#t?.on("error",i=>this.#o(new Ie(i))),this.once("end",()=>this.close)}close(){this.#t&&(this.#t.close(),this.#t=void 0,this.emit("close"))}reset(){if(!this.#e)return(0,ms.default)(this.#t,"zlib binding closed"),this.#t.reset?.()}flush(e){this.ended||(typeof e!="number"&&(e=this.#r),this.write(Object.assign(Le.Buffer.alloc(0),{[ds]:e})))}end(e,t,i){return typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&(t?this.write(e,t):this.write(e)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Ae](e){return super.write(e)}write(e,t,i){if(typeof t=="function"&&(i=t,t="utf8"),typeof e=="string"&&(e=Le.Buffer.from(e,t)),this.#e)return;(0,ms.default)(this.#t,"zlib binding closed");let r=this.#t._handle,n=r.close;r.close=()=>{};let o=this.#t.close;this.#t.close=()=>{},fs(!0);let a;try{let l=typeof e[ds]=="number"?e[ds]:this.#s;a=this.#t._processChunk(e,l),fs(!1)}catch(l){fs(!1),this.#o(new Ie(l,this.write))}finally{this.#t&&(this.#t._handle=r,r.close=n,this.#t.close=o,this.#t.removeAllListeners("error"))}this.#t&&this.#t.on("error",l=>this.#o(new Ie(l,this.write)));let h;if(a)if(Array.isArray(a)&&a.length>0){let l=a[0];h=this[Ae](Le.Buffer.from(l));for(let u=1;u{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(e,t)}finally{this.handle.flush=i}this.handle&&(this.#e=e,this.#i=t)}}}};f.Zlib=ie;var ps=class extends ie{constructor(e){super(e,"Deflate")}};f.Deflate=ps;var _s=class extends ie{constructor(e){super(e,"Inflate")}};f.Inflate=_s;var ws=class extends ie{#e;constructor(e){super(e,"Gzip"),this.#e=e&&!!e.portable}[Ae](e){return this.#e?(this.#e=!1,e[9]=255,super[Ae](e)):super[Ae](e)}};f.Gzip=ws;var ys=class extends ie{constructor(e){super(e,"Gunzip")}};f.Gunzip=ys;var Es=class extends ie{constructor(e){super(e,"DeflateRaw")}};f.DeflateRaw=Es;var bs=class extends ie{constructor(e){super(e,"InflateRaw")}};f.InflateRaw=bs;var Ss=class extends ie{constructor(e){super(e,"Unzip")}};f.Unzip=Ss;var Qt=class extends bt{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||te.constants.BROTLI_OPERATION_FINISH,e.fullFlushFlag=te.constants.BROTLI_OPERATION_FLUSH,super(e,t)}},gs=class extends Qt{constructor(e){super(e,"BrotliCompress")}};f.BrotliCompress=gs;var Os=class extends Qt{constructor(e){super(e,"BrotliDecompress")}};f.BrotliDecompress=Os;var Jt=class extends bt{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.ZSTD_e_continue,e.finishFlush=e.finishFlush||te.constants.ZSTD_e_end,e.fullFlushFlag=te.constants.ZSTD_e_flush,super(e,t)}},Rs=class extends Jt{constructor(e){super(e,"ZstdCompress")}};f.ZstdCompress=Rs;var vs=class extends Jt{constructor(e){super(e,"ZstdDecompress")}};f.ZstdDecompress=vs});var Zr=d(Xe=>{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.parse=Xe.encode=void 0;var oa=(s,e)=>{if(Number.isSafeInteger(s))s<0?ha(s,e):aa(s,e);else throw Error("cannot encode number outside of javascript safe integer range");return e};Xe.encode=oa;var aa=(s,e)=>{e[0]=128;for(var t=e.length;t>1;t--)e[t-1]=s&255,s=Math.floor(s/256)},ha=(s,e)=>{e[0]=255;var t=!1;s=s*-1;for(var i=e.length;i>1;i--){var r=s&255;s=Math.floor(s/256),t?e[i-1]=Wr(r):r===0?e[i-1]=0:(t=!0,e[i-1]=Hr(r))}},la=s=>{let e=s[0],t=e===128?ca(s.subarray(1,s.length)):e===255?ua(s):null;if(t===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(t))throw Error("parsed number outside of javascript safe integer range");return t};Xe.parse=la;var ua=s=>{for(var e=s.length,t=0,i=!1,r=e-1;r>-1;r--){var n=Number(s[r]),o;i?o=Wr(n):n===0?o=n:(i=!0,o=Hr(n)),o!==0&&(t-=o*Math.pow(256,e-r-1))}return t},ca=s=>{for(var e=s.length,t=0,i=e-1;i>-1;i--){var r=Number(s[i]);r!==0&&(t+=r*Math.pow(256,e-i-1))}return t},Wr=s=>(255^s)&255,Hr=s=>(255^s)+1&255});var Ds=d(j=>{"use strict";Object.defineProperty(j,"__esModule",{value:!0});j.code=j.name=j.isName=j.isCode=void 0;var fa=s=>j.name.has(s);j.isCode=fa;var da=s=>j.code.has(s);j.isName=da;j.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);j.code=new Map(Array.from(j.name).map(s=>[s[1],s[0]]))});var Je=d(se=>{"use strict";var ma=se&&se.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),pa=se&&se.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Gr=se&&se.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r=t+512))throw new Error("need 512 bytes for header");this.path=i?.path??Fe(e,t,100),this.mode=i?.mode??r?.mode??be(e,t+100,8),this.uid=i?.uid??r?.uid??be(e,t+108,8),this.gid=i?.gid??r?.gid??be(e,t+116,8),this.size=i?.size??r?.size??be(e,t+124,12),this.mtime=i?.mtime??r?.mtime??Ps(e,t+136,12),this.cksum=be(e,t+148,12),r&&this.#i(r,!0),i&&this.#i(i);let n=Fe(e,t+156,1);if(St.isCode(n)&&(this.#e=n||"0"),this.#e==="0"&&this.path.slice(-1)==="/"&&(this.#e="5"),this.#e==="5"&&(this.size=0),this.linkpath=Fe(e,t+157,100),e.subarray(t+257,t+265).toString()==="ustar\x0000")if(this.uname=i?.uname??r?.uname??Fe(e,t+265,32),this.gname=i?.gname??r?.gname??Fe(e,t+297,32),this.devmaj=i?.devmaj??r?.devmaj??be(e,t+329,8)??0,this.devmin=i?.devmin??r?.devmin??be(e,t+337,8)??0,e[t+475]!==0){let a=Fe(e,t+345,155);this.path=a+"/"+this.path}else{let a=Fe(e,t+345,130);a&&(this.path=a+"/"+this.path),this.atime=i?.atime??r?.atime??Ps(e,t+476,12),this.ctime=i?.ctime??r?.ctime??Ps(e,t+488,12)}let o=256;for(let a=t;a!(r==null||i==="path"&&t||i==="linkpath"&&t||i==="global"))))}encode(e,t=0){if(e||(e=this.block=Buffer.alloc(512)),this.#e==="Unsupported"&&(this.#e="0"),!(e.length>=t+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=_a(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Ce(e,t,100,n)||this.needPax,this.needPax=Se(e,t+100,8,this.mode)||this.needPax,this.needPax=Se(e,t+108,8,this.uid)||this.needPax,this.needPax=Se(e,t+116,8,this.gid)||this.needPax,this.needPax=Se(e,t+124,12,this.size)||this.needPax,this.needPax=Ns(e,t+136,12,this.mtime)||this.needPax,e[t+156]=Number(this.#e.codePointAt(0)),this.needPax=Ce(e,t+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",t+257,8),this.needPax=Ce(e,t+265,32,this.uname)||this.needPax,this.needPax=Ce(e,t+297,32,this.gname)||this.needPax,this.needPax=Se(e,t+329,8,this.devmaj)||this.needPax,this.needPax=Se(e,t+337,8,this.devmin)||this.needPax,this.needPax=Ce(e,t+345,i,o)||this.needPax,e[t+475]!==0?this.needPax=Ce(e,t+345,155,o)||this.needPax:(this.needPax=Ce(e,t+345,130,o)||this.needPax,this.needPax=Ns(e,t+476,12,this.atime)||this.needPax,this.needPax=Ns(e,t+488,12,this.ctime)||this.needPax);let a=256;for(let h=t;h{let i=s,r="",n,o=Qe.posix.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Qe.posix.dirname(i),i=Qe.posix.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=e?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=e?n=[i.slice(0,99),r,!0]:(i=Qe.posix.join(Qe.posix.basename(r),i),r=Qe.posix.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},Fe=(s,e,t)=>s.subarray(e,e+t).toString("utf8").replace(/\0.*/,""),Ps=(s,e,t)=>wa(be(s,e,t)),wa=s=>s===void 0?void 0:new Date(s*1e3),be=(s,e,t)=>Number(s[e])&128?Yr.parse(s.subarray(e,e+t)):Ea(s,e,t),ya=s=>isNaN(s)?void 0:s,Ea=(s,e,t)=>ya(parseInt(s.subarray(e,e+t).toString("utf8").replace(/\0.*$/,"").trim(),8)),ba={12:8589934591,8:2097151},Se=(s,e,t,i)=>i===void 0?!1:i>ba[t]||i<0?(Yr.encode(i,s.subarray(e,e+t)),!0):(Sa(s,e,t,i),!1),Sa=(s,e,t,i)=>s.write(ga(i,t),e,t,"ascii"),ga=(s,e)=>Oa(Math.floor(s).toString(8),e),Oa=(s,e)=>(s.length===e-1?s:new Array(e-s.length-1).join("0")+s+" ")+"\0",Ns=(s,e,t,i)=>i===void 0?!1:Se(s,e,t,i.getTime()/1e3),Ra=new Array(156).join("\0"),Ce=(s,e,t,i)=>i===void 0?!1:(s.write(i+Ra,e,t,"utf8"),i.length!==Buffer.byteLength(i)||i.length>t)});var ti=d(ei=>{"use strict";Object.defineProperty(ei,"__esModule",{value:!0});ei.Pax=void 0;var va=require("node:path"),Ta=Je(),Ls=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(e,t=!1){this.atime=e.atime,this.charset=e.charset,this.comment=e.comment,this.ctime=e.ctime,this.dev=e.dev,this.gid=e.gid,this.global=t,this.gname=e.gname,this.ino=e.ino,this.linkpath=e.linkpath,this.mtime=e.mtime,this.nlink=e.nlink,this.path=e.path,this.size=e.size,this.uid=e.uid,this.uname=e.uname}encode(){let e=this.encodeBody();if(e==="")return Buffer.allocUnsafe(0);let t=Buffer.byteLength(e),i=512*Math.ceil(1+t/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new Ta.Header({path:("PaxHeader/"+(0,va.basename)(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:t,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(e,512,t,"utf8");for(let n=t+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(e,t,i=!1){return new s(Da(Pa(e),t),i)}};ei.Pax=Ls;var Da=(s,e)=>e?Object.assign({},e,s):s,Pa=s=>s.replace(/\n$/,"").split(` -`).reduce(Na,Object.create(null)),Na=(s,e)=>{let t=parseInt(e,10);if(t!==Buffer.byteLength(e)+1)return s;e=e.slice((t+" ").length);let i=e.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=");return s[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(o)*1e3):/^[0-9]+$/.test(o)?+o:o,s}});var et=d(ii=>{"use strict";Object.defineProperty(ii,"__esModule",{value:!0});ii.normalizeWindowsPath=void 0;var Ma=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;ii.normalizeWindowsPath=Ma!=="win32"?s=>s:s=>s&&s.replaceAll(/\\/g,"/")});var ni=d(ri=>{"use strict";Object.defineProperty(ri,"__esModule",{value:!0});ri.ReadEntry=void 0;var La=We(),si=et(),As=class extends La.Minipass{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(e,t,i){switch(super({}),this.pause(),this.extended=t,this.globalExtended=i,this.header=e,this.remain=e.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=e.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!e.path)throw new Error("no path provided for tar.ReadEntry");this.path=(0,si.normalizeWindowsPath)(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=this.remain,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath?(0,si.normalizeWindowsPath)(e.linkpath):void 0,this.uname=e.uname,this.gname=e.gname,t&&this.#e(t),i&&this.#e(i,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-t),this.blockRemain=Math.max(0,r-t),this.ignore?!0:i>=t?super.write(e):super.write(e.subarray(0,i))}#e(e,t=!1){e.path&&(e.path=(0,si.normalizeWindowsPath)(e.path)),e.linkpath&&(e.linkpath=(0,si.normalizeWindowsPath)(e.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(e).filter(([i,r])=>!(r==null||i==="path"&&t))))}};ri.ReadEntry=As});var ai=d(oi=>{"use strict";Object.defineProperty(oi,"__esModule",{value:!0});oi.warnMethod=void 0;var Aa=(s,e,t,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=t instanceof Error&&t.code||e,i.tarCode=e,!s.strict&&i.recoverable!==!1?(t instanceof Error&&(i=Object.assign(t,i),t=t.message),s.emit("warn",e,t,i)):t instanceof Error?s.emit("error",Object.assign(t,i)):s.emit("error",Object.assign(new Error(`${e}: ${t}`),i))};oi.warnMethod=Aa});var pi=d(mi=>{"use strict";Object.defineProperty(mi,"__esModule",{value:!0});mi.Parser=void 0;var Ia=require("events"),Is=Ts(),Kr=Je(),Vr=ti(),Fa=ni(),Ca=ai(),Ba=1024*1024,ks=Buffer.from([31,139]),js=Buffer.from([40,181,47,253]),za=Math.max(ks.length,js.length),H=Symbol("state"),Be=Symbol("writeEntry"),de=Symbol("readEntry"),Fs=Symbol("nextEntry"),$r=Symbol("processEntry"),re=Symbol("extendedHeader"),gt=Symbol("globalExtendedHeader"),ge=Symbol("meta"),Xr=Symbol("emitMeta"),_=Symbol("buffer"),me=Symbol("queue"),Oe=Symbol("ended"),Cs=Symbol("emittedEnd"),ze=Symbol("emit"),S=Symbol("unzip"),hi=Symbol("consumeChunk"),li=Symbol("consumeChunkSub"),Bs=Symbol("consumeBody"),Qr=Symbol("consumeMeta"),Jr=Symbol("consumeHeader"),Ot=Symbol("consuming"),zs=Symbol("bufferConcat"),ui=Symbol("maybeEnd"),tt=Symbol("writing"),Re=Symbol("aborted"),ci=Symbol("onDone"),ke=Symbol("sawValidEntry"),fi=Symbol("sawNullBlock"),di=Symbol("sawEOF"),en=Symbol("closeStream"),ka=()=>!0,xs=class extends Ia.EventEmitter{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[me]=[];[_];[de];[Be];[H]="begin";[ge]="";[re];[gt];[Oe]=!1;[S];[Re]=!1;[ke];[fi]=!1;[di]=!1;[tt]=!1;[Ot]=!1;[Cs]=!1;constructor(e={}){super(),this.file=e.file||"",this.on(ci,()=>{(this[H]==="begin"||this[ke]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(ci,e.ondone):this.on(ci,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||Ba,this.filter=typeof e.filter=="function"?e.filter:ka;let t=e.file&&(e.file.endsWith(".tar.br")||e.file.endsWith(".tbr"));this.brotli=!(e.gzip||e.zstd)&&e.brotli!==void 0?e.brotli:t?void 0:!1;let i=e.file&&(e.file.endsWith(".tar.zst")||e.file.endsWith(".tzst"));this.zstd=!(e.gzip||e.brotli)&&e.zstd!==void 0?e.zstd:i?!0:void 0,this.on("end",()=>this[en]()),typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onReadEntry=="function"&&this.on("entry",e.onReadEntry)}warn(e,t,i={}){(0,Ca.warnMethod)(this,e,t,i)}[Jr](e,t){this[ke]===void 0&&(this[ke]=!1);let i;try{i=new Kr.Header(e,t,this[re],this[gt])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[fi]?(this[di]=!0,this[H]==="begin"&&(this[H]="header"),this[ze]("eof")):(this[fi]=!0,this[ze]("nullBlock"));else if(this[fi]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[Be]=new Fa.ReadEntry(i,this[re],this[gt]);if(!this[ke])if(n.remain){let o=()=>{n.invalid||(this[ke]=!0)};n.on("end",o)}else this[ke]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[ze]("ignoredEntry",n),this[H]="ignore",n.resume()):n.size>0&&(this[ge]="",n.on("data",o=>this[ge]+=o),this[H]="meta"):(this[re]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[ze]("ignoredEntry",n),this[H]=n.remain?"ignore":"header",n.resume()):(n.remain?this[H]="body":(this[H]="header",n.end()),this[de]?this[me].push(n):(this[me].push(n),this[Fs]())))}}}[en](){queueMicrotask(()=>this.emit("close"))}[$r](e){let t=!0;if(!e)this[de]=void 0,t=!1;else if(Array.isArray(e)){let[i,...r]=e;this.emit(i,...r)}else this[de]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",()=>this[Fs]()),t=!1);return t}[Fs](){do;while(this[$r](this[me].shift()));if(this[me].length===0){let e=this[de];!e||e.flowing||e.size===e.remain?this[tt]||this.emit("drain"):e.once("drain",()=>this.emit("drain"))}}[Bs](e,t){let i=this[Be];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=e.length&&t===0?e:e.subarray(t,t+r);return i.write(n),i.blockRemain||(this[H]="header",this[Be]=void 0,i.end()),n.length}[Qr](e,t){let i=this[Be],r=this[Bs](e,t);return!this[Be]&&i&&this[Xr](i),r}[ze](e,t,i){this[me].length===0&&!this[de]?this.emit(e,t,i):this[me].push([e,t,i])}[Xr](e){switch(this[ze]("meta",this[ge]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[re]=Vr.Pax.parse(this[ge],this[re],!1);break;case"GlobalExtendedHeader":this[gt]=Vr.Pax.parse(this[ge],this[gt],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let t=this[re]??Object.create(null);this[re]=t,t.path=this[ge].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let t=this[re]||Object.create(null);this[re]=t,t.linkpath=this[ge].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+e.type)}}abort(e){this[Re]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1})}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this[Re])return i?.(),!1;if((this[S]===void 0||this.brotli===void 0&&this[S]===!1)&&e){if(this[_]&&(e=Buffer.concat([this[_],e]),this[_]=void 0),e.lengththis[hi](u)),this[S].on("error",u=>this.abort(u)),this[S].on("end",()=>{this[Oe]=!0,this[hi]()}),this[tt]=!0;let l=!!this[S][h?"end":"write"](e);return this[tt]=!1,i?.(),l}}this[tt]=!0,this[S]?this[S].write(e):this[hi](e),this[tt]=!1;let n=this[me].length>0?!1:this[de]?this[de].flowing:!0;return!n&&this[me].length===0&&this[de]?.once("drain",()=>this.emit("drain")),i?.(),n}[zs](e){e&&!this[Re]&&(this[_]=this[_]?Buffer.concat([this[_],e]):e)}[ui](){if(this[Oe]&&!this[Cs]&&!this[Re]&&!this[Ot]){this[Cs]=!0;let e=this[Be];if(e&&e.blockRemain){let t=this[_]?this[_].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[_]&&e.write(this[_]),e.end()}this[ze](ci)}}[hi](e){if(this[Ot]&&e)this[zs](e);else if(!e&&!this[_])this[ui]();else if(e){if(this[Ot]=!0,this[_]){this[zs](e);let t=this[_];this[_]=void 0,this[li](t)}else this[li](e);for(;this[_]&&this[_]?.length>=512&&!this[Re]&&!this[di];){let t=this[_];this[_]=void 0,this[li](t)}this[Ot]=!1}(!this[_]||this[Oe])&&this[ui]()}[li](e){let t=0,i=e.length;for(;t+512<=i&&!this[Re]&&!this[di];)switch(this[H]){case"begin":case"header":this[Jr](e,t),t+=512;break;case"ignore":case"body":t+=this[Bs](e,t);break;case"meta":t+=this[Qr](e,t);break;default:throw new Error("invalid state: "+this[H])}t{"use strict";Object.defineProperty(_i,"__esModule",{value:!0});_i.stripTrailingSlashes=void 0;var ja=s=>{let e=s.length-1,t=-1;for(;e>-1&&s.charAt(e)==="/";)t=e,e--;return t===-1?s:s.slice(0,t)};_i.stripTrailingSlashes=ja});var st=d(C=>{"use strict";var xa=C&&C.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Ua=C&&C.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),qa=C&&C.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=s.onReadEntry;s.onReadEntry=e?t=>{e(t),t.resume()}:t=>t.resume()},Ya=(s,e)=>{let t=new Map(e.map(n=>[(0,Us.stripTrailingSlashes)(n),!0])),i=s.filter,r=(n,o="")=>{let a=o||(0,tn.parse)(n).root||".",h;if(n===a)h=!1;else{let l=t.get(n);h=l!==void 0?l:r((0,tn.dirname)(n),a)}return t.set(n,h),h};s.filter=i?(n,o)=>i(n,o)&&r((0,Us.stripTrailingSlashes)(n)):n=>r((0,Us.stripTrailingSlashes)(n))};C.filesFilter=Ya;var Ka=s=>{let e=new yi.Parser(s),t=s.file,i;try{i=it.default.openSync(t,"r");let r=it.default.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let t=new yi.Parser(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{t.on("error",a),t.on("end",o),it.default.stat(r,(h,l)=>{if(h)a(h);else{let u=new Ha.ReadStream(r,{readSize:i,size:l.size});u.on("error",a),u.pipe(t)}})})};C.list=(0,Za.makeCommand)(Ka,Va,s=>new yi.Parser(s),s=>new yi.Parser(s),(s,e)=>{e?.length&&(0,C.filesFilter)(s,e),s.noResume||Ga(s)})});var sn=d(Ei=>{"use strict";Object.defineProperty(Ei,"__esModule",{value:!0});Ei.modeFix=void 0;var $a=(s,e,t)=>(s&=4095,t&&(s=(s|384)&-19),e&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);Ei.modeFix=$a});var qs=d(bi=>{"use strict";Object.defineProperty(bi,"__esModule",{value:!0});bi.stripAbsolutePath=void 0;var Xa=require("node:path"),{isAbsolute:Qa,parse:rn}=Xa.win32,Ja=s=>{let e="",t=rn(s);for(;Qa(s)||t.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":t.root;s=s.slice(i.length),e+=i,t=rn(s)}return[e,s]};bi.stripAbsolutePath=Ja});var Hs=d(rt=>{"use strict";Object.defineProperty(rt,"__esModule",{value:!0});rt.decode=rt.encode=void 0;var Si=["|","<",">","?",":"],Ws=Si.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),eh=new Map(Si.map((s,e)=>[s,Ws[e]])),th=new Map(Ws.map((s,e)=>[s,Si[e]])),ih=s=>Si.reduce((e,t)=>e.split(t).join(eh.get(t)),s);rt.encode=ih;var sh=s=>Ws.reduce((e,t)=>e.split(t).join(th.get(t)),s);rt.decode=sh});var tr=d(M=>{"use strict";var rh=M&&M.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),nh=M&&M.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),oh=M&&M.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;re?(s=(0,ne.normalizeWindowsPath)(s).replace(/^\.(\/|$)/,""),(0,ah.stripTrailingSlashes)(e)+"/"+s):(0,ne.normalizeWindowsPath)(s),lh=16*1024*1024,on=Symbol("process"),an=Symbol("file"),hn=Symbol("directory"),Gs=Symbol("symlink"),ln=Symbol("hardlink"),Rt=Symbol("header"),gi=Symbol("read"),Ys=Symbol("lstat"),Oi=Symbol("onlstat"),Ks=Symbol("onread"),Vs=Symbol("onreadlink"),$s=Symbol("openfile"),Xs=Symbol("onopenfile"),ve=Symbol("close"),Ri=Symbol("mode"),Qs=Symbol("awaitDrain"),Zs=Symbol("ondrain"),ae=Symbol("prefix"),vi=class extends cn.Minipass{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#e=!1;constructor(e,t={}){let i=(0,mn.dealias)(t);super(),this.path=(0,ne.normalizeWindowsPath)(e),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||lh,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=(0,ne.normalizeWindowsPath)(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?(0,ne.normalizeWindowsPath)(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,a]=(0,_n.stripAbsolutePath)(this.path);o&&typeof a=="string"&&(this.path=a,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=hh.decode(this.path.replaceAll(/\\/g,"/")),e=e.replaceAll(/\\/g,"/")),this.absolute=(0,ne.normalizeWindowsPath)(i.absolute||nn.default.resolve(this.cwd,e)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[Oi](n):this[Ys]()}warn(e,t,i={}){return(0,wn.warnMethod)(this,e,t,i)}emit(e,...t){return e==="error"&&(this.#e=!0),super.emit(e,...t)}[Ys](){oe.default.lstat(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Oi](t)})}[Oi](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=uh(e),this.emit("stat",e),this[on]()}[on](){switch(this.type){case"File":return this[an]();case"Directory":return this[hn]();case"SymbolicLink":return this[Gs]();default:return this.end()}}[Ri](e){return(0,dn.modeFix)(e,this.type==="Directory",this.portable)}[ae](e){return yn(e,this.prefix)}[Rt](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new fn.Header({path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,mode:this[Ri](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new pn.Pax({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let e=this.header?.block;if(!e)throw new Error("failed to encode header");super.write(e)}[hn](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[Rt](),this.end()}[Gs](){oe.default.readlink(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Vs](t)})}[Vs](e){this.linkpath=(0,ne.normalizeWindowsPath)(e),this[Rt](),this.end()}[ln](e){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=(0,ne.normalizeWindowsPath)(nn.default.relative(this.cwd,e)),this.stat.size=0,this[Rt](),this.end()}[an](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let e=`${this.stat.dev}:${this.stat.ino}`,t=this.linkCache.get(e);if(t?.indexOf(this.cwd)===0)return this[ln](t);this.linkCache.set(e,this.absolute)}if(this[Rt](),this.stat.size===0)return this.end();this[$s]()}[$s](){oe.default.open(this.absolute,"r",(e,t)=>{if(e)return this.emit("error",e);this[Xs](t)})}[Xs](e){if(this.fd=e,this.#e)return this[ve]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let t=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(t),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[gi]()}[gi](){let{fd:e,buf:t,offset:i,length:r,pos:n}=this;if(e===void 0||t===void 0)throw new Error("cannot read file without first opening");oe.default.read(e,t,i,r,n,(o,a)=>{if(o)return this[ve](()=>this.emit("error",o));this[Ks](a)})}[ve](e=()=>{}){this.fd!==void 0&&oe.default.close(this.fd,e)}[Ks](e){if(e<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(e>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(e===this.remain)for(let r=e;rthis[Zs]())}[Qs](e){this.once("drain",e)}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this.blockRemaine?this.emit("error",e):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[gi]()}};M.WriteEntry=vi;var Js=class extends vi{sync=!0;[Ys](){this[Oi](oe.default.lstatSync(this.absolute))}[Gs](){this[Vs](oe.default.readlinkSync(this.absolute))}[$s](){this[Xs](oe.default.openSync(this.absolute,"r"))}[gi](){let e=!0;try{let{fd:t,buf:i,offset:r,length:n,pos:o}=this;if(t===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let a=oe.default.readSync(t,i,r,n,o);this[Ks](a),e=!1}finally{if(e)try{this[ve](()=>{})}catch{}}}[Qs](e){e()}[ve](e=()=>{}){this.fd!==void 0&&oe.default.closeSync(this.fd),e()}};M.WriteEntrySync=Js;var er=class extends cn.Minipass{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(e,t,i={}){return(0,wn.warnMethod)(this,e,t,i)}constructor(e,t={}){let i=(0,mn.dealias)(t);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=e;let{type:r}=e;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=(0,ne.normalizeWindowsPath)(e.path),this.mode=e.mode!==void 0?this[Ri](e.mode):void 0,this.uid=this.portable?void 0:e.uid,this.gid=this.portable?void 0:e.gid,this.uname=this.portable?void 0:e.uname,this.gname=this.portable?void 0:e.gname,this.size=e.size,this.mtime=this.noMtime?void 0:i.mtime||e.mtime,this.atime=this.portable?void 0:e.atime,this.ctime=this.portable?void 0:e.ctime,this.linkpath=e.linkpath!==void 0?(0,ne.normalizeWindowsPath)(e.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[a,h]=(0,_n.stripAbsolutePath)(this.path);a&&typeof h=="string"&&(this.path=h,n=a)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.onWriteEntry?.(this),this.header=new fn.Header({path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new pn.Pax({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),e.pipe(this)}[ae](e){return yn(e,this.prefix)}[Ri](e){return(0,dn.modeFix)(e,this.type==="Directory",this.portable)}write(e,t,i){typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8"));let r=e.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(e,i)}end(e,t,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,t??"utf8")),i&&this.once("finish",i),e?super.end(e,i):super.end(i),this}};M.WriteEntryTar=er;var uh=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported"});var En=d(ot=>{"use strict";Object.defineProperty(ot,"__esModule",{value:!0});ot.Node=ot.Yallist=void 0;var ir=class s{tail;head;length=0;static create(e=[]){return new s(e)}constructor(e=[]){for(let t of e)this.push(t)}*[Symbol.iterator](){for(let e=this.head;e;e=e.next)yield e.value}removeNode(e){if(e.list!==this)throw new Error("removing node which does not belong to this list");let t=e.next,i=e.prev;return t&&(t.prev=i),i&&(i.next=t),e===this.head&&(this.head=t),e===this.tail&&(this.tail=i),this.length--,e.next=void 0,e.prev=void 0,e.list=void 0,t}unshiftNode(e){if(e===this.head)return;e.list&&e.list.removeNode(e);let t=this.head;e.list=this,e.next=t,t&&(t.prev=e),this.head=e,this.tail||(this.tail=e),this.length++}pushNode(e){if(e===this.tail)return;e.list&&e.list.removeNode(e);let t=this.tail;e.list=this,e.prev=t,t&&(t.next=e),this.tail=e,this.head||(this.head=e),this.length++}push(...e){for(let t=0,i=e.length;t1)i=t;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=e(i,r.value,n),r=r.next;return i}reduceReverse(e,t){let i,r=this.tail;if(arguments.length>1)i=t;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=e(i,r.value,n),r=r.prev;return i}toArray(){let e=new Array(this.length);for(let t=0,i=this.head;i;t++)e[t]=i.value,i=i.next;return e}toArrayReverse(){let e=new Array(this.length);for(let t=0,i=this.tail;i;t++)e[t]=i.value,i=i.prev;return e}slice(e=0,t=this.length){t<0&&(t+=this.length),e<0&&(e+=this.length);let i=new s;if(tthis.length&&(t=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(t=this.length);let r=this.length,n=this.tail;for(;n&&r>t;r--)n=n.prev;for(;n&&r>e;r--,n=n.prev)i.push(n.value);return i}splice(e,t=0,...i){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);let r=this.head;for(let o=0;r&&o{"use strict";var mh=L&&L.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),ph=L&&L.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),_h=L&&L.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(e.gzip&&(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new sr.Gzip(e.gzip)),e.brotli&&(typeof e.brotli!="object"&&(e.brotli={}),this.zip=new sr.BrotliCompress(e.brotli)),e.zstd&&(typeof e.zstd!="object"&&(e.zstd={}),this.zip=new sr.ZstdCompress(e.zstd)),!this.zip)throw new Error("impossible");let t=this.zip;t.on("data",i=>super.write(i)),t.on("end",()=>super.end()),t.on("drain",()=>this[ar]()),this.on("resume",()=>t.resume())}else this.on("drain",this[ar]);this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,e.mtime&&(this.mtime=e.mtime),this.filter=typeof e.filter=="function"?e.filter:()=>!0,this[X]=new yh.Yallist,this[Q]=0,this.jobs=Number(e.jobs)||4,this[Tt]=!1,this[vt]=!1}[vn](e){return super.write(e)}add(e){return this.write(e),this}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&this.add(e),this[vt]=!0,this[xe](),i&&i(),this}write(e){if(this[vt])throw new Error("write after end");return e instanceof Eh.ReadEntry?this[Sn](e):this[Di](e),this.flowing}[Sn](e){let t=(0,hr.normalizeWindowsPath)(On.default.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let i=new Dt(e.path,t);i.entry=new lr.WriteEntryTar(e,this[or](i)),i.entry.on("end",()=>this[nr](i)),this[Q]+=1,this[X].push(i)}this[xe]()}[Di](e){let t=(0,hr.normalizeWindowsPath)(On.default.resolve(this.cwd,e));this[X].push(new Dt(e,t)),this[xe]()}[ur](e){e.pending=!0,this[Q]+=1;let t=this.follow?"stat":"lstat";Li.default[t](e.absolute,(i,r)=>{e.pending=!1,this[Q]-=1,i?this.emit("error",i):this[Ti](e,r)})}[Ti](e,t){this.statCache.set(e.absolute,t),e.stat=t,this.filter(e.path,t)?t.isFile()&&t.nlink>1&&e===this[je]&&!this.linkCache.get(`${t.dev}:${t.ino}`)&&!this.sync&&this[rr](e):e.ignore=!0,this[xe]()}[cr](e){e.pending=!0,this[Q]+=1,Li.default.readdir(e.absolute,(t,i)=>{if(e.pending=!1,this[Q]-=1,t)return this.emit("error",t);this[Pi](e,i)})}[Pi](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[xe]()}[xe](){if(!this[Tt]){this[Tt]=!0;for(let e=this[X].head;e&&this[Q]this.warn(t,i,r),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[gn](e){this[Q]+=1;try{return new this[Mi](e.path,this[or](e)).on("end",()=>this[nr](e)).on("error",i=>this.emit("error",i))}catch(t){this.emit("error",t)}}[ar](){this[je]&&this[je].entry&&this[je].entry.resume()}[Ni](e){e.piped=!0,e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Di](o+r)});let t=e.entry,i=this.zip;if(!t)throw new Error("cannot pipe without source");i?t.on("data",r=>{i.write(r)||t.pause()}):t.on("data",r=>{super.write(r)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(e,t,i={}){(0,bh.warnMethod)(this,e,t,i)}};L.Pack=Ai;var fr=class extends Ai{sync=!0;constructor(e){super(e),this[Mi]=lr.WriteEntrySync}pause(){}resume(){}[ur](e){let t=this.follow?"statSync":"lstatSync";this[Ti](e,Li.default[t](e.absolute))}[cr](e){this[Pi](e,Li.default.readdirSync(e.absolute))}[Ni](e){let t=e.entry,i=this.zip;if(e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Di](o+r)}),!t)throw new Error("Cannot pipe without source");i?t.on("data",r=>{i.write(r)}):t.on("data",r=>{super[vn](r)})}};L.PackSync=fr});var dr=d(at=>{"use strict";var Sh=at&&at.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(at,"__esModule",{value:!0});at.create=void 0;var Tn=Ke(),Dn=Sh(require("node:path")),Pn=st(),gh=Ve(),Fi=Ii(),Oh=(s,e)=>{let t=new Fi.PackSync(s),i=new Tn.WriteStreamSync(s.file,{mode:s.mode||438});t.pipe(i),Nn(t,e)},Rh=(s,e)=>{let t=new Fi.Pack(s),i=new Tn.WriteStream(s.file,{mode:s.mode||438});t.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),t.on("error",o)});return Mn(t,e).catch(n=>t.emit("error",n)),r},Nn=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,Pn.list)({file:Dn.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},Mn=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,Pn.list)({file:Dn.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(t);s.end()},vh=(s,e)=>{let t=new Fi.PackSync(s);return Nn(t,e),t},Th=(s,e)=>{let t=new Fi.Pack(s);return Mn(t,e).catch(i=>t.emit("error",i)),t};at.create=(0,gh.makeCommand)(Oh,Rh,vh,Th,(s,e)=>{if(!e?.length)throw new TypeError("no paths specified to add to archive")})});var jn=d(ht=>{"use strict";var Dh=ht&&ht.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ht,"__esModule",{value:!0});ht.getWriteFlag=void 0;var In=Dh(require("fs")),Ph=process.env.__FAKE_PLATFORM__||process.platform,Fn=Ph==="win32",{O_CREAT:Cn,O_NOFOLLOW:Ln,O_TRUNC:Bn,O_WRONLY:zn}=In.default.constants,kn=Number(process.env.__FAKE_FS_O_FILENAME__)||In.default.constants.UV_FS_O_FILEMAP||0,Nh=Fn&&!!kn,Mh=512*1024,Lh=kn|Bn|Cn|zn,An=!Fn&&typeof Ln=="number"?Ln|Bn|Cn|zn:null;ht.getWriteFlag=An!==null?()=>An:Nh?s=>s"w"});var Un=d(he=>{"use strict";var xn=he&&he.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(he,"__esModule",{value:!0});he.chownrSync=he.chownr=void 0;var Bi=xn(require("node:fs")),Pt=xn(require("node:path")),mr=(s,e,t)=>{try{return Bi.default.lchownSync(s,e,t)}catch(i){if(i?.code!=="ENOENT")throw i}},Ci=(s,e,t,i)=>{Bi.default.lchown(s,e,t,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},Ah=(s,e,t,i,r)=>{if(e.isDirectory())(0,he.chownr)(Pt.default.resolve(s,e.name),t,i,n=>{if(n)return r(n);let o=Pt.default.resolve(s,e.name);Ci(o,t,i,r)});else{let n=Pt.default.resolve(s,e.name);Ci(n,t,i,r)}},Ih=(s,e,t,i)=>{Bi.default.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return Ci(s,e,t,i);let o=n.length,a=null,h=l=>{if(!a){if(l)return i(a=l);if(--o===0)return Ci(s,e,t,i)}};for(let l of n)Ah(s,l,e,t,h)})};he.chownr=Ih;var Fh=(s,e,t,i)=>{e.isDirectory()&&(0,he.chownrSync)(Pt.default.resolve(s,e.name),t,i),mr(Pt.default.resolve(s,e.name),t,i)},Ch=(s,e,t)=>{let i;try{i=Bi.default.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return mr(s,e,t);throw n}for(let r of i)Fh(s,r,e,t);return mr(s,e,t)};he.chownrSync=Ch});var qn=d(zi=>{"use strict";Object.defineProperty(zi,"__esModule",{value:!0});zi.CwdError=void 0;var pr=class extends Error{path;code;syscall="chdir";constructor(e,t){super(`${t}: Cannot cd into '${e}'`),this.path=e,this.code=t}get name(){return"CwdError"}};zi.CwdError=pr});var wr=d(ki=>{"use strict";Object.defineProperty(ki,"__esModule",{value:!0});ki.SymlinkError=void 0;var _r=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(e,t){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=e,this.path=t}get name(){return"SymlinkError"}};ki.SymlinkError=_r});var Yn=d(Te=>{"use strict";var Er=Te&&Te.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(Te,"__esModule",{value:!0});Te.mkdirSync=Te.mkdir=void 0;var Wn=Un(),x=Er(require("node:fs")),Bh=Er(require("node:fs/promises")),ji=Er(require("node:path")),Hn=qn(),pe=et(),Zn=wr(),zh=(s,e)=>{x.default.stat(s,(t,i)=>{(t||!i.isDirectory())&&(t=new Hn.CwdError(s,t?.code||"ENOTDIR")),e(t)})},kh=(s,e,t)=>{s=(0,pe.normalizeWindowsPath)(s);let i=e.umask??18,r=e.mode|448,n=(r&i)!==0,o=e.uid,a=e.gid,h=typeof o=="number"&&typeof a=="number"&&(o!==e.processUid||a!==e.processGid),l=e.preserve,u=e.unlink,c=(0,pe.normalizeWindowsPath)(e.cwd),E=(w,P)=>{w?t(w):P&&h?(0,Wn.chownr)(P,o,a,zt=>E(zt)):n?x.default.chmod(s,r,t):t()};if(s===c)return zh(s,E);if(l)return Bh.default.mkdir(s,{mode:r,recursive:!0}).then(w=>E(null,w??void 0),E);let A=(0,pe.normalizeWindowsPath)(ji.default.relative(c,s)).split("/");yr(c,A,r,u,c,void 0,E)};Te.mkdir=kh;var yr=(s,e,t,i,r,n,o)=>{if(e.length===0)return o(null,n);let a=e.shift(),h=(0,pe.normalizeWindowsPath)(ji.default.resolve(s+"/"+a));x.default.mkdir(h,t,Gn(h,e,t,i,r,n,o))},Gn=(s,e,t,i,r,n,o)=>a=>{a?x.default.lstat(s,(h,l)=>{if(h)h.path=h.path&&(0,pe.normalizeWindowsPath)(h.path),o(h);else if(l.isDirectory())yr(s,e,t,i,r,n,o);else if(i)x.default.unlink(s,u=>{if(u)return o(u);x.default.mkdir(s,t,Gn(s,e,t,i,r,n,o))});else{if(l.isSymbolicLink())return o(new Zn.SymlinkError(s,s+"/"+e.join("/")));o(a)}}):(n=n||s,yr(s,e,t,i,r,n,o))},jh=s=>{let e=!1,t;try{e=x.default.statSync(s).isDirectory()}catch(i){t=i?.code}finally{if(!e)throw new Hn.CwdError(s,t??"ENOTDIR")}},xh=(s,e)=>{s=(0,pe.normalizeWindowsPath)(s);let t=e.umask??18,i=e.mode|448,r=(i&t)!==0,n=e.uid,o=e.gid,a=typeof n=="number"&&typeof o=="number"&&(n!==e.processUid||o!==e.processGid),h=e.preserve,l=e.unlink,u=(0,pe.normalizeWindowsPath)(e.cwd),c=w=>{w&&a&&(0,Wn.chownrSync)(w,n,o),r&&x.default.chmodSync(s,i)};if(s===u)return jh(u),c();if(h)return c(x.default.mkdirSync(s,{mode:i,recursive:!0})??void 0);let D=(0,pe.normalizeWindowsPath)(ji.default.relative(u,s)).split("/"),A;for(let w=D.shift(),P=u;w&&(P+="/"+w);w=D.shift()){P=(0,pe.normalizeWindowsPath)(ji.default.resolve(P));try{x.default.mkdirSync(P,i),A=A||P}catch{let zt=x.default.lstatSync(P);if(zt.isDirectory())continue;if(l){x.default.unlinkSync(P),x.default.mkdirSync(P,i),A=A||P;continue}else if(zt.isSymbolicLink())return new Zn.SymlinkError(P,P+"/"+D.join("/"))}}return c(A)};Te.mkdirSync=xh});var Vn=d(xi=>{"use strict";Object.defineProperty(xi,"__esModule",{value:!0});xi.normalizeUnicode=void 0;var br=Object.create(null),Kn=1e4,lt=new Set,Uh=s=>{lt.has(s)?lt.delete(s):br[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),lt.add(s);let e=br[s],t=lt.size-Kn;if(t>Kn/10){for(let i of lt)if(lt.delete(i),delete br[i],--t<=0)break}return e};xi.normalizeUnicode=Uh});var Xn=d(Ui=>{"use strict";Object.defineProperty(Ui,"__esModule",{value:!0});Ui.PathReservations=void 0;var $n=require("node:path"),qh=Vn(),Wh=wi(),Hh=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Zh=Hh==="win32",Gh=s=>s.split("/").slice(0,-1).reduce((t,i)=>{let r=t.at(-1);return r!==void 0&&(i=(0,$n.join)(r,i)),t.push(i||"/"),t},[]),Sr=class{#e=new Map;#i=new Map;#s=new Set;reserve(e,t){e=Zh?["win32 parallelization disabled"]:e.map(r=>(0,Wh.stripTrailingSlashes)((0,$n.join)((0,qh.normalizeUnicode)(r))));let i=new Set(e.map(r=>Gh(r)).reduce((r,n)=>r.concat(n)));this.#i.set(t,{dirs:i,paths:e});for(let r of e){let n=this.#e.get(r);n?n.push(t):this.#e.set(r,[t])}for(let r of i){let n=this.#e.get(r);if(!n)this.#e.set(r,[new Set([t])]);else{let o=n.at(-1);o instanceof Set?o.add(t):n.push(new Set([t]))}}return this.#r(t)}#n(e){let t=this.#i.get(e);if(!t)throw new Error("function does not have any path reservations");return{paths:t.paths.map(i=>this.#e.get(i)),dirs:[...t.dirs].map(i=>this.#e.get(i))}}check(e){let{paths:t,dirs:i}=this.#n(e);return t.every(r=>r&&r[0]===e)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(e))}#r(e){return this.#s.has(e)||!this.check(e)?!1:(this.#s.add(e),e(()=>this.#t(e)),!0)}#t(e){if(!this.#s.has(e))return!1;let t=this.#i.get(e);if(!t)throw new Error("invalid reservation");let{paths:i,dirs:r}=t,n=new Set;for(let o of i){let a=this.#e.get(o);if(!a||a?.[0]!==e)continue;let h=a[1];if(!h){this.#e.delete(o);continue}if(a.shift(),typeof h=="function")n.add(h);else for(let l of h)n.add(l)}for(let o of r){let a=this.#e.get(o),h=a?.[0];if(!(!a||!(h instanceof Set)))if(h.size===1&&a.length===1){this.#e.delete(o);continue}else if(h.size===1){a.shift();let l=a[0];typeof l=="function"&&n.add(l)}else h.delete(e)}return this.#s.delete(e),n.forEach(o=>this.#r(o)),!0}};Ui.PathReservations=Sr});var Qn=d(qi=>{"use strict";Object.defineProperty(qi,"__esModule",{value:!0});qi.umask=void 0;var Yh=()=>process.umask();qi.umask=Yh});var Ar=d(z=>{"use strict";var Kh=z&&z.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Vh=z&&z.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ho=z&&z.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{if(!Ct)return m.default.unlink(s,e);let t=s+".DELETE."+(0,lo.randomBytes)(16).toString("hex");m.default.rename(s,t,i=>{if(i)return e(i);m.default.unlink(t,e)})},nl=s=>{if(!Ct)return m.default.unlinkSync(s);let e=s+".DELETE."+(0,lo.randomBytes)(16).toString("hex");m.default.renameSync(s,e),m.default.unlinkSync(e)},ao=(s,e,t)=>s!==void 0&&s===s>>>0?s:e!==void 0&&e===e>>>0?e:t,Zi=class extends Qh.Parser{[Or]=!1;[Ft]=!1;[Wi]=0;reservations=new el.PathReservations;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(e={}){if(e.ondone=()=>{this[Or]=!0,this[Rr]()},super(e),this.transform=e.transform,this.chmod=!!e.chmod,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=e.preserveOwner===void 0&&typeof e.uid!="number"?!!(process.getuid&&process.getuid()===0):!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof e.maxDepth=="number"?e.maxDepth:sl,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||Ct,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=(0,U.normalizeWindowsPath)(g.default.resolve(e.cwd||process.cwd())),this.strip=Number(e.strip)||0,this.processUmask=this.chmod?typeof e.processUmask=="number"?e.processUmask:(0,tl.umask)():0,this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",t=>this[eo](t))}warn(e,t,i={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(i.recoverable=!1),super.warn(e,t,i)}[Rr](){this[Or]&&this[Wi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[gr](e,t){let i=e[t],{type:r}=e;if(!i||this.preservePaths)return!0;let[n,o]=(0,Jh.stripAbsolutePath)(i),a=o.replaceAll(/\\/g,"/").split("/");if(a.includes("..")||Ct&&/^[a-z]:\.\.$/i.test(a[0]??"")){if(t==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${t} contains '..'`,{entry:e,[t]:i}),!1;let h=g.default.posix.dirname(e.path),l=g.default.posix.normalize(g.default.posix.join(h,a.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${t} escapes extraction directory`,{entry:e,[t]:i}),!1}return n&&(e[t]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${t}`,{entry:e,[t]:i})),!0}[no](e){let t=(0,U.normalizeWindowsPath)(e.path),i=t.split("/");if(this.strip){if(i.length=this.strip)e.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),e.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:e,path:t,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[gr](e,"path")||!this[gr](e,"linkpath"))return!1;if(e.absolute=g.default.isAbsolute(e.path)?(0,U.normalizeWindowsPath)(g.default.resolve(e.path)):(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,e.path)),!this.preservePaths&&typeof e.absolute=="string"&&e.absolute.indexOf(this.cwd+"/")!==0&&e.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:e,path:(0,U.normalizeWindowsPath)(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!=="Directory"&&e.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=g.default.win32.parse(String(e.absolute));e.absolute=r+Jn.encode(String(e.absolute).slice(r.length));let{root:n}=g.default.win32.parse(e.path);e.path=n+Jn.encode(e.path.slice(n.length))}return!0}[eo](e){if(!this[no](e))return e.resume();switch(Xh.default.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[vr](e);default:return this[ro](e)}}[T](e,t){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:t}),this[ut](),t.resume())}[De](e,t,i){(0,co.mkdir)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t},i)}[Lt](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[At](e){return ao(this.uid,e.uid,this.processUid)}[It](e){return ao(this.gid,e.gid,this.processGid)}[Dr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=new $h.WriteStream(String(e.absolute),{flags:(0,uo.getWriteFlag)(e.size),mode:i,autoClose:!1});r.on("error",h=>{r.fd&&m.default.close(r.fd,()=>{}),r.write=()=>!0,this[T](h,e),t()});let n=1,o=h=>{if(h){r.fd&&m.default.close(r.fd,()=>{}),this[T](h,e),t();return}--n===0&&r.fd!==void 0&&m.default.close(r.fd,l=>{l?this[T](l,e):this[ut](),t()})};r.on("finish",()=>{let h=String(e.absolute),l=r.fd;if(typeof l=="number"&&e.mtime&&!this.noMtime){n++;let u=e.atime||new Date,c=e.mtime;m.default.futimes(l,u,c,E=>E?m.default.utimes(h,u,c,D=>o(D&&E)):o())}if(typeof l=="number"&&this[Lt](e)){n++;let u=this[At](e),c=this[It](e);typeof u=="number"&&typeof c=="number"&&m.default.fchown(l,u,c,E=>E?m.default.chown(h,u,c,D=>o(D&&E)):o())}o()});let a=this.transform&&this.transform(e)||e;a!==e&&(a.on("error",h=>{this[T](h,e),t()}),e.pipe(a)),a.pipe(r)}[Pr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode;this[De](String(e.absolute),i,r=>{if(r){this[T](r,e),t();return}let n=1,o=()=>{--n===0&&(t(),this[ut](),e.resume())};e.mtime&&!this.noMtime&&(n++,m.default.utimes(String(e.absolute),e.atime||new Date,e.mtime,o)),this[Lt](e)&&(n++,m.default.chown(String(e.absolute),Number(this[At](e)),Number(this[It](e)),o)),o()})}[ro](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[io](e,t){let i=(0,U.normalizeWindowsPath)(g.default.relative(this.cwd,g.default.resolve(g.default.dirname(String(e.absolute)),String(e.linkpath)))).split("/");this[Mt](e,this.cwd,i,()=>this[Hi](e,String(e.linkpath),"symlink",t),r=>{this[T](r,e),t()})}[so](e,t){let i=(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,String(e.linkpath))),r=(0,U.normalizeWindowsPath)(String(e.linkpath)).split("/");this[Mt](e,this.cwd,r,()=>this[Hi](e,i,"link",t),n=>{this[T](n,e),t()})}[Mt](e,t,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let a=g.default.resolve(t,o);m.default.lstat(a,(h,l)=>{if(h)return r();if(l?.isSymbolicLink())return n(new fo.SymlinkError(a,g.default.resolve(a,i.join("/"))));this[Mt](e,a,i,r,n)})}[oo](){this[Wi]++}[ut](){this[Wi]--,this[Rr]()}[Nr](e){this[ut](),e.resume()}[Tr](e,t){return e.type==="File"&&!this.unlink&&t.isFile()&&t.nlink<=1&&!Ct}[vr](e){this[oo]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,i=>this[to](e,i))}[to](e,t){let i=a=>{t(a)},r=()=>{this[De](this.cwd,this.dmode,a=>{if(a){this[T](a,e),i();return}this[Ft]=!0,n()})},n=()=>{if(e.absolute!==this.cwd){let a=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(a!==this.cwd)return this[De](a,this.dmode,h=>{if(h){this[T](h,e),i();return}o()})}o()},o=()=>{m.default.lstat(String(e.absolute),(a,h)=>{if(h&&(this.keep||this.newer&&h.mtime>(e.mtime??h.mtime))){this[Nr](e),i();return}if(a||this[Tr](e,h))return this[Z](null,e,i);if(h.isDirectory()){if(e.type==="Directory"){let l=this.chmod&&e.mode&&(h.mode&4095)!==e.mode,u=c=>this[Z](c??null,e,i);return l?m.default.chmod(String(e.absolute),Number(e.mode),u):u()}if(e.absolute!==this.cwd)return m.default.rmdir(String(e.absolute),l=>this[Z](l??null,e,i))}if(e.absolute===this.cwd)return this[Z](null,e,i);rl(String(e.absolute),l=>this[Z](l??null,e,i))})};this[Ft]?n():r()}[Z](e,t,i){if(e){this[T](e,t),i();return}switch(t.type){case"File":case"OldFile":case"ContiguousFile":return this[Dr](t,i);case"Link":return this[so](t,i);case"SymbolicLink":return this[io](t,i);case"Directory":case"GNUDumpDir":return this[Pr](t,i)}}[Hi](e,t,i,r){m.default[i](t,String(e.absolute),n=>{n?this[T](n,e):(this[ut](),e.resume()),r()})}};z.Unpack=Zi;var Nt=s=>{try{return[null,s()]}catch(e){return[e,null]}},Mr=class extends Zi{sync=!0;[Z](e,t){return super[Z](e,t,()=>{})}[vr](e){if(!this[Ft]){let n=this[De](this.cwd,this.dmode);if(n)return this[T](n,e);this[Ft]=!0}if(e.absolute!==this.cwd){let n=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(n!==this.cwd){let o=this[De](n,this.dmode);if(o)return this[T](o,e)}}let[t,i]=Nt(()=>m.default.lstatSync(String(e.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(e.mtime??i.mtime)))return this[Nr](e);if(t||this[Tr](e,i))return this[Z](null,e);if(i.isDirectory()){if(e.type==="Directory"){let o=this.chmod&&e.mode&&(i.mode&4095)!==e.mode,[a]=o?Nt(()=>{m.default.chmodSync(String(e.absolute),Number(e.mode))}):[];return this[Z](a,e)}let[n]=Nt(()=>m.default.rmdirSync(String(e.absolute)));this[Z](n,e)}let[r]=e.absolute===this.cwd?[]:Nt(()=>nl(String(e.absolute)));this[Z](r,e)}[Dr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=a=>{let h;try{m.default.closeSync(n)}catch(l){h=l}(a||h)&&this[T](a||h,e),t()},n;try{n=m.default.openSync(String(e.absolute),(0,uo.getWriteFlag)(e.size),i)}catch(a){return r(a)}let o=this.transform&&this.transform(e)||e;o!==e&&(o.on("error",a=>this[T](a,e)),e.pipe(o)),o.on("data",a=>{try{m.default.writeSync(n,a,0,a.length)}catch(h){r(h)}}),o.on("end",()=>{let a=null;if(e.mtime&&!this.noMtime){let h=e.atime||new Date,l=e.mtime;try{m.default.futimesSync(n,h,l)}catch(u){try{m.default.utimesSync(String(e.absolute),h,l)}catch{a=u}}}if(this[Lt](e)){let h=this[At](e),l=this[It](e);try{m.default.fchownSync(n,Number(h),Number(l))}catch(u){try{m.default.chownSync(String(e.absolute),Number(h),Number(l))}catch{a=a||u}}}r(a)})}[Pr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode,r=this[De](String(e.absolute),i);if(r){this[T](r,e),t();return}if(e.mtime&&!this.noMtime)try{m.default.utimesSync(String(e.absolute),e.atime||new Date,e.mtime)}catch{}if(this[Lt](e))try{m.default.chownSync(String(e.absolute),Number(this[At](e)),Number(this[It](e)))}catch{}t(),e.resume()}[De](e,t){try{return(0,co.mkdirSync)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t})}catch(i){return i}}[Mt](e,t,i,r,n){if(this.preservePaths||i.length===0)return r();let o=t;for(let a of i){o=g.default.resolve(o,a);let[h,l]=Nt(()=>m.default.lstatSync(o));if(h)return r();if(l.isSymbolicLink())return n(new fo.SymlinkError(o,g.default.resolve(t,i.join("/"))))}r()}[Hi](e,t,i,r){let n=`${i}Sync`;try{m.default[n](t,String(e.absolute)),r(),e.resume()}catch(o){return this[T](o,e)}}};z.UnpackSync=Mr});var Ir=d(G=>{"use strict";var ol=G&&G.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),al=G&&G.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),hl=G&&G.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=new Gi.UnpackSync(s),t=s.file,i=po.default.statSync(t),r=s.maxReadSize||16*1024*1024;new mo.ReadStreamSync(t,{readSize:r,size:i.size}).pipe(e)},dl=(s,e)=>{let t=new Gi.Unpack(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{t.on("error",a),t.on("close",o),po.default.stat(r,(h,l)=>{if(h)a(h);else{let u=new mo.ReadStream(r,{readSize:i,size:l.size});u.on("error",a),u.pipe(t)}})})};G.extract=(0,cl.makeCommand)(fl,dl,s=>new Gi.UnpackSync(s),s=>new Gi.Unpack(s),(s,e)=>{e?.length&&(0,ul.filesFilter)(s,e)})});var Yi=d(ct=>{"use strict";var _o=ct&&ct.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ct,"__esModule",{value:!0});ct.replace=void 0;var wo=Ke(),q=_o(require("node:fs")),yo=_o(require("node:path")),Eo=Je(),bo=st(),ml=Ve(),pl=$t(),So=Ii(),_l=(s,e)=>{let t=new So.PackSync(s),i=!0,r,n;try{try{r=q.default.openSync(s.file,"r+")}catch(h){if(h?.code==="ENOENT")r=q.default.openSync(s.file,"w+");else throw h}let o=q.default.fstatSync(r),a=Buffer.alloc(512);e:for(n=0;no.size)break;n+=l,s.mtimeCache&&h.mtime&&s.mtimeCache.set(String(h.path),h.mtime)}i=!1,wl(s,t,n,r,e)}finally{if(i)try{q.default.closeSync(r)}catch{}}},wl=(s,e,t,i,r)=>{let n=new wo.WriteStreamSync(s.file,{fd:i,start:t});e.pipe(n),El(e,r)},yl=(s,e)=>{e=Array.from(e);let t=new So.Pack(s),i=(n,o,a)=>{let h=(D,A)=>{D?q.default.close(n,w=>a(D)):a(null,A)},l=0;if(o===0)return h(null,0);let u=0,c=Buffer.alloc(512),E=(D,A)=>{if(D||A===void 0)return h(D);if(u+=A,u<512&&A)return q.default.read(n,c,u,c.length-u,l+u,E);if(l===0&&c[0]===31&&c[1]===139)return h(new Error("cannot append to compressed archives"));if(u<512)return h(null,l);let w=new Eo.Header(c);if(!w.cksumValid)return h(null,l);let P=512*Math.ceil((w.size??0)/512);if(l+P+512>o||(l+=P+512,l>=o))return h(null,l);s.mtimeCache&&w.mtime&&s.mtimeCache.set(String(w.path),w.mtime),u=0,q.default.read(n,c,0,512,l,E)};q.default.read(n,c,0,512,l,E)};return new Promise((n,o)=>{t.on("error",o);let a="r+",h=(l,u)=>{if(l&&l.code==="ENOENT"&&a==="r+")return a="w+",q.default.open(s.file,a,h);if(l||!u)return o(l);q.default.fstat(u,(c,E)=>{if(c)return q.default.close(u,()=>o(c));i(u,E.size,(D,A)=>{if(D)return o(D);let w=new wo.WriteStream(s.file,{fd:u,start:A});t.pipe(w),w.on("error",o),w.on("close",n),bl(t,e)})})};q.default.open(s.file,a,h)})},El=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,bo.list)({file:yo.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},bl=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,bo.list)({file:yo.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t);s.end()};ct.replace=(0,ml.makeCommand)(_l,yl,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,e)=>{if(!(0,pl.isFile)(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!e?.length)throw new TypeError("no paths specified to add/replace")})});var Fr=d(Ki=>{"use strict";Object.defineProperty(Ki,"__esModule",{value:!0});Ki.update=void 0;var Sl=Ve(),Bt=Yi();Ki.update=(0,Sl.makeCommand)(Bt.replace.syncFile,Bt.replace.asyncFile,Bt.replace.syncNoFile,Bt.replace.asyncNoFile,(s,e=[])=>{Bt.replace.validate?.(s,e),gl(s)});var gl=s=>{let e=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=e?(t,i)=>e(t,i)&&!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0)):(t,i)=>!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0))}});var go=exports&&exports.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Ol=exports&&exports.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Y=exports&&exports.__exportStar||function(s,e){for(var t in s)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&go(e,s,t)},Rl=exports&&exports.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r()=>(e||s((e={exports:{}}).exports,e),e.exports);var We=d(F=>{"use strict";var Ro=F&&F.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(F,"__esModule",{value:!0});F.Minipass=F.isWritable=F.isReadable=F.isStream=void 0;var Br=typeof process=="object"&&process?process:{stdout:null,stderr:null},ss=require("node:events"),jr=Ro(require("node:stream")),vo=require("node:string_decoder"),To=s=>!!s&&typeof s=="object"&&(s instanceof Zt||s instanceof jr.default||(0,F.isReadable)(s)||(0,F.isWritable)(s));F.isStream=To;var Do=s=>!!s&&typeof s=="object"&&s instanceof ss.EventEmitter&&typeof s.pipe=="function"&&s.pipe!==jr.default.Writable.prototype.pipe;F.isReadable=Do;var Po=s=>!!s&&typeof s=="object"&&s instanceof ss.EventEmitter&&typeof s.write=="function"&&typeof s.end=="function";F.isWritable=Po;var le=Symbol("EOF"),ue=Symbol("maybeEmitEnd"),_e=Symbol("emittedEnd"),xt=Symbol("emittingEnd"),ft=Symbol("emittedError"),jt=Symbol("closed"),zr=Symbol("read"),Ut=Symbol("flush"),kr=Symbol("flushChunk"),K=Symbol("encoding"),Ue=Symbol("decoder"),O=Symbol("flowing"),dt=Symbol("paused"),qe=Symbol("resume"),R=Symbol("buffer"),I=Symbol("pipes"),v=Symbol("bufferLength"),Xi=Symbol("bufferPush"),qt=Symbol("bufferShift"),N=Symbol("objectMode"),y=Symbol("destroyed"),Qi=Symbol("error"),Ji=Symbol("emitData"),xr=Symbol("emitEnd"),es=Symbol("emitEnd2"),J=Symbol("async"),ts=Symbol("abort"),Wt=Symbol("aborted"),mt=Symbol("signal"),Ne=Symbol("dataListeners"),k=Symbol("discarded"),pt=s=>Promise.resolve().then(s),No=s=>s(),Mo=s=>s==="end"||s==="finish"||s==="prefinish",Lo=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Ao=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Ht=class{src;dest;opts;ondrain;constructor(e,t,i){this.src=e,this.dest=t,this.opts=i,this.ondrain=()=>e[qe](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},is=class extends Ht{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,t,i){super(e,t,i),this.proxyErrors=r=>this.dest.emit("error",r),e.on("error",this.proxyErrors)}},Io=s=>!!s.objectMode,Fo=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",Zt=class extends ss.EventEmitter{[O]=!1;[dt]=!1;[I]=[];[R]=[];[N];[K];[J];[Ue];[le]=!1;[_e]=!1;[xt]=!1;[jt]=!1;[ft]=null;[v]=0;[y]=!1;[mt];[Wt]=!1;[Ne]=0;[k]=!1;writable=!0;readable=!0;constructor(...e){let t=e[0]||{};if(super(),t.objectMode&&typeof t.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Io(t)?(this[N]=!0,this[K]=null):Fo(t)?(this[K]=t.encoding,this[N]=!1):(this[N]=!1,this[K]=null),this[J]=!!t.async,this[Ue]=this[K]?new vo.StringDecoder(this[K]):null,t&&t.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[R]}),t&&t.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[I]});let{signal:i}=t;i&&(this[mt]=i,i.aborted?this[ts]():i.addEventListener("abort",()=>this[ts]()))}get bufferLength(){return this[v]}get encoding(){return this[K]}set encoding(e){throw new Error("Encoding must be set at instantiation time")}setEncoding(e){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[N]}set objectMode(e){throw new Error("objectMode must be set at instantiation time")}get async(){return this[J]}set async(e){this[J]=this[J]||!!e}[ts](){this[Wt]=!0,this.emit("abort",this[mt]?.reason),this.destroy(this[mt]?.reason)}get aborted(){return this[Wt]}set aborted(e){}write(e,t,i){if(this[Wt])return!1;if(this[le])throw new Error("write after end");if(this[y])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof t=="function"&&(i=t,t="utf8"),t||(t="utf8");let r=this[J]?pt:No;if(!this[N]&&!Buffer.isBuffer(e)){if(Ao(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(Lo(e))e=Buffer.from(e);else if(typeof e!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[N]?(this[O]&&this[v]!==0&&this[Ut](!0),this[O]?this.emit("data",e):this[Xi](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[O]):e.length?(typeof e=="string"&&!(t===this[K]&&!this[Ue]?.lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[K]&&(e=this[Ue].write(e)),this[O]&&this[v]!==0&&this[Ut](!0),this[O]?this.emit("data",e):this[Xi](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[O]):(this[v]!==0&&this.emit("readable"),i&&r(i),this[O])}read(e){if(this[y])return null;if(this[k]=!1,this[v]===0||e===0||e&&e>this[v])return this[ue](),null;this[N]&&(e=null),this[R].length>1&&!this[N]&&(this[R]=[this[K]?this[R].join(""):Buffer.concat(this[R],this[v])]);let t=this[zr](e||null,this[R][0]);return this[ue](),t}[zr](e,t){if(this[N])this[qt]();else{let i=t;e===i.length||e===null?this[qt]():typeof i=="string"?(this[R][0]=i.slice(e),t=i.slice(0,e),this[v]-=e):(this[R][0]=i.subarray(e),t=i.subarray(0,e),this[v]-=e)}return this.emit("data",t),!this[R].length&&!this[le]&&this.emit("drain"),t}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t="utf8"),e!==void 0&&this.write(e,t),i&&this.once("end",i),this[le]=!0,this.writable=!1,(this[O]||!this[dt])&&this[ue](),this}[qe](){this[y]||(!this[Ne]&&!this[I].length&&(this[k]=!0),this[dt]=!1,this[O]=!0,this.emit("resume"),this[R].length?this[Ut]():this[le]?this[ue]():this.emit("drain"))}resume(){return this[qe]()}pause(){this[O]=!1,this[dt]=!0,this[k]=!1}get destroyed(){return this[y]}get flowing(){return this[O]}get paused(){return this[dt]}[Xi](e){this[N]?this[v]+=1:this[v]+=e.length,this[R].push(e)}[qt](){return this[N]?this[v]-=1:this[v]-=this[R][0].length,this[R].shift()}[Ut](e=!1){do;while(this[kr](this[qt]())&&this[R].length);!e&&!this[R].length&&!this[le]&&this.emit("drain")}[kr](e){return this.emit("data",e),this[O]}pipe(e,t){if(this[y])return e;this[k]=!1;let i=this[_e];return t=t||{},e===Br.stdout||e===Br.stderr?t.end=!1:t.end=t.end!==!1,t.proxyErrors=!!t.proxyErrors,i?t.end&&e.end():(this[I].push(t.proxyErrors?new is(this,e,t):new Ht(this,e,t)),this[J]?pt(()=>this[qe]()):this[qe]()),e}unpipe(e){let t=this[I].find(i=>i.dest===e);t&&(this[I].length===1?(this[O]&&this[Ne]===0&&(this[O]=!1),this[I]=[]):this[I].splice(this[I].indexOf(t),1),t.unpipe())}addListener(e,t){return this.on(e,t)}on(e,t){let i=super.on(e,t);if(e==="data")this[k]=!1,this[Ne]++,!this[I].length&&!this[O]&&this[qe]();else if(e==="readable"&&this[v]!==0)super.emit("readable");else if(Mo(e)&&this[_e])super.emit(e),this.removeAllListeners(e);else if(e==="error"&&this[ft]){let r=t;this[J]?pt(()=>r.call(this,this[ft])):r.call(this,this[ft])}return i}removeListener(e,t){return this.off(e,t)}off(e,t){let i=super.off(e,t);return e==="data"&&(this[Ne]=this.listeners("data").length,this[Ne]===0&&!this[k]&&!this[I].length&&(this[O]=!1)),i}removeAllListeners(e){let t=super.removeAllListeners(e);return(e==="data"||e===void 0)&&(this[Ne]=0,!this[k]&&!this[I].length&&(this[O]=!1)),t}get emittedEnd(){return this[_e]}[ue](){!this[xt]&&!this[_e]&&!this[y]&&this[R].length===0&&this[le]&&(this[xt]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[jt]&&this.emit("close"),this[xt]=!1)}emit(e,...t){let i=t[0];if(e!=="error"&&e!=="close"&&e!==y&&this[y])return!1;if(e==="data")return!this[N]&&!i?!1:this[J]?(pt(()=>this[Ji](i)),!0):this[Ji](i);if(e==="end")return this[xr]();if(e==="close"){if(this[jt]=!0,!this[_e]&&!this[y])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(e==="error"){this[ft]=i,super.emit(Qi,i);let n=!this[mt]||this.listeners("error").length?super.emit("error",i):!1;return this[ue](),n}else if(e==="resume"){let n=super.emit("resume");return this[ue](),n}else if(e==="finish"||e==="prefinish"){let n=super.emit(e);return this.removeAllListeners(e),n}let r=super.emit(e,...t);return this[ue](),r}[Ji](e){for(let i of this[I])i.dest.write(e)===!1&&this.pause();let t=this[k]?!1:super.emit("data",e);return this[ue](),t}[xr](){return this[_e]?!1:(this[_e]=!0,this.readable=!1,this[J]?(pt(()=>this[es]()),!0):this[es]())}[es](){if(this[Ue]){let t=this[Ue].end();if(t){for(let i of this[I])i.dest.write(t);this[k]||super.emit("data",t)}}for(let t of this[I])t.end();let e=super.emit("end");return this.removeAllListeners("end"),e}async collect(){let e=Object.assign([],{dataLength:0});this[N]||(e.dataLength=0);let t=this.promise();return this.on("data",i=>{e.push(i),this[N]||(e.dataLength+=i.length)}),await t,e}async concat(){if(this[N])throw new Error("cannot concat in objectMode");let e=await this.collect();return this[K]?e.join(""):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,t)=>{this.on(y,()=>t(new Error("stream destroyed"))),this.on("error",i=>t(i)),this.on("end",()=>e())})}[Symbol.asyncIterator](){this[k]=!1;let e=!1,t=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return t();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[le])return t();let n,o,a=c=>{this.off("data",h),this.off("end",l),this.off(y,u),t(),o(c)},h=c=>{this.off("error",a),this.off("end",l),this.off(y,u),this.pause(),n({value:c,done:!!this[le]})},l=()=>{this.off("error",a),this.off("data",h),this.off(y,u),t(),n({done:!0,value:void 0})},u=()=>a(new Error("stream destroyed"));return new Promise((c,E)=>{o=E,n=c,this.once(y,u),this.once("error",a),this.once("end",l),this.once("data",h)})},throw:t,return:t,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[k]=!1;let e=!1,t=()=>(this.pause(),this.off(Qi,t),this.off(y,t),this.off("end",t),e=!0,{done:!0,value:void 0}),i=()=>{if(e)return t();let r=this.read();return r===null?t():{done:!1,value:r}};return this.once("end",t),this.once(Qi,t),this.once(y,t),{next:i,throw:t,return:t,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(e){if(this[y])return e?this.emit("error",e):this.emit(y),this;this[y]=!0,this[k]=!0,this[R].length=0,this[v]=0;let t=this;return typeof t.close=="function"&&!this[jt]&&t.close(),e?this.emit("error",e):this.emit(y),this}static get isStream(){return F.isStream}};F.Minipass=Zt});var Ke=d(W=>{"use strict";var Ur=W&&W.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(W,"__esModule",{value:!0});W.WriteStreamSync=W.WriteStream=W.ReadStreamSync=W.ReadStream=void 0;var Co=Ur(require("events")),B=Ur(require("fs")),Bo=We(),zo=B.default.writev,ye=Symbol("_autoClose"),$=Symbol("_close"),_t=Symbol("_ended"),p=Symbol("_fd"),rs=Symbol("_finished"),fe=Symbol("_flags"),ns=Symbol("_flush"),ls=Symbol("_handleChunk"),us=Symbol("_makeBuf"),yt=Symbol("_mode"),Gt=Symbol("_needDrain"),Ge=Symbol("_onerror"),Ye=Symbol("_onopen"),os=Symbol("_onread"),He=Symbol("_onwrite"),Ee=Symbol("_open"),V=Symbol("_path"),we=Symbol("_pos"),ee=Symbol("_queue"),Ze=Symbol("_read"),as=Symbol("_readSize"),ce=Symbol("_reading"),wt=Symbol("_remain"),hs=Symbol("_size"),Yt=Symbol("_write"),Me=Symbol("_writing"),Kt=Symbol("_defaultFlag"),Le=Symbol("_errored"),Vt=class extends Bo.Minipass{[Le]=!1;[p];[V];[as];[ce]=!1;[hs];[wt];[ye];constructor(e,t){if(t=t||{},super(t),this.readable=!0,this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[Le]=!1,this[p]=typeof t.fd=="number"?t.fd:void 0,this[V]=e,this[as]=t.readSize||16*1024*1024,this[ce]=!1,this[hs]=typeof t.size=="number"?t.size:1/0,this[wt]=this[hs],this[ye]=typeof t.autoClose=="boolean"?t.autoClose:!0,typeof this[p]=="number"?this[Ze]():this[Ee]()}get fd(){return this[p]}get path(){return this[V]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[Ee](){B.default.open(this[V],"r",(e,t)=>this[Ye](e,t))}[Ye](e,t){e?this[Ge](e):(this[p]=t,this.emit("open",t),this[Ze]())}[us](){return Buffer.allocUnsafe(Math.min(this[as],this[wt]))}[Ze](){if(!this[ce]){this[ce]=!0;let e=this[us]();if(e.length===0)return process.nextTick(()=>this[os](null,0,e));B.default.read(this[p],e,0,e.length,null,(t,i,r)=>this[os](t,i,r))}}[os](e,t,i){this[ce]=!1,e?this[Ge](e):this[ls](t,i)&&this[Ze]()}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,B.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}[Ge](e){this[ce]=!0,this[$](),this.emit("error",e)}[ls](e,t){let i=!1;return this[wt]-=e,e>0&&(i=super.write(ethis[Ye](e,t))}[Ye](e,t){this[Kt]&&this[fe]==="r+"&&e&&e.code==="ENOENT"?(this[fe]="w",this[Ee]()):e?this[Ge](e):(this[p]=t,this.emit("open",t),this[Me]||this[ns]())}end(e,t){return e&&this.write(e,t),this[_t]=!0,!this[Me]&&!this[ee].length&&typeof this[p]=="number"&&this[He](null,0),this}write(e,t){return typeof e=="string"&&(e=Buffer.from(e,t)),this[_t]?(this.emit("error",new Error("write() after end()")),!1):this[p]===void 0||this[Me]||this[ee].length?(this[ee].push(e),this[Gt]=!0,!1):(this[Me]=!0,this[Yt](e),!0)}[Yt](e){B.default.write(this[p],e,0,e.length,this[we],(t,i)=>this[He](t,i))}[He](e,t){e?this[Ge](e):(this[we]!==void 0&&typeof t=="number"&&(this[we]+=t),this[ee].length?this[ns]():(this[Me]=!1,this[_t]&&!this[rs]?(this[rs]=!0,this[$](),this.emit("finish")):this[Gt]&&(this[Gt]=!1,this.emit("drain"))))}[ns](){if(this[ee].length===0)this[_t]&&this[He](null,0);else if(this[ee].length===1)this[Yt](this[ee].pop());else{let e=this[ee];this[ee]=[],zo(this[p],e,this[we],(t,i)=>this[He](t,i))}}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,B.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}};W.WriteStream=$t;var fs=class extends $t{[Ee](){let e;if(this[Kt]&&this[fe]==="r+")try{e=B.default.openSync(this[V],this[fe],this[yt])}catch(t){if(t?.code==="ENOENT")return this[fe]="w",this[Ee]();throw t}else e=B.default.openSync(this[V],this[fe],this[yt]);this[Ye](null,e)}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,B.default.closeSync(e),this.emit("close")}}[Yt](e){let t=!0;try{this[He](null,B.default.writeSync(this[p],e,0,e.length,this[we])),t=!1}finally{if(t)try{this[$]()}catch{}}}};W.WriteStreamSync=fs});var Xt=d(b=>{"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.dealias=b.isNoFile=b.isFile=b.isAsync=b.isSync=b.isAsyncNoFile=b.isSyncNoFile=b.isAsyncFile=b.isSyncFile=void 0;var ko=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),xo=s=>!!s.sync&&!!s.file;b.isSyncFile=xo;var jo=s=>!s.sync&&!!s.file;b.isAsyncFile=jo;var Uo=s=>!!s.sync&&!s.file;b.isSyncNoFile=Uo;var qo=s=>!s.sync&&!s.file;b.isAsyncNoFile=qo;var Wo=s=>!!s.sync;b.isSync=Wo;var Ho=s=>!s.sync;b.isAsync=Ho;var Zo=s=>!!s.file;b.isFile=Zo;var Go=s=>!s.file;b.isNoFile=Go;var Yo=s=>{let e=ko.get(s);return e||s},Ko=(s={})=>{if(!s)return{};let e={};for(let[t,i]of Object.entries(s)){let r=Yo(t);e[r]=i}return e.chmod===void 0&&e.noChmod===!1&&(e.chmod=!0),delete e.noChmod,e};b.dealias=Ko});var Ve=d(Qt=>{"use strict";Object.defineProperty(Qt,"__esModule",{value:!0});Qt.makeCommand=void 0;var Et=Xt(),Vo=(s,e,t,i,r)=>Object.assign((n=[],o,a)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(a=o,o=void 0),o=o?Array.from(o):[];let h=(0,Et.dealias)(n);if(r?.(h,o),(0,Et.isSyncFile)(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return s(h,o)}else if((0,Et.isAsyncFile)(h)){let l=e(h,o);return a?l.then(()=>a(),a):l}else if((0,Et.isSyncNoFile)(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return t(h,o)}else if((0,Et.isAsyncNoFile)(h)){if(typeof a=="function")throw new TypeError("callback only supported with file option");return i(h,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:e,syncNoFile:t,asyncNoFile:i,validate:r});Qt.makeCommand=Vo});var ds=d($e=>{"use strict";var $o=$e&&$e.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty($e,"__esModule",{value:!0});$e.constants=void 0;var Xo=$o(require("zlib")),Qo=Xo.default.constants||{ZLIB_VERNUM:4736};$e.constants=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},Qo))});var Ps=d(f=>{"use strict";var Jo=f&&f.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),ea=f&&f.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ta=f&&f.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;rs,ms=Wr?.writable===!0||Wr?.set!==void 0?s=>{Ae.Buffer.concat=s?oa:na}:s=>{},Ie=Symbol("_superWrite"),Fe=class extends Error{code;errno;constructor(e,t){super("zlib: "+e.message,{cause:e}),this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,t??this.constructor)}get name(){return"ZlibError"}};f.ZlibError=Fe;var ps=Symbol("flushFlag"),bt=class extends sa.Minipass{#e=!1;#i=!1;#s;#n;#r;#t;#o;get sawError(){return this.#e}get handle(){return this.#t}get flushFlag(){return this.#s}constructor(e,t){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(e),this.#s=e.flush??0,this.#n=e.finishFlush??0,this.#r=e.fullFlushFlag??0,typeof qr[t]!="function")throw new TypeError("Compression method not supported: "+t);try{this.#t=new qr[t](e)}catch(i){throw new Fe(i,this.constructor)}this.#o=i=>{this.#e||(this.#e=!0,this.close(),this.emit("error",i))},this.#t?.on("error",i=>this.#o(new Fe(i))),this.once("end",()=>this.close)}close(){this.#t&&(this.#t.close(),this.#t=void 0,this.emit("close"))}reset(){if(!this.#e)return(0,_s.default)(this.#t,"zlib binding closed"),this.#t.reset?.()}flush(e){this.ended||(typeof e!="number"&&(e=this.#r),this.write(Object.assign(Ae.Buffer.alloc(0),{[ps]:e})))}end(e,t,i){return typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&(t?this.write(e,t):this.write(e)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Ie](e){return super.write(e)}write(e,t,i){if(typeof t=="function"&&(i=t,t="utf8"),typeof e=="string"&&(e=Ae.Buffer.from(e,t)),this.#e)return;(0,_s.default)(this.#t,"zlib binding closed");let r=this.#t._handle,n=r.close;r.close=()=>{};let o=this.#t.close;this.#t.close=()=>{},ms(!0);let a;try{let l=typeof e[ps]=="number"?e[ps]:this.#s;a=this.#t._processChunk(e,l),ms(!1)}catch(l){ms(!1),this.#o(new Fe(l,this.write))}finally{this.#t&&(this.#t._handle=r,r.close=n,this.#t.close=o,this.#t.removeAllListeners("error"))}this.#t&&this.#t.on("error",l=>this.#o(new Fe(l,this.write)));let h;if(a)if(Array.isArray(a)&&a.length>0){let l=a[0];h=this[Ie](Ae.Buffer.from(l));for(let u=1;u{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(e,t)}finally{this.handle.flush=i}this.handle&&(this.#e=e,this.#i=t)}}}};f.Zlib=ie;var ws=class extends ie{constructor(e){super(e,"Deflate")}};f.Deflate=ws;var ys=class extends ie{constructor(e){super(e,"Inflate")}};f.Inflate=ys;var Es=class extends ie{#e;constructor(e){super(e,"Gzip"),this.#e=e&&!!e.portable}[Ie](e){return this.#e?(this.#e=!1,e[9]=255,super[Ie](e)):super[Ie](e)}};f.Gzip=Es;var bs=class extends ie{constructor(e){super(e,"Gunzip")}};f.Gunzip=bs;var Ss=class extends ie{constructor(e){super(e,"DeflateRaw")}};f.DeflateRaw=Ss;var gs=class extends ie{constructor(e){super(e,"InflateRaw")}};f.InflateRaw=gs;var Os=class extends ie{constructor(e){super(e,"Unzip")}};f.Unzip=Os;var Jt=class extends bt{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||te.constants.BROTLI_OPERATION_FINISH,e.fullFlushFlag=te.constants.BROTLI_OPERATION_FLUSH,super(e,t)}},Rs=class extends Jt{constructor(e){super(e,"BrotliCompress")}};f.BrotliCompress=Rs;var vs=class extends Jt{constructor(e){super(e,"BrotliDecompress")}};f.BrotliDecompress=vs;var ei=class extends bt{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.ZSTD_e_continue,e.finishFlush=e.finishFlush||te.constants.ZSTD_e_end,e.fullFlushFlag=te.constants.ZSTD_e_flush,super(e,t)}},Ts=class extends ei{constructor(e){super(e,"ZstdCompress")}};f.ZstdCompress=Ts;var Ds=class extends ei{constructor(e){super(e,"ZstdDecompress")}};f.ZstdDecompress=Ds});var Gr=d(Xe=>{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.parse=Xe.encode=void 0;var aa=(s,e)=>{if(Number.isSafeInteger(s))s<0?la(s,e):ha(s,e);else throw Error("cannot encode number outside of javascript safe integer range");return e};Xe.encode=aa;var ha=(s,e)=>{e[0]=128;for(var t=e.length;t>1;t--)e[t-1]=s&255,s=Math.floor(s/256)},la=(s,e)=>{e[0]=255;var t=!1;s=s*-1;for(var i=e.length;i>1;i--){var r=s&255;s=Math.floor(s/256),t?e[i-1]=Hr(r):r===0?e[i-1]=0:(t=!0,e[i-1]=Zr(r))}},ua=s=>{let e=s[0],t=e===128?fa(s.subarray(1,s.length)):e===255?ca(s):null;if(t===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(t))throw Error("parsed number outside of javascript safe integer range");return t};Xe.parse=ua;var ca=s=>{for(var e=s.length,t=0,i=!1,r=e-1;r>-1;r--){var n=Number(s[r]),o;i?o=Hr(n):n===0?o=n:(i=!0,o=Zr(n)),o!==0&&(t-=o*Math.pow(256,e-r-1))}return t},fa=s=>{for(var e=s.length,t=0,i=e-1;i>-1;i--){var r=Number(s[i]);r!==0&&(t+=r*Math.pow(256,e-i-1))}return t},Hr=s=>(255^s)&255,Zr=s=>(255^s)+1&255});var Ns=d(x=>{"use strict";Object.defineProperty(x,"__esModule",{value:!0});x.code=x.name=x.isName=x.isCode=void 0;var da=s=>x.name.has(s);x.isCode=da;var ma=s=>x.code.has(s);x.isName=ma;x.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);x.code=new Map(Array.from(x.name).map(s=>[s[1],s[0]]))});var Je=d(se=>{"use strict";var pa=se&&se.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),_a=se&&se.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Yr=se&&se.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r=t+512))throw new Error("need 512 bytes for header");this.path=i?.path??Ce(e,t,100),this.mode=i?.mode??r?.mode??be(e,t+100,8),this.uid=i?.uid??r?.uid??be(e,t+108,8),this.gid=i?.gid??r?.gid??be(e,t+116,8),this.size=i?.size??r?.size??be(e,t+124,12),this.mtime=i?.mtime??r?.mtime??Ms(e,t+136,12),this.cksum=be(e,t+148,12),r&&this.#i(r,!0),i&&this.#i(i);let n=Ce(e,t+156,1);if(St.isCode(n)&&(this.#e=n||"0"),this.#e==="0"&&this.path.slice(-1)==="/"&&(this.#e="5"),this.#e==="5"&&(this.size=0),this.linkpath=Ce(e,t+157,100),e.subarray(t+257,t+265).toString()==="ustar\x0000")if(this.uname=i?.uname??r?.uname??Ce(e,t+265,32),this.gname=i?.gname??r?.gname??Ce(e,t+297,32),this.devmaj=i?.devmaj??r?.devmaj??be(e,t+329,8)??0,this.devmin=i?.devmin??r?.devmin??be(e,t+337,8)??0,e[t+475]!==0){let a=Ce(e,t+345,155);this.path=a+"/"+this.path}else{let a=Ce(e,t+345,130);a&&(this.path=a+"/"+this.path),this.atime=i?.atime??r?.atime??Ms(e,t+476,12),this.ctime=i?.ctime??r?.ctime??Ms(e,t+488,12)}let o=256;for(let a=t;a!(r==null||i==="path"&&t||i==="linkpath"&&t||i==="global"))))}encode(e,t=0){if(e||(e=this.block=Buffer.alloc(512)),this.#e==="Unsupported"&&(this.#e="0"),!(e.length>=t+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=wa(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Be(e,t,100,n)||this.needPax,this.needPax=Se(e,t+100,8,this.mode)||this.needPax,this.needPax=Se(e,t+108,8,this.uid)||this.needPax,this.needPax=Se(e,t+116,8,this.gid)||this.needPax,this.needPax=Se(e,t+124,12,this.size)||this.needPax,this.needPax=Ls(e,t+136,12,this.mtime)||this.needPax,e[t+156]=Number(this.#e.codePointAt(0)),this.needPax=Be(e,t+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",t+257,8),this.needPax=Be(e,t+265,32,this.uname)||this.needPax,this.needPax=Be(e,t+297,32,this.gname)||this.needPax,this.needPax=Se(e,t+329,8,this.devmaj)||this.needPax,this.needPax=Se(e,t+337,8,this.devmin)||this.needPax,this.needPax=Be(e,t+345,i,o)||this.needPax,e[t+475]!==0?this.needPax=Be(e,t+345,155,o)||this.needPax:(this.needPax=Be(e,t+345,130,o)||this.needPax,this.needPax=Ls(e,t+476,12,this.atime)||this.needPax,this.needPax=Ls(e,t+488,12,this.ctime)||this.needPax);let a=256;for(let h=t;h{let i=s,r="",n,o=Qe.posix.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Qe.posix.dirname(i),i=Qe.posix.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=e?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=e?n=[i.slice(0,99),r,!0]:(i=Qe.posix.join(Qe.posix.basename(r),i),r=Qe.posix.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},Ce=(s,e,t)=>s.subarray(e,e+t).toString("utf8").replace(/\0.*/,""),Ms=(s,e,t)=>ya(be(s,e,t)),ya=s=>s===void 0?void 0:new Date(s*1e3),be=(s,e,t)=>Number(s[e])&128?Kr.parse(s.subarray(e,e+t)):ba(s,e,t),Ea=s=>isNaN(s)?void 0:s,ba=(s,e,t)=>Ea(parseInt(s.subarray(e,e+t).toString("utf8").replace(/\0.*$/,"").trim(),8)),Sa={12:8589934591,8:2097151},Se=(s,e,t,i)=>i===void 0?!1:i>Sa[t]||i<0?(Kr.encode(i,s.subarray(e,e+t)),!0):(ga(s,e,t,i),!1),ga=(s,e,t,i)=>s.write(Oa(i,t),e,t,"ascii"),Oa=(s,e)=>Ra(Math.floor(s).toString(8),e),Ra=(s,e)=>(s.length===e-1?s:new Array(e-s.length-1).join("0")+s+" ")+"\0",Ls=(s,e,t,i)=>i===void 0?!1:Se(s,e,t,i.getTime()/1e3),va=new Array(156).join("\0"),Be=(s,e,t,i)=>i===void 0?!1:(s.write(i+va,e,t,"utf8"),i.length!==Buffer.byteLength(i)||i.length>t)});var ii=d(ti=>{"use strict";Object.defineProperty(ti,"__esModule",{value:!0});ti.Pax=void 0;var Ta=require("node:path"),Da=Je(),Is=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(e,t=!1){this.atime=e.atime,this.charset=e.charset,this.comment=e.comment,this.ctime=e.ctime,this.dev=e.dev,this.gid=e.gid,this.global=t,this.gname=e.gname,this.ino=e.ino,this.linkpath=e.linkpath,this.mtime=e.mtime,this.nlink=e.nlink,this.path=e.path,this.size=e.size,this.uid=e.uid,this.uname=e.uname}encode(){let e=this.encodeBody();if(e==="")return Buffer.allocUnsafe(0);let t=Buffer.byteLength(e),i=512*Math.ceil(1+t/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new Da.Header({path:("PaxHeader/"+(0,Ta.basename)(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:t,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(e,512,t,"utf8");for(let n=t+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(e,t,i=!1){return new s(Pa(Na(e),t),i)}};ti.Pax=Is;var Pa=(s,e)=>e?Object.assign({},e,s):s,Na=s=>s.replace(/\n$/,"").split(` +`).reduce(Ma,Object.create(null)),Ma=(s,e)=>{let t=parseInt(e,10);if(t!==Buffer.byteLength(e)+1)return s;e=e.slice((t+" ").length);let i=e.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=");return s[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(o)*1e3):/^[0-9]+$/.test(o)?+o:o,s}});var et=d(si=>{"use strict";Object.defineProperty(si,"__esModule",{value:!0});si.normalizeWindowsPath=void 0;var La=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;si.normalizeWindowsPath=La!=="win32"?s=>s:s=>s&&s.replaceAll(/\\/g,"/")});var oi=d(ni=>{"use strict";Object.defineProperty(ni,"__esModule",{value:!0});ni.ReadEntry=void 0;var Aa=We(),ri=et(),Fs=class extends Aa.Minipass{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(e,t,i){switch(super({}),this.pause(),this.extended=t,this.globalExtended=i,this.header=e,this.remain=e.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=e.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!e.path)throw new Error("no path provided for tar.ReadEntry");this.path=(0,ri.normalizeWindowsPath)(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=this.remain,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath?(0,ri.normalizeWindowsPath)(e.linkpath):void 0,this.uname=e.uname,this.gname=e.gname,t&&this.#e(t),i&&this.#e(i,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-t),this.blockRemain=Math.max(0,r-t),this.ignore?!0:i>=t?super.write(e):super.write(e.subarray(0,i))}#e(e,t=!1){e.path&&(e.path=(0,ri.normalizeWindowsPath)(e.path)),e.linkpath&&(e.linkpath=(0,ri.normalizeWindowsPath)(e.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(e).filter(([i,r])=>!(r==null||i==="path"&&t))))}};ni.ReadEntry=Fs});var hi=d(ai=>{"use strict";Object.defineProperty(ai,"__esModule",{value:!0});ai.warnMethod=void 0;var Ia=(s,e,t,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=t instanceof Error&&t.code||e,i.tarCode=e,!s.strict&&i.recoverable!==!1?(t instanceof Error&&(i=Object.assign(t,i),t=t.message),s.emit("warn",e,t,i)):t instanceof Error?s.emit("error",Object.assign(t,i)):s.emit("error",Object.assign(new Error(`${e}: ${t}`),i))};ai.warnMethod=Ia});var _i=d(pi=>{"use strict";Object.defineProperty(pi,"__esModule",{value:!0});pi.Parser=void 0;var Fa=require("events"),Cs=Ps(),Vr=Je(),$r=ii(),Ca=oi(),Ba=hi(),za=1024*1024,js=Buffer.from([31,139]),Us=Buffer.from([40,181,47,253]),ka=Math.max(js.length,Us.length),H=Symbol("state"),ze=Symbol("writeEntry"),de=Symbol("readEntry"),Bs=Symbol("nextEntry"),Xr=Symbol("processEntry"),re=Symbol("extendedHeader"),gt=Symbol("globalExtendedHeader"),ge=Symbol("meta"),Qr=Symbol("emitMeta"),_=Symbol("buffer"),me=Symbol("queue"),Oe=Symbol("ended"),zs=Symbol("emittedEnd"),ke=Symbol("emit"),S=Symbol("unzip"),li=Symbol("consumeChunk"),ui=Symbol("consumeChunkSub"),ks=Symbol("consumeBody"),Jr=Symbol("consumeMeta"),en=Symbol("consumeHeader"),Ot=Symbol("consuming"),xs=Symbol("bufferConcat"),ci=Symbol("maybeEnd"),tt=Symbol("writing"),Re=Symbol("aborted"),fi=Symbol("onDone"),xe=Symbol("sawValidEntry"),di=Symbol("sawNullBlock"),mi=Symbol("sawEOF"),tn=Symbol("closeStream"),xa=()=>!0,qs=class extends Fa.EventEmitter{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[me]=[];[_];[de];[ze];[H]="begin";[ge]="";[re];[gt];[Oe]=!1;[S];[Re]=!1;[xe];[di]=!1;[mi]=!1;[tt]=!1;[Ot]=!1;[zs]=!1;constructor(e={}){super(),this.file=e.file||"",this.on(fi,()=>{(this[H]==="begin"||this[xe]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(fi,e.ondone):this.on(fi,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||za,this.filter=typeof e.filter=="function"?e.filter:xa;let t=e.file&&(e.file.endsWith(".tar.br")||e.file.endsWith(".tbr"));this.brotli=!(e.gzip||e.zstd)&&e.brotli!==void 0?e.brotli:t?void 0:!1;let i=e.file&&(e.file.endsWith(".tar.zst")||e.file.endsWith(".tzst"));this.zstd=!(e.gzip||e.brotli)&&e.zstd!==void 0?e.zstd:i?!0:void 0,this.on("end",()=>this[tn]()),typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onReadEntry=="function"&&this.on("entry",e.onReadEntry)}warn(e,t,i={}){(0,Ba.warnMethod)(this,e,t,i)}[en](e,t){this[xe]===void 0&&(this[xe]=!1);let i;try{i=new Vr.Header(e,t,this[re],this[gt])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[di]?(this[mi]=!0,this[H]==="begin"&&(this[H]="header"),this[ke]("eof")):(this[di]=!0,this[ke]("nullBlock"));else if(this[di]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[ze]=new Ca.ReadEntry(i,this[re],this[gt]);if(!this[xe])if(n.remain){let o=()=>{n.invalid||(this[xe]=!0)};n.on("end",o)}else this[xe]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[ke]("ignoredEntry",n),this[H]="ignore",n.resume()):n.size>0&&(this[ge]="",n.on("data",o=>this[ge]+=o),this[H]="meta"):(this[re]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[ke]("ignoredEntry",n),this[H]=n.remain?"ignore":"header",n.resume()):(n.remain?this[H]="body":(this[H]="header",n.end()),this[de]?this[me].push(n):(this[me].push(n),this[Bs]())))}}}[tn](){queueMicrotask(()=>this.emit("close"))}[Xr](e){let t=!0;if(!e)this[de]=void 0,t=!1;else if(Array.isArray(e)){let[i,...r]=e;this.emit(i,...r)}else this[de]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",()=>this[Bs]()),t=!1);return t}[Bs](){do;while(this[Xr](this[me].shift()));if(this[me].length===0){let e=this[de];!e||e.flowing||e.size===e.remain?this[tt]||this.emit("drain"):e.once("drain",()=>this.emit("drain"))}}[ks](e,t){let i=this[ze];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=e.length&&t===0?e:e.subarray(t,t+r);return i.write(n),i.blockRemain||(this[H]="header",this[ze]=void 0,i.end()),n.length}[Jr](e,t){let i=this[ze],r=this[ks](e,t);return!this[ze]&&i&&this[Qr](i),r}[ke](e,t,i){this[me].length===0&&!this[de]?this.emit(e,t,i):this[me].push([e,t,i])}[Qr](e){switch(this[ke]("meta",this[ge]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[re]=$r.Pax.parse(this[ge],this[re],!1);break;case"GlobalExtendedHeader":this[gt]=$r.Pax.parse(this[ge],this[gt],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let t=this[re]??Object.create(null);this[re]=t,t.path=this[ge].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let t=this[re]||Object.create(null);this[re]=t,t.linkpath=this[ge].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+e.type)}}abort(e){this[Re]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1})}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this[Re])return i?.(),!1;if((this[S]===void 0||this.brotli===void 0&&this[S]===!1)&&e){if(this[_]&&(e=Buffer.concat([this[_],e]),this[_]=void 0),e.lengththis[li](u)),this[S].on("error",u=>this.abort(u)),this[S].on("end",()=>{this[Oe]=!0,this[li]()}),this[tt]=!0;let l=!!this[S][h?"end":"write"](e);return this[tt]=!1,i?.(),l}}this[tt]=!0,this[S]?this[S].write(e):this[li](e),this[tt]=!1;let n=this[me].length>0?!1:this[de]?this[de].flowing:!0;return!n&&this[me].length===0&&this[de]?.once("drain",()=>this.emit("drain")),i?.(),n}[xs](e){e&&!this[Re]&&(this[_]=this[_]?Buffer.concat([this[_],e]):e)}[ci](){if(this[Oe]&&!this[zs]&&!this[Re]&&!this[Ot]){this[zs]=!0;let e=this[ze];if(e&&e.blockRemain){let t=this[_]?this[_].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[_]&&e.write(this[_]),e.end()}this[ke](fi)}}[li](e){if(this[Ot]&&e)this[xs](e);else if(!e&&!this[_])this[ci]();else if(e){if(this[Ot]=!0,this[_]){this[xs](e);let t=this[_];this[_]=void 0,this[ui](t)}else this[ui](e);for(;this[_]&&this[_]?.length>=512&&!this[Re]&&!this[mi];){let t=this[_];this[_]=void 0,this[ui](t)}this[Ot]=!1}(!this[_]||this[Oe])&&this[ci]()}[ui](e){let t=0,i=e.length;for(;t+512<=i&&!this[Re]&&!this[mi];)switch(this[H]){case"begin":case"header":this[en](e,t),t+=512;break;case"ignore":case"body":t+=this[ks](e,t);break;case"meta":t+=this[Jr](e,t);break;default:throw new Error("invalid state: "+this[H])}t{"use strict";Object.defineProperty(wi,"__esModule",{value:!0});wi.stripTrailingSlashes=void 0;var ja=s=>{let e=s.length-1,t=-1;for(;e>-1&&s.charAt(e)==="/";)t=e,e--;return t===-1?s:s.slice(0,t)};wi.stripTrailingSlashes=ja});var st=d(C=>{"use strict";var Ua=C&&C.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),qa=C&&C.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Wa=C&&C.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=s.onReadEntry;s.onReadEntry=e?t=>{e(t),t.resume()}:t=>t.resume()},Ka=(s,e)=>{let t=new Map(e.map(n=>[(0,Ws.stripTrailingSlashes)(n),!0])),i=s.filter,r=(n,o="")=>{let a=o||(0,sn.parse)(n).root||".",h;if(n===a)h=!1;else{let l=t.get(n);h=l!==void 0?l:r((0,sn.dirname)(n),a)}return t.set(n,h),h};s.filter=i?(n,o)=>i(n,o)&&r((0,Ws.stripTrailingSlashes)(n)):n=>r((0,Ws.stripTrailingSlashes)(n))};C.filesFilter=Ka;var Va=s=>{let e=new Ei.Parser(s),t=s.file,i;try{i=it.default.openSync(t,"r");let r=it.default.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let t=new Ei.Parser(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{t.on("error",a),t.on("end",o),it.default.stat(r,(h,l)=>{if(h)a(h);else{let u=new Za.ReadStream(r,{readSize:i,size:l.size});u.on("error",a),u.pipe(t)}})})};C.list=(0,Ga.makeCommand)(Va,$a,s=>new Ei.Parser(s),s=>new Ei.Parser(s),(s,e)=>{e?.length&&(0,C.filesFilter)(s,e),s.noResume||Ya(s)})});var rn=d(bi=>{"use strict";Object.defineProperty(bi,"__esModule",{value:!0});bi.modeFix=void 0;var Xa=(s,e,t)=>(s&=4095,t&&(s=(s|384)&-19),e&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);bi.modeFix=Xa});var Hs=d(Si=>{"use strict";Object.defineProperty(Si,"__esModule",{value:!0});Si.stripAbsolutePath=void 0;var Qa=require("node:path"),{isAbsolute:Ja,parse:nn}=Qa.win32,eh=s=>{let e="",t=nn(s);for(;Ja(s)||t.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":t.root;s=s.slice(i.length),e+=i,t=nn(s)}return[e,s]};Si.stripAbsolutePath=eh});var Gs=d(rt=>{"use strict";Object.defineProperty(rt,"__esModule",{value:!0});rt.decode=rt.encode=void 0;var gi=["|","<",">","?",":"],Zs=gi.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),th=new Map(gi.map((s,e)=>[s,Zs[e]])),ih=new Map(Zs.map((s,e)=>[s,gi[e]])),sh=s=>gi.reduce((e,t)=>e.split(t).join(th.get(t)),s);rt.encode=sh;var rh=s=>Zs.reduce((e,t)=>e.split(t).join(ih.get(t)),s);rt.decode=rh});var sr=d(M=>{"use strict";var nh=M&&M.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),oh=M&&M.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ah=M&&M.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;re?(s=(0,ne.normalizeWindowsPath)(s).replace(/^\.(\/|$)/,""),(0,hh.stripTrailingSlashes)(e)+"/"+s):(0,ne.normalizeWindowsPath)(s),uh=16*1024*1024,an=Symbol("process"),hn=Symbol("file"),ln=Symbol("directory"),Ks=Symbol("symlink"),un=Symbol("hardlink"),Rt=Symbol("header"),Oi=Symbol("read"),Vs=Symbol("lstat"),Ri=Symbol("onlstat"),$s=Symbol("onread"),Xs=Symbol("onreadlink"),Qs=Symbol("openfile"),Js=Symbol("onopenfile"),ve=Symbol("close"),vi=Symbol("mode"),er=Symbol("awaitDrain"),Ys=Symbol("ondrain"),ae=Symbol("prefix"),Ti=class extends fn.Minipass{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#e=!1;constructor(e,t={}){let i=(0,pn.dealias)(t);super(),this.path=(0,ne.normalizeWindowsPath)(e),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||uh,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=(0,ne.normalizeWindowsPath)(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?(0,ne.normalizeWindowsPath)(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,a]=(0,wn.stripAbsolutePath)(this.path);o&&typeof a=="string"&&(this.path=a,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=lh.decode(this.path.replaceAll(/\\/g,"/")),e=e.replaceAll(/\\/g,"/")),this.absolute=(0,ne.normalizeWindowsPath)(i.absolute||on.default.resolve(this.cwd,e)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[Ri](n):this[Vs]()}warn(e,t,i={}){return(0,yn.warnMethod)(this,e,t,i)}emit(e,...t){return e==="error"&&(this.#e=!0),super.emit(e,...t)}[Vs](){oe.default.lstat(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Ri](t)})}[Ri](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=ch(e),this.emit("stat",e),this[an]()}[an](){switch(this.type){case"File":return this[hn]();case"Directory":return this[ln]();case"SymbolicLink":return this[Ks]();default:return this.end()}}[vi](e){return(0,mn.modeFix)(e,this.type==="Directory",this.portable)}[ae](e){return En(e,this.prefix)}[Rt](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new dn.Header({path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,mode:this[vi](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new _n.Pax({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let e=this.header?.block;if(!e)throw new Error("failed to encode header");super.write(e)}[ln](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[Rt](),this.end()}[Ks](){oe.default.readlink(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Xs](t)})}[Xs](e){this.linkpath=(0,ne.normalizeWindowsPath)(e),this[Rt](),this.end()}[un](e){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=(0,ne.normalizeWindowsPath)(on.default.relative(this.cwd,e)),this.stat.size=0,this[Rt](),this.end()}[hn](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let e=`${this.stat.dev}:${this.stat.ino}`,t=this.linkCache.get(e);if(t?.indexOf(this.cwd)===0)return this[un](t);this.linkCache.set(e,this.absolute)}if(this[Rt](),this.stat.size===0)return this.end();this[Qs]()}[Qs](){oe.default.open(this.absolute,"r",(e,t)=>{if(e)return this.emit("error",e);this[Js](t)})}[Js](e){if(this.fd=e,this.#e)return this[ve]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let t=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(t),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[Oi]()}[Oi](){let{fd:e,buf:t,offset:i,length:r,pos:n}=this;if(e===void 0||t===void 0)throw new Error("cannot read file without first opening");oe.default.read(e,t,i,r,n,(o,a)=>{if(o)return this[ve](()=>this.emit("error",o));this[$s](a)})}[ve](e=()=>{}){this.fd!==void 0&&oe.default.close(this.fd,e)}[$s](e){if(e<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(e>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(e===this.remain)for(let r=e;rthis[Ys]())}[er](e){this.once("drain",e)}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this.blockRemaine?this.emit("error",e):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[Oi]()}};M.WriteEntry=Ti;var tr=class extends Ti{sync=!0;[Vs](){this[Ri](oe.default.lstatSync(this.absolute))}[Ks](){this[Xs](oe.default.readlinkSync(this.absolute))}[Qs](){this[Js](oe.default.openSync(this.absolute,"r"))}[Oi](){let e=!0;try{let{fd:t,buf:i,offset:r,length:n,pos:o}=this;if(t===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let a=oe.default.readSync(t,i,r,n,o);this[$s](a),e=!1}finally{if(e)try{this[ve](()=>{})}catch{}}}[er](e){e()}[ve](e=()=>{}){this.fd!==void 0&&oe.default.closeSync(this.fd),e()}};M.WriteEntrySync=tr;var ir=class extends fn.Minipass{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(e,t,i={}){return(0,yn.warnMethod)(this,e,t,i)}constructor(e,t={}){let i=(0,pn.dealias)(t);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=e;let{type:r}=e;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=(0,ne.normalizeWindowsPath)(e.path),this.mode=e.mode!==void 0?this[vi](e.mode):void 0,this.uid=this.portable?void 0:e.uid,this.gid=this.portable?void 0:e.gid,this.uname=this.portable?void 0:e.uname,this.gname=this.portable?void 0:e.gname,this.size=e.size,this.mtime=this.noMtime?void 0:i.mtime||e.mtime,this.atime=this.portable?void 0:e.atime,this.ctime=this.portable?void 0:e.ctime,this.linkpath=e.linkpath!==void 0?(0,ne.normalizeWindowsPath)(e.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[a,h]=(0,wn.stripAbsolutePath)(this.path);a&&typeof h=="string"&&(this.path=h,n=a)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.onWriteEntry?.(this),this.header=new dn.Header({path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new _n.Pax({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),e.pipe(this)}[ae](e){return En(e,this.prefix)}[vi](e){return(0,mn.modeFix)(e,this.type==="Directory",this.portable)}write(e,t,i){typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8"));let r=e.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(e,i)}end(e,t,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,t??"utf8")),i&&this.once("finish",i),e?super.end(e,i):super.end(i),this}};M.WriteEntryTar=ir;var ch=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported"});var bn=d(ot=>{"use strict";Object.defineProperty(ot,"__esModule",{value:!0});ot.Node=ot.Yallist=void 0;var rr=class s{tail;head;length=0;static create(e=[]){return new s(e)}constructor(e=[]){for(let t of e)this.push(t)}*[Symbol.iterator](){for(let e=this.head;e;e=e.next)yield e.value}removeNode(e){if(e.list!==this)throw new Error("removing node which does not belong to this list");let t=e.next,i=e.prev;return t&&(t.prev=i),i&&(i.next=t),e===this.head&&(this.head=t),e===this.tail&&(this.tail=i),this.length--,e.next=void 0,e.prev=void 0,e.list=void 0,t}unshiftNode(e){if(e===this.head)return;e.list&&e.list.removeNode(e);let t=this.head;e.list=this,e.next=t,t&&(t.prev=e),this.head=e,this.tail||(this.tail=e),this.length++}pushNode(e){if(e===this.tail)return;e.list&&e.list.removeNode(e);let t=this.tail;e.list=this,e.prev=t,t&&(t.next=e),this.tail=e,this.head||(this.head=e),this.length++}push(...e){for(let t=0,i=e.length;t1)i=t;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=e(i,r.value,n),r=r.next;return i}reduceReverse(e,t){let i,r=this.tail;if(arguments.length>1)i=t;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=e(i,r.value,n),r=r.prev;return i}toArray(){let e=new Array(this.length);for(let t=0,i=this.head;i;t++)e[t]=i.value,i=i.next;return e}toArrayReverse(){let e=new Array(this.length);for(let t=0,i=this.tail;i;t++)e[t]=i.value,i=i.prev;return e}slice(e=0,t=this.length){t<0&&(t+=this.length),e<0&&(e+=this.length);let i=new s;if(tthis.length&&(t=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(t=this.length);let r=this.length,n=this.tail;for(;n&&r>t;r--)n=n.prev;for(;n&&r>e;r--,n=n.prev)i.push(n.value);return i}splice(e,t=0,...i){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);let r=this.head;for(let o=0;r&&o{"use strict";var ph=L&&L.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),_h=L&&L.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),wh=L&&L.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(e.gzip&&(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new nr.Gzip(e.gzip)),e.brotli&&(typeof e.brotli!="object"&&(e.brotli={}),this.zip=new nr.BrotliCompress(e.brotli)),e.zstd&&(typeof e.zstd!="object"&&(e.zstd={}),this.zip=new nr.ZstdCompress(e.zstd)),!this.zip)throw new Error("impossible");let t=this.zip;t.on("data",i=>super.write(i)),t.on("end",()=>super.end()),t.on("drain",()=>this[hr]()),this.on("resume",()=>t.resume())}else this.on("drain",this[hr]);this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,e.mtime&&(this.mtime=e.mtime),this.filter=typeof e.filter=="function"?e.filter:()=>!0,this[X]=new Eh.Yallist,this[Q]=0,this.jobs=Number(e.jobs)||4,this[Dt]=!1,this[vt]=!1}[Tn](e){return super.write(e)}add(e){return this.write(e),this}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&this.add(e),this[vt]=!0,this[je](),i&&i(),this}write(e){if(this[vt])throw new Error("write after end");return e instanceof bh.ReadEntry?this[gn](e):this[Ni](e),this.flowing}[gn](e){let t=(0,lr.normalizeWindowsPath)(Rn.default.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let i=new Pt(e.path,t);i.entry=new ur.WriteEntryTar(e,this[ar](i)),i.entry.on("end",()=>this[or](i)),this[Q]+=1,this[X].push(i)}this[je]()}[Ni](e){let t=(0,lr.normalizeWindowsPath)(Rn.default.resolve(this.cwd,e));this[X].push(new Pt(e,t)),this[je]()}[cr](e){e.pending=!0,this[Q]+=1;let t=this.follow?"stat":"lstat";Ii.default[t](e.absolute,(i,r)=>{e.pending=!1,this[Q]-=1,i?this.emit("error",i):this[Pi](e,r)})}[Pi](e,t){if(this.statCache.set(e.absolute,t),e.stat=t,!this.filter(e.path,t))e.ignore=!0;else if(t.isFile()&&t.nlink>1&&!this.linkCache.get(`${t.dev}:${t.ino}`)&&!this.sync)if(e===this[Te])this[Di](e);else{let i=`${t.dev}:${t.ino}`,r=this[Tt].get(i);r?r.push(e):this[Tt].set(i,[e]),e.pendingLink=!0,e.pending=!0}this[je]()}[fr](e){e.pending=!0,this[Q]+=1,Ii.default.readdir(e.absolute,(t,i)=>{if(e.pending=!1,this[Q]-=1,t)return this.emit("error",t);this[Mi](e,i)})}[Mi](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[je]()}[je](){if(!this[Dt]){this[Dt]=!0;for(let e=this[X].head;e&&this[Q]1){let i=`${t.dev}:${t.ino}`,r=this[Tt].get(i);if(r){this[Tt].delete(i);for(let n of r)n.pending=!1,this[Di](n)}}this[je]()}[Di](e){if(e.pending&&e.pendingLink&&e===this[Te]&&(e.pending=!1,e.pendingLink=!1),!e.pending){if(e.entry){e===this[Te]&&!e.piped&&this[Li](e);return}if(!e.stat){let t=this.statCache.get(e.absolute);t?this[Pi](e,t):this[cr](e)}if(e.stat&&!e.ignore){if(!this.noDirRecurse&&e.stat.isDirectory()&&!e.readdir){let t=this.readdirCache.get(e.absolute);if(t?this[Mi](e,t):this[fr](e),!e.readdir)return}if(e.entry=this[On](e),!e.entry){e.ignore=!0;return}e===this[Te]&&!e.piped&&this[Li](e)}}}[ar](e){return{onwarn:(t,i,r)=>this.warn(t,i,r),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[On](e){this[Q]+=1;try{return new this[Ai](e.path,this[ar](e)).on("end",()=>this[or](e)).on("error",i=>this.emit("error",i))}catch(t){this.emit("error",t)}}[hr](){this[Te]&&this[Te].entry&&this[Te].entry.resume()}[Li](e){e.piped=!0,e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Ni](o+r)});let t=e.entry,i=this.zip;if(!t)throw new Error("cannot pipe without source");i?t.on("data",r=>{i.write(r)||t.pause()}):t.on("data",r=>{super.write(r)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(e,t,i={}){(0,Sh.warnMethod)(this,e,t,i)}};L.Pack=Fi;var dr=class extends Fi{sync=!0;constructor(e){super(e),this[Ai]=ur.WriteEntrySync}pause(){}resume(){}[cr](e){let t=this.follow?"statSync":"lstatSync";this[Pi](e,Ii.default[t](e.absolute))}[fr](e){this[Mi](e,Ii.default.readdirSync(e.absolute))}[Li](e){let t=e.entry,i=this.zip;if(e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Ni](o+r)}),!t)throw new Error("Cannot pipe without source");i?t.on("data",r=>{i.write(r)}):t.on("data",r=>{super[Tn](r)})}};L.PackSync=dr});var mr=d(at=>{"use strict";var gh=at&&at.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(at,"__esModule",{value:!0});at.create=void 0;var Dn=Ke(),Pn=gh(require("node:path")),Nn=st(),Oh=Ve(),Bi=Ci(),Rh=(s,e)=>{let t=new Bi.PackSync(s),i=new Dn.WriteStreamSync(s.file,{mode:s.mode||438});t.pipe(i),Mn(t,e)},vh=(s,e)=>{let t=new Bi.Pack(s),i=new Dn.WriteStream(s.file,{mode:s.mode||438});t.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),t.on("error",o)});return Ln(t,e).catch(n=>t.emit("error",n)),r},Mn=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,Nn.list)({file:Pn.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},Ln=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,Nn.list)({file:Pn.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(t);s.end()},Th=(s,e)=>{let t=new Bi.PackSync(s);return Mn(t,e),t},Dh=(s,e)=>{let t=new Bi.Pack(s);return Ln(t,e).catch(i=>t.emit("error",i)),t};at.create=(0,Oh.makeCommand)(Rh,vh,Th,Dh,(s,e)=>{if(!e?.length)throw new TypeError("no paths specified to add to archive")})});var jn=d(ht=>{"use strict";var Ph=ht&&ht.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ht,"__esModule",{value:!0});ht.getWriteFlag=void 0;var Fn=Ph(require("fs")),Nh=process.env.__FAKE_PLATFORM__||process.platform,Cn=Nh==="win32",{O_CREAT:Bn,O_NOFOLLOW:An,O_TRUNC:zn,O_WRONLY:kn}=Fn.default.constants,xn=Number(process.env.__FAKE_FS_O_FILENAME__)||Fn.default.constants.UV_FS_O_FILEMAP||0,Mh=Cn&&!!xn,Lh=512*1024,Ah=xn|zn|Bn|kn,In=!Cn&&typeof An=="number"?An|zn|Bn|kn:null;ht.getWriteFlag=In!==null?()=>In:Mh?s=>s"w"});var qn=d(he=>{"use strict";var Un=he&&he.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(he,"__esModule",{value:!0});he.chownrSync=he.chownr=void 0;var ki=Un(require("node:fs")),Nt=Un(require("node:path")),pr=(s,e,t)=>{try{return ki.default.lchownSync(s,e,t)}catch(i){if(i?.code!=="ENOENT")throw i}},zi=(s,e,t,i)=>{ki.default.lchown(s,e,t,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},Ih=(s,e,t,i,r)=>{if(e.isDirectory())(0,he.chownr)(Nt.default.resolve(s,e.name),t,i,n=>{if(n)return r(n);let o=Nt.default.resolve(s,e.name);zi(o,t,i,r)});else{let n=Nt.default.resolve(s,e.name);zi(n,t,i,r)}},Fh=(s,e,t,i)=>{ki.default.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return zi(s,e,t,i);let o=n.length,a=null,h=l=>{if(!a){if(l)return i(a=l);if(--o===0)return zi(s,e,t,i)}};for(let l of n)Ih(s,l,e,t,h)})};he.chownr=Fh;var Ch=(s,e,t,i)=>{e.isDirectory()&&(0,he.chownrSync)(Nt.default.resolve(s,e.name),t,i),pr(Nt.default.resolve(s,e.name),t,i)},Bh=(s,e,t)=>{let i;try{i=ki.default.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return pr(s,e,t);throw n}for(let r of i)Ch(s,r,e,t);return pr(s,e,t)};he.chownrSync=Bh});var Wn=d(xi=>{"use strict";Object.defineProperty(xi,"__esModule",{value:!0});xi.CwdError=void 0;var _r=class extends Error{path;code;syscall="chdir";constructor(e,t){super(`${t}: Cannot cd into '${e}'`),this.path=e,this.code=t}get name(){return"CwdError"}};xi.CwdError=_r});var yr=d(ji=>{"use strict";Object.defineProperty(ji,"__esModule",{value:!0});ji.SymlinkError=void 0;var wr=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(e,t){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=e,this.path=t}get name(){return"SymlinkError"}};ji.SymlinkError=wr});var Kn=d(De=>{"use strict";var br=De&&De.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(De,"__esModule",{value:!0});De.mkdirSync=De.mkdir=void 0;var Hn=qn(),j=br(require("node:fs")),zh=br(require("node:fs/promises")),Ui=br(require("node:path")),Zn=Wn(),pe=et(),Gn=yr(),kh=(s,e)=>{j.default.stat(s,(t,i)=>{(t||!i.isDirectory())&&(t=new Zn.CwdError(s,t?.code||"ENOTDIR")),e(t)})},xh=(s,e,t)=>{s=(0,pe.normalizeWindowsPath)(s);let i=e.umask??18,r=e.mode|448,n=(r&i)!==0,o=e.uid,a=e.gid,h=typeof o=="number"&&typeof a=="number"&&(o!==e.processUid||a!==e.processGid),l=e.preserve,u=e.unlink,c=(0,pe.normalizeWindowsPath)(e.cwd),E=(w,P)=>{w?t(w):P&&h?(0,Hn.chownr)(P,o,a,kt=>E(kt)):n?j.default.chmod(s,r,t):t()};if(s===c)return kh(s,E);if(l)return zh.default.mkdir(s,{mode:r,recursive:!0}).then(w=>E(null,w??void 0),E);let A=(0,pe.normalizeWindowsPath)(Ui.default.relative(c,s)).split("/");Er(c,A,r,u,c,void 0,E)};De.mkdir=xh;var Er=(s,e,t,i,r,n,o)=>{if(e.length===0)return o(null,n);let a=e.shift(),h=(0,pe.normalizeWindowsPath)(Ui.default.resolve(s+"/"+a));j.default.mkdir(h,t,Yn(h,e,t,i,r,n,o))},Yn=(s,e,t,i,r,n,o)=>a=>{a?j.default.lstat(s,(h,l)=>{if(h)h.path=h.path&&(0,pe.normalizeWindowsPath)(h.path),o(h);else if(l.isDirectory())Er(s,e,t,i,r,n,o);else if(i)j.default.unlink(s,u=>{if(u)return o(u);j.default.mkdir(s,t,Yn(s,e,t,i,r,n,o))});else{if(l.isSymbolicLink())return o(new Gn.SymlinkError(s,s+"/"+e.join("/")));o(a)}}):(n=n||s,Er(s,e,t,i,r,n,o))},jh=s=>{let e=!1,t;try{e=j.default.statSync(s).isDirectory()}catch(i){t=i?.code}finally{if(!e)throw new Zn.CwdError(s,t??"ENOTDIR")}},Uh=(s,e)=>{s=(0,pe.normalizeWindowsPath)(s);let t=e.umask??18,i=e.mode|448,r=(i&t)!==0,n=e.uid,o=e.gid,a=typeof n=="number"&&typeof o=="number"&&(n!==e.processUid||o!==e.processGid),h=e.preserve,l=e.unlink,u=(0,pe.normalizeWindowsPath)(e.cwd),c=w=>{w&&a&&(0,Hn.chownrSync)(w,n,o),r&&j.default.chmodSync(s,i)};if(s===u)return jh(u),c();if(h)return c(j.default.mkdirSync(s,{mode:i,recursive:!0})??void 0);let D=(0,pe.normalizeWindowsPath)(Ui.default.relative(u,s)).split("/"),A;for(let w=D.shift(),P=u;w&&(P+="/"+w);w=D.shift()){P=(0,pe.normalizeWindowsPath)(Ui.default.resolve(P));try{j.default.mkdirSync(P,i),A=A||P}catch{let kt=j.default.lstatSync(P);if(kt.isDirectory())continue;if(l){j.default.unlinkSync(P),j.default.mkdirSync(P,i),A=A||P;continue}else if(kt.isSymbolicLink())return new Gn.SymlinkError(P,P+"/"+D.join("/"))}}return c(A)};De.mkdirSync=Uh});var $n=d(qi=>{"use strict";Object.defineProperty(qi,"__esModule",{value:!0});qi.normalizeUnicode=void 0;var Sr=Object.create(null),Vn=1e4,lt=new Set,qh=s=>{lt.has(s)?lt.delete(s):Sr[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),lt.add(s);let e=Sr[s],t=lt.size-Vn;if(t>Vn/10){for(let i of lt)if(lt.delete(i),delete Sr[i],--t<=0)break}return e};qi.normalizeUnicode=qh});var Qn=d(Wi=>{"use strict";Object.defineProperty(Wi,"__esModule",{value:!0});Wi.PathReservations=void 0;var Xn=require("node:path"),Wh=$n(),Hh=yi(),Zh=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Gh=Zh==="win32",Yh=s=>s.split("/").slice(0,-1).reduce((t,i)=>{let r=t.at(-1);return r!==void 0&&(i=(0,Xn.join)(r,i)),t.push(i||"/"),t},[]),gr=class{#e=new Map;#i=new Map;#s=new Set;reserve(e,t){e=Gh?["win32 parallelization disabled"]:e.map(r=>(0,Hh.stripTrailingSlashes)((0,Xn.join)((0,Wh.normalizeUnicode)(r))));let i=new Set(e.map(r=>Yh(r)).reduce((r,n)=>r.concat(n)));this.#i.set(t,{dirs:i,paths:e});for(let r of e){let n=this.#e.get(r);n?n.push(t):this.#e.set(r,[t])}for(let r of i){let n=this.#e.get(r);if(!n)this.#e.set(r,[new Set([t])]);else{let o=n.at(-1);o instanceof Set?o.add(t):n.push(new Set([t]))}}return this.#r(t)}#n(e){let t=this.#i.get(e);if(!t)throw new Error("function does not have any path reservations");return{paths:t.paths.map(i=>this.#e.get(i)),dirs:[...t.dirs].map(i=>this.#e.get(i))}}check(e){let{paths:t,dirs:i}=this.#n(e);return t.every(r=>r&&r[0]===e)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(e))}#r(e){return this.#s.has(e)||!this.check(e)?!1:(this.#s.add(e),e(()=>this.#t(e)),!0)}#t(e){if(!this.#s.has(e))return!1;let t=this.#i.get(e);if(!t)throw new Error("invalid reservation");let{paths:i,dirs:r}=t,n=new Set;for(let o of i){let a=this.#e.get(o);if(!a||a?.[0]!==e)continue;let h=a[1];if(!h){this.#e.delete(o);continue}if(a.shift(),typeof h=="function")n.add(h);else for(let l of h)n.add(l)}for(let o of r){let a=this.#e.get(o),h=a?.[0];if(!(!a||!(h instanceof Set)))if(h.size===1&&a.length===1){this.#e.delete(o);continue}else if(h.size===1){a.shift();let l=a[0];typeof l=="function"&&n.add(l)}else h.delete(e)}return this.#s.delete(e),n.forEach(o=>this.#r(o)),!0}};Wi.PathReservations=gr});var Jn=d(Hi=>{"use strict";Object.defineProperty(Hi,"__esModule",{value:!0});Hi.umask=void 0;var Kh=()=>process.umask();Hi.umask=Kh});var Ir=d(z=>{"use strict";var Vh=z&&z.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),$h=z&&z.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),lo=z&&z.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{if(!Bt)return m.default.unlink(s,e);let t=s+".DELETE."+(0,uo.randomBytes)(16).toString("hex");m.default.rename(s,t,i=>{if(i)return e(i);m.default.unlink(t,e)})},ol=s=>{if(!Bt)return m.default.unlinkSync(s);let e=s+".DELETE."+(0,uo.randomBytes)(16).toString("hex");m.default.renameSync(s,e),m.default.unlinkSync(e)},ho=(s,e,t)=>s!==void 0&&s===s>>>0?s:e!==void 0&&e===e>>>0?e:t,Yi=class extends Jh.Parser{[Rr]=!1;[Ct]=!1;[Zi]=0;reservations=new tl.PathReservations;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(e={}){if(e.ondone=()=>{this[Rr]=!0,this[vr]()},super(e),this.transform=e.transform,this.chmod=!!e.chmod,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=e.preserveOwner===void 0&&typeof e.uid!="number"?!!(process.getuid&&process.getuid()===0):!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof e.maxDepth=="number"?e.maxDepth:rl,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||Bt,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=(0,U.normalizeWindowsPath)(g.default.resolve(e.cwd||process.cwd())),this.strip=Number(e.strip)||0,this.processUmask=this.chmod?typeof e.processUmask=="number"?e.processUmask:(0,il.umask)():0,this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",t=>this[to](t))}warn(e,t,i={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(i.recoverable=!1),super.warn(e,t,i)}[vr](){this[Rr]&&this[Zi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[Or](e,t){let i=e[t],{type:r}=e;if(!i||this.preservePaths)return!0;let[n,o]=(0,el.stripAbsolutePath)(i),a=o.replaceAll(/\\/g,"/").split("/");if(a.includes("..")||Bt&&/^[a-z]:\.\.$/i.test(a[0]??"")){if(t==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${t} contains '..'`,{entry:e,[t]:i}),!1;let h=g.default.posix.dirname(e.path),l=g.default.posix.normalize(g.default.posix.join(h,a.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${t} escapes extraction directory`,{entry:e,[t]:i}),!1}return n&&(e[t]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${t}`,{entry:e,[t]:i})),!0}[oo](e){let t=(0,U.normalizeWindowsPath)(e.path),i=t.split("/");if(this.strip){if(i.length=this.strip)e.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),e.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:e,path:t,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[Or](e,"path")||!this[Or](e,"linkpath"))return!1;if(e.absolute=g.default.isAbsolute(e.path)?(0,U.normalizeWindowsPath)(g.default.resolve(e.path)):(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,e.path)),!this.preservePaths&&typeof e.absolute=="string"&&e.absolute.indexOf(this.cwd+"/")!==0&&e.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:e,path:(0,U.normalizeWindowsPath)(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!=="Directory"&&e.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=g.default.win32.parse(String(e.absolute));e.absolute=r+eo.encode(String(e.absolute).slice(r.length));let{root:n}=g.default.win32.parse(e.path);e.path=n+eo.encode(e.path.slice(n.length))}return!0}[to](e){if(!this[oo](e))return e.resume();switch(Qh.default.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[Tr](e);default:return this[no](e)}}[T](e,t){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:t}),this[ut](),t.resume())}[Pe](e,t,i){(0,fo.mkdir)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t},i)}[At](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[It](e){return ho(this.uid,e.uid,this.processUid)}[Ft](e){return ho(this.gid,e.gid,this.processGid)}[Pr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=new Xh.WriteStream(String(e.absolute),{flags:(0,co.getWriteFlag)(e.size),mode:i,autoClose:!1});r.on("error",h=>{r.fd&&m.default.close(r.fd,()=>{}),r.write=()=>!0,this[T](h,e),t()});let n=1,o=h=>{if(h){r.fd&&m.default.close(r.fd,()=>{}),this[T](h,e),t();return}--n===0&&r.fd!==void 0&&m.default.close(r.fd,l=>{l?this[T](l,e):this[ut](),t()})};r.on("finish",()=>{let h=String(e.absolute),l=r.fd;if(typeof l=="number"&&e.mtime&&!this.noMtime){n++;let u=e.atime||new Date,c=e.mtime;m.default.futimes(l,u,c,E=>E?m.default.utimes(h,u,c,D=>o(D&&E)):o())}if(typeof l=="number"&&this[At](e)){n++;let u=this[It](e),c=this[Ft](e);typeof u=="number"&&typeof c=="number"&&m.default.fchown(l,u,c,E=>E?m.default.chown(h,u,c,D=>o(D&&E)):o())}o()});let a=this.transform&&this.transform(e)||e;a!==e&&(a.on("error",h=>{this[T](h,e),t()}),e.pipe(a)),a.pipe(r)}[Nr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode;this[Pe](String(e.absolute),i,r=>{if(r){this[T](r,e),t();return}let n=1,o=()=>{--n===0&&(t(),this[ut](),e.resume())};e.mtime&&!this.noMtime&&(n++,m.default.utimes(String(e.absolute),e.atime||new Date,e.mtime,o)),this[At](e)&&(n++,m.default.chown(String(e.absolute),Number(this[It](e)),Number(this[Ft](e)),o)),o()})}[no](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[so](e,t){let i=(0,U.normalizeWindowsPath)(g.default.relative(this.cwd,g.default.resolve(g.default.dirname(String(e.absolute)),String(e.linkpath)))).split("/");this[Lt](e,this.cwd,i,()=>this[Gi](e,String(e.linkpath),"symlink",t),r=>{this[T](r,e),t()})}[ro](e,t){let i=(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,String(e.linkpath))),r=(0,U.normalizeWindowsPath)(String(e.linkpath)).split("/");this[Lt](e,this.cwd,r,()=>this[Gi](e,i,"link",t),n=>{this[T](n,e),t()})}[Lt](e,t,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let a=g.default.resolve(t,o);m.default.lstat(a,(h,l)=>{if(h)return r();if(l?.isSymbolicLink())return n(new mo.SymlinkError(a,g.default.resolve(a,i.join("/"))));this[Lt](e,a,i,r,n)})}[ao](){this[Zi]++}[ut](){this[Zi]--,this[vr]()}[Mr](e){this[ut](),e.resume()}[Dr](e,t){return e.type==="File"&&!this.unlink&&t.isFile()&&t.nlink<=1&&!Bt}[Tr](e){this[ao]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,i=>this[io](e,i))}[io](e,t){let i=a=>{t(a)},r=()=>{this[Pe](this.cwd,this.dmode,a=>{if(a){this[T](a,e),i();return}this[Ct]=!0,n()})},n=()=>{if(e.absolute!==this.cwd){let a=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(a!==this.cwd)return this[Pe](a,this.dmode,h=>{if(h){this[T](h,e),i();return}o()})}o()},o=()=>{m.default.lstat(String(e.absolute),(a,h)=>{if(h&&(this.keep||this.newer&&h.mtime>(e.mtime??h.mtime))){this[Mr](e),i();return}if(a||this[Dr](e,h))return this[Z](null,e,i);if(h.isDirectory()){if(e.type==="Directory"){let l=this.chmod&&e.mode&&(h.mode&4095)!==e.mode,u=c=>this[Z](c??null,e,i);return l?m.default.chmod(String(e.absolute),Number(e.mode),u):u()}if(e.absolute!==this.cwd)return m.default.rmdir(String(e.absolute),l=>this[Z](l??null,e,i))}if(e.absolute===this.cwd)return this[Z](null,e,i);nl(String(e.absolute),l=>this[Z](l??null,e,i))})};this[Ct]?n():r()}[Z](e,t,i){if(e){this[T](e,t),i();return}switch(t.type){case"File":case"OldFile":case"ContiguousFile":return this[Pr](t,i);case"Link":return this[ro](t,i);case"SymbolicLink":return this[so](t,i);case"Directory":case"GNUDumpDir":return this[Nr](t,i)}}[Gi](e,t,i,r){m.default[i](t,String(e.absolute),n=>{n?this[T](n,e):(this[ut](),e.resume()),r()})}};z.Unpack=Yi;var Mt=s=>{try{return[null,s()]}catch(e){return[e,null]}},Lr=class extends Yi{sync=!0;[Z](e,t){return super[Z](e,t,()=>{})}[Tr](e){if(!this[Ct]){let n=this[Pe](this.cwd,this.dmode);if(n)return this[T](n,e);this[Ct]=!0}if(e.absolute!==this.cwd){let n=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(n!==this.cwd){let o=this[Pe](n,this.dmode);if(o)return this[T](o,e)}}let[t,i]=Mt(()=>m.default.lstatSync(String(e.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(e.mtime??i.mtime)))return this[Mr](e);if(t||this[Dr](e,i))return this[Z](null,e);if(i.isDirectory()){if(e.type==="Directory"){let o=this.chmod&&e.mode&&(i.mode&4095)!==e.mode,[a]=o?Mt(()=>{m.default.chmodSync(String(e.absolute),Number(e.mode))}):[];return this[Z](a,e)}let[n]=Mt(()=>m.default.rmdirSync(String(e.absolute)));this[Z](n,e)}let[r]=e.absolute===this.cwd?[]:Mt(()=>ol(String(e.absolute)));this[Z](r,e)}[Pr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=a=>{let h;try{m.default.closeSync(n)}catch(l){h=l}(a||h)&&this[T](a||h,e),t()},n;try{n=m.default.openSync(String(e.absolute),(0,co.getWriteFlag)(e.size),i)}catch(a){return r(a)}let o=this.transform&&this.transform(e)||e;o!==e&&(o.on("error",a=>this[T](a,e)),e.pipe(o)),o.on("data",a=>{try{m.default.writeSync(n,a,0,a.length)}catch(h){r(h)}}),o.on("end",()=>{let a=null;if(e.mtime&&!this.noMtime){let h=e.atime||new Date,l=e.mtime;try{m.default.futimesSync(n,h,l)}catch(u){try{m.default.utimesSync(String(e.absolute),h,l)}catch{a=u}}}if(this[At](e)){let h=this[It](e),l=this[Ft](e);try{m.default.fchownSync(n,Number(h),Number(l))}catch(u){try{m.default.chownSync(String(e.absolute),Number(h),Number(l))}catch{a=a||u}}}r(a)})}[Nr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode,r=this[Pe](String(e.absolute),i);if(r){this[T](r,e),t();return}if(e.mtime&&!this.noMtime)try{m.default.utimesSync(String(e.absolute),e.atime||new Date,e.mtime)}catch{}if(this[At](e))try{m.default.chownSync(String(e.absolute),Number(this[It](e)),Number(this[Ft](e)))}catch{}t(),e.resume()}[Pe](e,t){try{return(0,fo.mkdirSync)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t})}catch(i){return i}}[Lt](e,t,i,r,n){if(this.preservePaths||i.length===0)return r();let o=t;for(let a of i){o=g.default.resolve(o,a);let[h,l]=Mt(()=>m.default.lstatSync(o));if(h)return r();if(l.isSymbolicLink())return n(new mo.SymlinkError(o,g.default.resolve(t,i.join("/"))))}r()}[Gi](e,t,i,r){let n=`${i}Sync`;try{m.default[n](t,String(e.absolute)),r(),e.resume()}catch(o){return this[T](o,e)}}};z.UnpackSync=Lr});var Fr=d(G=>{"use strict";var al=G&&G.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),hl=G&&G.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ll=G&&G.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=new Ki.UnpackSync(s),t=s.file,i=_o.default.statSync(t),r=s.maxReadSize||16*1024*1024;new po.ReadStreamSync(t,{readSize:r,size:i.size}).pipe(e)},ml=(s,e)=>{let t=new Ki.Unpack(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{t.on("error",a),t.on("close",o),_o.default.stat(r,(h,l)=>{if(h)a(h);else{let u=new po.ReadStream(r,{readSize:i,size:l.size});u.on("error",a),u.pipe(t)}})})};G.extract=(0,fl.makeCommand)(dl,ml,s=>new Ki.UnpackSync(s),s=>new Ki.Unpack(s),(s,e)=>{e?.length&&(0,cl.filesFilter)(s,e)})});var Vi=d(ct=>{"use strict";var wo=ct&&ct.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ct,"__esModule",{value:!0});ct.replace=void 0;var yo=Ke(),q=wo(require("node:fs")),Eo=wo(require("node:path")),bo=Je(),So=st(),pl=Ve(),_l=Xt(),go=Ci(),wl=(s,e)=>{let t=new go.PackSync(s),i=!0,r,n;try{try{r=q.default.openSync(s.file,"r+")}catch(h){if(h?.code==="ENOENT")r=q.default.openSync(s.file,"w+");else throw h}let o=q.default.fstatSync(r),a=Buffer.alloc(512);e:for(n=0;no.size)break;n+=l,s.mtimeCache&&h.mtime&&s.mtimeCache.set(String(h.path),h.mtime)}i=!1,yl(s,t,n,r,e)}finally{if(i)try{q.default.closeSync(r)}catch{}}},yl=(s,e,t,i,r)=>{let n=new yo.WriteStreamSync(s.file,{fd:i,start:t});e.pipe(n),bl(e,r)},El=(s,e)=>{e=Array.from(e);let t=new go.Pack(s),i=(n,o,a)=>{let h=(D,A)=>{D?q.default.close(n,w=>a(D)):a(null,A)},l=0;if(o===0)return h(null,0);let u=0,c=Buffer.alloc(512),E=(D,A)=>{if(D||A===void 0)return h(D);if(u+=A,u<512&&A)return q.default.read(n,c,u,c.length-u,l+u,E);if(l===0&&c[0]===31&&c[1]===139)return h(new Error("cannot append to compressed archives"));if(u<512)return h(null,l);let w=new bo.Header(c);if(!w.cksumValid)return h(null,l);let P=512*Math.ceil((w.size??0)/512);if(l+P+512>o||(l+=P+512,l>=o))return h(null,l);s.mtimeCache&&w.mtime&&s.mtimeCache.set(String(w.path),w.mtime),u=0,q.default.read(n,c,0,512,l,E)};q.default.read(n,c,0,512,l,E)};return new Promise((n,o)=>{t.on("error",o);let a="r+",h=(l,u)=>{if(l&&l.code==="ENOENT"&&a==="r+")return a="w+",q.default.open(s.file,a,h);if(l||!u)return o(l);q.default.fstat(u,(c,E)=>{if(c)return q.default.close(u,()=>o(c));i(u,E.size,(D,A)=>{if(D)return o(D);let w=new yo.WriteStream(s.file,{fd:u,start:A});t.pipe(w),w.on("error",o),w.on("close",n),Sl(t,e)})})};q.default.open(s.file,a,h)})},bl=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,So.list)({file:Eo.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},Sl=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,So.list)({file:Eo.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t);s.end()};ct.replace=(0,pl.makeCommand)(wl,El,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,e)=>{if(!(0,_l.isFile)(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!e?.length)throw new TypeError("no paths specified to add/replace")})});var Cr=d($i=>{"use strict";Object.defineProperty($i,"__esModule",{value:!0});$i.update=void 0;var gl=Ve(),zt=Vi();$i.update=(0,gl.makeCommand)(zt.replace.syncFile,zt.replace.asyncFile,zt.replace.syncNoFile,zt.replace.asyncNoFile,(s,e=[])=>{zt.replace.validate?.(s,e),Ol(s)});var Ol=s=>{let e=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=e?(t,i)=>e(t,i)&&!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0)):(t,i)=>!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0))}});var Oo=exports&&exports.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Rl=exports&&exports.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Y=exports&&exports.__exportStar||function(s,e){for(var t in s)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&Oo(e,s,t)},vl=exports&&exports.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r 1 && - job === this[CURRENT] && !this.linkCache.get(`${stat.dev}:${stat.ino}`) && !this.sync) { - // if it's not filtered, and it's a new File entry, - // jump the queue in case any pending Link entries are about - // to try to link to it. This prevents a hardlink from coming ahead - // of its target in the archive. - this[PROCESSJOB](job); + // if it's not filtered, and it's a new File entry, and next anyway + // process right away in case any pending Link entries are about + // to try to link to it. + if (job === this[CURRENT]) { + this[PROCESSJOB](job); + } + else { + // if it's NOT the current entry, it needs to be deferred, + // so that the link target can be built first. + const key = `${stat.dev}:${stat.ino}`; + const pending = this[PENDINGLINKS].get(key); + if (pending) + pending.push(job); + else + this[PENDINGLINKS].set(key, [job]); + job.pendingLink = true; + job.pending = true; + } } this[PROCESS](); } @@ -334,12 +349,31 @@ class Pack extends minipass_1.Minipass { get [CURRENT]() { return this[QUEUE] && this[QUEUE].head && this[QUEUE].head.value; } - [JOBDONE](_job) { + [JOBDONE](job) { this[QUEUE].shift(); this[JOBS] -= 1; + const { stat } = job; + if (stat && stat.isFile() && stat.nlink > 1) { + // might be a file with pending links + const key = `${stat.dev}:${stat.ino}`; + const pending = this[PENDINGLINKS].get(key); + if (pending) { + this[PENDINGLINKS].delete(key); + for (const job of pending) { + job.pending = false; + this[PROCESSJOB](job); + } + } + } this[PROCESS](); } [PROCESSJOB](job) { + if (job.pending && job.pendingLink && job === this[CURRENT]) { + // At least one of the links to this file are not being included + // in the tarball, so we need to just proceed. + job.pending = false; + job.pendingLink = false; + } if (job.pending) { return; } diff --git a/deps/npm/node_modules/tar/dist/esm/index.min.js b/deps/npm/node_modules/tar/dist/esm/index.min.js index 8b043cabab710d..89c9806c4bd74e 100644 --- a/deps/npm/node_modules/tar/dist/esm/index.min.js +++ b/deps/npm/node_modules/tar/dist/esm/index.min.js @@ -1,4 +1,4 @@ -var kr=Object.defineProperty;var vr=(s,t)=>{for(var e in t)kr(s,e,{get:t[e],enumerable:!0})};import Kr from"events";import I from"fs";import{EventEmitter as Ti}from"node:events";import Ns from"node:stream";import{StringDecoder as Mr}from"node:string_decoder";var Os=typeof process=="object"&&process?process:{stdout:null,stderr:null},Br=s=>!!s&&typeof s=="object"&&(s instanceof D||s instanceof Ns||Pr(s)||zr(s)),Pr=s=>!!s&&typeof s=="object"&&s instanceof Ti&&typeof s.pipe=="function"&&s.pipe!==Ns.Writable.prototype.pipe,zr=s=>!!s&&typeof s=="object"&&s instanceof Ti&&typeof s.write=="function"&&typeof s.end=="function",q=Symbol("EOF"),j=Symbol("maybeEmitEnd"),rt=Symbol("emittedEnd"),Le=Symbol("emittingEnd"),jt=Symbol("emittedError"),Ne=Symbol("closed"),Ts=Symbol("read"),Ae=Symbol("flush"),xs=Symbol("flushChunk"),z=Symbol("encoding"),Mt=Symbol("decoder"),b=Symbol("flowing"),Qt=Symbol("paused"),Bt=Symbol("resume"),_=Symbol("buffer"),A=Symbol("pipes"),g=Symbol("bufferLength"),yi=Symbol("bufferPush"),De=Symbol("bufferShift"),L=Symbol("objectMode"),w=Symbol("destroyed"),Ri=Symbol("error"),bi=Symbol("emitData"),Ls=Symbol("emitEnd"),_i=Symbol("emitEnd2"),Z=Symbol("async"),gi=Symbol("abort"),Ie=Symbol("aborted"),Jt=Symbol("signal"),yt=Symbol("dataListeners"),C=Symbol("discarded"),te=s=>Promise.resolve().then(s),Ur=s=>s(),Hr=s=>s==="end"||s==="finish"||s==="prefinish",Wr=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Gr=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Ce=class{src;dest;opts;ondrain;constructor(t,e,i){this.src=t,this.dest=e,this.opts=i,this.ondrain=()=>t[Bt](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(t){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},Oi=class extends Ce{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(t,e,i){super(t,e,i),this.proxyErrors=r=>this.dest.emit("error",r),t.on("error",this.proxyErrors)}},Zr=s=>!!s.objectMode,Yr=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",D=class extends Ti{[b]=!1;[Qt]=!1;[A]=[];[_]=[];[L];[z];[Z];[Mt];[q]=!1;[rt]=!1;[Le]=!1;[Ne]=!1;[jt]=null;[g]=0;[w]=!1;[Jt];[Ie]=!1;[yt]=0;[C]=!1;writable=!0;readable=!0;constructor(...t){let e=t[0]||{};if(super(),e.objectMode&&typeof e.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Zr(e)?(this[L]=!0,this[z]=null):Yr(e)?(this[z]=e.encoding,this[L]=!1):(this[L]=!1,this[z]=null),this[Z]=!!e.async,this[Mt]=this[z]?new Mr(this[z]):null,e&&e.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[_]}),e&&e.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[A]});let{signal:i}=e;i&&(this[Jt]=i,i.aborted?this[gi]():i.addEventListener("abort",()=>this[gi]()))}get bufferLength(){return this[g]}get encoding(){return this[z]}set encoding(t){throw new Error("Encoding must be set at instantiation time")}setEncoding(t){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[L]}set objectMode(t){throw new Error("objectMode must be set at instantiation time")}get async(){return this[Z]}set async(t){this[Z]=this[Z]||!!t}[gi](){this[Ie]=!0,this.emit("abort",this[Jt]?.reason),this.destroy(this[Jt]?.reason)}get aborted(){return this[Ie]}set aborted(t){}write(t,e,i){if(this[Ie])return!1;if(this[q])throw new Error("write after end");if(this[w])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof e=="function"&&(i=e,e="utf8"),e||(e="utf8");let r=this[Z]?te:Ur;if(!this[L]&&!Buffer.isBuffer(t)){if(Gr(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(Wr(t))t=Buffer.from(t);else if(typeof t!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[L]?(this[b]&&this[g]!==0&&this[Ae](!0),this[b]?this.emit("data",t):this[yi](t),this[g]!==0&&this.emit("readable"),i&&r(i),this[b]):t.length?(typeof t=="string"&&!(e===this[z]&&!this[Mt]?.lastNeed)&&(t=Buffer.from(t,e)),Buffer.isBuffer(t)&&this[z]&&(t=this[Mt].write(t)),this[b]&&this[g]!==0&&this[Ae](!0),this[b]?this.emit("data",t):this[yi](t),this[g]!==0&&this.emit("readable"),i&&r(i),this[b]):(this[g]!==0&&this.emit("readable"),i&&r(i),this[b])}read(t){if(this[w])return null;if(this[C]=!1,this[g]===0||t===0||t&&t>this[g])return this[j](),null;this[L]&&(t=null),this[_].length>1&&!this[L]&&(this[_]=[this[z]?this[_].join(""):Buffer.concat(this[_],this[g])]);let e=this[Ts](t||null,this[_][0]);return this[j](),e}[Ts](t,e){if(this[L])this[De]();else{let i=e;t===i.length||t===null?this[De]():typeof i=="string"?(this[_][0]=i.slice(t),e=i.slice(0,t),this[g]-=t):(this[_][0]=i.subarray(t),e=i.subarray(0,t),this[g]-=t)}return this.emit("data",e),!this[_].length&&!this[q]&&this.emit("drain"),e}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e="utf8"),t!==void 0&&this.write(t,e),i&&this.once("end",i),this[q]=!0,this.writable=!1,(this[b]||!this[Qt])&&this[j](),this}[Bt](){this[w]||(!this[yt]&&!this[A].length&&(this[C]=!0),this[Qt]=!1,this[b]=!0,this.emit("resume"),this[_].length?this[Ae]():this[q]?this[j]():this.emit("drain"))}resume(){return this[Bt]()}pause(){this[b]=!1,this[Qt]=!0,this[C]=!1}get destroyed(){return this[w]}get flowing(){return this[b]}get paused(){return this[Qt]}[yi](t){this[L]?this[g]+=1:this[g]+=t.length,this[_].push(t)}[De](){return this[L]?this[g]-=1:this[g]-=this[_][0].length,this[_].shift()}[Ae](t=!1){do;while(this[xs](this[De]())&&this[_].length);!t&&!this[_].length&&!this[q]&&this.emit("drain")}[xs](t){return this.emit("data",t),this[b]}pipe(t,e){if(this[w])return t;this[C]=!1;let i=this[rt];return e=e||{},t===Os.stdout||t===Os.stderr?e.end=!1:e.end=e.end!==!1,e.proxyErrors=!!e.proxyErrors,i?e.end&&t.end():(this[A].push(e.proxyErrors?new Oi(this,t,e):new Ce(this,t,e)),this[Z]?te(()=>this[Bt]()):this[Bt]()),t}unpipe(t){let e=this[A].find(i=>i.dest===t);e&&(this[A].length===1?(this[b]&&this[yt]===0&&(this[b]=!1),this[A]=[]):this[A].splice(this[A].indexOf(e),1),e.unpipe())}addListener(t,e){return this.on(t,e)}on(t,e){let i=super.on(t,e);if(t==="data")this[C]=!1,this[yt]++,!this[A].length&&!this[b]&&this[Bt]();else if(t==="readable"&&this[g]!==0)super.emit("readable");else if(Hr(t)&&this[rt])super.emit(t),this.removeAllListeners(t);else if(t==="error"&&this[jt]){let r=e;this[Z]?te(()=>r.call(this,this[jt])):r.call(this,this[jt])}return i}removeListener(t,e){return this.off(t,e)}off(t,e){let i=super.off(t,e);return t==="data"&&(this[yt]=this.listeners("data").length,this[yt]===0&&!this[C]&&!this[A].length&&(this[b]=!1)),i}removeAllListeners(t){let e=super.removeAllListeners(t);return(t==="data"||t===void 0)&&(this[yt]=0,!this[C]&&!this[A].length&&(this[b]=!1)),e}get emittedEnd(){return this[rt]}[j](){!this[Le]&&!this[rt]&&!this[w]&&this[_].length===0&&this[q]&&(this[Le]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[Ne]&&this.emit("close"),this[Le]=!1)}emit(t,...e){let i=e[0];if(t!=="error"&&t!=="close"&&t!==w&&this[w])return!1;if(t==="data")return!this[L]&&!i?!1:this[Z]?(te(()=>this[bi](i)),!0):this[bi](i);if(t==="end")return this[Ls]();if(t==="close"){if(this[Ne]=!0,!this[rt]&&!this[w])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(t==="error"){this[jt]=i,super.emit(Ri,i);let n=!this[Jt]||this.listeners("error").length?super.emit("error",i):!1;return this[j](),n}else if(t==="resume"){let n=super.emit("resume");return this[j](),n}else if(t==="finish"||t==="prefinish"){let n=super.emit(t);return this.removeAllListeners(t),n}let r=super.emit(t,...e);return this[j](),r}[bi](t){for(let i of this[A])i.dest.write(t)===!1&&this.pause();let e=this[C]?!1:super.emit("data",t);return this[j](),e}[Ls](){return this[rt]?!1:(this[rt]=!0,this.readable=!1,this[Z]?(te(()=>this[_i]()),!0):this[_i]())}[_i](){if(this[Mt]){let e=this[Mt].end();if(e){for(let i of this[A])i.dest.write(e);this[C]||super.emit("data",e)}}for(let e of this[A])e.end();let t=super.emit("end");return this.removeAllListeners("end"),t}async collect(){let t=Object.assign([],{dataLength:0});this[L]||(t.dataLength=0);let e=this.promise();return this.on("data",i=>{t.push(i),this[L]||(t.dataLength+=i.length)}),await e,t}async concat(){if(this[L])throw new Error("cannot concat in objectMode");let t=await this.collect();return this[z]?t.join(""):Buffer.concat(t,t.dataLength)}async promise(){return new Promise((t,e)=>{this.on(w,()=>e(new Error("stream destroyed"))),this.on("error",i=>e(i)),this.on("end",()=>t())})}[Symbol.asyncIterator](){this[C]=!1;let t=!1,e=async()=>(this.pause(),t=!0,{value:void 0,done:!0});return{next:()=>{if(t)return e();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[q])return e();let n,o,h=d=>{this.off("data",a),this.off("end",l),this.off(w,c),e(),o(d)},a=d=>{this.off("error",h),this.off("end",l),this.off(w,c),this.pause(),n({value:d,done:!!this[q]})},l=()=>{this.off("error",h),this.off("data",a),this.off(w,c),e(),n({done:!0,value:void 0})},c=()=>h(new Error("stream destroyed"));return new Promise((d,S)=>{o=S,n=d,this.once(w,c),this.once("error",h),this.once("end",l),this.once("data",a)})},throw:e,return:e,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[C]=!1;let t=!1,e=()=>(this.pause(),this.off(Ri,e),this.off(w,e),this.off("end",e),t=!0,{done:!0,value:void 0}),i=()=>{if(t)return e();let r=this.read();return r===null?e():{done:!1,value:r}};return this.once("end",e),this.once(Ri,e),this.once(w,e),{next:i,throw:e,return:e,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(t){if(this[w])return t?this.emit("error",t):this.emit(w),this;this[w]=!0,this[C]=!0,this[_].length=0,this[g]=0;let e=this;return typeof e.close=="function"&&!this[Ne]&&e.close(),t?this.emit("error",t):this.emit(w),this}static get isStream(){return Br}};var Vr=I.writev,ot=Symbol("_autoClose"),H=Symbol("_close"),ee=Symbol("_ended"),m=Symbol("_fd"),xi=Symbol("_finished"),J=Symbol("_flags"),Li=Symbol("_flush"),Ii=Symbol("_handleChunk"),Ci=Symbol("_makeBuf"),se=Symbol("_mode"),Fe=Symbol("_needDrain"),Ut=Symbol("_onerror"),Ht=Symbol("_onopen"),Ni=Symbol("_onread"),Pt=Symbol("_onwrite"),ht=Symbol("_open"),U=Symbol("_path"),nt=Symbol("_pos"),Y=Symbol("_queue"),zt=Symbol("_read"),Ai=Symbol("_readSize"),Q=Symbol("_reading"),ie=Symbol("_remain"),Di=Symbol("_size"),ke=Symbol("_write"),Rt=Symbol("_writing"),ve=Symbol("_defaultFlag"),bt=Symbol("_errored"),_t=class extends D{[bt]=!1;[m];[U];[Ai];[Q]=!1;[Di];[ie];[ot];constructor(t,e){if(e=e||{},super(e),this.readable=!0,this.writable=!1,typeof t!="string")throw new TypeError("path must be a string");this[bt]=!1,this[m]=typeof e.fd=="number"?e.fd:void 0,this[U]=t,this[Ai]=e.readSize||16*1024*1024,this[Q]=!1,this[Di]=typeof e.size=="number"?e.size:1/0,this[ie]=this[Di],this[ot]=typeof e.autoClose=="boolean"?e.autoClose:!0,typeof this[m]=="number"?this[zt]():this[ht]()}get fd(){return this[m]}get path(){return this[U]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[ht](){I.open(this[U],"r",(t,e)=>this[Ht](t,e))}[Ht](t,e){t?this[Ut](t):(this[m]=e,this.emit("open",e),this[zt]())}[Ci](){return Buffer.allocUnsafe(Math.min(this[Ai],this[ie]))}[zt](){if(!this[Q]){this[Q]=!0;let t=this[Ci]();if(t.length===0)return process.nextTick(()=>this[Ni](null,0,t));I.read(this[m],t,0,t.length,null,(e,i,r)=>this[Ni](e,i,r))}}[Ni](t,e,i){this[Q]=!1,t?this[Ut](t):this[Ii](e,i)&&this[zt]()}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}[Ut](t){this[Q]=!0,this[H](),this.emit("error",t)}[Ii](t,e){let i=!1;return this[ie]-=t,t>0&&(i=super.write(tthis[Ht](t,e))}[Ht](t,e){this[ve]&&this[J]==="r+"&&t&&t.code==="ENOENT"?(this[J]="w",this[ht]()):t?this[Ut](t):(this[m]=e,this.emit("open",e),this[Rt]||this[Li]())}end(t,e){return t&&this.write(t,e),this[ee]=!0,!this[Rt]&&!this[Y].length&&typeof this[m]=="number"&&this[Pt](null,0),this}write(t,e){return typeof t=="string"&&(t=Buffer.from(t,e)),this[ee]?(this.emit("error",new Error("write() after end()")),!1):this[m]===void 0||this[Rt]||this[Y].length?(this[Y].push(t),this[Fe]=!0,!1):(this[Rt]=!0,this[ke](t),!0)}[ke](t){I.write(this[m],t,0,t.length,this[nt],(e,i)=>this[Pt](e,i))}[Pt](t,e){t?this[Ut](t):(this[nt]!==void 0&&typeof e=="number"&&(this[nt]+=e),this[Y].length?this[Li]():(this[Rt]=!1,this[ee]&&!this[xi]?(this[xi]=!0,this[H](),this.emit("finish")):this[Fe]&&(this[Fe]=!1,this.emit("drain"))))}[Li](){if(this[Y].length===0)this[ee]&&this[Pt](null,0);else if(this[Y].length===1)this[ke](this[Y].pop());else{let t=this[Y];this[Y]=[],Vr(this[m],t,this[nt],(e,i)=>this[Pt](e,i))}}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}},Wt=class extends tt{[ht](){let t;if(this[ve]&&this[J]==="r+")try{t=I.openSync(this[U],this[J],this[se])}catch(e){if(e?.code==="ENOENT")return this[J]="w",this[ht]();throw e}else t=I.openSync(this[U],this[J],this[se]);this[Ht](null,t)}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.closeSync(t),this.emit("close")}}[ke](t){let e=!0;try{this[Pt](null,I.writeSync(this[m],t,0,t.length,this[nt])),e=!1}finally{if(e)try{this[H]()}catch{}}}};import nr from"node:path";import Vt from"node:fs";import{dirname as xn,parse as Ln}from"path";var $r=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),As=s=>!!s.sync&&!!s.file,Ds=s=>!s.sync&&!!s.file,Is=s=>!!s.sync&&!s.file,Cs=s=>!s.sync&&!s.file;var Fs=s=>!!s.file;var Xr=s=>{let t=$r.get(s);return t||s},re=(s={})=>{if(!s)return{};let t={};for(let[e,i]of Object.entries(s)){let r=Xr(e);t[r]=i}return t.chmod===void 0&&t.noChmod===!1&&(t.chmod=!0),delete t.noChmod,t};var K=(s,t,e,i,r)=>Object.assign((n=[],o,h)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(h=o,o=void 0),o=o?Array.from(o):[];let a=re(n);if(r?.(a,o),As(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return s(a,o)}else if(Ds(a)){let l=t(a,o);return h?l.then(()=>h(),h):l}else if(Is(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return e(a,o)}else if(Cs(a)){if(typeof h=="function")throw new TypeError("callback only supported with file option");return i(a,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:t,syncNoFile:e,asyncNoFile:i,validate:r});import{EventEmitter as _n}from"events";import Mi from"assert";import{Buffer as gt}from"buffer";import*as ks from"zlib";import qr from"zlib";var jr=qr.constants||{ZLIB_VERNUM:4736},M=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},jr));var Qr=gt.concat,vs=Object.getOwnPropertyDescriptor(gt,"concat"),Jr=s=>s,ki=vs?.writable===!0||vs?.set!==void 0?s=>{gt.concat=s?Jr:Qr}:s=>{},Ot=Symbol("_superWrite"),Gt=class extends Error{code;errno;constructor(t,e){super("zlib: "+t.message,{cause:t}),this.code=t.code,this.errno=t.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+t.message,Error.captureStackTrace(this,e??this.constructor)}get name(){return"ZlibError"}},vi=Symbol("flushFlag"),ne=class extends D{#t=!1;#i=!1;#s;#n;#r;#e;#o;get sawError(){return this.#t}get handle(){return this.#e}get flushFlag(){return this.#s}constructor(t,e){if(!t||typeof t!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(t),this.#s=t.flush??0,this.#n=t.finishFlush??0,this.#r=t.fullFlushFlag??0,typeof ks[e]!="function")throw new TypeError("Compression method not supported: "+e);try{this.#e=new ks[e](t)}catch(i){throw new Gt(i,this.constructor)}this.#o=i=>{this.#t||(this.#t=!0,this.close(),this.emit("error",i))},this.#e?.on("error",i=>this.#o(new Gt(i))),this.once("end",()=>this.close)}close(){this.#e&&(this.#e.close(),this.#e=void 0,this.emit("close"))}reset(){if(!this.#t)return Mi(this.#e,"zlib binding closed"),this.#e.reset?.()}flush(t){this.ended||(typeof t!="number"&&(t=this.#r),this.write(Object.assign(gt.alloc(0),{[vi]:t})))}end(t,e,i){return typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&(e?this.write(t,e):this.write(t)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Ot](t){return super.write(t)}write(t,e,i){if(typeof e=="function"&&(i=e,e="utf8"),typeof t=="string"&&(t=gt.from(t,e)),this.#t)return;Mi(this.#e,"zlib binding closed");let r=this.#e._handle,n=r.close;r.close=()=>{};let o=this.#e.close;this.#e.close=()=>{},ki(!0);let h;try{let l=typeof t[vi]=="number"?t[vi]:this.#s;h=this.#e._processChunk(t,l),ki(!1)}catch(l){ki(!1),this.#o(new Gt(l,this.write))}finally{this.#e&&(this.#e._handle=r,r.close=n,this.#e.close=o,this.#e.removeAllListeners("error"))}this.#e&&this.#e.on("error",l=>this.#o(new Gt(l,this.write)));let a;if(h)if(Array.isArray(h)&&h.length>0){let l=h[0];a=this[Ot](gt.from(l));for(let c=1;c{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(t,e)}finally{this.handle.flush=i}this.handle&&(this.#t=t,this.#i=e)}}}};var Pe=class extends Be{#t;constructor(t){super(t,"Gzip"),this.#t=t&&!!t.portable}[Ot](t){return this.#t?(this.#t=!1,t[9]=255,super[Ot](t)):super[Ot](t)}};var ze=class extends Be{constructor(t){super(t,"Unzip")}},Ue=class extends ne{constructor(t,e){t=t||{},t.flush=t.flush||M.BROTLI_OPERATION_PROCESS,t.finishFlush=t.finishFlush||M.BROTLI_OPERATION_FINISH,t.fullFlushFlag=M.BROTLI_OPERATION_FLUSH,super(t,e)}},He=class extends Ue{constructor(t){super(t,"BrotliCompress")}},We=class extends Ue{constructor(t){super(t,"BrotliDecompress")}},Ge=class extends ne{constructor(t,e){t=t||{},t.flush=t.flush||M.ZSTD_e_continue,t.finishFlush=t.finishFlush||M.ZSTD_e_end,t.fullFlushFlag=M.ZSTD_e_flush,super(t,e)}},Ze=class extends Ge{constructor(t){super(t,"ZstdCompress")}},Ye=class extends Ge{constructor(t){super(t,"ZstdDecompress")}};import{posix as Zt}from"node:path";var Ms=(s,t)=>{if(Number.isSafeInteger(s))s<0?sn(s,t):en(s,t);else throw Error("cannot encode number outside of javascript safe integer range");return t},en=(s,t)=>{t[0]=128;for(var e=t.length;e>1;e--)t[e-1]=s&255,s=Math.floor(s/256)},sn=(s,t)=>{t[0]=255;var e=!1;s=s*-1;for(var i=t.length;i>1;i--){var r=s&255;s=Math.floor(s/256),e?t[i-1]=Ps(r):r===0?t[i-1]=0:(e=!0,t[i-1]=zs(r))}},Bs=s=>{let t=s[0],e=t===128?nn(s.subarray(1,s.length)):t===255?rn(s):null;if(e===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(e))throw Error("parsed number outside of javascript safe integer range");return e},rn=s=>{for(var t=s.length,e=0,i=!1,r=t-1;r>-1;r--){var n=Number(s[r]),o;i?o=Ps(n):n===0?o=n:(i=!0,o=zs(n)),o!==0&&(e-=o*Math.pow(256,t-r-1))}return e},nn=s=>{for(var t=s.length,e=0,i=t-1;i>-1;i--){var r=Number(s[i]);r!==0&&(e+=r*Math.pow(256,t-i-1))}return e},Ps=s=>(255^s)&255,zs=s=>(255^s)+1&255;var Bi={};vr(Bi,{code:()=>Ke,isCode:()=>oe,isName:()=>hn,name:()=>he});var oe=s=>he.has(s),hn=s=>Ke.has(s),he=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]),Ke=new Map(Array.from(he).map(s=>[s[1],s[0]]));var F=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#t="Unsupported";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(t,e=0,i,r){Buffer.isBuffer(t)?this.decode(t,e||0,i,r):t&&this.#i(t)}decode(t,e,i,r){if(e||(e=0),!t||!(t.length>=e+512))throw new Error("need 512 bytes for header");this.path=i?.path??Tt(t,e,100),this.mode=i?.mode??r?.mode??at(t,e+100,8),this.uid=i?.uid??r?.uid??at(t,e+108,8),this.gid=i?.gid??r?.gid??at(t,e+116,8),this.size=i?.size??r?.size??at(t,e+124,12),this.mtime=i?.mtime??r?.mtime??Pi(t,e+136,12),this.cksum=at(t,e+148,12),r&&this.#i(r,!0),i&&this.#i(i);let n=Tt(t,e+156,1);if(oe(n)&&(this.#t=n||"0"),this.#t==="0"&&this.path.slice(-1)==="/"&&(this.#t="5"),this.#t==="5"&&(this.size=0),this.linkpath=Tt(t,e+157,100),t.subarray(e+257,e+265).toString()==="ustar\x0000")if(this.uname=i?.uname??r?.uname??Tt(t,e+265,32),this.gname=i?.gname??r?.gname??Tt(t,e+297,32),this.devmaj=i?.devmaj??r?.devmaj??at(t,e+329,8)??0,this.devmin=i?.devmin??r?.devmin??at(t,e+337,8)??0,t[e+475]!==0){let h=Tt(t,e+345,155);this.path=h+"/"+this.path}else{let h=Tt(t,e+345,130);h&&(this.path=h+"/"+this.path),this.atime=i?.atime??r?.atime??Pi(t,e+476,12),this.ctime=i?.ctime??r?.ctime??Pi(t,e+488,12)}let o=256;for(let h=e;h!(r==null||i==="path"&&e||i==="linkpath"&&e||i==="global"))))}encode(t,e=0){if(t||(t=this.block=Buffer.alloc(512)),this.#t==="Unsupported"&&(this.#t="0"),!(t.length>=e+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=an(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=xt(t,e,100,n)||this.needPax,this.needPax=lt(t,e+100,8,this.mode)||this.needPax,this.needPax=lt(t,e+108,8,this.uid)||this.needPax,this.needPax=lt(t,e+116,8,this.gid)||this.needPax,this.needPax=lt(t,e+124,12,this.size)||this.needPax,this.needPax=zi(t,e+136,12,this.mtime)||this.needPax,t[e+156]=Number(this.#t.codePointAt(0)),this.needPax=xt(t,e+157,100,this.linkpath)||this.needPax,t.write("ustar\x0000",e+257,8),this.needPax=xt(t,e+265,32,this.uname)||this.needPax,this.needPax=xt(t,e+297,32,this.gname)||this.needPax,this.needPax=lt(t,e+329,8,this.devmaj)||this.needPax,this.needPax=lt(t,e+337,8,this.devmin)||this.needPax,this.needPax=xt(t,e+345,i,o)||this.needPax,t[e+475]!==0?this.needPax=xt(t,e+345,155,o)||this.needPax:(this.needPax=xt(t,e+345,130,o)||this.needPax,this.needPax=zi(t,e+476,12,this.atime)||this.needPax,this.needPax=zi(t,e+488,12,this.ctime)||this.needPax);let h=256;for(let a=e;a{let i=s,r="",n,o=Zt.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Zt.dirname(i),i=Zt.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=t?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=t?n=[i.slice(0,99),r,!0]:(i=Zt.join(Zt.basename(r),i),r=Zt.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},Tt=(s,t,e)=>s.subarray(t,t+e).toString("utf8").replace(/\0.*/,""),Pi=(s,t,e)=>ln(at(s,t,e)),ln=s=>s===void 0?void 0:new Date(s*1e3),at=(s,t,e)=>Number(s[t])&128?Bs(s.subarray(t,t+e)):fn(s,t,e),cn=s=>isNaN(s)?void 0:s,fn=(s,t,e)=>cn(parseInt(s.subarray(t,t+e).toString("utf8").replace(/\0.*$/,"").trim(),8)),dn={12:8589934591,8:2097151},lt=(s,t,e,i)=>i===void 0?!1:i>dn[e]||i<0?(Ms(i,s.subarray(t,t+e)),!0):(un(s,t,e,i),!1),un=(s,t,e,i)=>s.write(mn(i,e),t,e,"ascii"),mn=(s,t)=>pn(Math.floor(s).toString(8),t),pn=(s,t)=>(s.length===t-1?s:new Array(t-s.length-1).join("0")+s+" ")+"\0",zi=(s,t,e,i)=>i===void 0?!1:lt(s,t,e,i.getTime()/1e3),En=new Array(156).join("\0"),xt=(s,t,e,i)=>i===void 0?!1:(s.write(i+En,t,e,"utf8"),i.length!==Buffer.byteLength(i)||i.length>e);import{basename as wn}from"node:path";var ct=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(t,e=!1){this.atime=t.atime,this.charset=t.charset,this.comment=t.comment,this.ctime=t.ctime,this.dev=t.dev,this.gid=t.gid,this.global=e,this.gname=t.gname,this.ino=t.ino,this.linkpath=t.linkpath,this.mtime=t.mtime,this.nlink=t.nlink,this.path=t.path,this.size=t.size,this.uid=t.uid,this.uname=t.uname}encode(){let t=this.encodeBody();if(t==="")return Buffer.allocUnsafe(0);let e=Buffer.byteLength(t),i=512*Math.ceil(1+e/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new F({path:("PaxHeader/"+wn(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:e,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(t,512,e,"utf8");for(let n=e+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(t,e,i=!1){return new s(Sn(yn(t),e),i)}},Sn=(s,t)=>t?Object.assign({},t,s):s,yn=s=>s.replace(/\n$/,"").split(` -`).reduce(Rn,Object.create(null)),Rn=(s,t)=>{let e=parseInt(t,10);if(e!==Buffer.byteLength(t)+1)return s;t=t.slice((e+" ").length);let i=t.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=");return s[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(o)*1e3):/^[0-9]+$/.test(o)?+o:o,s};var bn=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,f=bn!=="win32"?s=>s:s=>s&&s.replaceAll(/\\/g,"/");var Yt=class extends D{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(t,e,i){switch(super({}),this.pause(),this.extended=e,this.globalExtended=i,this.header=t,this.remain=t.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=t.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!t.path)throw new Error("no path provided for tar.ReadEntry");this.path=f(t.path),this.mode=t.mode,this.mode&&(this.mode=this.mode&4095),this.uid=t.uid,this.gid=t.gid,this.uname=t.uname,this.gname=t.gname,this.size=this.remain,this.mtime=t.mtime,this.atime=t.atime,this.ctime=t.ctime,this.linkpath=t.linkpath?f(t.linkpath):void 0,this.uname=t.uname,this.gname=t.gname,e&&this.#t(e),i&&this.#t(i,!0)}write(t){let e=t.length;if(e>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-e),this.blockRemain=Math.max(0,r-e),this.ignore?!0:i>=e?super.write(t):super.write(t.subarray(0,i))}#t(t,e=!1){t.path&&(t.path=f(t.path)),t.linkpath&&(t.linkpath=f(t.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(t).filter(([i,r])=>!(r==null||i==="path"&&e))))}};var Lt=(s,t,e,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=e instanceof Error&&e.code||t,i.tarCode=t,!s.strict&&i.recoverable!==!1?(e instanceof Error&&(i=Object.assign(e,i),e=e.message),s.emit("warn",t,e,i)):e instanceof Error?s.emit("error",Object.assign(e,i)):s.emit("error",Object.assign(new Error(`${t}: ${e}`),i))};var gn=1024*1024,Zi=Buffer.from([31,139]),Yi=Buffer.from([40,181,47,253]),On=Math.max(Zi.length,Yi.length),B=Symbol("state"),Nt=Symbol("writeEntry"),et=Symbol("readEntry"),Ui=Symbol("nextEntry"),Us=Symbol("processEntry"),V=Symbol("extendedHeader"),ae=Symbol("globalExtendedHeader"),ft=Symbol("meta"),Hs=Symbol("emitMeta"),p=Symbol("buffer"),it=Symbol("queue"),dt=Symbol("ended"),Hi=Symbol("emittedEnd"),At=Symbol("emit"),y=Symbol("unzip"),Ve=Symbol("consumeChunk"),$e=Symbol("consumeChunkSub"),Wi=Symbol("consumeBody"),Ws=Symbol("consumeMeta"),Gs=Symbol("consumeHeader"),le=Symbol("consuming"),Gi=Symbol("bufferConcat"),Xe=Symbol("maybeEnd"),Kt=Symbol("writing"),ut=Symbol("aborted"),qe=Symbol("onDone"),Dt=Symbol("sawValidEntry"),je=Symbol("sawNullBlock"),Qe=Symbol("sawEOF"),Zs=Symbol("closeStream"),Tn=()=>!0,st=class extends _n{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[it]=[];[p];[et];[Nt];[B]="begin";[ft]="";[V];[ae];[dt]=!1;[y];[ut]=!1;[Dt];[je]=!1;[Qe]=!1;[Kt]=!1;[le]=!1;[Hi]=!1;constructor(t={}){super(),this.file=t.file||"",this.on(qe,()=>{(this[B]==="begin"||this[Dt]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),t.ondone?this.on(qe,t.ondone):this.on(qe,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!t.strict,this.maxMetaEntrySize=t.maxMetaEntrySize||gn,this.filter=typeof t.filter=="function"?t.filter:Tn;let e=t.file&&(t.file.endsWith(".tar.br")||t.file.endsWith(".tbr"));this.brotli=!(t.gzip||t.zstd)&&t.brotli!==void 0?t.brotli:e?void 0:!1;let i=t.file&&(t.file.endsWith(".tar.zst")||t.file.endsWith(".tzst"));this.zstd=!(t.gzip||t.brotli)&&t.zstd!==void 0?t.zstd:i?!0:void 0,this.on("end",()=>this[Zs]()),typeof t.onwarn=="function"&&this.on("warn",t.onwarn),typeof t.onReadEntry=="function"&&this.on("entry",t.onReadEntry)}warn(t,e,i={}){Lt(this,t,e,i)}[Gs](t,e){this[Dt]===void 0&&(this[Dt]=!1);let i;try{i=new F(t,e,this[V],this[ae])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[je]?(this[Qe]=!0,this[B]==="begin"&&(this[B]="header"),this[At]("eof")):(this[je]=!0,this[At]("nullBlock"));else if(this[je]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[Nt]=new Yt(i,this[V],this[ae]);if(!this[Dt])if(n.remain){let o=()=>{n.invalid||(this[Dt]=!0)};n.on("end",o)}else this[Dt]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[At]("ignoredEntry",n),this[B]="ignore",n.resume()):n.size>0&&(this[ft]="",n.on("data",o=>this[ft]+=o),this[B]="meta"):(this[V]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[At]("ignoredEntry",n),this[B]=n.remain?"ignore":"header",n.resume()):(n.remain?this[B]="body":(this[B]="header",n.end()),this[et]?this[it].push(n):(this[it].push(n),this[Ui]())))}}}[Zs](){queueMicrotask(()=>this.emit("close"))}[Us](t){let e=!0;if(!t)this[et]=void 0,e=!1;else if(Array.isArray(t)){let[i,...r]=t;this.emit(i,...r)}else this[et]=t,this.emit("entry",t),t.emittedEnd||(t.on("end",()=>this[Ui]()),e=!1);return e}[Ui](){do;while(this[Us](this[it].shift()));if(this[it].length===0){let t=this[et];!t||t.flowing||t.size===t.remain?this[Kt]||this.emit("drain"):t.once("drain",()=>this.emit("drain"))}}[Wi](t,e){let i=this[Nt];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=t.length&&e===0?t:t.subarray(e,e+r);return i.write(n),i.blockRemain||(this[B]="header",this[Nt]=void 0,i.end()),n.length}[Ws](t,e){let i=this[Nt],r=this[Wi](t,e);return!this[Nt]&&i&&this[Hs](i),r}[At](t,e,i){this[it].length===0&&!this[et]?this.emit(t,e,i):this[it].push([t,e,i])}[Hs](t){switch(this[At]("meta",this[ft]),t.type){case"ExtendedHeader":case"OldExtendedHeader":this[V]=ct.parse(this[ft],this[V],!1);break;case"GlobalExtendedHeader":this[ae]=ct.parse(this[ft],this[ae],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let e=this[V]??Object.create(null);this[V]=e,e.path=this[ft].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let e=this[V]||Object.create(null);this[V]=e,e.linkpath=this[ft].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+t.type)}}abort(t){this[ut]=!0,this.emit("abort",t),this.warn("TAR_ABORT",t,{recoverable:!1})}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this[ut])return i?.(),!1;if((this[y]===void 0||this.brotli===void 0&&this[y]===!1)&&t){if(this[p]&&(t=Buffer.concat([this[p],t]),this[p]=void 0),t.lengththis[Ve](c)),this[y].on("error",c=>this.abort(c)),this[y].on("end",()=>{this[dt]=!0,this[Ve]()}),this[Kt]=!0;let l=!!this[y][a?"end":"write"](t);return this[Kt]=!1,i?.(),l}}this[Kt]=!0,this[y]?this[y].write(t):this[Ve](t),this[Kt]=!1;let n=this[it].length>0?!1:this[et]?this[et].flowing:!0;return!n&&this[it].length===0&&this[et]?.once("drain",()=>this.emit("drain")),i?.(),n}[Gi](t){t&&!this[ut]&&(this[p]=this[p]?Buffer.concat([this[p],t]):t)}[Xe](){if(this[dt]&&!this[Hi]&&!this[ut]&&!this[le]){this[Hi]=!0;let t=this[Nt];if(t&&t.blockRemain){let e=this[p]?this[p].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${t.blockRemain} more bytes, only ${e} available)`,{entry:t}),this[p]&&t.write(this[p]),t.end()}this[At](qe)}}[Ve](t){if(this[le]&&t)this[Gi](t);else if(!t&&!this[p])this[Xe]();else if(t){if(this[le]=!0,this[p]){this[Gi](t);let e=this[p];this[p]=void 0,this[$e](e)}else this[$e](t);for(;this[p]&&this[p]?.length>=512&&!this[ut]&&!this[Qe];){let e=this[p];this[p]=void 0,this[$e](e)}this[le]=!1}(!this[p]||this[dt])&&this[Xe]()}[$e](t){let e=0,i=t.length;for(;e+512<=i&&!this[ut]&&!this[Qe];)switch(this[B]){case"begin":case"header":this[Gs](t,e),e+=512;break;case"ignore":case"body":e+=this[Wi](t,e);break;case"meta":e+=this[Ws](t,e);break;default:throw new Error("invalid state: "+this[B])}e{let t=s.length-1,e=-1;for(;t>-1&&s.charAt(t)==="/";)e=t,t--;return e===-1?s:s.slice(0,e)};var Nn=s=>{let t=s.onReadEntry;s.onReadEntry=t?e=>{t(e),e.resume()}:e=>e.resume()},Ki=(s,t)=>{let e=new Map(t.map(n=>[mt(n),!0])),i=s.filter,r=(n,o="")=>{let h=o||Ln(n).root||".",a;if(n===h)a=!1;else{let l=e.get(n);a=l!==void 0?l:r(xn(n),h)}return e.set(n,a),a};s.filter=i?(n,o)=>i(n,o)&&r(mt(n)):n=>r(mt(n))},An=s=>{let t=new st(s),e=s.file,i;try{i=Vt.openSync(e,"r");let r=Vt.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let e=new st(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{e.on("error",h),e.on("end",o),Vt.stat(r,(a,l)=>{if(a)h(a);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",h),c.pipe(e)}})})},It=K(An,Dn,s=>new st(s),s=>new st(s),(s,t)=>{t?.length&&Ki(s,t),s.noResume||Nn(s)});import fi from"fs";import $ from"fs";import $s from"path";var Vi=(s,t,e)=>(s&=4095,e&&(s=(s|384)&-19),t&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);import{win32 as In}from"node:path";var{isAbsolute:Cn,parse:Ys}=In,ce=s=>{let t="",e=Ys(s);for(;Cn(s)||e.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":e.root;s=s.slice(i.length),t+=i,e=Ys(s)}return[t,s]};var Je=["|","<",">","?",":"],$i=Je.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),Fn=new Map(Je.map((s,t)=>[s,$i[t]])),kn=new Map($i.map((s,t)=>[s,Je[t]])),Xi=s=>Je.reduce((t,e)=>t.split(e).join(Fn.get(e)),s),Ks=s=>$i.reduce((t,e)=>t.split(e).join(kn.get(e)),s);var Js=(s,t)=>t?(s=f(s).replace(/^\.(\/|$)/,""),mt(t)+"/"+s):f(s),vn=16*1024*1024,Xs=Symbol("process"),qs=Symbol("file"),js=Symbol("directory"),ji=Symbol("symlink"),Qs=Symbol("hardlink"),fe=Symbol("header"),ti=Symbol("read"),Qi=Symbol("lstat"),ei=Symbol("onlstat"),Ji=Symbol("onread"),ts=Symbol("onreadlink"),es=Symbol("openfile"),is=Symbol("onopenfile"),pt=Symbol("close"),ii=Symbol("mode"),ss=Symbol("awaitDrain"),qi=Symbol("ondrain"),X=Symbol("prefix"),de=class extends D{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#t=!1;constructor(t,e={}){let i=re(e);super(),this.path=f(t),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||vn,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=f(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?f(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,h]=ce(this.path);o&&typeof h=="string"&&(this.path=h,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=Ks(this.path.replaceAll(/\\/g,"/")),t=t.replaceAll(/\\/g,"/")),this.absolute=f(i.absolute||$s.resolve(this.cwd,t)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[ei](n):this[Qi]()}warn(t,e,i={}){return Lt(this,t,e,i)}emit(t,...e){return t==="error"&&(this.#t=!0),super.emit(t,...e)}[Qi](){$.lstat(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[ei](e)})}[ei](t){this.statCache.set(this.absolute,t),this.stat=t,t.isFile()||(t.size=0),this.type=Mn(t),this.emit("stat",t),this[Xs]()}[Xs](){switch(this.type){case"File":return this[qs]();case"Directory":return this[js]();case"SymbolicLink":return this[ji]();default:return this.end()}}[ii](t){return Vi(t,this.type==="Directory",this.portable)}[X](t){return Js(t,this.prefix)}[fe](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new F({path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,mode:this[ii](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new ct({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let t=this.header?.block;if(!t)throw new Error("failed to encode header");super.write(t)}[js](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[fe](),this.end()}[ji](){$.readlink(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[ts](e)})}[ts](t){this.linkpath=f(t),this[fe](),this.end()}[Qs](t){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=f($s.relative(this.cwd,t)),this.stat.size=0,this[fe](),this.end()}[qs](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let t=`${this.stat.dev}:${this.stat.ino}`,e=this.linkCache.get(t);if(e?.indexOf(this.cwd)===0)return this[Qs](e);this.linkCache.set(t,this.absolute)}if(this[fe](),this.stat.size===0)return this.end();this[es]()}[es](){$.open(this.absolute,"r",(t,e)=>{if(t)return this.emit("error",t);this[is](e)})}[is](t){if(this.fd=t,this.#t)return this[pt]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let e=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(e),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[ti]()}[ti](){let{fd:t,buf:e,offset:i,length:r,pos:n}=this;if(t===void 0||e===void 0)throw new Error("cannot read file without first opening");$.read(t,e,i,r,n,(o,h)=>{if(o)return this[pt](()=>this.emit("error",o));this[Ji](h)})}[pt](t=()=>{}){this.fd!==void 0&&$.close(this.fd,t)}[Ji](t){if(t<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(t>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(t===this.remain)for(let r=t;rthis[qi]())}[ss](t){this.once("drain",t)}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this.blockRemaint?this.emit("error",t):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[ti]()}},si=class extends de{sync=!0;[Qi](){this[ei]($.lstatSync(this.absolute))}[ji](){this[ts]($.readlinkSync(this.absolute))}[es](){this[is]($.openSync(this.absolute,"r"))}[ti](){let t=!0;try{let{fd:e,buf:i,offset:r,length:n,pos:o}=this;if(e===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let h=$.readSync(e,i,r,n,o);this[Ji](h),t=!1}finally{if(t)try{this[pt](()=>{})}catch{}}}[ss](t){t()}[pt](t=()=>{}){this.fd!==void 0&&$.closeSync(this.fd),t()}},ri=class extends D{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(t,e,i={}){return Lt(this,t,e,i)}constructor(t,e={}){let i=re(e);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=t;let{type:r}=t;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=f(t.path),this.mode=t.mode!==void 0?this[ii](t.mode):void 0,this.uid=this.portable?void 0:t.uid,this.gid=this.portable?void 0:t.gid,this.uname=this.portable?void 0:t.uname,this.gname=this.portable?void 0:t.gname,this.size=t.size,this.mtime=this.noMtime?void 0:i.mtime||t.mtime,this.atime=this.portable?void 0:t.atime,this.ctime=this.portable?void 0:t.ctime,this.linkpath=t.linkpath!==void 0?f(t.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[h,a]=ce(this.path);h&&typeof a=="string"&&(this.path=a,n=h)}this.remain=t.size,this.blockRemain=t.startBlockSize,this.onWriteEntry?.(this),this.header=new F({path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new ct({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),t.pipe(this)}[X](t){return Js(t,this.prefix)}[ii](t){return Vi(t,this.type==="Directory",this.portable)}write(t,e,i){typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8"));let r=t.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(t,i)}end(t,e,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,e??"utf8")),i&&this.once("finish",i),t?super.end(t,i):super.end(i),this}},Mn=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported";var ni=class s{tail;head;length=0;static create(t=[]){return new s(t)}constructor(t=[]){for(let e of t)this.push(e)}*[Symbol.iterator](){for(let t=this.head;t;t=t.next)yield t.value}removeNode(t){if(t.list!==this)throw new Error("removing node which does not belong to this list");let e=t.next,i=t.prev;return e&&(e.prev=i),i&&(i.next=e),t===this.head&&(this.head=e),t===this.tail&&(this.tail=i),this.length--,t.next=void 0,t.prev=void 0,t.list=void 0,e}unshiftNode(t){if(t===this.head)return;t.list&&t.list.removeNode(t);let e=this.head;t.list=this,t.next=e,e&&(e.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}pushNode(t){if(t===this.tail)return;t.list&&t.list.removeNode(t);let e=this.tail;t.list=this,t.prev=e,e&&(e.next=t),this.tail=t,this.head||(this.head=t),this.length++}push(...t){for(let e=0,i=t.length;e1)i=e;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=t(i,r.value,n),r=r.next;return i}reduceReverse(t,e){let i,r=this.tail;if(arguments.length>1)i=e;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=t(i,r.value,n),r=r.prev;return i}toArray(){let t=new Array(this.length);for(let e=0,i=this.head;i;e++)t[e]=i.value,i=i.next;return t}toArrayReverse(){let t=new Array(this.length);for(let e=0,i=this.tail;i;e++)t[e]=i.value,i=i.prev;return t}slice(t=0,e=this.length){e<0&&(e+=this.length),t<0&&(t+=this.length);let i=new s;if(ethis.length&&(e=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(e=this.length);let r=this.length,n=this.tail;for(;n&&r>e;r--)n=n.prev;for(;n&&r>t;r--,n=n.prev)i.push(n.value);return i}splice(t,e=0,...i){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);let r=this.head;for(let o=0;r&&o1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(t.gzip&&(typeof t.gzip!="object"&&(t.gzip={}),this.portable&&(t.gzip.portable=!0),this.zip=new Pe(t.gzip)),t.brotli&&(typeof t.brotli!="object"&&(t.brotli={}),this.zip=new He(t.brotli)),t.zstd&&(typeof t.zstd!="object"&&(t.zstd={}),this.zip=new Ze(t.zstd)),!this.zip)throw new Error("impossible");let e=this.zip;e.on("data",i=>super.write(i)),e.on("end",()=>super.end()),e.on("drain",()=>this[hs]()),this.on("resume",()=>e.resume())}else this.on("drain",this[hs]);this.noDirRecurse=!!t.noDirRecurse,this.follow=!!t.follow,this.noMtime=!!t.noMtime,t.mtime&&(this.mtime=t.mtime),this.filter=typeof t.filter=="function"?t.filter:()=>!0,this[W]=new ni,this[G]=0,this.jobs=Number(t.jobs)||4,this[pe]=!1,this[me]=!1}[rr](t){return super.write(t)}add(t){return this.write(t),this}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&this.add(t),this[me]=!0,this[Ft](),i&&i(),this}write(t){if(this[me])throw new Error("write after end");return t instanceof Yt?this[er](t):this[hi](t),this.flowing}[er](t){let e=f(sr.resolve(this.cwd,t.path));if(!this.filter(t.path,t))t.resume();else{let i=new di(t.path,e);i.entry=new ri(t,this[os](i)),i.entry.on("end",()=>this[ns](i)),this[G]+=1,this[W].push(i)}this[Ft]()}[hi](t){let e=f(sr.resolve(this.cwd,t));this[W].push(new di(t,e)),this[Ft]()}[as](t){t.pending=!0,this[G]+=1;let e=this.follow?"stat":"lstat";fi[e](t.absolute,(i,r)=>{t.pending=!1,this[G]-=1,i?this.emit("error",i):this[oi](t,r)})}[oi](t,e){this.statCache.set(t.absolute,e),t.stat=e,this.filter(t.path,e)?e.isFile()&&e.nlink>1&&t===this[Ct]&&!this.linkCache.get(`${e.dev}:${e.ino}`)&&!this.sync&&this[rs](t):t.ignore=!0,this[Ft]()}[ls](t){t.pending=!0,this[G]+=1,fi.readdir(t.absolute,(e,i)=>{if(t.pending=!1,this[G]-=1,e)return this.emit("error",e);this[ai](t,i)})}[ai](t,e){this.readdirCache.set(t.absolute,e),t.readdir=e,this[Ft]()}[Ft](){if(!this[pe]){this[pe]=!0;for(let t=this[W].head;t&&this[G]this.warn(e,i,r),noPax:this.noPax,cwd:this.cwd,absolute:t.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[ir](t){this[G]+=1;try{return new this[ci](t.path,this[os](t)).on("end",()=>this[ns](t)).on("error",i=>this.emit("error",i))}catch(e){this.emit("error",e)}}[hs](){this[Ct]&&this[Ct].entry&&this[Ct].entry.resume()}[li](t){t.piped=!0,t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[hi](o+r)});let e=t.entry,i=this.zip;if(!e)throw new Error("cannot pipe without source");i?e.on("data",r=>{i.write(r)||e.pause()}):e.on("data",r=>{super.write(r)||e.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(t,e,i={}){Lt(this,t,e,i)}},kt=class extends Et{sync=!0;constructor(t){super(t),this[ci]=si}pause(){}resume(){}[as](t){let e=this.follow?"statSync":"lstatSync";this[oi](t,fi[e](t.absolute))}[ls](t){this[ai](t,fi.readdirSync(t.absolute))}[li](t){let e=t.entry,i=this.zip;if(t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[hi](o+r)}),!e)throw new Error("Cannot pipe without source");i?e.on("data",r=>{i.write(r)}):e.on("data",r=>{super[rr](r)})}};var Un=(s,t)=>{let e=new kt(s),i=new Wt(s.file,{mode:s.mode||438});e.pipe(i),or(e,t)},Hn=(s,t)=>{let e=new Et(s),i=new tt(s.file,{mode:s.mode||438});e.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),e.on("error",o)});return hr(e,t).catch(n=>e.emit("error",n)),r},or=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?It({file:nr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},hr=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await It({file:nr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(e);s.end()},Wn=(s,t)=>{let e=new kt(s);return or(e,t),e},Gn=(s,t)=>{let e=new Et(s);return hr(e,t).catch(i=>e.emit("error",i)),e},Zn=K(Un,Hn,Wn,Gn,(s,t)=>{if(!t?.length)throw new TypeError("no paths specified to add to archive")});import Cr from"node:fs";import so from"node:assert";import{randomBytes as Ir}from"node:crypto";import u from"node:fs";import R from"node:path";import cr from"fs";var Yn=process.env.__FAKE_PLATFORM__||process.platform,fr=Yn==="win32",{O_CREAT:dr,O_NOFOLLOW:ar,O_TRUNC:ur,O_WRONLY:mr}=cr.constants,pr=Number(process.env.__FAKE_FS_O_FILENAME__)||cr.constants.UV_FS_O_FILEMAP||0,Kn=fr&&!!pr,Vn=512*1024,$n=pr|ur|dr|mr,lr=!fr&&typeof ar=="number"?ar|ur|dr|mr:null,cs=lr!==null?()=>lr:Kn?s=>s"w";import mi from"node:fs";import Ee from"node:path";var fs=(s,t,e)=>{try{return mi.lchownSync(s,t,e)}catch(i){if(i?.code!=="ENOENT")throw i}},ui=(s,t,e,i)=>{mi.lchown(s,t,e,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},Xn=(s,t,e,i,r)=>{if(t.isDirectory())ds(Ee.resolve(s,t.name),e,i,n=>{if(n)return r(n);let o=Ee.resolve(s,t.name);ui(o,e,i,r)});else{let n=Ee.resolve(s,t.name);ui(n,e,i,r)}},ds=(s,t,e,i)=>{mi.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return ui(s,t,e,i);let o=n.length,h=null,a=l=>{if(!h){if(l)return i(h=l);if(--o===0)return ui(s,t,e,i)}};for(let l of n)Xn(s,l,t,e,a)})},qn=(s,t,e,i)=>{t.isDirectory()&&us(Ee.resolve(s,t.name),e,i),fs(Ee.resolve(s,t.name),e,i)},us=(s,t,e)=>{let i;try{i=mi.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return fs(s,t,e);throw n}for(let r of i)qn(s,r,t,e);return fs(s,t,e)};import k from"node:fs";import jn from"node:fs/promises";import pi from"node:path";var we=class extends Error{path;code;syscall="chdir";constructor(t,e){super(`${e}: Cannot cd into '${t}'`),this.path=t,this.code=e}get name(){return"CwdError"}};var wt=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(t,e){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=t,this.path=e}get name(){return"SymlinkError"}};var Qn=(s,t)=>{k.stat(s,(e,i)=>{(e||!i.isDirectory())&&(e=new we(s,e?.code||"ENOTDIR")),t(e)})},Er=(s,t,e)=>{s=f(s);let i=t.umask??18,r=t.mode|448,n=(r&i)!==0,o=t.uid,h=t.gid,a=typeof o=="number"&&typeof h=="number"&&(o!==t.processUid||h!==t.processGid),l=t.preserve,c=t.unlink,d=f(t.cwd),S=(E,x)=>{E?e(E):x&&a?ds(x,o,h,xe=>S(xe)):n?k.chmod(s,r,e):e()};if(s===d)return Qn(s,S);if(l)return jn.mkdir(s,{mode:r,recursive:!0}).then(E=>S(null,E??void 0),S);let N=f(pi.relative(d,s)).split("/");ms(d,N,r,c,d,void 0,S)},ms=(s,t,e,i,r,n,o)=>{if(t.length===0)return o(null,n);let h=t.shift(),a=f(pi.resolve(s+"/"+h));k.mkdir(a,e,wr(a,t,e,i,r,n,o))},wr=(s,t,e,i,r,n,o)=>h=>{h?k.lstat(s,(a,l)=>{if(a)a.path=a.path&&f(a.path),o(a);else if(l.isDirectory())ms(s,t,e,i,r,n,o);else if(i)k.unlink(s,c=>{if(c)return o(c);k.mkdir(s,e,wr(s,t,e,i,r,n,o))});else{if(l.isSymbolicLink())return o(new wt(s,s+"/"+t.join("/")));o(h)}}):(n=n||s,ms(s,t,e,i,r,n,o))},Jn=s=>{let t=!1,e;try{t=k.statSync(s).isDirectory()}catch(i){e=i?.code}finally{if(!t)throw new we(s,e??"ENOTDIR")}},Sr=(s,t)=>{s=f(s);let e=t.umask??18,i=t.mode|448,r=(i&e)!==0,n=t.uid,o=t.gid,h=typeof n=="number"&&typeof o=="number"&&(n!==t.processUid||o!==t.processGid),a=t.preserve,l=t.unlink,c=f(t.cwd),d=E=>{E&&h&&us(E,n,o),r&&k.chmodSync(s,i)};if(s===c)return Jn(c),d();if(a)return d(k.mkdirSync(s,{mode:i,recursive:!0})??void 0);let T=f(pi.relative(c,s)).split("/"),N;for(let E=T.shift(),x=c;E&&(x+="/"+E);E=T.shift()){x=f(pi.resolve(x));try{k.mkdirSync(x,i),N=N||x}catch{let xe=k.lstatSync(x);if(xe.isDirectory())continue;if(l){k.unlinkSync(x),k.mkdirSync(x,i),N=N||x;continue}else if(xe.isSymbolicLink())return new wt(x,x+"/"+T.join("/"))}}return d(N)};import{join as br}from"node:path";var ps=Object.create(null),yr=1e4,$t=new Set,Rr=s=>{$t.has(s)?$t.delete(s):ps[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),$t.add(s);let t=ps[s],e=$t.size-yr;if(e>yr/10){for(let i of $t)if($t.delete(i),delete ps[i],--e<=0)break}return t};var to=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,eo=to==="win32",io=s=>s.split("/").slice(0,-1).reduce((e,i)=>{let r=e.at(-1);return r!==void 0&&(i=br(r,i)),e.push(i||"/"),e},[]),Ei=class{#t=new Map;#i=new Map;#s=new Set;reserve(t,e){t=eo?["win32 parallelization disabled"]:t.map(r=>mt(br(Rr(r))));let i=new Set(t.map(r=>io(r)).reduce((r,n)=>r.concat(n)));this.#i.set(e,{dirs:i,paths:t});for(let r of t){let n=this.#t.get(r);n?n.push(e):this.#t.set(r,[e])}for(let r of i){let n=this.#t.get(r);if(!n)this.#t.set(r,[new Set([e])]);else{let o=n.at(-1);o instanceof Set?o.add(e):n.push(new Set([e]))}}return this.#r(e)}#n(t){let e=this.#i.get(t);if(!e)throw new Error("function does not have any path reservations");return{paths:e.paths.map(i=>this.#t.get(i)),dirs:[...e.dirs].map(i=>this.#t.get(i))}}check(t){let{paths:e,dirs:i}=this.#n(t);return e.every(r=>r&&r[0]===t)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(t))}#r(t){return this.#s.has(t)||!this.check(t)?!1:(this.#s.add(t),t(()=>this.#e(t)),!0)}#e(t){if(!this.#s.has(t))return!1;let e=this.#i.get(t);if(!e)throw new Error("invalid reservation");let{paths:i,dirs:r}=e,n=new Set;for(let o of i){let h=this.#t.get(o);if(!h||h?.[0]!==t)continue;let a=h[1];if(!a){this.#t.delete(o);continue}if(h.shift(),typeof a=="function")n.add(a);else for(let l of a)n.add(l)}for(let o of r){let h=this.#t.get(o),a=h?.[0];if(!(!h||!(a instanceof Set)))if(a.size===1&&h.length===1){this.#t.delete(o);continue}else if(a.size===1){h.shift();let l=h[0];typeof l=="function"&&n.add(l)}else a.delete(t)}return this.#s.delete(t),n.forEach(o=>this.#r(o)),!0}};var _r=()=>process.umask();var gr=Symbol("onEntry"),ys=Symbol("checkFs"),Or=Symbol("checkFs2"),Rs=Symbol("isReusable"),P=Symbol("makeFs"),bs=Symbol("file"),_s=Symbol("directory"),Si=Symbol("link"),Tr=Symbol("symlink"),xr=Symbol("hardlink"),ye=Symbol("ensureNoSymlink"),Lr=Symbol("unsupported"),Nr=Symbol("checkPath"),Es=Symbol("stripAbsolutePath"),St=Symbol("mkdir"),O=Symbol("onError"),wi=Symbol("pending"),Ar=Symbol("pend"),Xt=Symbol("unpend"),ws=Symbol("ended"),Ss=Symbol("maybeClose"),gs=Symbol("skip"),Re=Symbol("doChown"),be=Symbol("uid"),_e=Symbol("gid"),ge=Symbol("checkedCwd"),ro=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Oe=ro==="win32",no=1024,oo=(s,t)=>{if(!Oe)return u.unlink(s,t);let e=s+".DELETE."+Ir(16).toString("hex");u.rename(s,e,i=>{if(i)return t(i);u.unlink(e,t)})},ho=s=>{if(!Oe)return u.unlinkSync(s);let t=s+".DELETE."+Ir(16).toString("hex");u.renameSync(s,t),u.unlinkSync(t)},Dr=(s,t,e)=>s!==void 0&&s===s>>>0?s:t!==void 0&&t===t>>>0?t:e,qt=class extends st{[ws]=!1;[ge]=!1;[wi]=0;reservations=new Ei;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(t={}){if(t.ondone=()=>{this[ws]=!0,this[Ss]()},super(t),this.transform=t.transform,this.chmod=!!t.chmod,typeof t.uid=="number"||typeof t.gid=="number"){if(typeof t.uid!="number"||typeof t.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(t.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=t.uid,this.gid=t.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=t.preserveOwner===void 0&&typeof t.uid!="number"?!!(process.getuid&&process.getuid()===0):!!t.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof t.maxDepth=="number"?t.maxDepth:no,this.forceChown=t.forceChown===!0,this.win32=!!t.win32||Oe,this.newer=!!t.newer,this.keep=!!t.keep,this.noMtime=!!t.noMtime,this.preservePaths=!!t.preservePaths,this.unlink=!!t.unlink,this.cwd=f(R.resolve(t.cwd||process.cwd())),this.strip=Number(t.strip)||0,this.processUmask=this.chmod?typeof t.processUmask=="number"?t.processUmask:_r():0,this.umask=typeof t.umask=="number"?t.umask:this.processUmask,this.dmode=t.dmode||511&~this.umask,this.fmode=t.fmode||438&~this.umask,this.on("entry",e=>this[gr](e))}warn(t,e,i={}){return(t==="TAR_BAD_ARCHIVE"||t==="TAR_ABORT")&&(i.recoverable=!1),super.warn(t,e,i)}[Ss](){this[ws]&&this[wi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[Es](t,e){let i=t[e],{type:r}=t;if(!i||this.preservePaths)return!0;let[n,o]=ce(i),h=o.replaceAll(/\\/g,"/").split("/");if(h.includes("..")||Oe&&/^[a-z]:\.\.$/i.test(h[0]??"")){if(e==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${e} contains '..'`,{entry:t,[e]:i}),!1;let a=R.posix.dirname(t.path),l=R.posix.normalize(R.posix.join(a,h.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${e} escapes extraction directory`,{entry:t,[e]:i}),!1}return n&&(t[e]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${e}`,{entry:t,[e]:i})),!0}[Nr](t){let e=f(t.path),i=e.split("/");if(this.strip){if(i.length=this.strip)t.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),t.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:t,path:e,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[Es](t,"path")||!this[Es](t,"linkpath"))return!1;if(t.absolute=R.isAbsolute(t.path)?f(R.resolve(t.path)):f(R.resolve(this.cwd,t.path)),!this.preservePaths&&typeof t.absolute=="string"&&t.absolute.indexOf(this.cwd+"/")!==0&&t.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:t,path:f(t.path),resolvedPath:t.absolute,cwd:this.cwd}),!1;if(t.absolute===this.cwd&&t.type!=="Directory"&&t.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=R.win32.parse(String(t.absolute));t.absolute=r+Xi(String(t.absolute).slice(r.length));let{root:n}=R.win32.parse(t.path);t.path=n+Xi(t.path.slice(n.length))}return!0}[gr](t){if(!this[Nr](t))return t.resume();switch(so.equal(typeof t.absolute,"string"),t.type){case"Directory":case"GNUDumpDir":t.mode&&(t.mode=t.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[ys](t);default:return this[Lr](t)}}[O](t,e){t.name==="CwdError"?this.emit("error",t):(this.warn("TAR_ENTRY_ERROR",t,{entry:e}),this[Xt](),e.resume())}[St](t,e,i){Er(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e},i)}[Re](t){return this.forceChown||this.preserveOwner&&(typeof t.uid=="number"&&t.uid!==this.processUid||typeof t.gid=="number"&&t.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[be](t){return Dr(this.uid,t.uid,this.processUid)}[_e](t){return Dr(this.gid,t.gid,this.processGid)}[bs](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=new tt(String(t.absolute),{flags:cs(t.size),mode:i,autoClose:!1});r.on("error",a=>{r.fd&&u.close(r.fd,()=>{}),r.write=()=>!0,this[O](a,t),e()});let n=1,o=a=>{if(a){r.fd&&u.close(r.fd,()=>{}),this[O](a,t),e();return}--n===0&&r.fd!==void 0&&u.close(r.fd,l=>{l?this[O](l,t):this[Xt](),e()})};r.on("finish",()=>{let a=String(t.absolute),l=r.fd;if(typeof l=="number"&&t.mtime&&!this.noMtime){n++;let c=t.atime||new Date,d=t.mtime;u.futimes(l,c,d,S=>S?u.utimes(a,c,d,T=>o(T&&S)):o())}if(typeof l=="number"&&this[Re](t)){n++;let c=this[be](t),d=this[_e](t);typeof c=="number"&&typeof d=="number"&&u.fchown(l,c,d,S=>S?u.chown(a,c,d,T=>o(T&&S)):o())}o()});let h=this.transform&&this.transform(t)||t;h!==t&&(h.on("error",a=>{this[O](a,t),e()}),t.pipe(h)),h.pipe(r)}[_s](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode;this[St](String(t.absolute),i,r=>{if(r){this[O](r,t),e();return}let n=1,o=()=>{--n===0&&(e(),this[Xt](),t.resume())};t.mtime&&!this.noMtime&&(n++,u.utimes(String(t.absolute),t.atime||new Date,t.mtime,o)),this[Re](t)&&(n++,u.chown(String(t.absolute),Number(this[be](t)),Number(this[_e](t)),o)),o()})}[Lr](t){t.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${t.type}`,{entry:t}),t.resume()}[Tr](t,e){let i=f(R.relative(this.cwd,R.resolve(R.dirname(String(t.absolute)),String(t.linkpath)))).split("/");this[ye](t,this.cwd,i,()=>this[Si](t,String(t.linkpath),"symlink",e),r=>{this[O](r,t),e()})}[xr](t,e){let i=f(R.resolve(this.cwd,String(t.linkpath))),r=f(String(t.linkpath)).split("/");this[ye](t,this.cwd,r,()=>this[Si](t,i,"link",e),n=>{this[O](n,t),e()})}[ye](t,e,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let h=R.resolve(e,o);u.lstat(h,(a,l)=>{if(a)return r();if(l?.isSymbolicLink())return n(new wt(h,R.resolve(h,i.join("/"))));this[ye](t,h,i,r,n)})}[Ar](){this[wi]++}[Xt](){this[wi]--,this[Ss]()}[gs](t){this[Xt](),t.resume()}[Rs](t,e){return t.type==="File"&&!this.unlink&&e.isFile()&&e.nlink<=1&&!Oe}[ys](t){this[Ar]();let e=[t.path];t.linkpath&&e.push(t.linkpath),this.reservations.reserve(e,i=>this[Or](t,i))}[Or](t,e){let i=h=>{e(h)},r=()=>{this[St](this.cwd,this.dmode,h=>{if(h){this[O](h,t),i();return}this[ge]=!0,n()})},n=()=>{if(t.absolute!==this.cwd){let h=f(R.dirname(String(t.absolute)));if(h!==this.cwd)return this[St](h,this.dmode,a=>{if(a){this[O](a,t),i();return}o()})}o()},o=()=>{u.lstat(String(t.absolute),(h,a)=>{if(a&&(this.keep||this.newer&&a.mtime>(t.mtime??a.mtime))){this[gs](t),i();return}if(h||this[Rs](t,a))return this[P](null,t,i);if(a.isDirectory()){if(t.type==="Directory"){let l=this.chmod&&t.mode&&(a.mode&4095)!==t.mode,c=d=>this[P](d??null,t,i);return l?u.chmod(String(t.absolute),Number(t.mode),c):c()}if(t.absolute!==this.cwd)return u.rmdir(String(t.absolute),l=>this[P](l??null,t,i))}if(t.absolute===this.cwd)return this[P](null,t,i);oo(String(t.absolute),l=>this[P](l??null,t,i))})};this[ge]?n():r()}[P](t,e,i){if(t){this[O](t,e),i();return}switch(e.type){case"File":case"OldFile":case"ContiguousFile":return this[bs](e,i);case"Link":return this[xr](e,i);case"SymbolicLink":return this[Tr](e,i);case"Directory":case"GNUDumpDir":return this[_s](e,i)}}[Si](t,e,i,r){u[i](e,String(t.absolute),n=>{n?this[O](n,t):(this[Xt](),t.resume()),r()})}},Se=s=>{try{return[null,s()]}catch(t){return[t,null]}},Te=class extends qt{sync=!0;[P](t,e){return super[P](t,e,()=>{})}[ys](t){if(!this[ge]){let n=this[St](this.cwd,this.dmode);if(n)return this[O](n,t);this[ge]=!0}if(t.absolute!==this.cwd){let n=f(R.dirname(String(t.absolute)));if(n!==this.cwd){let o=this[St](n,this.dmode);if(o)return this[O](o,t)}}let[e,i]=Se(()=>u.lstatSync(String(t.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(t.mtime??i.mtime)))return this[gs](t);if(e||this[Rs](t,i))return this[P](null,t);if(i.isDirectory()){if(t.type==="Directory"){let o=this.chmod&&t.mode&&(i.mode&4095)!==t.mode,[h]=o?Se(()=>{u.chmodSync(String(t.absolute),Number(t.mode))}):[];return this[P](h,t)}let[n]=Se(()=>u.rmdirSync(String(t.absolute)));this[P](n,t)}let[r]=t.absolute===this.cwd?[]:Se(()=>ho(String(t.absolute)));this[P](r,t)}[bs](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=h=>{let a;try{u.closeSync(n)}catch(l){a=l}(h||a)&&this[O](h||a,t),e()},n;try{n=u.openSync(String(t.absolute),cs(t.size),i)}catch(h){return r(h)}let o=this.transform&&this.transform(t)||t;o!==t&&(o.on("error",h=>this[O](h,t)),t.pipe(o)),o.on("data",h=>{try{u.writeSync(n,h,0,h.length)}catch(a){r(a)}}),o.on("end",()=>{let h=null;if(t.mtime&&!this.noMtime){let a=t.atime||new Date,l=t.mtime;try{u.futimesSync(n,a,l)}catch(c){try{u.utimesSync(String(t.absolute),a,l)}catch{h=c}}}if(this[Re](t)){let a=this[be](t),l=this[_e](t);try{u.fchownSync(n,Number(a),Number(l))}catch(c){try{u.chownSync(String(t.absolute),Number(a),Number(l))}catch{h=h||c}}}r(h)})}[_s](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode,r=this[St](String(t.absolute),i);if(r){this[O](r,t),e();return}if(t.mtime&&!this.noMtime)try{u.utimesSync(String(t.absolute),t.atime||new Date,t.mtime)}catch{}if(this[Re](t))try{u.chownSync(String(t.absolute),Number(this[be](t)),Number(this[_e](t)))}catch{}e(),t.resume()}[St](t,e){try{return Sr(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e})}catch(i){return i}}[ye](t,e,i,r,n){if(this.preservePaths||i.length===0)return r();let o=e;for(let h of i){o=R.resolve(o,h);let[a,l]=Se(()=>u.lstatSync(o));if(a)return r();if(l.isSymbolicLink())return n(new wt(o,R.resolve(e,i.join("/"))))}r()}[Si](t,e,i,r){let n=`${i}Sync`;try{u[n](e,String(t.absolute)),r(),t.resume()}catch(o){return this[O](o,t)}}};var ao=s=>{let t=new Te(s),e=s.file,i=Cr.statSync(e),r=s.maxReadSize||16*1024*1024;new Me(e,{readSize:r,size:i.size}).pipe(t)},lo=(s,t)=>{let e=new qt(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{e.on("error",h),e.on("close",o),Cr.stat(r,(a,l)=>{if(a)h(a);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",h),c.pipe(e)}})})},co=K(ao,lo,s=>new Te(s),s=>new qt(s),(s,t)=>{t?.length&&Ki(s,t)});import v from"node:fs";import Fr from"node:path";var fo=(s,t)=>{let e=new kt(s),i=!0,r,n;try{try{r=v.openSync(s.file,"r+")}catch(a){if(a?.code==="ENOENT")r=v.openSync(s.file,"w+");else throw a}let o=v.fstatSync(r),h=Buffer.alloc(512);t:for(n=0;no.size)break;n+=l,s.mtimeCache&&a.mtime&&s.mtimeCache.set(String(a.path),a.mtime)}i=!1,uo(s,e,n,r,t)}finally{if(i)try{v.closeSync(r)}catch{}}},uo=(s,t,e,i,r)=>{let n=new Wt(s.file,{fd:i,start:e});t.pipe(n),po(t,r)},mo=(s,t)=>{t=Array.from(t);let e=new Et(s),i=(n,o,h)=>{let a=(T,N)=>{T?v.close(n,E=>h(T)):h(null,N)},l=0;if(o===0)return a(null,0);let c=0,d=Buffer.alloc(512),S=(T,N)=>{if(T||N===void 0)return a(T);if(c+=N,c<512&&N)return v.read(n,d,c,d.length-c,l+c,S);if(l===0&&d[0]===31&&d[1]===139)return a(new Error("cannot append to compressed archives"));if(c<512)return a(null,l);let E=new F(d);if(!E.cksumValid)return a(null,l);let x=512*Math.ceil((E.size??0)/512);if(l+x+512>o||(l+=x+512,l>=o))return a(null,l);s.mtimeCache&&E.mtime&&s.mtimeCache.set(String(E.path),E.mtime),c=0,v.read(n,d,0,512,l,S)};v.read(n,d,0,512,l,S)};return new Promise((n,o)=>{e.on("error",o);let h="r+",a=(l,c)=>{if(l&&l.code==="ENOENT"&&h==="r+")return h="w+",v.open(s.file,h,a);if(l||!c)return o(l);v.fstat(c,(d,S)=>{if(d)return v.close(c,()=>o(d));i(c,S.size,(T,N)=>{if(T)return o(T);let E=new tt(s.file,{fd:c,start:N});e.pipe(E),E.on("error",o),E.on("close",n),Eo(e,t)})})};v.open(s.file,h,a)})},po=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?It({file:Fr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},Eo=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await It({file:Fr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e);s.end()},vt=K(fo,mo,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,t)=>{if(!Fs(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!t?.length)throw new TypeError("no paths specified to add/replace")});var wo=K(vt.syncFile,vt.asyncFile,vt.syncNoFile,vt.asyncNoFile,(s,t=[])=>{vt.validate?.(s,t),So(s)}),So=s=>{let t=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=t?(e,i)=>t(e,i)&&!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0)):(e,i)=>!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0))};export{F as Header,Et as Pack,di as PackJob,kt as PackSync,st as Parser,ct as Pax,Yt as ReadEntry,qt as Unpack,Te as UnpackSync,de as WriteEntry,si as WriteEntrySync,ri as WriteEntryTar,Zn as c,Zn as create,co as extract,Ki as filesFilter,It as list,vt as r,vt as replace,It as t,Bi as types,wo as u,wo as update,co as x}; +var vr=Object.defineProperty;var Mr=(s,t)=>{for(var e in t)vr(s,e,{get:t[e],enumerable:!0})};import Vr from"events";import I from"fs";import{EventEmitter as Li}from"node:events";import Ds from"node:stream";import{StringDecoder as Br}from"node:string_decoder";var Ts=typeof process=="object"&&process?process:{stdout:null,stderr:null},Pr=s=>!!s&&typeof s=="object"&&(s instanceof A||s instanceof Ds||zr(s)||Ur(s)),zr=s=>!!s&&typeof s=="object"&&s instanceof Li&&typeof s.pipe=="function"&&s.pipe!==Ds.Writable.prototype.pipe,Ur=s=>!!s&&typeof s=="object"&&s instanceof Li&&typeof s.write=="function"&&typeof s.end=="function",q=Symbol("EOF"),Q=Symbol("maybeEmitEnd"),rt=Symbol("emittedEnd"),Ne=Symbol("emittingEnd"),Qt=Symbol("emittedError"),De=Symbol("closed"),xs=Symbol("read"),Ae=Symbol("flush"),Ls=Symbol("flushChunk"),z=Symbol("encoding"),Mt=Symbol("decoder"),g=Symbol("flowing"),Jt=Symbol("paused"),Bt=Symbol("resume"),b=Symbol("buffer"),D=Symbol("pipes"),_=Symbol("bufferLength"),gi=Symbol("bufferPush"),Ie=Symbol("bufferShift"),L=Symbol("objectMode"),w=Symbol("destroyed"),bi=Symbol("error"),_i=Symbol("emitData"),Ns=Symbol("emitEnd"),Oi=Symbol("emitEnd2"),Z=Symbol("async"),Ti=Symbol("abort"),Ce=Symbol("aborted"),jt=Symbol("signal"),Rt=Symbol("dataListeners"),C=Symbol("discarded"),te=s=>Promise.resolve().then(s),Hr=s=>s(),Wr=s=>s==="end"||s==="finish"||s==="prefinish",Gr=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Zr=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),ke=class{src;dest;opts;ondrain;constructor(t,e,i){this.src=t,this.dest=e,this.opts=i,this.ondrain=()=>t[Bt](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(t){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},xi=class extends ke{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(t,e,i){super(t,e,i),this.proxyErrors=r=>this.dest.emit("error",r),t.on("error",this.proxyErrors)}},Yr=s=>!!s.objectMode,Kr=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",A=class extends Li{[g]=!1;[Jt]=!1;[D]=[];[b]=[];[L];[z];[Z];[Mt];[q]=!1;[rt]=!1;[Ne]=!1;[De]=!1;[Qt]=null;[_]=0;[w]=!1;[jt];[Ce]=!1;[Rt]=0;[C]=!1;writable=!0;readable=!0;constructor(...t){let e=t[0]||{};if(super(),e.objectMode&&typeof e.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Yr(e)?(this[L]=!0,this[z]=null):Kr(e)?(this[z]=e.encoding,this[L]=!1):(this[L]=!1,this[z]=null),this[Z]=!!e.async,this[Mt]=this[z]?new Br(this[z]):null,e&&e.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[b]}),e&&e.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[D]});let{signal:i}=e;i&&(this[jt]=i,i.aborted?this[Ti]():i.addEventListener("abort",()=>this[Ti]()))}get bufferLength(){return this[_]}get encoding(){return this[z]}set encoding(t){throw new Error("Encoding must be set at instantiation time")}setEncoding(t){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[L]}set objectMode(t){throw new Error("objectMode must be set at instantiation time")}get async(){return this[Z]}set async(t){this[Z]=this[Z]||!!t}[Ti](){this[Ce]=!0,this.emit("abort",this[jt]?.reason),this.destroy(this[jt]?.reason)}get aborted(){return this[Ce]}set aborted(t){}write(t,e,i){if(this[Ce])return!1;if(this[q])throw new Error("write after end");if(this[w])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof e=="function"&&(i=e,e="utf8"),e||(e="utf8");let r=this[Z]?te:Hr;if(!this[L]&&!Buffer.isBuffer(t)){if(Zr(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(Gr(t))t=Buffer.from(t);else if(typeof t!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[L]?(this[g]&&this[_]!==0&&this[Ae](!0),this[g]?this.emit("data",t):this[gi](t),this[_]!==0&&this.emit("readable"),i&&r(i),this[g]):t.length?(typeof t=="string"&&!(e===this[z]&&!this[Mt]?.lastNeed)&&(t=Buffer.from(t,e)),Buffer.isBuffer(t)&&this[z]&&(t=this[Mt].write(t)),this[g]&&this[_]!==0&&this[Ae](!0),this[g]?this.emit("data",t):this[gi](t),this[_]!==0&&this.emit("readable"),i&&r(i),this[g]):(this[_]!==0&&this.emit("readable"),i&&r(i),this[g])}read(t){if(this[w])return null;if(this[C]=!1,this[_]===0||t===0||t&&t>this[_])return this[Q](),null;this[L]&&(t=null),this[b].length>1&&!this[L]&&(this[b]=[this[z]?this[b].join(""):Buffer.concat(this[b],this[_])]);let e=this[xs](t||null,this[b][0]);return this[Q](),e}[xs](t,e){if(this[L])this[Ie]();else{let i=e;t===i.length||t===null?this[Ie]():typeof i=="string"?(this[b][0]=i.slice(t),e=i.slice(0,t),this[_]-=t):(this[b][0]=i.subarray(t),e=i.subarray(0,t),this[_]-=t)}return this.emit("data",e),!this[b].length&&!this[q]&&this.emit("drain"),e}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e="utf8"),t!==void 0&&this.write(t,e),i&&this.once("end",i),this[q]=!0,this.writable=!1,(this[g]||!this[Jt])&&this[Q](),this}[Bt](){this[w]||(!this[Rt]&&!this[D].length&&(this[C]=!0),this[Jt]=!1,this[g]=!0,this.emit("resume"),this[b].length?this[Ae]():this[q]?this[Q]():this.emit("drain"))}resume(){return this[Bt]()}pause(){this[g]=!1,this[Jt]=!0,this[C]=!1}get destroyed(){return this[w]}get flowing(){return this[g]}get paused(){return this[Jt]}[gi](t){this[L]?this[_]+=1:this[_]+=t.length,this[b].push(t)}[Ie](){return this[L]?this[_]-=1:this[_]-=this[b][0].length,this[b].shift()}[Ae](t=!1){do;while(this[Ls](this[Ie]())&&this[b].length);!t&&!this[b].length&&!this[q]&&this.emit("drain")}[Ls](t){return this.emit("data",t),this[g]}pipe(t,e){if(this[w])return t;this[C]=!1;let i=this[rt];return e=e||{},t===Ts.stdout||t===Ts.stderr?e.end=!1:e.end=e.end!==!1,e.proxyErrors=!!e.proxyErrors,i?e.end&&t.end():(this[D].push(e.proxyErrors?new xi(this,t,e):new ke(this,t,e)),this[Z]?te(()=>this[Bt]()):this[Bt]()),t}unpipe(t){let e=this[D].find(i=>i.dest===t);e&&(this[D].length===1?(this[g]&&this[Rt]===0&&(this[g]=!1),this[D]=[]):this[D].splice(this[D].indexOf(e),1),e.unpipe())}addListener(t,e){return this.on(t,e)}on(t,e){let i=super.on(t,e);if(t==="data")this[C]=!1,this[Rt]++,!this[D].length&&!this[g]&&this[Bt]();else if(t==="readable"&&this[_]!==0)super.emit("readable");else if(Wr(t)&&this[rt])super.emit(t),this.removeAllListeners(t);else if(t==="error"&&this[Qt]){let r=e;this[Z]?te(()=>r.call(this,this[Qt])):r.call(this,this[Qt])}return i}removeListener(t,e){return this.off(t,e)}off(t,e){let i=super.off(t,e);return t==="data"&&(this[Rt]=this.listeners("data").length,this[Rt]===0&&!this[C]&&!this[D].length&&(this[g]=!1)),i}removeAllListeners(t){let e=super.removeAllListeners(t);return(t==="data"||t===void 0)&&(this[Rt]=0,!this[C]&&!this[D].length&&(this[g]=!1)),e}get emittedEnd(){return this[rt]}[Q](){!this[Ne]&&!this[rt]&&!this[w]&&this[b].length===0&&this[q]&&(this[Ne]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[De]&&this.emit("close"),this[Ne]=!1)}emit(t,...e){let i=e[0];if(t!=="error"&&t!=="close"&&t!==w&&this[w])return!1;if(t==="data")return!this[L]&&!i?!1:this[Z]?(te(()=>this[_i](i)),!0):this[_i](i);if(t==="end")return this[Ns]();if(t==="close"){if(this[De]=!0,!this[rt]&&!this[w])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(t==="error"){this[Qt]=i,super.emit(bi,i);let n=!this[jt]||this.listeners("error").length?super.emit("error",i):!1;return this[Q](),n}else if(t==="resume"){let n=super.emit("resume");return this[Q](),n}else if(t==="finish"||t==="prefinish"){let n=super.emit(t);return this.removeAllListeners(t),n}let r=super.emit(t,...e);return this[Q](),r}[_i](t){for(let i of this[D])i.dest.write(t)===!1&&this.pause();let e=this[C]?!1:super.emit("data",t);return this[Q](),e}[Ns](){return this[rt]?!1:(this[rt]=!0,this.readable=!1,this[Z]?(te(()=>this[Oi]()),!0):this[Oi]())}[Oi](){if(this[Mt]){let e=this[Mt].end();if(e){for(let i of this[D])i.dest.write(e);this[C]||super.emit("data",e)}}for(let e of this[D])e.end();let t=super.emit("end");return this.removeAllListeners("end"),t}async collect(){let t=Object.assign([],{dataLength:0});this[L]||(t.dataLength=0);let e=this.promise();return this.on("data",i=>{t.push(i),this[L]||(t.dataLength+=i.length)}),await e,t}async concat(){if(this[L])throw new Error("cannot concat in objectMode");let t=await this.collect();return this[z]?t.join(""):Buffer.concat(t,t.dataLength)}async promise(){return new Promise((t,e)=>{this.on(w,()=>e(new Error("stream destroyed"))),this.on("error",i=>e(i)),this.on("end",()=>t())})}[Symbol.asyncIterator](){this[C]=!1;let t=!1,e=async()=>(this.pause(),t=!0,{value:void 0,done:!0});return{next:()=>{if(t)return e();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[q])return e();let n,o,h=d=>{this.off("data",a),this.off("end",l),this.off(w,c),e(),o(d)},a=d=>{this.off("error",h),this.off("end",l),this.off(w,c),this.pause(),n({value:d,done:!!this[q]})},l=()=>{this.off("error",h),this.off("data",a),this.off(w,c),e(),n({done:!0,value:void 0})},c=()=>h(new Error("stream destroyed"));return new Promise((d,S)=>{o=S,n=d,this.once(w,c),this.once("error",h),this.once("end",l),this.once("data",a)})},throw:e,return:e,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[C]=!1;let t=!1,e=()=>(this.pause(),this.off(bi,e),this.off(w,e),this.off("end",e),t=!0,{done:!0,value:void 0}),i=()=>{if(t)return e();let r=this.read();return r===null?e():{done:!1,value:r}};return this.once("end",e),this.once(bi,e),this.once(w,e),{next:i,throw:e,return:e,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(t){if(this[w])return t?this.emit("error",t):this.emit(w),this;this[w]=!0,this[C]=!0,this[b].length=0,this[_]=0;let e=this;return typeof e.close=="function"&&!this[De]&&e.close(),t?this.emit("error",t):this.emit(w),this}static get isStream(){return Pr}};var $r=I.writev,ot=Symbol("_autoClose"),H=Symbol("_close"),ee=Symbol("_ended"),m=Symbol("_fd"),Ni=Symbol("_finished"),j=Symbol("_flags"),Di=Symbol("_flush"),ki=Symbol("_handleChunk"),Fi=Symbol("_makeBuf"),se=Symbol("_mode"),Fe=Symbol("_needDrain"),Ut=Symbol("_onerror"),Ht=Symbol("_onopen"),Ai=Symbol("_onread"),Pt=Symbol("_onwrite"),ht=Symbol("_open"),U=Symbol("_path"),nt=Symbol("_pos"),Y=Symbol("_queue"),zt=Symbol("_read"),Ii=Symbol("_readSize"),J=Symbol("_reading"),ie=Symbol("_remain"),Ci=Symbol("_size"),ve=Symbol("_write"),gt=Symbol("_writing"),Me=Symbol("_defaultFlag"),bt=Symbol("_errored"),_t=class extends A{[bt]=!1;[m];[U];[Ii];[J]=!1;[Ci];[ie];[ot];constructor(t,e){if(e=e||{},super(e),this.readable=!0,this.writable=!1,typeof t!="string")throw new TypeError("path must be a string");this[bt]=!1,this[m]=typeof e.fd=="number"?e.fd:void 0,this[U]=t,this[Ii]=e.readSize||16*1024*1024,this[J]=!1,this[Ci]=typeof e.size=="number"?e.size:1/0,this[ie]=this[Ci],this[ot]=typeof e.autoClose=="boolean"?e.autoClose:!0,typeof this[m]=="number"?this[zt]():this[ht]()}get fd(){return this[m]}get path(){return this[U]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[ht](){I.open(this[U],"r",(t,e)=>this[Ht](t,e))}[Ht](t,e){t?this[Ut](t):(this[m]=e,this.emit("open",e),this[zt]())}[Fi](){return Buffer.allocUnsafe(Math.min(this[Ii],this[ie]))}[zt](){if(!this[J]){this[J]=!0;let t=this[Fi]();if(t.length===0)return process.nextTick(()=>this[Ai](null,0,t));I.read(this[m],t,0,t.length,null,(e,i,r)=>this[Ai](e,i,r))}}[Ai](t,e,i){this[J]=!1,t?this[Ut](t):this[ki](e,i)&&this[zt]()}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}[Ut](t){this[J]=!0,this[H](),this.emit("error",t)}[ki](t,e){let i=!1;return this[ie]-=t,t>0&&(i=super.write(tthis[Ht](t,e))}[Ht](t,e){this[Me]&&this[j]==="r+"&&t&&t.code==="ENOENT"?(this[j]="w",this[ht]()):t?this[Ut](t):(this[m]=e,this.emit("open",e),this[gt]||this[Di]())}end(t,e){return t&&this.write(t,e),this[ee]=!0,!this[gt]&&!this[Y].length&&typeof this[m]=="number"&&this[Pt](null,0),this}write(t,e){return typeof t=="string"&&(t=Buffer.from(t,e)),this[ee]?(this.emit("error",new Error("write() after end()")),!1):this[m]===void 0||this[gt]||this[Y].length?(this[Y].push(t),this[Fe]=!0,!1):(this[gt]=!0,this[ve](t),!0)}[ve](t){I.write(this[m],t,0,t.length,this[nt],(e,i)=>this[Pt](e,i))}[Pt](t,e){t?this[Ut](t):(this[nt]!==void 0&&typeof e=="number"&&(this[nt]+=e),this[Y].length?this[Di]():(this[gt]=!1,this[ee]&&!this[Ni]?(this[Ni]=!0,this[H](),this.emit("finish")):this[Fe]&&(this[Fe]=!1,this.emit("drain"))))}[Di](){if(this[Y].length===0)this[ee]&&this[Pt](null,0);else if(this[Y].length===1)this[ve](this[Y].pop());else{let t=this[Y];this[Y]=[],$r(this[m],t,this[nt],(e,i)=>this[Pt](e,i))}}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}},Wt=class extends tt{[ht](){let t;if(this[Me]&&this[j]==="r+")try{t=I.openSync(this[U],this[j],this[se])}catch(e){if(e?.code==="ENOENT")return this[j]="w",this[ht]();throw e}else t=I.openSync(this[U],this[j],this[se]);this[Ht](null,t)}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.closeSync(t),this.emit("close")}}[ve](t){let e=!0;try{this[Pt](null,I.writeSync(this[m],t,0,t.length,this[nt])),e=!1}finally{if(e)try{this[H]()}catch{}}}};import or from"node:path";import Vt from"node:fs";import{dirname as Ln,parse as Nn}from"path";var Xr=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),As=s=>!!s.sync&&!!s.file,Is=s=>!s.sync&&!!s.file,Cs=s=>!!s.sync&&!s.file,ks=s=>!s.sync&&!s.file;var Fs=s=>!!s.file;var qr=s=>{let t=Xr.get(s);return t||s},re=(s={})=>{if(!s)return{};let t={};for(let[e,i]of Object.entries(s)){let r=qr(e);t[r]=i}return t.chmod===void 0&&t.noChmod===!1&&(t.chmod=!0),delete t.noChmod,t};var K=(s,t,e,i,r)=>Object.assign((n=[],o,h)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(h=o,o=void 0),o=o?Array.from(o):[];let a=re(n);if(r?.(a,o),As(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return s(a,o)}else if(Is(a)){let l=t(a,o);return h?l.then(()=>h(),h):l}else if(Cs(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return e(a,o)}else if(ks(a)){if(typeof h=="function")throw new TypeError("callback only supported with file option");return i(a,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:t,syncNoFile:e,asyncNoFile:i,validate:r});import{EventEmitter as _n}from"events";import Pi from"assert";import{Buffer as Ot}from"buffer";import*as vs from"zlib";import Qr from"zlib";var Jr=Qr.constants||{ZLIB_VERNUM:4736},M=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},Jr));var jr=Ot.concat,Ms=Object.getOwnPropertyDescriptor(Ot,"concat"),tn=s=>s,Mi=Ms?.writable===!0||Ms?.set!==void 0?s=>{Ot.concat=s?tn:jr}:s=>{},Tt=Symbol("_superWrite"),Gt=class extends Error{code;errno;constructor(t,e){super("zlib: "+t.message,{cause:t}),this.code=t.code,this.errno=t.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+t.message,Error.captureStackTrace(this,e??this.constructor)}get name(){return"ZlibError"}},Bi=Symbol("flushFlag"),ne=class extends A{#t=!1;#i=!1;#s;#n;#r;#e;#o;get sawError(){return this.#t}get handle(){return this.#e}get flushFlag(){return this.#s}constructor(t,e){if(!t||typeof t!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(t),this.#s=t.flush??0,this.#n=t.finishFlush??0,this.#r=t.fullFlushFlag??0,typeof vs[e]!="function")throw new TypeError("Compression method not supported: "+e);try{this.#e=new vs[e](t)}catch(i){throw new Gt(i,this.constructor)}this.#o=i=>{this.#t||(this.#t=!0,this.close(),this.emit("error",i))},this.#e?.on("error",i=>this.#o(new Gt(i))),this.once("end",()=>this.close)}close(){this.#e&&(this.#e.close(),this.#e=void 0,this.emit("close"))}reset(){if(!this.#t)return Pi(this.#e,"zlib binding closed"),this.#e.reset?.()}flush(t){this.ended||(typeof t!="number"&&(t=this.#r),this.write(Object.assign(Ot.alloc(0),{[Bi]:t})))}end(t,e,i){return typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&(e?this.write(t,e):this.write(t)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Tt](t){return super.write(t)}write(t,e,i){if(typeof e=="function"&&(i=e,e="utf8"),typeof t=="string"&&(t=Ot.from(t,e)),this.#t)return;Pi(this.#e,"zlib binding closed");let r=this.#e._handle,n=r.close;r.close=()=>{};let o=this.#e.close;this.#e.close=()=>{},Mi(!0);let h;try{let l=typeof t[Bi]=="number"?t[Bi]:this.#s;h=this.#e._processChunk(t,l),Mi(!1)}catch(l){Mi(!1),this.#o(new Gt(l,this.write))}finally{this.#e&&(this.#e._handle=r,r.close=n,this.#e.close=o,this.#e.removeAllListeners("error"))}this.#e&&this.#e.on("error",l=>this.#o(new Gt(l,this.write)));let a;if(h)if(Array.isArray(h)&&h.length>0){let l=h[0];a=this[Tt](Ot.from(l));for(let c=1;c{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(t,e)}finally{this.handle.flush=i}this.handle&&(this.#t=t,this.#i=e)}}}};var ze=class extends Pe{#t;constructor(t){super(t,"Gzip"),this.#t=t&&!!t.portable}[Tt](t){return this.#t?(this.#t=!1,t[9]=255,super[Tt](t)):super[Tt](t)}};var Ue=class extends Pe{constructor(t){super(t,"Unzip")}},He=class extends ne{constructor(t,e){t=t||{},t.flush=t.flush||M.BROTLI_OPERATION_PROCESS,t.finishFlush=t.finishFlush||M.BROTLI_OPERATION_FINISH,t.fullFlushFlag=M.BROTLI_OPERATION_FLUSH,super(t,e)}},We=class extends He{constructor(t){super(t,"BrotliCompress")}},Ge=class extends He{constructor(t){super(t,"BrotliDecompress")}},Ze=class extends ne{constructor(t,e){t=t||{},t.flush=t.flush||M.ZSTD_e_continue,t.finishFlush=t.finishFlush||M.ZSTD_e_end,t.fullFlushFlag=M.ZSTD_e_flush,super(t,e)}},Ye=class extends Ze{constructor(t){super(t,"ZstdCompress")}},Ke=class extends Ze{constructor(t){super(t,"ZstdDecompress")}};import{posix as Zt}from"node:path";var Bs=(s,t)=>{if(Number.isSafeInteger(s))s<0?rn(s,t):sn(s,t);else throw Error("cannot encode number outside of javascript safe integer range");return t},sn=(s,t)=>{t[0]=128;for(var e=t.length;e>1;e--)t[e-1]=s&255,s=Math.floor(s/256)},rn=(s,t)=>{t[0]=255;var e=!1;s=s*-1;for(var i=t.length;i>1;i--){var r=s&255;s=Math.floor(s/256),e?t[i-1]=zs(r):r===0?t[i-1]=0:(e=!0,t[i-1]=Us(r))}},Ps=s=>{let t=s[0],e=t===128?on(s.subarray(1,s.length)):t===255?nn(s):null;if(e===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(e))throw Error("parsed number outside of javascript safe integer range");return e},nn=s=>{for(var t=s.length,e=0,i=!1,r=t-1;r>-1;r--){var n=Number(s[r]),o;i?o=zs(n):n===0?o=n:(i=!0,o=Us(n)),o!==0&&(e-=o*Math.pow(256,t-r-1))}return e},on=s=>{for(var t=s.length,e=0,i=t-1;i>-1;i--){var r=Number(s[i]);r!==0&&(e+=r*Math.pow(256,t-i-1))}return e},zs=s=>(255^s)&255,Us=s=>(255^s)+1&255;var zi={};Mr(zi,{code:()=>Ve,isCode:()=>oe,isName:()=>an,name:()=>he});var oe=s=>he.has(s),an=s=>Ve.has(s),he=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]),Ve=new Map(Array.from(he).map(s=>[s[1],s[0]]));var k=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#t="Unsupported";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(t,e=0,i,r){Buffer.isBuffer(t)?this.decode(t,e||0,i,r):t&&this.#i(t)}decode(t,e,i,r){if(e||(e=0),!t||!(t.length>=e+512))throw new Error("need 512 bytes for header");this.path=i?.path??xt(t,e,100),this.mode=i?.mode??r?.mode??at(t,e+100,8),this.uid=i?.uid??r?.uid??at(t,e+108,8),this.gid=i?.gid??r?.gid??at(t,e+116,8),this.size=i?.size??r?.size??at(t,e+124,12),this.mtime=i?.mtime??r?.mtime??Ui(t,e+136,12),this.cksum=at(t,e+148,12),r&&this.#i(r,!0),i&&this.#i(i);let n=xt(t,e+156,1);if(oe(n)&&(this.#t=n||"0"),this.#t==="0"&&this.path.slice(-1)==="/"&&(this.#t="5"),this.#t==="5"&&(this.size=0),this.linkpath=xt(t,e+157,100),t.subarray(e+257,e+265).toString()==="ustar\x0000")if(this.uname=i?.uname??r?.uname??xt(t,e+265,32),this.gname=i?.gname??r?.gname??xt(t,e+297,32),this.devmaj=i?.devmaj??r?.devmaj??at(t,e+329,8)??0,this.devmin=i?.devmin??r?.devmin??at(t,e+337,8)??0,t[e+475]!==0){let h=xt(t,e+345,155);this.path=h+"/"+this.path}else{let h=xt(t,e+345,130);h&&(this.path=h+"/"+this.path),this.atime=i?.atime??r?.atime??Ui(t,e+476,12),this.ctime=i?.ctime??r?.ctime??Ui(t,e+488,12)}let o=256;for(let h=e;h!(r==null||i==="path"&&e||i==="linkpath"&&e||i==="global"))))}encode(t,e=0){if(t||(t=this.block=Buffer.alloc(512)),this.#t==="Unsupported"&&(this.#t="0"),!(t.length>=e+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=ln(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Lt(t,e,100,n)||this.needPax,this.needPax=lt(t,e+100,8,this.mode)||this.needPax,this.needPax=lt(t,e+108,8,this.uid)||this.needPax,this.needPax=lt(t,e+116,8,this.gid)||this.needPax,this.needPax=lt(t,e+124,12,this.size)||this.needPax,this.needPax=Hi(t,e+136,12,this.mtime)||this.needPax,t[e+156]=Number(this.#t.codePointAt(0)),this.needPax=Lt(t,e+157,100,this.linkpath)||this.needPax,t.write("ustar\x0000",e+257,8),this.needPax=Lt(t,e+265,32,this.uname)||this.needPax,this.needPax=Lt(t,e+297,32,this.gname)||this.needPax,this.needPax=lt(t,e+329,8,this.devmaj)||this.needPax,this.needPax=lt(t,e+337,8,this.devmin)||this.needPax,this.needPax=Lt(t,e+345,i,o)||this.needPax,t[e+475]!==0?this.needPax=Lt(t,e+345,155,o)||this.needPax:(this.needPax=Lt(t,e+345,130,o)||this.needPax,this.needPax=Hi(t,e+476,12,this.atime)||this.needPax,this.needPax=Hi(t,e+488,12,this.ctime)||this.needPax);let h=256;for(let a=e;a{let i=s,r="",n,o=Zt.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Zt.dirname(i),i=Zt.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=t?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=t?n=[i.slice(0,99),r,!0]:(i=Zt.join(Zt.basename(r),i),r=Zt.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},xt=(s,t,e)=>s.subarray(t,t+e).toString("utf8").replace(/\0.*/,""),Ui=(s,t,e)=>cn(at(s,t,e)),cn=s=>s===void 0?void 0:new Date(s*1e3),at=(s,t,e)=>Number(s[t])&128?Ps(s.subarray(t,t+e)):dn(s,t,e),fn=s=>isNaN(s)?void 0:s,dn=(s,t,e)=>fn(parseInt(s.subarray(t,t+e).toString("utf8").replace(/\0.*$/,"").trim(),8)),un={12:8589934591,8:2097151},lt=(s,t,e,i)=>i===void 0?!1:i>un[e]||i<0?(Bs(i,s.subarray(t,t+e)),!0):(mn(s,t,e,i),!1),mn=(s,t,e,i)=>s.write(pn(i,e),t,e,"ascii"),pn=(s,t)=>En(Math.floor(s).toString(8),t),En=(s,t)=>(s.length===t-1?s:new Array(t-s.length-1).join("0")+s+" ")+"\0",Hi=(s,t,e,i)=>i===void 0?!1:lt(s,t,e,i.getTime()/1e3),wn=new Array(156).join("\0"),Lt=(s,t,e,i)=>i===void 0?!1:(s.write(i+wn,t,e,"utf8"),i.length!==Buffer.byteLength(i)||i.length>e);import{basename as Sn}from"node:path";var ct=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(t,e=!1){this.atime=t.atime,this.charset=t.charset,this.comment=t.comment,this.ctime=t.ctime,this.dev=t.dev,this.gid=t.gid,this.global=e,this.gname=t.gname,this.ino=t.ino,this.linkpath=t.linkpath,this.mtime=t.mtime,this.nlink=t.nlink,this.path=t.path,this.size=t.size,this.uid=t.uid,this.uname=t.uname}encode(){let t=this.encodeBody();if(t==="")return Buffer.allocUnsafe(0);let e=Buffer.byteLength(t),i=512*Math.ceil(1+e/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new k({path:("PaxHeader/"+Sn(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:e,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(t,512,e,"utf8");for(let n=e+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(t,e,i=!1){return new s(yn(Rn(t),e),i)}},yn=(s,t)=>t?Object.assign({},t,s):s,Rn=s=>s.replace(/\n$/,"").split(` +`).reduce(gn,Object.create(null)),gn=(s,t)=>{let e=parseInt(t,10);if(e!==Buffer.byteLength(t)+1)return s;t=t.slice((e+" ").length);let i=t.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=");return s[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(o)*1e3):/^[0-9]+$/.test(o)?+o:o,s};var bn=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,f=bn!=="win32"?s=>s:s=>s&&s.replaceAll(/\\/g,"/");var Yt=class extends A{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(t,e,i){switch(super({}),this.pause(),this.extended=e,this.globalExtended=i,this.header=t,this.remain=t.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=t.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!t.path)throw new Error("no path provided for tar.ReadEntry");this.path=f(t.path),this.mode=t.mode,this.mode&&(this.mode=this.mode&4095),this.uid=t.uid,this.gid=t.gid,this.uname=t.uname,this.gname=t.gname,this.size=this.remain,this.mtime=t.mtime,this.atime=t.atime,this.ctime=t.ctime,this.linkpath=t.linkpath?f(t.linkpath):void 0,this.uname=t.uname,this.gname=t.gname,e&&this.#t(e),i&&this.#t(i,!0)}write(t){let e=t.length;if(e>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-e),this.blockRemain=Math.max(0,r-e),this.ignore?!0:i>=e?super.write(t):super.write(t.subarray(0,i))}#t(t,e=!1){t.path&&(t.path=f(t.path)),t.linkpath&&(t.linkpath=f(t.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(t).filter(([i,r])=>!(r==null||i==="path"&&e))))}};var Nt=(s,t,e,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=e instanceof Error&&e.code||t,i.tarCode=t,!s.strict&&i.recoverable!==!1?(e instanceof Error&&(i=Object.assign(e,i),e=e.message),s.emit("warn",t,e,i)):e instanceof Error?s.emit("error",Object.assign(e,i)):s.emit("error",Object.assign(new Error(`${t}: ${e}`),i))};var On=1024*1024,Ki=Buffer.from([31,139]),Vi=Buffer.from([40,181,47,253]),Tn=Math.max(Ki.length,Vi.length),B=Symbol("state"),Dt=Symbol("writeEntry"),et=Symbol("readEntry"),Wi=Symbol("nextEntry"),Hs=Symbol("processEntry"),V=Symbol("extendedHeader"),ae=Symbol("globalExtendedHeader"),ft=Symbol("meta"),Ws=Symbol("emitMeta"),p=Symbol("buffer"),it=Symbol("queue"),dt=Symbol("ended"),Gi=Symbol("emittedEnd"),At=Symbol("emit"),y=Symbol("unzip"),$e=Symbol("consumeChunk"),Xe=Symbol("consumeChunkSub"),Zi=Symbol("consumeBody"),Gs=Symbol("consumeMeta"),Zs=Symbol("consumeHeader"),le=Symbol("consuming"),Yi=Symbol("bufferConcat"),qe=Symbol("maybeEnd"),Kt=Symbol("writing"),ut=Symbol("aborted"),Qe=Symbol("onDone"),It=Symbol("sawValidEntry"),Je=Symbol("sawNullBlock"),je=Symbol("sawEOF"),Ys=Symbol("closeStream"),xn=()=>!0,st=class extends _n{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[it]=[];[p];[et];[Dt];[B]="begin";[ft]="";[V];[ae];[dt]=!1;[y];[ut]=!1;[It];[Je]=!1;[je]=!1;[Kt]=!1;[le]=!1;[Gi]=!1;constructor(t={}){super(),this.file=t.file||"",this.on(Qe,()=>{(this[B]==="begin"||this[It]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),t.ondone?this.on(Qe,t.ondone):this.on(Qe,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!t.strict,this.maxMetaEntrySize=t.maxMetaEntrySize||On,this.filter=typeof t.filter=="function"?t.filter:xn;let e=t.file&&(t.file.endsWith(".tar.br")||t.file.endsWith(".tbr"));this.brotli=!(t.gzip||t.zstd)&&t.brotli!==void 0?t.brotli:e?void 0:!1;let i=t.file&&(t.file.endsWith(".tar.zst")||t.file.endsWith(".tzst"));this.zstd=!(t.gzip||t.brotli)&&t.zstd!==void 0?t.zstd:i?!0:void 0,this.on("end",()=>this[Ys]()),typeof t.onwarn=="function"&&this.on("warn",t.onwarn),typeof t.onReadEntry=="function"&&this.on("entry",t.onReadEntry)}warn(t,e,i={}){Nt(this,t,e,i)}[Zs](t,e){this[It]===void 0&&(this[It]=!1);let i;try{i=new k(t,e,this[V],this[ae])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[Je]?(this[je]=!0,this[B]==="begin"&&(this[B]="header"),this[At]("eof")):(this[Je]=!0,this[At]("nullBlock"));else if(this[Je]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[Dt]=new Yt(i,this[V],this[ae]);if(!this[It])if(n.remain){let o=()=>{n.invalid||(this[It]=!0)};n.on("end",o)}else this[It]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[At]("ignoredEntry",n),this[B]="ignore",n.resume()):n.size>0&&(this[ft]="",n.on("data",o=>this[ft]+=o),this[B]="meta"):(this[V]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[At]("ignoredEntry",n),this[B]=n.remain?"ignore":"header",n.resume()):(n.remain?this[B]="body":(this[B]="header",n.end()),this[et]?this[it].push(n):(this[it].push(n),this[Wi]())))}}}[Ys](){queueMicrotask(()=>this.emit("close"))}[Hs](t){let e=!0;if(!t)this[et]=void 0,e=!1;else if(Array.isArray(t)){let[i,...r]=t;this.emit(i,...r)}else this[et]=t,this.emit("entry",t),t.emittedEnd||(t.on("end",()=>this[Wi]()),e=!1);return e}[Wi](){do;while(this[Hs](this[it].shift()));if(this[it].length===0){let t=this[et];!t||t.flowing||t.size===t.remain?this[Kt]||this.emit("drain"):t.once("drain",()=>this.emit("drain"))}}[Zi](t,e){let i=this[Dt];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=t.length&&e===0?t:t.subarray(e,e+r);return i.write(n),i.blockRemain||(this[B]="header",this[Dt]=void 0,i.end()),n.length}[Gs](t,e){let i=this[Dt],r=this[Zi](t,e);return!this[Dt]&&i&&this[Ws](i),r}[At](t,e,i){this[it].length===0&&!this[et]?this.emit(t,e,i):this[it].push([t,e,i])}[Ws](t){switch(this[At]("meta",this[ft]),t.type){case"ExtendedHeader":case"OldExtendedHeader":this[V]=ct.parse(this[ft],this[V],!1);break;case"GlobalExtendedHeader":this[ae]=ct.parse(this[ft],this[ae],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let e=this[V]??Object.create(null);this[V]=e,e.path=this[ft].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let e=this[V]||Object.create(null);this[V]=e,e.linkpath=this[ft].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+t.type)}}abort(t){this[ut]=!0,this.emit("abort",t),this.warn("TAR_ABORT",t,{recoverable:!1})}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this[ut])return i?.(),!1;if((this[y]===void 0||this.brotli===void 0&&this[y]===!1)&&t){if(this[p]&&(t=Buffer.concat([this[p],t]),this[p]=void 0),t.lengththis[$e](c)),this[y].on("error",c=>this.abort(c)),this[y].on("end",()=>{this[dt]=!0,this[$e]()}),this[Kt]=!0;let l=!!this[y][a?"end":"write"](t);return this[Kt]=!1,i?.(),l}}this[Kt]=!0,this[y]?this[y].write(t):this[$e](t),this[Kt]=!1;let n=this[it].length>0?!1:this[et]?this[et].flowing:!0;return!n&&this[it].length===0&&this[et]?.once("drain",()=>this.emit("drain")),i?.(),n}[Yi](t){t&&!this[ut]&&(this[p]=this[p]?Buffer.concat([this[p],t]):t)}[qe](){if(this[dt]&&!this[Gi]&&!this[ut]&&!this[le]){this[Gi]=!0;let t=this[Dt];if(t&&t.blockRemain){let e=this[p]?this[p].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${t.blockRemain} more bytes, only ${e} available)`,{entry:t}),this[p]&&t.write(this[p]),t.end()}this[At](Qe)}}[$e](t){if(this[le]&&t)this[Yi](t);else if(!t&&!this[p])this[qe]();else if(t){if(this[le]=!0,this[p]){this[Yi](t);let e=this[p];this[p]=void 0,this[Xe](e)}else this[Xe](t);for(;this[p]&&this[p]?.length>=512&&!this[ut]&&!this[je];){let e=this[p];this[p]=void 0,this[Xe](e)}this[le]=!1}(!this[p]||this[dt])&&this[qe]()}[Xe](t){let e=0,i=t.length;for(;e+512<=i&&!this[ut]&&!this[je];)switch(this[B]){case"begin":case"header":this[Zs](t,e),e+=512;break;case"ignore":case"body":e+=this[Zi](t,e);break;case"meta":e+=this[Gs](t,e);break;default:throw new Error("invalid state: "+this[B])}e{let t=s.length-1,e=-1;for(;t>-1&&s.charAt(t)==="/";)e=t,t--;return e===-1?s:s.slice(0,e)};var Dn=s=>{let t=s.onReadEntry;s.onReadEntry=t?e=>{t(e),e.resume()}:e=>e.resume()},$i=(s,t)=>{let e=new Map(t.map(n=>[mt(n),!0])),i=s.filter,r=(n,o="")=>{let h=o||Nn(n).root||".",a;if(n===h)a=!1;else{let l=e.get(n);a=l!==void 0?l:r(Ln(n),h)}return e.set(n,a),a};s.filter=i?(n,o)=>i(n,o)&&r(mt(n)):n=>r(mt(n))},An=s=>{let t=new st(s),e=s.file,i;try{i=Vt.openSync(e,"r");let r=Vt.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let e=new st(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{e.on("error",h),e.on("end",o),Vt.stat(r,(a,l)=>{if(a)h(a);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",h),c.pipe(e)}})})},Ct=K(An,In,s=>new st(s),s=>new st(s),(s,t)=>{t?.length&&$i(s,t),s.noResume||Dn(s)});import ui from"fs";import $ from"fs";import Xs from"path";var Xi=(s,t,e)=>(s&=4095,e&&(s=(s|384)&-19),t&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);import{win32 as Cn}from"node:path";var{isAbsolute:kn,parse:Ks}=Cn,ce=s=>{let t="",e=Ks(s);for(;kn(s)||e.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":e.root;s=s.slice(i.length),t+=i,e=Ks(s)}return[t,s]};var ti=["|","<",">","?",":"],qi=ti.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),Fn=new Map(ti.map((s,t)=>[s,qi[t]])),vn=new Map(qi.map((s,t)=>[s,ti[t]])),Qi=s=>ti.reduce((t,e)=>t.split(e).join(Fn.get(e)),s),Vs=s=>qi.reduce((t,e)=>t.split(e).join(vn.get(e)),s);var tr=(s,t)=>t?(s=f(s).replace(/^\.(\/|$)/,""),mt(t)+"/"+s):f(s),Mn=16*1024*1024,qs=Symbol("process"),Qs=Symbol("file"),Js=Symbol("directory"),ji=Symbol("symlink"),js=Symbol("hardlink"),fe=Symbol("header"),ei=Symbol("read"),ts=Symbol("lstat"),ii=Symbol("onlstat"),es=Symbol("onread"),is=Symbol("onreadlink"),ss=Symbol("openfile"),rs=Symbol("onopenfile"),pt=Symbol("close"),si=Symbol("mode"),ns=Symbol("awaitDrain"),Ji=Symbol("ondrain"),X=Symbol("prefix"),de=class extends A{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#t=!1;constructor(t,e={}){let i=re(e);super(),this.path=f(t),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||Mn,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=f(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?f(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,h]=ce(this.path);o&&typeof h=="string"&&(this.path=h,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=Vs(this.path.replaceAll(/\\/g,"/")),t=t.replaceAll(/\\/g,"/")),this.absolute=f(i.absolute||Xs.resolve(this.cwd,t)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[ii](n):this[ts]()}warn(t,e,i={}){return Nt(this,t,e,i)}emit(t,...e){return t==="error"&&(this.#t=!0),super.emit(t,...e)}[ts](){$.lstat(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[ii](e)})}[ii](t){this.statCache.set(this.absolute,t),this.stat=t,t.isFile()||(t.size=0),this.type=Bn(t),this.emit("stat",t),this[qs]()}[qs](){switch(this.type){case"File":return this[Qs]();case"Directory":return this[Js]();case"SymbolicLink":return this[ji]();default:return this.end()}}[si](t){return Xi(t,this.type==="Directory",this.portable)}[X](t){return tr(t,this.prefix)}[fe](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new k({path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,mode:this[si](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new ct({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let t=this.header?.block;if(!t)throw new Error("failed to encode header");super.write(t)}[Js](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[fe](),this.end()}[ji](){$.readlink(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[is](e)})}[is](t){this.linkpath=f(t),this[fe](),this.end()}[js](t){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=f(Xs.relative(this.cwd,t)),this.stat.size=0,this[fe](),this.end()}[Qs](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let t=`${this.stat.dev}:${this.stat.ino}`,e=this.linkCache.get(t);if(e?.indexOf(this.cwd)===0)return this[js](e);this.linkCache.set(t,this.absolute)}if(this[fe](),this.stat.size===0)return this.end();this[ss]()}[ss](){$.open(this.absolute,"r",(t,e)=>{if(t)return this.emit("error",t);this[rs](e)})}[rs](t){if(this.fd=t,this.#t)return this[pt]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let e=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(e),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[ei]()}[ei](){let{fd:t,buf:e,offset:i,length:r,pos:n}=this;if(t===void 0||e===void 0)throw new Error("cannot read file without first opening");$.read(t,e,i,r,n,(o,h)=>{if(o)return this[pt](()=>this.emit("error",o));this[es](h)})}[pt](t=()=>{}){this.fd!==void 0&&$.close(this.fd,t)}[es](t){if(t<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(t>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(t===this.remain)for(let r=t;rthis[Ji]())}[ns](t){this.once("drain",t)}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this.blockRemaint?this.emit("error",t):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[ei]()}},ri=class extends de{sync=!0;[ts](){this[ii]($.lstatSync(this.absolute))}[ji](){this[is]($.readlinkSync(this.absolute))}[ss](){this[rs]($.openSync(this.absolute,"r"))}[ei](){let t=!0;try{let{fd:e,buf:i,offset:r,length:n,pos:o}=this;if(e===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let h=$.readSync(e,i,r,n,o);this[es](h),t=!1}finally{if(t)try{this[pt](()=>{})}catch{}}}[ns](t){t()}[pt](t=()=>{}){this.fd!==void 0&&$.closeSync(this.fd),t()}},ni=class extends A{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(t,e,i={}){return Nt(this,t,e,i)}constructor(t,e={}){let i=re(e);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=t;let{type:r}=t;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=f(t.path),this.mode=t.mode!==void 0?this[si](t.mode):void 0,this.uid=this.portable?void 0:t.uid,this.gid=this.portable?void 0:t.gid,this.uname=this.portable?void 0:t.uname,this.gname=this.portable?void 0:t.gname,this.size=t.size,this.mtime=this.noMtime?void 0:i.mtime||t.mtime,this.atime=this.portable?void 0:t.atime,this.ctime=this.portable?void 0:t.ctime,this.linkpath=t.linkpath!==void 0?f(t.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[h,a]=ce(this.path);h&&typeof a=="string"&&(this.path=a,n=h)}this.remain=t.size,this.blockRemain=t.startBlockSize,this.onWriteEntry?.(this),this.header=new k({path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new ct({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),t.pipe(this)}[X](t){return tr(t,this.prefix)}[si](t){return Xi(t,this.type==="Directory",this.portable)}write(t,e,i){typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8"));let r=t.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(t,i)}end(t,e,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,e??"utf8")),i&&this.once("finish",i),t?super.end(t,i):super.end(i),this}},Bn=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported";var oi=class s{tail;head;length=0;static create(t=[]){return new s(t)}constructor(t=[]){for(let e of t)this.push(e)}*[Symbol.iterator](){for(let t=this.head;t;t=t.next)yield t.value}removeNode(t){if(t.list!==this)throw new Error("removing node which does not belong to this list");let e=t.next,i=t.prev;return e&&(e.prev=i),i&&(i.next=e),t===this.head&&(this.head=e),t===this.tail&&(this.tail=i),this.length--,t.next=void 0,t.prev=void 0,t.list=void 0,e}unshiftNode(t){if(t===this.head)return;t.list&&t.list.removeNode(t);let e=this.head;t.list=this,t.next=e,e&&(e.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}pushNode(t){if(t===this.tail)return;t.list&&t.list.removeNode(t);let e=this.tail;t.list=this,t.prev=e,e&&(e.next=t),this.tail=t,this.head||(this.head=t),this.length++}push(...t){for(let e=0,i=t.length;e1)i=e;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=t(i,r.value,n),r=r.next;return i}reduceReverse(t,e){let i,r=this.tail;if(arguments.length>1)i=e;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=t(i,r.value,n),r=r.prev;return i}toArray(){let t=new Array(this.length);for(let e=0,i=this.head;i;e++)t[e]=i.value,i=i.next;return t}toArrayReverse(){let t=new Array(this.length);for(let e=0,i=this.tail;i;e++)t[e]=i.value,i=i.prev;return t}slice(t=0,e=this.length){e<0&&(e+=this.length),t<0&&(t+=this.length);let i=new s;if(ethis.length&&(e=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(e=this.length);let r=this.length,n=this.tail;for(;n&&r>e;r--)n=n.prev;for(;n&&r>t;r--,n=n.prev)i.push(n.value);return i}splice(t,e=0,...i){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);let r=this.head;for(let o=0;r&&o1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(t.gzip&&(typeof t.gzip!="object"&&(t.gzip={}),this.portable&&(t.gzip.portable=!0),this.zip=new ze(t.gzip)),t.brotli&&(typeof t.brotli!="object"&&(t.brotli={}),this.zip=new We(t.brotli)),t.zstd&&(typeof t.zstd!="object"&&(t.zstd={}),this.zip=new Ye(t.zstd)),!this.zip)throw new Error("impossible");let e=this.zip;e.on("data",i=>super.write(i)),e.on("end",()=>super.end()),e.on("drain",()=>this[as]()),this.on("resume",()=>e.resume())}else this.on("drain",this[as]);this.noDirRecurse=!!t.noDirRecurse,this.follow=!!t.follow,this.noMtime=!!t.noMtime,t.mtime&&(this.mtime=t.mtime),this.filter=typeof t.filter=="function"?t.filter:()=>!0,this[W]=new oi,this[G]=0,this.jobs=Number(t.jobs)||4,this[Ee]=!1,this[me]=!1}[nr](t){return super.write(t)}add(t){return this.write(t),this}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&this.add(t),this[me]=!0,this[kt](),i&&i(),this}write(t){if(this[me])throw new Error("write after end");return t instanceof Yt?this[ir](t):this[li](t),this.flowing}[ir](t){let e=f(rr.resolve(this.cwd,t.path));if(!this.filter(t.path,t))t.resume();else{let i=new mi(t.path,e);i.entry=new ni(t,this[hs](i)),i.entry.on("end",()=>this[os](i)),this[G]+=1,this[W].push(i)}this[kt]()}[li](t){let e=f(rr.resolve(this.cwd,t));this[W].push(new mi(t,e)),this[kt]()}[ls](t){t.pending=!0,this[G]+=1;let e=this.follow?"stat":"lstat";ui[e](t.absolute,(i,r)=>{t.pending=!1,this[G]-=1,i?this.emit("error",i):this[ai](t,r)})}[ai](t,e){if(this.statCache.set(t.absolute,e),t.stat=e,!this.filter(t.path,e))t.ignore=!0;else if(e.isFile()&&e.nlink>1&&!this.linkCache.get(`${e.dev}:${e.ino}`)&&!this.sync)if(t===this[Et])this[hi](t);else{let i=`${e.dev}:${e.ino}`,r=this[pe].get(i);r?r.push(t):this[pe].set(i,[t]),t.pendingLink=!0,t.pending=!0}this[kt]()}[cs](t){t.pending=!0,this[G]+=1,ui.readdir(t.absolute,(e,i)=>{if(t.pending=!1,this[G]-=1,e)return this.emit("error",e);this[ci](t,i)})}[ci](t,e){this.readdirCache.set(t.absolute,e),t.readdir=e,this[kt]()}[kt](){if(!this[Ee]){this[Ee]=!0;for(let t=this[W].head;t&&this[G]1){let i=`${e.dev}:${e.ino}`,r=this[pe].get(i);if(r){this[pe].delete(i);for(let n of r)n.pending=!1,this[hi](n)}}this[kt]()}[hi](t){if(t.pending&&t.pendingLink&&t===this[Et]&&(t.pending=!1,t.pendingLink=!1),!t.pending){if(t.entry){t===this[Et]&&!t.piped&&this[fi](t);return}if(!t.stat){let e=this.statCache.get(t.absolute);e?this[ai](t,e):this[ls](t)}if(t.stat&&!t.ignore){if(!this.noDirRecurse&&t.stat.isDirectory()&&!t.readdir){let e=this.readdirCache.get(t.absolute);if(e?this[ci](t,e):this[cs](t),!t.readdir)return}if(t.entry=this[sr](t),!t.entry){t.ignore=!0;return}t===this[Et]&&!t.piped&&this[fi](t)}}}[hs](t){return{onwarn:(e,i,r)=>this.warn(e,i,r),noPax:this.noPax,cwd:this.cwd,absolute:t.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[sr](t){this[G]+=1;try{return new this[di](t.path,this[hs](t)).on("end",()=>this[os](t)).on("error",i=>this.emit("error",i))}catch(e){this.emit("error",e)}}[as](){this[Et]&&this[Et].entry&&this[Et].entry.resume()}[fi](t){t.piped=!0,t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[li](o+r)});let e=t.entry,i=this.zip;if(!e)throw new Error("cannot pipe without source");i?e.on("data",r=>{i.write(r)||e.pause()}):e.on("data",r=>{super.write(r)||e.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(t,e,i={}){Nt(this,t,e,i)}},Ft=class extends wt{sync=!0;constructor(t){super(t),this[di]=ri}pause(){}resume(){}[ls](t){let e=this.follow?"statSync":"lstatSync";this[ai](t,ui[e](t.absolute))}[cs](t){this[ci](t,ui.readdirSync(t.absolute))}[fi](t){let e=t.entry,i=this.zip;if(t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[li](o+r)}),!e)throw new Error("Cannot pipe without source");i?e.on("data",r=>{i.write(r)}):e.on("data",r=>{super[nr](r)})}};var Hn=(s,t)=>{let e=new Ft(s),i=new Wt(s.file,{mode:s.mode||438});e.pipe(i),hr(e,t)},Wn=(s,t)=>{let e=new wt(s),i=new tt(s.file,{mode:s.mode||438});e.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),e.on("error",o)});return ar(e,t).catch(n=>e.emit("error",n)),r},hr=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?Ct({file:or.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},ar=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await Ct({file:or.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(e);s.end()},Gn=(s,t)=>{let e=new Ft(s);return hr(e,t),e},Zn=(s,t)=>{let e=new wt(s);return ar(e,t).catch(i=>e.emit("error",i)),e},Yn=K(Hn,Wn,Gn,Zn,(s,t)=>{if(!t?.length)throw new TypeError("no paths specified to add to archive")});import kr from"node:fs";import ro from"node:assert";import{randomBytes as Cr}from"node:crypto";import u from"node:fs";import R from"node:path";import fr from"fs";var Kn=process.env.__FAKE_PLATFORM__||process.platform,dr=Kn==="win32",{O_CREAT:ur,O_NOFOLLOW:lr,O_TRUNC:mr,O_WRONLY:pr}=fr.constants,Er=Number(process.env.__FAKE_FS_O_FILENAME__)||fr.constants.UV_FS_O_FILEMAP||0,Vn=dr&&!!Er,$n=512*1024,Xn=Er|mr|ur|pr,cr=!dr&&typeof lr=="number"?lr|mr|ur|pr:null,fs=cr!==null?()=>cr:Vn?s=>s<$n?Xn:"w":()=>"w";import Ei from"node:fs";import we from"node:path";var ds=(s,t,e)=>{try{return Ei.lchownSync(s,t,e)}catch(i){if(i?.code!=="ENOENT")throw i}},pi=(s,t,e,i)=>{Ei.lchown(s,t,e,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},qn=(s,t,e,i,r)=>{if(t.isDirectory())us(we.resolve(s,t.name),e,i,n=>{if(n)return r(n);let o=we.resolve(s,t.name);pi(o,e,i,r)});else{let n=we.resolve(s,t.name);pi(n,e,i,r)}},us=(s,t,e,i)=>{Ei.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return pi(s,t,e,i);let o=n.length,h=null,a=l=>{if(!h){if(l)return i(h=l);if(--o===0)return pi(s,t,e,i)}};for(let l of n)qn(s,l,t,e,a)})},Qn=(s,t,e,i)=>{t.isDirectory()&&ms(we.resolve(s,t.name),e,i),ds(we.resolve(s,t.name),e,i)},ms=(s,t,e)=>{let i;try{i=Ei.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return ds(s,t,e);throw n}for(let r of i)Qn(s,r,t,e);return ds(s,t,e)};import F from"node:fs";import Jn from"node:fs/promises";import wi from"node:path";var Se=class extends Error{path;code;syscall="chdir";constructor(t,e){super(`${e}: Cannot cd into '${t}'`),this.path=t,this.code=e}get name(){return"CwdError"}};var St=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(t,e){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=t,this.path=e}get name(){return"SymlinkError"}};var jn=(s,t)=>{F.stat(s,(e,i)=>{(e||!i.isDirectory())&&(e=new Se(s,e?.code||"ENOTDIR")),t(e)})},wr=(s,t,e)=>{s=f(s);let i=t.umask??18,r=t.mode|448,n=(r&i)!==0,o=t.uid,h=t.gid,a=typeof o=="number"&&typeof h=="number"&&(o!==t.processUid||h!==t.processGid),l=t.preserve,c=t.unlink,d=f(t.cwd),S=(E,x)=>{E?e(E):x&&a?us(x,o,h,Le=>S(Le)):n?F.chmod(s,r,e):e()};if(s===d)return jn(s,S);if(l)return Jn.mkdir(s,{mode:r,recursive:!0}).then(E=>S(null,E??void 0),S);let N=f(wi.relative(d,s)).split("/");ps(d,N,r,c,d,void 0,S)},ps=(s,t,e,i,r,n,o)=>{if(t.length===0)return o(null,n);let h=t.shift(),a=f(wi.resolve(s+"/"+h));F.mkdir(a,e,Sr(a,t,e,i,r,n,o))},Sr=(s,t,e,i,r,n,o)=>h=>{h?F.lstat(s,(a,l)=>{if(a)a.path=a.path&&f(a.path),o(a);else if(l.isDirectory())ps(s,t,e,i,r,n,o);else if(i)F.unlink(s,c=>{if(c)return o(c);F.mkdir(s,e,Sr(s,t,e,i,r,n,o))});else{if(l.isSymbolicLink())return o(new St(s,s+"/"+t.join("/")));o(h)}}):(n=n||s,ps(s,t,e,i,r,n,o))},to=s=>{let t=!1,e;try{t=F.statSync(s).isDirectory()}catch(i){e=i?.code}finally{if(!t)throw new Se(s,e??"ENOTDIR")}},yr=(s,t)=>{s=f(s);let e=t.umask??18,i=t.mode|448,r=(i&e)!==0,n=t.uid,o=t.gid,h=typeof n=="number"&&typeof o=="number"&&(n!==t.processUid||o!==t.processGid),a=t.preserve,l=t.unlink,c=f(t.cwd),d=E=>{E&&h&&ms(E,n,o),r&&F.chmodSync(s,i)};if(s===c)return to(c),d();if(a)return d(F.mkdirSync(s,{mode:i,recursive:!0})??void 0);let T=f(wi.relative(c,s)).split("/"),N;for(let E=T.shift(),x=c;E&&(x+="/"+E);E=T.shift()){x=f(wi.resolve(x));try{F.mkdirSync(x,i),N=N||x}catch{let Le=F.lstatSync(x);if(Le.isDirectory())continue;if(l){F.unlinkSync(x),F.mkdirSync(x,i),N=N||x;continue}else if(Le.isSymbolicLink())return new St(x,x+"/"+T.join("/"))}}return d(N)};import{join as br}from"node:path";var Es=Object.create(null),Rr=1e4,$t=new Set,gr=s=>{$t.has(s)?$t.delete(s):Es[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),$t.add(s);let t=Es[s],e=$t.size-Rr;if(e>Rr/10){for(let i of $t)if($t.delete(i),delete Es[i],--e<=0)break}return t};var eo=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,io=eo==="win32",so=s=>s.split("/").slice(0,-1).reduce((e,i)=>{let r=e.at(-1);return r!==void 0&&(i=br(r,i)),e.push(i||"/"),e},[]),Si=class{#t=new Map;#i=new Map;#s=new Set;reserve(t,e){t=io?["win32 parallelization disabled"]:t.map(r=>mt(br(gr(r))));let i=new Set(t.map(r=>so(r)).reduce((r,n)=>r.concat(n)));this.#i.set(e,{dirs:i,paths:t});for(let r of t){let n=this.#t.get(r);n?n.push(e):this.#t.set(r,[e])}for(let r of i){let n=this.#t.get(r);if(!n)this.#t.set(r,[new Set([e])]);else{let o=n.at(-1);o instanceof Set?o.add(e):n.push(new Set([e]))}}return this.#r(e)}#n(t){let e=this.#i.get(t);if(!e)throw new Error("function does not have any path reservations");return{paths:e.paths.map(i=>this.#t.get(i)),dirs:[...e.dirs].map(i=>this.#t.get(i))}}check(t){let{paths:e,dirs:i}=this.#n(t);return e.every(r=>r&&r[0]===t)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(t))}#r(t){return this.#s.has(t)||!this.check(t)?!1:(this.#s.add(t),t(()=>this.#e(t)),!0)}#e(t){if(!this.#s.has(t))return!1;let e=this.#i.get(t);if(!e)throw new Error("invalid reservation");let{paths:i,dirs:r}=e,n=new Set;for(let o of i){let h=this.#t.get(o);if(!h||h?.[0]!==t)continue;let a=h[1];if(!a){this.#t.delete(o);continue}if(h.shift(),typeof a=="function")n.add(a);else for(let l of a)n.add(l)}for(let o of r){let h=this.#t.get(o),a=h?.[0];if(!(!h||!(a instanceof Set)))if(a.size===1&&h.length===1){this.#t.delete(o);continue}else if(a.size===1){h.shift();let l=h[0];typeof l=="function"&&n.add(l)}else a.delete(t)}return this.#s.delete(t),n.forEach(o=>this.#r(o)),!0}};var _r=()=>process.umask();var Or=Symbol("onEntry"),Rs=Symbol("checkFs"),Tr=Symbol("checkFs2"),gs=Symbol("isReusable"),P=Symbol("makeFs"),bs=Symbol("file"),_s=Symbol("directory"),Ri=Symbol("link"),xr=Symbol("symlink"),Lr=Symbol("hardlink"),Re=Symbol("ensureNoSymlink"),Nr=Symbol("unsupported"),Dr=Symbol("checkPath"),ws=Symbol("stripAbsolutePath"),yt=Symbol("mkdir"),O=Symbol("onError"),yi=Symbol("pending"),Ar=Symbol("pend"),Xt=Symbol("unpend"),Ss=Symbol("ended"),ys=Symbol("maybeClose"),Os=Symbol("skip"),ge=Symbol("doChown"),be=Symbol("uid"),_e=Symbol("gid"),Oe=Symbol("checkedCwd"),no=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Te=no==="win32",oo=1024,ho=(s,t)=>{if(!Te)return u.unlink(s,t);let e=s+".DELETE."+Cr(16).toString("hex");u.rename(s,e,i=>{if(i)return t(i);u.unlink(e,t)})},ao=s=>{if(!Te)return u.unlinkSync(s);let t=s+".DELETE."+Cr(16).toString("hex");u.renameSync(s,t),u.unlinkSync(t)},Ir=(s,t,e)=>s!==void 0&&s===s>>>0?s:t!==void 0&&t===t>>>0?t:e,qt=class extends st{[Ss]=!1;[Oe]=!1;[yi]=0;reservations=new Si;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(t={}){if(t.ondone=()=>{this[Ss]=!0,this[ys]()},super(t),this.transform=t.transform,this.chmod=!!t.chmod,typeof t.uid=="number"||typeof t.gid=="number"){if(typeof t.uid!="number"||typeof t.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(t.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=t.uid,this.gid=t.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=t.preserveOwner===void 0&&typeof t.uid!="number"?!!(process.getuid&&process.getuid()===0):!!t.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof t.maxDepth=="number"?t.maxDepth:oo,this.forceChown=t.forceChown===!0,this.win32=!!t.win32||Te,this.newer=!!t.newer,this.keep=!!t.keep,this.noMtime=!!t.noMtime,this.preservePaths=!!t.preservePaths,this.unlink=!!t.unlink,this.cwd=f(R.resolve(t.cwd||process.cwd())),this.strip=Number(t.strip)||0,this.processUmask=this.chmod?typeof t.processUmask=="number"?t.processUmask:_r():0,this.umask=typeof t.umask=="number"?t.umask:this.processUmask,this.dmode=t.dmode||511&~this.umask,this.fmode=t.fmode||438&~this.umask,this.on("entry",e=>this[Or](e))}warn(t,e,i={}){return(t==="TAR_BAD_ARCHIVE"||t==="TAR_ABORT")&&(i.recoverable=!1),super.warn(t,e,i)}[ys](){this[Ss]&&this[yi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[ws](t,e){let i=t[e],{type:r}=t;if(!i||this.preservePaths)return!0;let[n,o]=ce(i),h=o.replaceAll(/\\/g,"/").split("/");if(h.includes("..")||Te&&/^[a-z]:\.\.$/i.test(h[0]??"")){if(e==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${e} contains '..'`,{entry:t,[e]:i}),!1;let a=R.posix.dirname(t.path),l=R.posix.normalize(R.posix.join(a,h.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${e} escapes extraction directory`,{entry:t,[e]:i}),!1}return n&&(t[e]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${e}`,{entry:t,[e]:i})),!0}[Dr](t){let e=f(t.path),i=e.split("/");if(this.strip){if(i.length=this.strip)t.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),t.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:t,path:e,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[ws](t,"path")||!this[ws](t,"linkpath"))return!1;if(t.absolute=R.isAbsolute(t.path)?f(R.resolve(t.path)):f(R.resolve(this.cwd,t.path)),!this.preservePaths&&typeof t.absolute=="string"&&t.absolute.indexOf(this.cwd+"/")!==0&&t.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:t,path:f(t.path),resolvedPath:t.absolute,cwd:this.cwd}),!1;if(t.absolute===this.cwd&&t.type!=="Directory"&&t.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=R.win32.parse(String(t.absolute));t.absolute=r+Qi(String(t.absolute).slice(r.length));let{root:n}=R.win32.parse(t.path);t.path=n+Qi(t.path.slice(n.length))}return!0}[Or](t){if(!this[Dr](t))return t.resume();switch(ro.equal(typeof t.absolute,"string"),t.type){case"Directory":case"GNUDumpDir":t.mode&&(t.mode=t.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[Rs](t);default:return this[Nr](t)}}[O](t,e){t.name==="CwdError"?this.emit("error",t):(this.warn("TAR_ENTRY_ERROR",t,{entry:e}),this[Xt](),e.resume())}[yt](t,e,i){wr(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e},i)}[ge](t){return this.forceChown||this.preserveOwner&&(typeof t.uid=="number"&&t.uid!==this.processUid||typeof t.gid=="number"&&t.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[be](t){return Ir(this.uid,t.uid,this.processUid)}[_e](t){return Ir(this.gid,t.gid,this.processGid)}[bs](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=new tt(String(t.absolute),{flags:fs(t.size),mode:i,autoClose:!1});r.on("error",a=>{r.fd&&u.close(r.fd,()=>{}),r.write=()=>!0,this[O](a,t),e()});let n=1,o=a=>{if(a){r.fd&&u.close(r.fd,()=>{}),this[O](a,t),e();return}--n===0&&r.fd!==void 0&&u.close(r.fd,l=>{l?this[O](l,t):this[Xt](),e()})};r.on("finish",()=>{let a=String(t.absolute),l=r.fd;if(typeof l=="number"&&t.mtime&&!this.noMtime){n++;let c=t.atime||new Date,d=t.mtime;u.futimes(l,c,d,S=>S?u.utimes(a,c,d,T=>o(T&&S)):o())}if(typeof l=="number"&&this[ge](t)){n++;let c=this[be](t),d=this[_e](t);typeof c=="number"&&typeof d=="number"&&u.fchown(l,c,d,S=>S?u.chown(a,c,d,T=>o(T&&S)):o())}o()});let h=this.transform&&this.transform(t)||t;h!==t&&(h.on("error",a=>{this[O](a,t),e()}),t.pipe(h)),h.pipe(r)}[_s](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode;this[yt](String(t.absolute),i,r=>{if(r){this[O](r,t),e();return}let n=1,o=()=>{--n===0&&(e(),this[Xt](),t.resume())};t.mtime&&!this.noMtime&&(n++,u.utimes(String(t.absolute),t.atime||new Date,t.mtime,o)),this[ge](t)&&(n++,u.chown(String(t.absolute),Number(this[be](t)),Number(this[_e](t)),o)),o()})}[Nr](t){t.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${t.type}`,{entry:t}),t.resume()}[xr](t,e){let i=f(R.relative(this.cwd,R.resolve(R.dirname(String(t.absolute)),String(t.linkpath)))).split("/");this[Re](t,this.cwd,i,()=>this[Ri](t,String(t.linkpath),"symlink",e),r=>{this[O](r,t),e()})}[Lr](t,e){let i=f(R.resolve(this.cwd,String(t.linkpath))),r=f(String(t.linkpath)).split("/");this[Re](t,this.cwd,r,()=>this[Ri](t,i,"link",e),n=>{this[O](n,t),e()})}[Re](t,e,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let h=R.resolve(e,o);u.lstat(h,(a,l)=>{if(a)return r();if(l?.isSymbolicLink())return n(new St(h,R.resolve(h,i.join("/"))));this[Re](t,h,i,r,n)})}[Ar](){this[yi]++}[Xt](){this[yi]--,this[ys]()}[Os](t){this[Xt](),t.resume()}[gs](t,e){return t.type==="File"&&!this.unlink&&e.isFile()&&e.nlink<=1&&!Te}[Rs](t){this[Ar]();let e=[t.path];t.linkpath&&e.push(t.linkpath),this.reservations.reserve(e,i=>this[Tr](t,i))}[Tr](t,e){let i=h=>{e(h)},r=()=>{this[yt](this.cwd,this.dmode,h=>{if(h){this[O](h,t),i();return}this[Oe]=!0,n()})},n=()=>{if(t.absolute!==this.cwd){let h=f(R.dirname(String(t.absolute)));if(h!==this.cwd)return this[yt](h,this.dmode,a=>{if(a){this[O](a,t),i();return}o()})}o()},o=()=>{u.lstat(String(t.absolute),(h,a)=>{if(a&&(this.keep||this.newer&&a.mtime>(t.mtime??a.mtime))){this[Os](t),i();return}if(h||this[gs](t,a))return this[P](null,t,i);if(a.isDirectory()){if(t.type==="Directory"){let l=this.chmod&&t.mode&&(a.mode&4095)!==t.mode,c=d=>this[P](d??null,t,i);return l?u.chmod(String(t.absolute),Number(t.mode),c):c()}if(t.absolute!==this.cwd)return u.rmdir(String(t.absolute),l=>this[P](l??null,t,i))}if(t.absolute===this.cwd)return this[P](null,t,i);ho(String(t.absolute),l=>this[P](l??null,t,i))})};this[Oe]?n():r()}[P](t,e,i){if(t){this[O](t,e),i();return}switch(e.type){case"File":case"OldFile":case"ContiguousFile":return this[bs](e,i);case"Link":return this[Lr](e,i);case"SymbolicLink":return this[xr](e,i);case"Directory":case"GNUDumpDir":return this[_s](e,i)}}[Ri](t,e,i,r){u[i](e,String(t.absolute),n=>{n?this[O](n,t):(this[Xt](),t.resume()),r()})}},ye=s=>{try{return[null,s()]}catch(t){return[t,null]}},xe=class extends qt{sync=!0;[P](t,e){return super[P](t,e,()=>{})}[Rs](t){if(!this[Oe]){let n=this[yt](this.cwd,this.dmode);if(n)return this[O](n,t);this[Oe]=!0}if(t.absolute!==this.cwd){let n=f(R.dirname(String(t.absolute)));if(n!==this.cwd){let o=this[yt](n,this.dmode);if(o)return this[O](o,t)}}let[e,i]=ye(()=>u.lstatSync(String(t.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(t.mtime??i.mtime)))return this[Os](t);if(e||this[gs](t,i))return this[P](null,t);if(i.isDirectory()){if(t.type==="Directory"){let o=this.chmod&&t.mode&&(i.mode&4095)!==t.mode,[h]=o?ye(()=>{u.chmodSync(String(t.absolute),Number(t.mode))}):[];return this[P](h,t)}let[n]=ye(()=>u.rmdirSync(String(t.absolute)));this[P](n,t)}let[r]=t.absolute===this.cwd?[]:ye(()=>ao(String(t.absolute)));this[P](r,t)}[bs](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=h=>{let a;try{u.closeSync(n)}catch(l){a=l}(h||a)&&this[O](h||a,t),e()},n;try{n=u.openSync(String(t.absolute),fs(t.size),i)}catch(h){return r(h)}let o=this.transform&&this.transform(t)||t;o!==t&&(o.on("error",h=>this[O](h,t)),t.pipe(o)),o.on("data",h=>{try{u.writeSync(n,h,0,h.length)}catch(a){r(a)}}),o.on("end",()=>{let h=null;if(t.mtime&&!this.noMtime){let a=t.atime||new Date,l=t.mtime;try{u.futimesSync(n,a,l)}catch(c){try{u.utimesSync(String(t.absolute),a,l)}catch{h=c}}}if(this[ge](t)){let a=this[be](t),l=this[_e](t);try{u.fchownSync(n,Number(a),Number(l))}catch(c){try{u.chownSync(String(t.absolute),Number(a),Number(l))}catch{h=h||c}}}r(h)})}[_s](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode,r=this[yt](String(t.absolute),i);if(r){this[O](r,t),e();return}if(t.mtime&&!this.noMtime)try{u.utimesSync(String(t.absolute),t.atime||new Date,t.mtime)}catch{}if(this[ge](t))try{u.chownSync(String(t.absolute),Number(this[be](t)),Number(this[_e](t)))}catch{}e(),t.resume()}[yt](t,e){try{return yr(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e})}catch(i){return i}}[Re](t,e,i,r,n){if(this.preservePaths||i.length===0)return r();let o=e;for(let h of i){o=R.resolve(o,h);let[a,l]=ye(()=>u.lstatSync(o));if(a)return r();if(l.isSymbolicLink())return n(new St(o,R.resolve(e,i.join("/"))))}r()}[Ri](t,e,i,r){let n=`${i}Sync`;try{u[n](e,String(t.absolute)),r(),t.resume()}catch(o){return this[O](o,t)}}};var lo=s=>{let t=new xe(s),e=s.file,i=kr.statSync(e),r=s.maxReadSize||16*1024*1024;new Be(e,{readSize:r,size:i.size}).pipe(t)},co=(s,t)=>{let e=new qt(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{e.on("error",h),e.on("close",o),kr.stat(r,(a,l)=>{if(a)h(a);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",h),c.pipe(e)}})})},fo=K(lo,co,s=>new xe(s),s=>new qt(s),(s,t)=>{t?.length&&$i(s,t)});import v from"node:fs";import Fr from"node:path";var uo=(s,t)=>{let e=new Ft(s),i=!0,r,n;try{try{r=v.openSync(s.file,"r+")}catch(a){if(a?.code==="ENOENT")r=v.openSync(s.file,"w+");else throw a}let o=v.fstatSync(r),h=Buffer.alloc(512);t:for(n=0;no.size)break;n+=l,s.mtimeCache&&a.mtime&&s.mtimeCache.set(String(a.path),a.mtime)}i=!1,mo(s,e,n,r,t)}finally{if(i)try{v.closeSync(r)}catch{}}},mo=(s,t,e,i,r)=>{let n=new Wt(s.file,{fd:i,start:e});t.pipe(n),Eo(t,r)},po=(s,t)=>{t=Array.from(t);let e=new wt(s),i=(n,o,h)=>{let a=(T,N)=>{T?v.close(n,E=>h(T)):h(null,N)},l=0;if(o===0)return a(null,0);let c=0,d=Buffer.alloc(512),S=(T,N)=>{if(T||N===void 0)return a(T);if(c+=N,c<512&&N)return v.read(n,d,c,d.length-c,l+c,S);if(l===0&&d[0]===31&&d[1]===139)return a(new Error("cannot append to compressed archives"));if(c<512)return a(null,l);let E=new k(d);if(!E.cksumValid)return a(null,l);let x=512*Math.ceil((E.size??0)/512);if(l+x+512>o||(l+=x+512,l>=o))return a(null,l);s.mtimeCache&&E.mtime&&s.mtimeCache.set(String(E.path),E.mtime),c=0,v.read(n,d,0,512,l,S)};v.read(n,d,0,512,l,S)};return new Promise((n,o)=>{e.on("error",o);let h="r+",a=(l,c)=>{if(l&&l.code==="ENOENT"&&h==="r+")return h="w+",v.open(s.file,h,a);if(l||!c)return o(l);v.fstat(c,(d,S)=>{if(d)return v.close(c,()=>o(d));i(c,S.size,(T,N)=>{if(T)return o(T);let E=new tt(s.file,{fd:c,start:N});e.pipe(E),E.on("error",o),E.on("close",n),wo(e,t)})})};v.open(s.file,h,a)})},Eo=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?Ct({file:Fr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},wo=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await Ct({file:Fr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e);s.end()},vt=K(uo,po,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,t)=>{if(!Fs(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!t?.length)throw new TypeError("no paths specified to add/replace")});var So=K(vt.syncFile,vt.asyncFile,vt.syncNoFile,vt.asyncNoFile,(s,t=[])=>{vt.validate?.(s,t),yo(s)}),yo=s=>{let t=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=t?(e,i)=>t(e,i)&&!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0)):(e,i)=>!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0))};export{k as Header,wt as Pack,mi as PackJob,Ft as PackSync,st as Parser,ct as Pax,Yt as ReadEntry,qt as Unpack,xe as UnpackSync,de as WriteEntry,ri as WriteEntrySync,ni as WriteEntryTar,Yn as c,Yn as create,fo as extract,$i as filesFilter,Ct as list,vt as r,vt as replace,Ct as t,zi as types,So as u,So as update,fo as x}; //# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/tar/dist/esm/pack.js b/deps/npm/node_modules/tar/dist/esm/pack.js index ed87ffd49b787c..6eae0ee73bda00 100644 --- a/deps/npm/node_modules/tar/dist/esm/pack.js +++ b/deps/npm/node_modules/tar/dist/esm/pack.js @@ -15,6 +15,7 @@ export class PackJob { stat; readdir; pending = false; + pendingLink = false; ignore = false; piped = false; constructor(path, absolute) { @@ -31,6 +32,7 @@ const EOF = Buffer.alloc(1024); const ONSTAT = Symbol('onStat'); const ENDED = Symbol('ended'); const QUEUE = Symbol('queue'); +const PENDINGLINKS = Symbol('queue'); const CURRENT = Symbol('current'); const PROCESS = Symbol('process'); const PROCESSING = Symbol('processing'); @@ -82,6 +84,7 @@ export class Pack extends Minipass { // class, but then we'd have to be tracking the tail of the queue the // whole time, and Yallist just does that for us anyway. [QUEUE]; + [PENDINGLINKS] = new Map(); [JOBS] = 0; [PROCESSING] = false; [ENDED] = false; @@ -239,14 +242,26 @@ export class Pack extends Minipass { } else if (stat.isFile() && stat.nlink > 1 && - job === this[CURRENT] && !this.linkCache.get(`${stat.dev}:${stat.ino}`) && !this.sync) { - // if it's not filtered, and it's a new File entry, - // jump the queue in case any pending Link entries are about - // to try to link to it. This prevents a hardlink from coming ahead - // of its target in the archive. - this[PROCESSJOB](job); + // if it's not filtered, and it's a new File entry, and next anyway + // process right away in case any pending Link entries are about + // to try to link to it. + if (job === this[CURRENT]) { + this[PROCESSJOB](job); + } + else { + // if it's NOT the current entry, it needs to be deferred, + // so that the link target can be built first. + const key = `${stat.dev}:${stat.ino}`; + const pending = this[PENDINGLINKS].get(key); + if (pending) + pending.push(job); + else + this[PENDINGLINKS].set(key, [job]); + job.pendingLink = true; + job.pending = true; + } } this[PROCESS](); } @@ -294,12 +309,31 @@ export class Pack extends Minipass { get [CURRENT]() { return this[QUEUE] && this[QUEUE].head && this[QUEUE].head.value; } - [JOBDONE](_job) { + [JOBDONE](job) { this[QUEUE].shift(); this[JOBS] -= 1; + const { stat } = job; + if (stat && stat.isFile() && stat.nlink > 1) { + // might be a file with pending links + const key = `${stat.dev}:${stat.ino}`; + const pending = this[PENDINGLINKS].get(key); + if (pending) { + this[PENDINGLINKS].delete(key); + for (const job of pending) { + job.pending = false; + this[PROCESSJOB](job); + } + } + } this[PROCESS](); } [PROCESSJOB](job) { + if (job.pending && job.pendingLink && job === this[CURRENT]) { + // At least one of the links to this file are not being included + // in the tarball, so we need to just proceed. + job.pending = false; + job.pendingLink = false; + } if (job.pending) { return; } diff --git a/deps/npm/node_modules/tar/package.json b/deps/npm/node_modules/tar/package.json index 55d21bcf0b535d..2ad5841e328515 100644 --- a/deps/npm/node_modules/tar/package.json +++ b/deps/npm/node_modules/tar/package.json @@ -2,7 +2,7 @@ "author": "Isaac Z. Schlueter", "name": "tar", "description": "tar for node", - "version": "7.5.13", + "version": "7.5.15", "repository": { "type": "git", "url": "https://github.com/isaacs/node-tar.git" @@ -38,13 +38,13 @@ "events-to-array": "^2.0.3", "mutate-fs": "^2.1.1", "nock": "^13.5.4", - "oxlint": "^1.56.0", - "oxlint-tsgolint": "^0.17.0", + "oxlint": "^1.57.0", + "oxlint-tsgolint": "^0.17.3", "prettier": "^3.8.1", "rimraf": "^6.1.2", "tap": "^21.6.2", "tshy": "^3.3.2", - "typedoc": "^0.28.17" + "typedoc": "^0.28.18" }, "license": "BlueOak-1.0.0", "engines": { diff --git a/deps/npm/node_modules/undici/lib/dispatcher/agent.js b/deps/npm/node_modules/undici/lib/dispatcher/agent.js index db2f817d0fe978..90b46fe3aeb4b4 100644 --- a/deps/npm/node_modules/undici/lib/dispatcher/agent.js +++ b/deps/npm/node_modules/undici/lib/dispatcher/agent.js @@ -24,7 +24,6 @@ function defaultFactory (origin, opts) { class Agent extends DispatcherBase { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } diff --git a/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js b/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js index 2b8fa05da29427..ef3d38ea4f2ed3 100644 --- a/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js +++ b/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js @@ -279,29 +279,71 @@ class Parser { const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr - if (ret === constants.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)) - } else if (ret === constants.ERROR.PAUSED) { - this.paused = true - socket.unshift(data.slice(offset)) - } else if (ret !== constants.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr) - let message = '' - /* istanbul ignore else: difficult to make a test case for */ - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) - message = - 'Response does not match the HTTP/1.1 protocol (' + - Buffer.from(llhttp.memory.buffer, ptr, len).toString() + - ')' + if (ret !== constants.ERROR.OK) { + const body = data.subarray(offset) + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(body) + } else { + throw this.createError(ret, body) } - throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) } } catch (err) { util.destroy(socket, err) } } + finish () { + assert(currentParser === null) + assert(this.ptr != null) + assert(!this.paused) + + const { llhttp } = this + + let ret + + try { + currentParser = this + ret = llhttp.llhttp_finish(this.ptr) + } finally { + currentParser = null + } + + if (ret === constants.ERROR.OK) { + return null + } + + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true + return null + } + + return this.createError(ret, EMPTY_BUF) + } + + createError (ret, data) { + const { llhttp, contentLength, bytesRead } = this + + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError() + } + + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + + return new HTTPParserError(message, constants.ERROR[ret], data) + } + destroy () { assert(this.ptr != null) assert(currentParser == null) @@ -673,8 +715,11 @@ async function connectH1 (client, socket) { // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // to the user. if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + this[kError] = parserErr + this[kClient][kOnError](parserErr) + } return } @@ -693,8 +738,10 @@ async function connectH1 (client, socket) { const parser = this[kParser] if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + util.destroy(this, parserErr) + } return } @@ -706,8 +753,7 @@ async function connectH1 (client, socket) { if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + this[kError] = parser.finish() || this[kError] } this[kParser].destroy() diff --git a/deps/npm/node_modules/undici/package.json b/deps/npm/node_modules/undici/package.json index 46cb9a8292618f..d1eef502c4169f 100644 --- a/deps/npm/node_modules/undici/package.json +++ b/deps/npm/node_modules/undici/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.25.0", + "version": "6.26.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/npm/package.json b/deps/npm/package.json index 67186d5c05daa7..e600a3c0095246 100644 --- a/deps/npm/package.json +++ b/deps/npm/package.json @@ -1,5 +1,5 @@ { - "version": "11.13.0", + "version": "11.16.0", "name": "npm", "description": "a package manager for JavaScript", "workspaces": [ @@ -52,8 +52,8 @@ }, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.4.3", - "@npmcli/config": "^10.8.1", + "@npmcli/arborist": "^9.7.0", + "@npmcli/config": "^10.10.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -71,22 +71,22 @@ "fs-minipass": "^3.0.3", "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "hosted-git-info": "^9.0.2", + "hosted-git-info": "^9.0.3", "ini": "^6.0.0", "init-package-json": "^8.2.5", "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.6", - "libnpmexec": "^10.2.6", - "libnpmfund": "^7.0.20", + "libnpmdiff": "^8.1.9", + "libnpmexec": "^10.2.9", + "libnpmfund": "^7.0.23", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.6", - "libnpmpublish": "^11.1.3", + "libnpmpack": "^9.1.9", + "libnpmpublish": "^11.2.0", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", - "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.5", + "libnpmversion": "^8.0.4", + "make-fetch-happen": "^15.0.6", "minimatch": "^10.2.5", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", @@ -106,11 +106,11 @@ "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", - "semver": "^7.7.4", + "semver": "^7.8.1", "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.13", + "tar": "^7.5.15", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", diff --git a/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs index 43a99252d4200a..12f1c803cd7695 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -62,6 +62,7 @@ Array [ String( access adduser + approve-scripts audit author add @@ -105,10 +106,16 @@ Array [ --repo --environment --env + --allow-publish + --allow-stage-publish + --allow-staged-publish --dry-run --json --registry --yes + --no-allow-publish + --no-allow-stage-publish + --no-allow-staged-publish --no-dry-run --no-json --no-yes @@ -123,10 +130,16 @@ Array [ --project --environment --env + --allow-publish + --allow-stage-publish + --allow-staged-publish --dry-run --json --registry --yes + --no-allow-publish + --no-allow-stage-publish + --no-allow-staged-publish --no-dry-run --no-json --no-yes diff --git a/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs index 6617b3a0827f76..829b64b3f800b6 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -16,7 +16,13 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "access": null, "all": false, "allow-same-version": false, + "allow-directory": "all", + "allow-file": "all", "allow-git": "all", + "allow-remote": "all", + "allow-scripts": [ + "" + ], "also": null, "audit": true, "audit-level": null, @@ -34,6 +40,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "cidr": null, "commit-hooks": true, "cpu": null, + "dangerously-allow-all-scripts": false, "depth": null, "description": true, "dev": false, @@ -124,6 +131,8 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "pack-destination": ".", "packages": [], "parseable": false, + "allow-scripts-pending": false, + "allow-scripts-pin": true, "prefer-dedupe": false, "prefer-offline": false, "prefer-online": false, @@ -164,6 +173,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "sign-git-commit": false, "sign-git-tag": false, "strict-peer-deps": false, + "strict-allow-scripts": false, "strict-ssl": true, "tag": "latest", "tag-version-prefix": "v", @@ -192,8 +202,14 @@ exports[`test/lib/commands/config.js TAP config list --long > output matches sna _auth = (protected) access = null all = false +allow-directory = "all" +allow-file = "all" allow-git = "all" +allow-remote = "all" allow-same-version = false +allow-scripts = [""] +allow-scripts-pending = false +allow-scripts-pin = true also = null audit = true audit-level = null @@ -213,6 +229,7 @@ cidr = null ; color = {COLOR} commit-hooks = true cpu = null +dangerously-allow-all-scripts = false depth = null description = true dev = false @@ -342,6 +359,7 @@ shell = "{SHELL}" shrinkwrap = true sign-git-commit = false sign-git-tag = false +strict-allow-scripts = false strict-peer-deps = false strict-ssl = true tag = "latest" diff --git a/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs index e7507118a28f50..143d08dda8ff4b 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -87,23 +87,25 @@ Array [ exports[`test/lib/commands/publish.js TAP json > new package json 1`] = ` { - "id": "@npmcli/test-package@1.0.0", - "name": "@npmcli/test-package", - "version": "1.0.0", - "size": "{size}", - "unpackedSize": 95, - "shasum": "{sha}", - "integrity": "{integrity}", - "filename": "npmcli-test-package-1.0.0.tgz", - "files": [ - { - "path": "package.json", - "size": "{size}", - "mode": 420 - } - ], - "entryCount": 1, - "bundled": [] + "@npmcli/test-package": { + "id": "@npmcli/test-package@1.0.0", + "name": "@npmcli/test-package", + "version": "1.0.0", + "size": "{size}", + "unpackedSize": 95, + "shasum": "{sha}", + "integrity": "{integrity}", + "filename": "npmcli-test-package-1.0.0.tgz", + "files": [ + { + "path": "package.json", + "size": "{size}", + "mode": 420 + } + ], + "entryCount": 1, + "bundled": [] + } } ` @@ -154,6 +156,7 @@ Object { "man": Array [ "man/man1/npm-access.1", "man/man1/npm-adduser.1", + "man/man1/npm-approve-scripts.1", "man/man1/npm-audit.1", "man/man1/npm-bugs.1", "man/man1/npm-cache.1", @@ -161,6 +164,7 @@ Object { "man/man1/npm-completion.1", "man/man1/npm-config.1", "man/man1/npm-dedupe.1", + "man/man1/npm-deny-scripts.1", "man/man1/npm-deprecate.1", "man/man1/npm-diff.1", "man/man1/npm-dist-tag.1", @@ -204,6 +208,7 @@ Object { "man/man1/npm-search.1", "man/man1/npm-set.1", "man/man1/npm-shrinkwrap.1", + "man/man1/npm-stage.1", "man/man1/npm-star.1", "man/man1/npm-stars.1", "man/man1/npm-start.1", @@ -258,7 +263,7 @@ exports[`test/lib/commands/publish.js TAP no auth dry-run > must match snapshot exports[`test/lib/commands/publish.js TAP no auth dry-run > warns about auth being needed 1`] = ` Array [ - "This command requires you to be logged in to https://registry.npmjs.org/ (dry-run)", + "publish This command requires you to be logged in to https://registry.npmjs.org/ (dry-run)", ] ` @@ -266,6 +271,28 @@ exports[`test/lib/commands/publish.js TAP prioritize CLI flags over publishConfi + @npmcli/test-package@1.0.0 ` +exports[`test/lib/commands/publish.js TAP private access > must match snapshot 1`] = ` +Array [ + "package: @npm/test-package@1.0.0", + "Tarball Contents", + "55B package.json", + "Tarball Details", + "name: @npm/test-package", + "version: 1.0.0", + "filename: npm-test-package-1.0.0.tgz", + "package size: {size}", + "unpacked size: 55 B", + "shasum: {sha}", + "integrity: {integrity} + "total files: 1", + "Publishing to https://registry.npmjs.org/ with tag latest and restricted access", +] +` + +exports[`test/lib/commands/publish.js TAP private access > new package version 1`] = ` ++ @npm/test-package@1.0.0 +` + exports[`test/lib/commands/publish.js TAP public access > must match snapshot 1`] = ` Array [ "package: @npm/test-package@1.0.0", diff --git a/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs b/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs index 3db9e9d2473285..72671906850ee0 100644 --- a/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs @@ -99,6 +99,7 @@ exports[`test/lib/docs.js TAP command list > commands 1`] = ` Array [ "access", "adduser", + "approve-scripts", "audit", "bugs", "cache", @@ -106,6 +107,7 @@ Array [ "completion", "config", "dedupe", + "deny-scripts", "deprecate", "diff", "dist-tag", @@ -149,6 +151,7 @@ Array [ "search", "set", "shrinkwrap", + "stage", "star", "stars", "start", @@ -192,7 +195,7 @@ safer to use a registry-provided authentication bearer token stored in the * Default: 'public' for new packages, existing packages it will not change the current level -* Type: null, "restricted", or "public" +* Type: null, "restricted", "public", or "private" If you do not want your scoped package to be publicly viewable (and installable) set \`--access=restricted\`. @@ -204,6 +207,8 @@ packages. Specifying a value of \`restricted\` or \`public\` during publish will change the access for an existing package the same way that \`npm access set status\` would. +The value \`private\` is an alias for \`restricted\`. + #### \`all\` @@ -217,6 +222,42 @@ upon by the current project. +#### \`allow-directory\` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from directories. That +is, dependencies that point to a directory instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +\`all\` allows any directories to be installed. \`none\` prevents any +directories from being installed. \`root\` only allows directories defined in +your project's package.json to be installed. Also allows directory +dependencies to be used for other commands like \`npm view\` + + + +#### \`allow-file\` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to install dependencies from tarball files. That +is, dependencies that point to a local tarball file instead of a version or +semver range. Please note that this could leave your tree incomplete and +some packages may not function as intended or designed. Changing this +setting will not remove dependencies that are already installed. + +\`all\` allows any tarball file to be installed. \`none\` prevents any tarball +file from being installed. \`root\` only allows tarball files defined in your +project's package.json to be installed. Also allows tarball file +dependencies to be used for other commands like \`npm view\` + + + #### \`allow-git\` * Default: "all" @@ -225,12 +266,31 @@ upon by the current project. Limits the ability for npm to fetch dependencies from git references. That is, dependencies that point to a git repo instead of a version or semver range. Please note that this could leave your tree incomplete and some -packages may not function as intended or designed. +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. \`all\` allows any git dependencies to be fetched and installed. \`none\` prevents any git dependencies from being fetched and installed. \`root\` only allows git dependencies defined in your project's package.json to be fetched -installed. Also allows git dependencies to be fetched for other commands +and installed. Also allows git dependencies to be fetched for other commands +like \`npm view\` + + + +#### \`allow-remote\` + +* Default: "all" +* Type: "all", "none", or "root" + +Limits the ability for npm to fetch dependencies from urls. That is, +dependencies that point to a tarball url instead of a version or semver +range. Please note that this could leave your tree incomplete and some +packages may not function as intended or designed. Changing this setting +will not remove dependencies that are already installed. + +\`all\` allows any url to be installed. \`none\` prevents any url from being +installed. \`root\` only allows urls defined in your project's package.json to +be installed. Also allows url dependencies to be used for other commands like \`npm view\` @@ -245,6 +305,51 @@ to the same value as the current version. +#### \`allow-scripts\` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(\`preinstall\`, \`install\`, \`postinstall\`, and \`prepare\` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: \`npm exec\`, \`npx\`, +and \`npm install -g\`, where no project \`package.json\` is involved. For +team-wide policy in a project, use the \`allowScripts\` field in +\`package.json\` (which also supports explicit denials), or configure it in +\`.npmrc\`. Passing \`--allow-scripts\` on the command line during a +project-scoped \`npm install\`, \`ci\`, \`update\`, or \`rebuild\` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. \`--ignore-scripts\` and +\`--dangerously-allow-all-scripts\` both override this setting. + + + +#### \`allow-scripts-pending\` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +\`allowScripts\` policy, without modifying \`package.json\`. Only meaningful for +\`npm approve-scripts\`. + + + +#### \`allow-scripts-pin\` + +* Default: true +* Type: Boolean + +Write pinned (\`pkg@version\`) entries when approving install scripts. Set to +\`false\` to write name-only entries that allow any version. Has no effect on +\`npm deny-scripts\`, which always writes name-only entries regardless of this +setting. + + + #### \`audit\` * Default: true @@ -292,7 +397,13 @@ If the requested version is a \`dist-tag\` and the given tag does not pass the will be used. For example, \`foo@latest\` might install \`foo@1.2\` even though \`latest\` is \`2.0\`. -This config cannot be used with: \`min-release-age\` +If \`before\` and \`min-release-age\` are both set in the same source, \`before\` +wins (an explicit absolute date overrides a relative window). Across +sources, the standard precedence applies (cli > env > project > user > +global), so a higher-priority source can always relax or override a +lower-priority one. + + #### \`bin-links\` @@ -434,6 +545,18 @@ are same as \`cpu\` field of package.json, which comes from \`process.arch\`. +#### \`dangerously-allow-all-scripts\` + +* Default: false +* Type: Boolean + +If \`true\`, bypass the \`allowScripts\` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +\`--ignore-scripts\` still takes precedence over this setting. + + + #### \`depth\` * Default: \`Infinity\` if \`--all\` is set; otherwise, \`0\` @@ -1146,9 +1269,11 @@ are no versions available for the current set of dependencies, the command will error. This flag is a complement to \`before\`, which accepts an exact date instead -of a relative number of days. - -This config cannot be used with: \`before\` +of a relative number of days. The two may coexist (e.g. \`min-release-age\` in +your \`.npmrc\` is preserved when npm internally spawns a sub-process with +\`--before\` while preparing a \`git:\` or \`github:\` dependency); when both +apply, \`before\` wins within a single source and across sources the standard +precedence rules apply. This value is not exported to the environment for child processes. @@ -1758,6 +1883,22 @@ this to work properly. +#### \`strict-allow-scripts\` + +* Default: false +* Type: Boolean + +If \`true\`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by \`allowScripts\` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with \`false\` in \`allowScripts\` are always +silently skipped; this setting only affects unreviewed entries. +\`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both override this +setting. + + + #### \`strict-peer-deps\` * Default: false @@ -2259,7 +2400,11 @@ Array [ "access", "all", "allow-same-version", + "allow-directory", + "allow-file", "allow-git", + "allow-remote", + "allow-scripts", "also", "audit", "audit-level", @@ -2279,6 +2424,7 @@ Array [ "color", "commit-hooks", "cpu", + "dangerously-allow-all-scripts", "depth", "description", "dev", @@ -2368,6 +2514,8 @@ Array [ "pack-destination", "packages", "parseable", + "allow-scripts-pending", + "allow-scripts-pin", "prefer-dedupe", "prefer-offline", "prefer-online", @@ -2409,6 +2557,7 @@ Array [ "sign-git-commit", "sign-git-tag", "strict-peer-deps", + "strict-allow-scripts", "strict-ssl", "tag", "tag-version-prefix", @@ -2436,7 +2585,11 @@ Array [ "access", "all", "allow-same-version", + "allow-directory", + "allow-file", "allow-git", + "allow-remote", + "allow-scripts", "also", "audit", "audit-level", @@ -2456,6 +2609,7 @@ Array [ "color", "commit-hooks", "cpu", + "dangerously-allow-all-scripts", "depth", "description", "dev", @@ -2525,6 +2679,8 @@ Array [ "pack-destination", "packages", "parseable", + "allow-scripts-pending", + "allow-scripts-pin", "prefer-dedupe", "prefer-offline", "prefer-online", @@ -2565,6 +2721,7 @@ Array [ "sign-git-commit", "sign-git-tag", "strict-peer-deps", + "strict-allow-scripts", "strict-ssl", "tag", "tag-version-prefix", @@ -2617,8 +2774,14 @@ Object { "_auth": null, "access": null, "all": false, + "allowDirectory": "all", + "allowFile": "all", "allowGit": "all", + "allowRemote": "all", "allowSameVersion": false, + "allowScripts": Array [], + "allowScriptsPending": false, + "allowScriptsPin": true, "audit": true, "auditLevel": null, "authType": "web", @@ -2634,6 +2797,7 @@ Object { "color": false, "commitHooks": true, "cpu": null, + "dangerouslyAllowAllScripts": false, "defaultTag": "latest", "depth": null, "diff": Array [], @@ -2739,6 +2903,7 @@ Object { "signGitCommit": false, "signGitTag": false, "silent": false, + "strictAllowScripts": false, "strictPeerDeps": false, "strictSSL": true, "tagVersionPrefix": "v", @@ -2876,6 +3041,46 @@ Note: This command is unaware of workspaces. #### \`auth-type\` ` +exports[`test/lib/docs.js TAP usage approve-scripts > must match snapshot 1`] = ` +Approve install scripts for specific dependencies + +Usage: +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending + +Options: +[-a|--all] [--allow-scripts-pending] [--no-allow-scripts-pin] [--json] + + -a|--all + When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show + + --allow-scripts-pending + List packages with install scripts that are not yet covered by the + + --allow-scripts-pin + Write pinned (\`pkg@version\`) entries when approving install scripts. + + --json + Whether or not to output JSON data, rather than the normal output. + + +Run "npm help approve-scripts" for more info + +\`\`\`bash +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +\`\`\` + +Note: This command is unaware of workspaces. + +#### \`all\` +#### \`allow-scripts-pending\` +#### \`allow-scripts-pin\` +#### \`json\` +` + exports[`test/lib/docs.js TAP usage audit > must match snapshot 1`] = ` Run a security audit @@ -3051,8 +3256,11 @@ Options: [--global-style] [--omit [--omit ...]] [--include [--include ...]] [--strict-peer-deps] [--foreground-scripts] [--ignore-scripts] -[--allow-git ] [--no-audit] [--no-bin-links] [--no-fund] -[--dry-run] +[--allow-directory ] [--allow-file ] +[--allow-git ] [--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -3080,9 +3288,27 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-directory + Limits the ability for npm to install dependencies from directories. + + --allow-file + Limits the ability for npm to install dependencies from tarball files. + --allow-git Limits the ability for npm to fetch dependencies from git references. + --allow-remote + Limits the ability for npm to fetch dependencies from urls. + + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -3126,7 +3352,13 @@ aliases: clean-install, ic, install-clean, isntall-clean #### \`strict-peer-deps\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-directory\` +#### \`allow-file\` #### \`allow-git\` +#### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`bin-links\` #### \`fund\` @@ -3223,8 +3455,10 @@ Options: [--global-style] [--strict-peer-deps] [--no-package-lock] [--omit [--omit ...]] [--include [--include ...]] -[--ignore-scripts] [--allow-git ] [--no-audit] [--no-bin-links] -[--no-fund] [--dry-run] +[--ignore-scripts] [--allow-directory ] +[--allow-file ] [--allow-git ] +[--allow-remote ] [--no-audit] [--no-bin-links] [--no-fund] +[--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -3252,9 +3486,18 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-directory + Limits the ability for npm to install dependencies from directories. + + --allow-file + Limits the ability for npm to install dependencies from tarball files. + --allow-git Limits the ability for npm to fetch dependencies from git references. + --allow-remote + Limits the ability for npm to fetch dependencies from urls. + --audit When "true" submit audit reports alongside the current npm command to the @@ -3298,7 +3541,10 @@ alias: ddp #### \`omit\` #### \`include\` #### \`ignore-scripts\` +#### \`allow-directory\` +#### \`allow-file\` #### \`allow-git\` +#### \`allow-remote\` #### \`audit\` #### \`bin-links\` #### \`fund\` @@ -3309,6 +3555,44 @@ alias: ddp #### \`install-links\` ` +exports[`test/lib/docs.js TAP usage deny-scripts > must match snapshot 1`] = ` +Deny install scripts for specific dependencies + +Usage: +npm deny-scripts [ ...] +npm deny-scripts --all + +Options: +[-a|--all] [--allow-scripts-pending] [--no-allow-scripts-pin] [--json] + + -a|--all + When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show + + --allow-scripts-pending + List packages with install scripts that are not yet covered by the + + --allow-scripts-pin + Write pinned (\`pkg@version\`) entries when approving install scripts. + + --json + Whether or not to output JSON data, rather than the normal output. + + +Run "npm help deny-scripts" for more info + +\`\`\`bash +npm deny-scripts [ ...] +npm deny-scripts --all +\`\`\` + +Note: This command is unaware of workspaces. + +#### \`all\` +#### \`allow-scripts-pending\` +#### \`allow-scripts-pin\` +#### \`json\` +` + exports[`test/lib/docs.js TAP usage deprecate > must match snapshot 1`] = ` Deprecate a version of a package @@ -3560,6 +3844,8 @@ Options: [--package [--package ...]] [-c|--call ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] --package The package or packages to install for [\`npm exec\`](/commands/npm-exec) @@ -3576,6 +3862,15 @@ Options: --include-workspace-root Include the workspace root when workspaces are enabled for a command. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + alias: x @@ -3595,6 +3890,9 @@ alias: x #### \`workspace\` #### \`workspaces\` #### \`include-workspace-root\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` ` exports[`test/lib/docs.js TAP usage explain > must match snapshot 1`] = ` @@ -3948,9 +4246,13 @@ Options: [--global-style] [--omit [--omit ...]] [--include [--include ...]] [--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only] -[--foreground-scripts] [--ignore-scripts] [--allow-git ] -[--no-audit] [--before |--min-release-age ] [--no-bin-links] -[--no-fund] [--dry-run] [--cpu ] [--os ] [--libc ] +[--foreground-scripts] [--ignore-scripts] [--allow-directory ] +[--allow-file ] [--allow-git ] +[--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--before ] [--min-release-age ] [--no-bin-links] [--no-fund] +[--dry-run] [--cpu ] [--os ] [--libc ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -3996,9 +4298,27 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-directory + Limits the ability for npm to install dependencies from directories. + + --allow-file + Limits the ability for npm to install dependencies from tarball files. + --allow-git Limits the ability for npm to fetch dependencies from git references. + --allow-remote + Limits the ability for npm to fetch dependencies from urls. + + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -4063,7 +4383,13 @@ aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall #### \`package-lock-only\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-directory\` +#### \`allow-file\` #### \`allow-git\` +#### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`before\` #### \`min-release-age\` @@ -4090,8 +4416,11 @@ Options: [--global-style] [--omit [--omit ...]] [--include [--include ...]] [--strict-peer-deps] [--foreground-scripts] [--ignore-scripts] -[--allow-git ] [--no-audit] [--no-bin-links] [--no-fund] -[--dry-run] +[--allow-directory ] [--allow-file ] +[--allow-git ] [--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -4119,9 +4448,27 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-directory + Limits the ability for npm to install dependencies from directories. + + --allow-file + Limits the ability for npm to install dependencies from tarball files. + --allow-git Limits the ability for npm to fetch dependencies from git references. + --allow-remote + Limits the ability for npm to fetch dependencies from urls. + + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -4165,7 +4512,13 @@ aliases: cit, clean-install-test, sit #### \`strict-peer-deps\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-directory\` +#### \`allow-file\` #### \`allow-git\` +#### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`bin-links\` #### \`fund\` @@ -4189,9 +4542,13 @@ Options: [--global-style] [--omit [--omit ...]] [--include [--include ...]] [--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only] -[--foreground-scripts] [--ignore-scripts] [--allow-git ] -[--no-audit] [--before |--min-release-age ] [--no-bin-links] -[--no-fund] [--dry-run] [--cpu ] [--os ] [--libc ] +[--foreground-scripts] [--ignore-scripts] [--allow-directory ] +[--allow-file ] [--allow-git ] +[--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--before ] [--min-release-age ] [--no-bin-links] [--no-fund] +[--dry-run] [--cpu ] [--os ] [--libc ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -4237,9 +4594,27 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-directory + Limits the ability for npm to install dependencies from directories. + + --allow-file + Limits the ability for npm to install dependencies from tarball files. + --allow-git Limits the ability for npm to fetch dependencies from git references. + --allow-remote + Limits the ability for npm to fetch dependencies from urls. + + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -4304,7 +4679,13 @@ alias: it #### \`package-lock-only\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-directory\` +#### \`allow-file\` #### \`allow-git\` +#### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`before\` #### \`min-release-age\` @@ -4333,8 +4714,10 @@ Options: [--global-style] [--strict-peer-deps] [--no-package-lock] [--omit [--omit ...]] [--include [--include ...]] -[--ignore-scripts] [--allow-git ] [--no-audit] [--no-bin-links] -[--no-fund] [--dry-run] +[--ignore-scripts] [--allow-directory ] +[--allow-file ] [--allow-git ] +[--allow-remote ] [--no-audit] [--no-bin-links] [--no-fund] +[--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -4371,9 +4754,18 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-directory + Limits the ability for npm to install dependencies from directories. + + --allow-file + Limits the ability for npm to install dependencies from tarball files. + --allow-git Limits the ability for npm to fetch dependencies from git references. + --allow-remote + Limits the ability for npm to fetch dependencies from urls. + --audit When "true" submit audit reports alongside the current npm command to the @@ -4420,7 +4812,10 @@ alias: ln #### \`omit\` #### \`include\` #### \`ignore-scripts\` +#### \`allow-directory\` +#### \`allow-file\` #### \`allow-git\` +#### \`allow-remote\` #### \`audit\` #### \`bin-links\` #### \`fund\` @@ -4739,7 +5134,7 @@ npm outdated [ ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [-w|--workspace [-w|--workspace ...]] -[--before |--min-release-age ] +[--before ] [--min-release-age ] -a|--all When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show @@ -4762,6 +5157,9 @@ Options: --before If passed to \`npm install\`, will rebuild the npm tree such that only + --min-release-age + If set, npm will build the npm tree such that only versions that were + Run "npm help outdated" for more info @@ -5076,7 +5474,7 @@ Usage: npm publish Options: -[--tag ] [--access ] [--dry-run] [--otp ] +[--tag ] [--access ] [--dry-run] [--otp ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--provenance|--provenance-file ] @@ -5176,6 +5574,8 @@ npm rebuild [] ...] Options: [-g|--global] [--no-bin-links] [--foreground-scripts] [--ignore-scripts] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -5191,6 +5591,15 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + -w|--workspace Enable running a command in the context of the configured workspaces of the @@ -5218,6 +5627,9 @@ alias: rb #### \`bin-links\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`workspace\` #### \`workspaces\` #### \`include-workspace-root\` @@ -5528,6 +5940,61 @@ Note: This command is unaware of workspaces. NO PARAMS ` +exports[`test/lib/docs.js TAP usage stage > must match snapshot 1`] = ` +Stage packages for publishing, deferring proof-of-presence (2FA) to a later point in time + +Usage: +npm stage +npm stage publish +npm stage list [] +npm stage view +npm stage approve +npm stage reject +npm stage download + +Subcommands: + publish + Stage a package for publishing, deferring proof-of-presence (2FA) to a later point in time + + list + List all staged package versions + + view + View details of a specific staged package + + approve + Approve a staged package, publishing it to the npm registry + + reject + Reject a staged package, removing it from the registry + + download + Download the tarball of a staged package for inspection + +Run "npm stage --help" for more info on a subcommand. + +Run "npm help stage" for more info + +\`\`\`bash +npm stage +\`\`\` + +Note: This command is unaware of workspaces. + +#### Synopsis +#### Flags +#### Synopsis +#### Flags +#### Synopsis +#### Flags +#### Synopsis +#### Flags +#### Synopsis +#### Flags +#### Synopsis +#### Flags +` + exports[`test/lib/docs.js TAP usage star > must match snapshot 1`] = ` Mark your favorite packages @@ -5804,9 +6271,9 @@ exports[`test/lib/docs.js TAP usage trust > must match snapshot 1`] = ` Create a trusted relationship between a package and a OIDC provider Usage: -npm trust github [package] --file [--repo|--repository] [--env|--environment] [-y|--yes] -npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes] -npm trust circleci [package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [-y|--yes] +npm trust github [package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes] +npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes] +npm trust circleci [package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [--allow-publish] [--allow-stage-publish] [-y|--yes] npm trust list [package] npm trust revoke [package] --id= @@ -6006,8 +6473,11 @@ Options: [--omit [--omit ...]] [--include [--include ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] -[--ignore-scripts] [--no-audit] [--before |--min-release-age ] -[--no-bin-links] [--no-fund] [--dry-run] +[--ignore-scripts] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--before ] [--min-release-age ] [--no-bin-links] [--no-fund] +[--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -6044,12 +6514,24 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the --before If passed to \`npm install\`, will rebuild the npm tree such that only + --min-release-age + If set, npm will build the npm tree such that only versions that were + --bin-links Tells npm to create symlinks (or \`.cmd\` shims on Windows) for package @@ -6093,6 +6575,9 @@ aliases: u, up, upgrade, udpate #### \`package-lock\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`before\` #### \`min-release-age\` diff --git a/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs b/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs index 888af047882fa7..dc1c95cd3c5763 100644 --- a/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs @@ -31,16 +31,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -69,9 +69,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -87,13 +89,13 @@ All commands: query, rebuild, repo, restart, root, run, sbom, search, set, - shrinkwrap, star, stars, - start, stop, team, test, - token, trust, - undeprecate, uninstall, - unpublish, unstar, - update, version, view, - whoami + shrinkwrap, stage, star, + stars, start, stop, + team, test, token, + trust, undeprecate, + uninstall, unpublish, + unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -122,9 +124,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -140,13 +144,13 @@ All commands: query, rebuild, repo, restart, root, run, sbom, search, set, - shrinkwrap, star, stars, - start, stop, team, test, - token, trust, - undeprecate, uninstall, - unpublish, unstar, - update, version, view, - whoami + shrinkwrap, stage, star, + stars, start, stop, + team, test, token, + trust, undeprecate, + uninstall, unpublish, + unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -174,16 +178,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -212,9 +216,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -230,13 +236,13 @@ All commands: query, rebuild, repo, restart, root, run, sbom, search, set, - shrinkwrap, star, stars, - start, stop, team, test, - token, trust, - undeprecate, uninstall, - unpublish, unstar, - update, version, view, - whoami + shrinkwrap, stage, star, + stars, start, stop, + team, test, token, + trust, undeprecate, + uninstall, unpublish, + unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -265,9 +271,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -283,13 +291,13 @@ All commands: query, rebuild, repo, restart, root, run, sbom, search, set, - shrinkwrap, star, stars, - start, stop, team, test, - token, trust, - undeprecate, uninstall, - unpublish, unstar, - update, version, view, - whoami + shrinkwrap, stage, star, + stars, start, stop, + team, test, token, + trust, undeprecate, + uninstall, unpublish, + unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -317,10 +325,12 @@ npm help npm more involved overview All commands: - access, adduser, audit, + access, adduser, + approve-scripts, audit, bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -335,8 +345,9 @@ All commands: query, rebuild, repo, restart, root, run, sbom, search, set, shrinkwrap, - star, stars, start, stop, - team, test, token, trust, + stage, star, stars, + start, stop, team, test, + token, trust, undeprecate, uninstall, unpublish, unstar, update, version, view, @@ -368,16 +379,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -405,16 +416,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -442,16 +453,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} diff --git a/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs index 8bc81cc4f69c15..124478bc829938 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs @@ -142,6 +142,66 @@ exports[`test/lib/utils/sbom-cyclonedx.js TAP node - with duplicate deps > must } ` +exports[`test/lib/utils/sbom-cyclonedx.js TAP node - with duplicate edges to same dep > must match snapshot 1`] = ` +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000", + "version": 1, + "metadata": { + "timestamp": "2020-01-01T00:00:00.000Z", + "lifecycles": [ + { + "phase": "build" + } + ], + "tools": [ + { + "vendor": "npm", + "name": "cli", + "version": "10.0.0 " + } + ], + "component": { + "bom-ref": "root@1.0.0", + "type": "library", + "name": "root", + "version": "1.0.0", + "scope": "required", + "author": "Author", + "purl": "pkg:npm/root@1.0.0", + "properties": [], + "externalReferences": [] + } + }, + "components": [ + { + "bom-ref": "dep1@0.0.1", + "type": "library", + "name": "dep1", + "version": "0.0.1", + "scope": "required", + "purl": "pkg:npm/dep1@0.0.1", + "properties": [], + "externalReferences": [] + } + ], + "dependencies": [ + { + "ref": "root@1.0.0", + "dependsOn": [ + "dep1@0.0.1" + ] + }, + { + "ref": "dep1@0.0.1", + "dependsOn": [] + } + ] +} +` + exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - application package type > must match snapshot 1`] = ` { "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", diff --git a/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs index 26931d78124a70..6adb6d26de1435 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs @@ -271,6 +271,73 @@ exports[`test/lib/utils/sbom-spdx.js TAP node - with duplicate deps > must match } ` +exports[`test/lib/utils/sbom-spdx.js TAP node - with duplicate edges to same dep > must match snapshot 1`] = ` +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "root@1.0.0", + "documentNamespace": "docns", + "creationInfo": { + "created": "2020-01-01T00:00:00.000Z", + "creators": [ + "Tool: npm/cli-10.0.0 " + ] + }, + "documentDescribes": [ + "SPDXRef-Package-root-1.0.0" + ], + "packages": [ + { + "name": "root", + "SPDXID": "SPDXRef-Package-root-1.0.0", + "versionInfo": "1.0.0", + "packageFileName": "", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "homepage": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/root@1.0.0" + } + ] + }, + { + "name": "dep1", + "SPDXID": "SPDXRef-Package-dep1-0.0.1", + "versionInfo": "0.0.1", + "packageFileName": "node_modules/dep1", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "homepage": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/dep1@0.0.1" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Package-root-1.0.0", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-Package-dep1-0.0.1", + "relatedSpdxElement": "SPDXRef-Package-root-1.0.0", + "relationshipType": "DEPENDENCY_OF" + } + ] +} +` + exports[`test/lib/utils/sbom-spdx.js TAP single node - application package type > must match snapshot 1`] = ` { "spdxVersion": "SPDX-2.3", diff --git a/deps/npm/test/lib/commands/approve-scripts.js b/deps/npm/test/lib/commands/approve-scripts.js new file mode 100644 index 00000000000000..dde7a358b12e2b --- /dev/null +++ b/deps/npm/test/lib/commands/approve-scripts.js @@ -0,0 +1,562 @@ +const t = require('tap') +const fs = require('node:fs') +const { resolve } = require('node:path') +const _mockNpm = require('../../fixtures/mock-npm') + +const mockNpm = async (t, opts = {}) => { + return _mockNpm(t, opts) +} + +const setupProject = ({ allowScripts, withScripts = ['canvas'] } = {}) => { + const pkg = { + name: 'host', + version: '1.0.0', + dependencies: Object.fromEntries(withScripts.map((n) => [n, '*'])), + } + if (allowScripts !== undefined) { + pkg.allowScripts = allowScripts + } + + const lockPackages = { '': pkg } + const nodeModules = {} + for (const name of withScripts) { + const tarUrl = `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz` + nodeModules[name] = { + 'package.json': JSON.stringify({ + name, + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + } + lockPackages[`node_modules/${name}`] = { + version: '1.0.0', + resolved: tarUrl, + hasInstallScript: true, + } + } + + return { + 'package.json': JSON.stringify(pkg, null, 2), + 'package-lock.json': JSON.stringify({ + name: pkg.name, + version: pkg.version, + lockfileVersion: 3, + requires: true, + packages: lockPackages, + }), + node_modules: nodeModules, + } +} + +t.test('approve-scripts --pending lists unreviewed packages', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + const out = joinedOutput() + t.match(out, /2 packages have install scripts not yet covered/) + t.match(out, /canvas@1\.0\.0/) + t.match(out, /sharp@1\.0\.0/) +}) + +t.test('approve-scripts --pending with no unreviewed says so', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + allowScripts: { canvas: true }, + withScripts: ['canvas'], + }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('approve-scripts writes pinned entry by default', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) +}) + +t.test('approve-scripts --no-pin writes name-only entry', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pin': false }, + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { canvas: true }) +}) + +t.test('approve-scripts --all approves every unreviewed package', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { + 'canvas@1.0.0': true, + 'sharp@1.0.0': true, + }) +}) + +t.test('approve-scripts errors on unknown package', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('approve-scripts', ['not-installed']), + { code: 'ENOMATCH' } + ) +}) + +t.test('approve-scripts respects existing deny entry', async t => { + const { npm, prefix, logs } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { canvas: false }, + }), + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + // Deny wins; unchanged. + t.strictSame(pkg.allowScripts, { canvas: false }) + t.match(logs.warn.byTitle('approve-scripts'), [/canvas is denied/]) +}) + +t.test('approve-scripts requires positional args, --all, or --pending', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects(npm.exec('approve-scripts', []), { code: 'EUSAGE' }) +}) + +t.test('approve-scripts --pending cannot be combined with positional', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await t.rejects(npm.exec('approve-scripts', ['canvas']), { code: 'EUSAGE' }) +}) + +t.test('approve-scripts fails on global', async t => { + const { npm } = await mockNpm(t, { + config: { global: true }, + }) + await t.rejects(npm.exec('approve-scripts', ['canvas']), { code: 'EGLOBAL' }) +}) + +t.test('approve-scripts --json outputs structured summary', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { json: true }, + }) + await npm.exec('approve-scripts', ['canvas']) + const parsed = JSON.parse(joinedOutput()) + t.match(parsed, { + allowScripts: [{ name: 'canvas', changes: [{ key: 'canvas@1.0.0', change: 'added' }] }], + }) +}) + +t.test('approve-scripts --all with no unreviewed packages prints message', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'host', version: '1.0.0' } }, + }), + node_modules: {}, + }, + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('approve-scripts on a package already at the right pin is no-op', async t => { + const { npm, prefix, joinedOutput } = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { 'canvas@1.0.0': true }, + }), + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) + t.match(joinedOutput(), /Nothing to approve/) +}) + +t.test('approve-scripts --pending with single package uses singular wording', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /1 package has install scripts/) +}) + +t.test('approve-scripts --pending lists package with no version', async t => { + // Use a fixture where the lockfile records a synthetic node without a version + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + // Just exercising; no assertion needed for additional coverage. + t.pass() +}) + +t.test('approve-scripts groups multiple installed versions of the same package', async t => { + // Two versions of lodash exist in the tree; both have install scripts. + // groupByPackage should put them in the same group (hits the + // `if (!groups[key])` falsy branch on the second node). + const { npm, prefix } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'top-of-tree': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'top-of-tree': '*' } }, + 'node_modules/lodash': { + version: '4.17.21', + resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz', + hasInstallScript: true, + }, + 'node_modules/top-of-tree': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/top-of-tree/-/top-of-tree-1.0.0.tgz', + dependencies: { lodash: '3.10.1' }, + }, + 'node_modules/top-of-tree/node_modules/lodash': { + version: '3.10.1', + resolved: 'https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz', + hasInstallScript: true, + }, + }, + }), + node_modules: { + lodash: { + 'package.json': JSON.stringify({ + name: 'lodash', + version: '4.17.21', + scripts: { install: 'echo install' }, + }), + }, + 'top-of-tree': { + 'package.json': JSON.stringify({ name: 'top-of-tree', version: '1.0.0' }), + node_modules: { + lodash: { + 'package.json': JSON.stringify({ + name: 'lodash', + version: '3.10.1', + scripts: { install: 'echo install' }, + }), + }, + }, + }, + }, + }, + }) + await npm.exec('approve-scripts', ['lodash']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + // Both versions get pinned. + t.strictSame(pkg.allowScripts, { + 'lodash@3.10.1': true, + 'lodash@4.17.21': true, + }) +}) + +t.test('approve-scripts --pending handles node with no version', async t => { + // Exercise the ternary's falsy branch in runPending: `node.version ? '@'... : ''` + // when the node has no version field. + const mockSync = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'host', version: '1.0.0' } }, + }), + node_modules: {}, + }, + config: { 'allow-scripts-pending': true }, + mocks: { + // Make the walker return a synthetic node with no version + '{LIB}/utils/check-allow-scripts.js': async () => [{ + node: { packageName: 'no-version-pkg', name: 'no-version-pkg', version: undefined }, + scripts: { install: 'do-stuff' }, + }], + }, + }) + await mockSync.npm.exec('approve-scripts', []) + // Output should mention the package without an @version suffix. + t.match(mockSync.joinedOutput(), / no-version-pkg \(install: do-stuff\)/) +}) + +t.test('forbidden semver range in package.json#allowScripts is dropped with a warning', async t => { + // End-to-end: project declares a caret range in allowScripts. The + // resolver must drop the entry, emit a warning, and the matching node + // must remain unreviewed (listed by --pending). + const mock = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + // ^0.33.0 is a forbidden range per RFC. + allowScripts: { 'canvas@^0.33.0': true }, + }), + config: { 'allow-scripts-pending': true }, + }) + await mock.npm.exec('approve-scripts', []) + + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.ok( + warnings.some(m => /semver ranges/.test(m) && /canvas@\^0\.33\.0/.test(m)), + 'resolver emits warning about forbidden range' + ) + // canvas was installed with version 1.0.0 (setupProject default) and + // the forbidden allowlist entry was dropped, so canvas appears in the + // pending list. + t.match(mock.joinedOutput(), /canvas@1\.0\.0/) +}) + +t.test('approve-scripts --pending lists packages that only have binding.gyp', async t => { + // End-to-end: a package with no preinstall/install/postinstall but a + // binding.gyp on disk gets a synthetic `node-gyp rebuild` install + // script. The runtime isNodeGypPackage check must see it and surface + // the package in --pending output. + const mock = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'native-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'native-pkg': '*' } }, + 'node_modules/native-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/native-pkg/-/native-pkg-1.0.0.tgz', + // No hasInstallScript — the synthetic node-gyp injection is + // what we want this test to exercise. + }, + }, + }), + node_modules: { + 'native-pkg': { + 'package.json': JSON.stringify({ name: 'native-pkg', version: '1.0.0' }), + // The file that triggers isNodeGypPackage to return true. + 'binding.gyp': '{}', + }, + }, + }, + config: { 'allow-scripts-pending': true }, + }) + await mock.npm.exec('approve-scripts', []) + + const out = mock.joinedOutput() + t.match(out, /native-pkg@1\.0\.0/, 'binding.gyp-only package appears in --pending') + t.match(out, /install: node-gyp rebuild/, 'synthetic node-gyp install is named') +}) + +t.test('approve-scripts --all skips bundled deps with a notice', async t => { + // Bundled deps cannot be allowlisted in Phase 1 (RFC defers their + // allowlisting to a follow-up). --all must not silently write a key + // derived from the bundled tarball's self-claimed identity. + const { npm, logs, prefix } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'parent-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'parent-pkg': '*' } }, + 'node_modules/parent-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/parent-pkg/-/parent-pkg-1.0.0.tgz', + hasInstallScript: true, + }, + 'node_modules/parent-pkg/node_modules/inner': { + version: '1.0.0', + inBundle: true, + hasInstallScript: true, + }, + }, + }), + node_modules: { + 'parent-pkg': { + 'package.json': JSON.stringify({ + name: 'parent-pkg', + version: '1.0.0', + scripts: { install: 'echo install' }, + bundleDependencies: ['inner'], + }), + node_modules: { + inner: { + 'package.json': JSON.stringify({ + name: 'inner', + version: '1.0.0', + scripts: { install: 'echo bundled-install' }, + }), + }, + }, + }, + }, + }, + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + // parent-pkg is approvable. inner is bundled and must be excluded. + t.equal(pkg.allowScripts['parent-pkg@1.0.0'], true, + 'non-bundled parent gets approved') + t.notOk(Object.keys(pkg.allowScripts).some(k => k.startsWith('inner')), + 'bundled inner is not approved') + t.match(logs.warn.byTitle('approve-scripts'), [/Skipping 1 bundled dependency/]) +}) + +t.test('approve-scripts positional is ignored', async t => { + // Same protection on the positional path: a user typing a bundled + // package name must not get a policy entry written. + const { npm } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'parent-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'parent-pkg': '*' } }, + 'node_modules/parent-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/parent-pkg/-/parent-pkg-1.0.0.tgz', + hasInstallScript: true, + }, + 'node_modules/parent-pkg/node_modules/inner': { + version: '1.0.0', + inBundle: true, + hasInstallScript: true, + }, + }, + }), + node_modules: { + 'parent-pkg': { + 'package.json': JSON.stringify({ + name: 'parent-pkg', + version: '1.0.0', + scripts: { install: 'echo install' }, + bundleDependencies: ['inner'], + }), + node_modules: { + inner: { + 'package.json': JSON.stringify({ + name: 'inner', + version: '1.0.0', + scripts: { install: 'echo bundled' }, + }), + }, + }, + }, + }, + }, + }) + await t.rejects( + npm.exec('approve-scripts', ['inner']), + { code: 'ENOMATCH' }, + 'typing the bundled package name does not match any approvable node' + ) +}) + +t.test('approve-scripts --all with only bundled deps prints "no eligible" notice', async t => { + const { npm, logs, joinedOutput, prefix } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'parent-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'parent-pkg': '*' } }, + 'node_modules/parent-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/parent-pkg/-/parent-pkg-1.0.0.tgz', + // parent-pkg has NO install scripts; only the bundled child does. + }, + 'node_modules/parent-pkg/node_modules/only-bundled': { + version: '1.0.0', + inBundle: true, + hasInstallScript: true, + }, + }, + }), + node_modules: { + 'parent-pkg': { + 'package.json': JSON.stringify({ + name: 'parent-pkg', + version: '1.0.0', + bundleDependencies: ['only-bundled'], + }), + node_modules: { + 'only-bundled': { + 'package.json': JSON.stringify({ + name: 'only-bundled', + version: '1.0.0', + scripts: { install: 'echo evil' }, + }), + }, + }, + }, + }, + }, + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /No packages eligible for approval/) + t.match(logs.warn.byTitle('approve-scripts'), [/Skipping 1 bundled dependency/]) + // Ensure no policy entry was written. + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.notOk(pkg.allowScripts, 'no allowScripts written') +}) diff --git a/deps/npm/test/lib/commands/config.js b/deps/npm/test/lib/commands/config.js index 9a65e883cfebc1..8237ffff22a42a 100644 --- a/deps/npm/test/lib/commands/config.js +++ b/deps/npm/test/lib/commands/config.js @@ -582,6 +582,11 @@ t.test('config edit', async t => { }, }) + const inputEvents = [] + const inputListener = (level) => inputEvents.push(level) + process.on('input', inputListener) + t.teardown(() => process.off('input', inputListener)) + await npm.exec('config', ['edit']) t.ok(editor.called, 'editor was spawned') @@ -590,6 +595,7 @@ t.test('config edit', async t => { [join(home, '.npmrc')], 'editor opened the user config file' ) + t.same(inputEvents.slice(0, 2), ['start', 'end'], 'progress paused and resumed around editor') const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) t.ok(contents.includes('foo=bar'), 'kept foo') diff --git a/deps/npm/test/lib/commands/deny-scripts.js b/deps/npm/test/lib/commands/deny-scripts.js new file mode 100644 index 00000000000000..fd9031c665d6a8 --- /dev/null +++ b/deps/npm/test/lib/commands/deny-scripts.js @@ -0,0 +1,163 @@ +const t = require('tap') +const fs = require('node:fs') +const { resolve } = require('node:path') +const _mockNpm = require('../../fixtures/mock-npm') + +const setupProject = ({ allowScripts, withScripts = ['core-js'] } = {}) => { + const pkg = { + name: 'host', + version: '1.0.0', + dependencies: Object.fromEntries(withScripts.map((n) => [n, '*'])), + } + if (allowScripts !== undefined) { + pkg.allowScripts = allowScripts + } + const lockPackages = { '': pkg } + const nodeModules = {} + for (const name of withScripts) { + nodeModules[name] = { + 'package.json': JSON.stringify({ + name, + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + } + lockPackages[`node_modules/${name}`] = { + version: '1.0.0', + resolved: `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz`, + hasInstallScript: true, + } + } + return { + 'package.json': JSON.stringify(pkg, null, 2), + 'package-lock.json': JSON.stringify({ + name: pkg.name, + version: pkg.version, + lockfileVersion: 3, + requires: true, + packages: lockPackages, + }), + node_modules: nodeModules, + } +} + +t.test('deny-scripts writes name-only false entry', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + }) + await npm.exec('deny-scripts', ['core-js']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) +}) + +t.test('deny-scripts ignores --pin and always writes name-only', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + config: { 'allow-scripts-pin': true }, + }) + await npm.exec('deny-scripts', ['core-js']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) +}) + +t.test('deny-scripts replaces existing pinned allow', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['core-js'], + allowScripts: { 'core-js@1.0.0': true }, + }), + }) + await npm.exec('deny-scripts', ['core-js']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) +}) + +t.test('deny-scripts --pending is rejected', async t => { + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + config: { 'allow-scripts-pending': true }, + }) + await t.rejects(npm.exec('deny-scripts', []), { code: 'EUSAGE' }) +}) + +t.test('deny-scripts --all denies every unreviewed package', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js', 'telemetry'] }), + config: { all: true }, + }) + await npm.exec('deny-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false, telemetry: false }) +}) + +t.test('deny-scripts errors on unknown package', async t => { + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + }) + await t.rejects( + npm.exec('deny-scripts', ['not-installed']), + { code: 'ENOMATCH' } + ) +}) + +t.test('deny-scripts requires positional args or --all', async t => { + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + }) + await t.rejects(npm.exec('deny-scripts', []), { code: 'EUSAGE' }) +}) + +t.test('deny-scripts --all with no unreviewed packages prints message', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'host', version: '1.0.0' } }, + }), + node_modules: {}, + }, + config: { all: true }, + }) + await npm.exec('deny-scripts', []) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('deny-scripts fails on global', async t => { + const { npm } = await _mockNpm(t, { + config: { global: true }, + }) + await t.rejects(npm.exec('deny-scripts', ['canvas']), { code: 'EGLOBAL' }) +}) + +t.test('deny-scripts on a package already denied is no-op', async t => { + const { npm, joinedOutput, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['core-js'], + allowScripts: { 'core-js': false }, + }), + }) + await npm.exec('deny-scripts', ['core-js']) + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) + t.match(joinedOutput(), /Nothing to deny/) +}) + +t.test('deny-scripts --json outputs structured summary', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + config: { json: true }, + }) + await npm.exec('deny-scripts', ['core-js']) + const parsed = JSON.parse(joinedOutput()) + t.match(parsed, { + allowScripts: [{ name: 'core-js', changes: [{ key: 'core-js', change: 'added' }] }], + }) +}) diff --git a/deps/npm/test/lib/commands/edit.js b/deps/npm/test/lib/commands/edit.js index b55bb2df218ba2..915241c82f6da8 100644 --- a/deps/npm/test/lib/commands/edit.js +++ b/deps/npm/test/lib/commands/edit.js @@ -58,8 +58,14 @@ t.test('npm edit', async t => { : ['-c', 'testinstall'] spawk.spawn(scriptShell, scriptArgs, { cwd: semverPath }) + const inputEvents = [] + const inputListener = (level) => inputEvents.push(level) + process.on('input', inputListener) + t.teardown(() => process.off('input', inputListener)) + await npm.exec('edit', ['semver']) t.match(joinedOutput(), 'rebuilt dependencies successfully') + t.same(inputEvents.slice(0, 2), ['start', 'end'], 'progress paused and resumed around editor') }) t.test('rebuild failure', async t => { diff --git a/deps/npm/test/lib/commands/exec.js b/deps/npm/test/lib/commands/exec.js index 2a6d3f6b8e0aff..92ea993e3edfb2 100644 --- a/deps/npm/test/lib/commands/exec.js +++ b/deps/npm/test/lib/commands/exec.js @@ -303,3 +303,68 @@ t.test('can run packages with keywords', async t => { t.fail(err, 'should not throw') } }) + +t.test('exec threads allowScripts policy from .npmrc through to libexec', async t => { + let capturedOpts + const fakeLibexec = async (opts) => { + capturedOpts = opts + } + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + '.npmrc': 'allow-scripts = canvas', + }, + mocks: { + libnpmexec: fakeLibexec, + }, + }) + await npm.exec('exec', ['some-pkg']) + t.strictSame(capturedOpts.allowScripts, { canvas: true }, + 'allowScripts populated from .npmrc layer') +}) + +t.test('exec ignores project package.json#allowScripts (RFC: .npmrc-only)', async t => { + // Per RFC line 299, exec/npx consults only user/global .npmrc. Project + // package.json policy must NOT influence npx behaviour, even when the + // user is running npx inside a project that has its own allowScripts. + let capturedOpts + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + allowScripts: { sharp: true }, + }), + }, + mocks: { + libnpmexec: async (opts) => { + capturedOpts = opts + }, + }, + }) + await npm.exec('exec', ['some-pkg']) + // package.json policy is skipped; no other layer has policy; result is null. + t.equal(capturedOpts.allowScripts, null) +}) + +t.test('exec reads .npmrc policy even when project package.json has a different policy', async t => { + // .npmrc-tier policy wins because package.json is skipped entirely. + let capturedOpts + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + mocks: { + libnpmexec: async (opts) => { + capturedOpts = opts + }, + }, + }) + await npm.exec('exec', ['some-pkg']) + t.strictSame(capturedOpts.allowScripts, { canvas: true }) +}) diff --git a/deps/npm/test/lib/commands/install.js b/deps/npm/test/lib/commands/install.js index 584690b68b5c62..2aa56a3dcd1759 100644 --- a/deps/npm/test/lib/commands/install.js +++ b/deps/npm/test/lib/commands/install.js @@ -242,6 +242,7 @@ t.test('exec commands', async t => { const { npm } = await loadMockNpm(t, { config: { 'allow-git': 'none', + audit: false, }, }) await t.rejects( @@ -257,7 +258,8 @@ t.test('exec commands', async t => { t.test('allow-git=root refuses non-root git dependency', async t => { const { npm } = await loadMockNpm(t, { config: { - 'allow-git': 'none', + 'allow-git': 'root', + audit: false, }, prefixDir: { 'package.json': JSON.stringify({ name: '@npmcli/test-package', version: '1.0.0' }), @@ -268,7 +270,129 @@ t.test('exec commands', async t => { }) await t.rejects( npm.exec('install', ['./abbrev']), - /Fetching packages of type "git" have been disabled/ + /Fetching non-root packages of type "git" have been disabled/ + ) + }) + + t.test('allow-directory=none blocks default symlink install', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'allow-directory': 'none', + audit: false, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/test-package', + version: '1.0.0', + dependencies: { 'dir-dep': 'file:./dir-dep' }, + }), + 'dir-dep': { + 'package.json': JSON.stringify({ name: 'dir-dep', version: '1.0.0' }), + }, + }, + }) + await t.rejects( + npm.exec('install', []), + { + code: 'EALLOWDIRECTORY', + message: 'Fetching packages of type "directory" have been disabled', + } + ) + }) + + t.test('allow-directory=root permits top-level directory dependency', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'allow-directory': 'root', + audit: false, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/test-package', + version: '1.0.0', + dependencies: { 'dir-dep': 'file:./dir-dep' }, + }), + 'dir-dep': { + 'package.json': JSON.stringify({ name: 'dir-dep', version: '1.0.0' }), + }, + }, + }) + await npm.exec('install', []) + const installedPkg = require(path.join(npm.prefix, 'node_modules', 'dir-dep', 'package.json')) + t.equal(installedPkg.name, 'dir-dep', 'dir-dep is installed and readable through node_modules') + }) + + t.test('allow-git=root soft-skips transitive optional git dependency', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'allow-git': 'root', + audit: false, + }, + prefixDir: { + 'package.json': JSON.stringify({ name: '@npmcli/test-package', version: '1.0.0' }), + abbrev: { + 'package.json': JSON.stringify({ + name: 'abbrev', + version: '1.0.0', + optionalDependencies: { npm: 'npm/npm' }, + }), + }, + }, + }) + await npm.exec('install', ['./abbrev']) + t.ok( + fs.existsSync(path.join(npm.prefix, 'node_modules', 'abbrev', 'package.json')), + 'abbrev (the legitimate parent) is installed' + ) + t.notOk( + fs.existsSync(path.join(npm.prefix, 'node_modules', 'npm')), + 'optional transitive git dep is silently skipped' + ) + }) + + t.test('allow-remote=none does not block registry tarballs', async t => { + const { npm, registry } = await loadMockNpm(t, { + config: { + 'allow-remote': 'none', + audit: false, + }, + prefixDir: { + 'package.json': JSON.stringify({ + ...packageJson, + dependencies: { abbrev: '^1.0.0' }, + }), + abbrev, + }, + }) + const manifest = registry.manifest({ name: 'abbrev' }) + await registry.package({ manifest }) + await registry.tarball({ + manifest: manifest.versions['1.0.0'], + tarball: path.join(npm.prefix, 'abbrev'), + }) + await npm.exec('install', []) + const installed = require(path.join(npm.prefix, 'node_modules', 'abbrev', 'package.json')) + t.equal(installed.name, 'abbrev', 'registry dep is installed despite allow-remote=none') + }) + + t.test('allow-remote=none still blocks a user-supplied remote URL', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'allow-remote': 'none', + audit: false, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/test-package', + version: '1.0.0', + dependencies: { abbrev: 'https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz' }, + }), + }, + }) + await t.rejects( + npm.exec('install', []), + { code: 'EALLOWREMOTE' }, + 'user-supplied remote URL is still blocked' ) }) }) diff --git a/deps/npm/test/lib/commands/publish.js b/deps/npm/test/lib/commands/publish.js index ad528c2c8dd3ef..acf8c4c96a93d6 100644 --- a/deps/npm/test/lib/commands/publish.js +++ b/deps/npm/test/lib/commands/publish.js @@ -742,6 +742,27 @@ t.test('restricted access', async t => { t.matchSnapshot(logs.notice) }) +t.test('private access', async t => { + const packageJson = { + name: '@npm/test-package', + version: '1.0.0', + } + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { + config: { + ...auth, + access: 'private', + }, + prefixDir: { + 'package.json': JSON.stringify(packageJson, null, 2), + }, + authorization: token, + }) + registry.publish('@npm/test-package', { packageJson, access: 'restricted' }) + await npm.exec('publish', []) + t.matchSnapshot(joinedOutput(), 'new package version') + t.matchSnapshot(logs.notice) +}) + t.test('public access', async t => { const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { diff --git a/deps/npm/test/lib/commands/rebuild.js b/deps/npm/test/lib/commands/rebuild.js index 0062362b61329b..de91fd3471b4e1 100644 --- a/deps/npm/test/lib/commands/rebuild.js +++ b/deps/npm/test/lib/commands/rebuild.js @@ -221,3 +221,63 @@ t.test('completion', async t => { const res = await rebuild.completion({ conf: { argv: { remain: ['npm', 'rebuild'] } } }) t.type(res, Array) }) + +t.test('emits Phase 1 advisory warning for unreviewed install scripts', async t => { + const { npm, logs } = await setupMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + node_modules: { + canvas: { + 'package.json': JSON.stringify({ + name: 'canvas', + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + }, + }, + }, + }) + await npm.exec('rebuild', []) + t.match( + logs.warn.byTitle('rebuild'), + [/install scripts not yet covered by allowScripts/] + ) +}) + +t.test('no advisory warning when allowScripts covers the package', async t => { + const { npm, logs } = await setupMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { canvas: '1.0.0' }, + allowScripts: { canvas: true }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { canvas: '1.0.0' } }, + 'node_modules/canvas': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/canvas/-/canvas-1.0.0.tgz', + hasInstallScript: true, + }, + }, + }), + node_modules: { + canvas: { + 'package.json': JSON.stringify({ + name: 'canvas', + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + }, + }, + }, + }) + await npm.exec('rebuild', []) + t.strictSame(logs.warn.byTitle('rebuild'), []) +}) diff --git a/deps/npm/test/lib/commands/stage/approve.js b/deps/npm/test/lib/commands/stage/approve.js new file mode 100644 index 00000000000000..d62ea04d431014 --- /dev/null +++ b/deps/npm/test/lib/commands/stage/approve.js @@ -0,0 +1,72 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm.js') +const MockRegistry = require('@npmcli/mock-registry') + +const token = 'test-auth-token' +const authConfig = { '//registry.npmjs.org/:_authToken': token } +const stageId = '1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f' + +t.test('approves a staged package', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, otp: '123456' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post(`/-/stage/${stageId}/approve`) + .reply(201, { message: 'Package version approved and published successfully.' }) + await npm.exec('stage', ['approve', stageId]) + t.match(joinedOutput(), /approved and published successfully/) +}) + +t.test('throws usageError without stage-id', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['approve']), { + code: 'EUSAGE', + }) +}) + +t.test('throws on invalid uuid', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['approve', 'not-a-uuid']), { + message: /stage-id must be a valid UUID/, + }) +}) + +t.test('throws on 404 (stage-id not found)', async t => { + const { npm } = await loadMockNpm(t, { + config: { ...authConfig, otp: '123456' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post(`/-/stage/${stageId}/approve`) + .reply(404, { error: 'Not found' }) + await t.rejects(npm.exec('stage', ['approve', stageId]), { + statusCode: 404, + }) +}) + +t.test('throws on 403 (not an owner)', async t => { + const { npm } = await loadMockNpm(t, { + config: { ...authConfig, otp: '123456' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post(`/-/stage/${stageId}/approve`) + .reply(403, { error: 'You do not have permission to approve this package' }) + await t.rejects(npm.exec('stage', ['approve', stageId]), { + statusCode: 403, + }) +}) diff --git a/deps/npm/test/lib/commands/stage/download.js b/deps/npm/test/lib/commands/stage/download.js new file mode 100644 index 00000000000000..807c1282edc863 --- /dev/null +++ b/deps/npm/test/lib/commands/stage/download.js @@ -0,0 +1,139 @@ +const t = require('tap') +const fs = require('node:fs') +const path = require('node:path') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm.js') +const MockRegistry = require('@npmcli/mock-registry') +const mockGlobals = require('@npmcli/mock-globals') +const libpack = require('libnpmpack') + +const token = 'test-auth-token' +const authConfig = { '//registry.npmjs.org/:_authToken': token } +const stageId = '1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f' + +t.test('downloads a staged tarball', async t => { + const { npm, joinedOutput, prefix } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/test-package', + version: '1.0.0', + }), + 'index.js': 'module.exports = 42', + }, + }) + const tarballData = await libpack(prefix) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageId}/tarball`) + .reply(200, tarballData, { 'content-type': 'application/octet-stream' }) + + mockGlobals(t, { 'process.cwd': () => prefix }) + + await npm.exec('stage', ['download', stageId]) + const out = joinedOutput() + const expectedFilename = `npmcli-test-package-1.0.0-${stageId}.tgz` + t.match(out, expectedFilename) + t.ok(fs.existsSync(path.join(prefix, expectedFilename))) +}) + +t.test('downloads with --json', async t => { + const { npm, joinedOutput, prefix } = await loadMockNpm(t, { + config: { ...authConfig, json: true }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/test-package', + version: '1.0.0', + }), + }, + }) + const tarballData = await libpack(prefix) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageId}/tarball`) + .reply(200, tarballData, { 'content-type': 'application/octet-stream' }) + + mockGlobals(t, { 'process.cwd': () => prefix }) + + await npm.exec('stage', ['download', stageId]) + const out = joinedOutput() + t.notMatch(out, `npmcli-test-package-1.0.0-${stageId}.tgz`) + const expectedFilename = `npmcli-test-package-1.0.0-${stageId}.tgz` + t.ok(fs.existsSync(path.join(prefix, expectedFilename))) +}) + +t.test('throws usageError without stage-id', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['download']), { + code: 'EUSAGE', + }) +}) + +t.test('throws on invalid uuid', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['download', 'not-a-uuid']), { + message: /stage-id must be a valid UUID/, + }) +}) + +t.test('throws on 404 (stage-id not found)', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageId}/tarball`) + .reply(404, { error: 'Not found' }) + await t.rejects(npm.exec('stage', ['download', stageId]), { + statusCode: 404, + }) +}) + +t.test('throws on 403 (not an owner)', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageId}/tarball`) + .reply(403, { error: 'You do not have permission to download this package' }) + await t.rejects(npm.exec('stage', ['download', stageId]), { + statusCode: 403, + }) +}) + +t.test('throws when tarball has no package.json', async t => { + const { npm, prefix } = await loadMockNpm(t, { + config: authConfig, + prefixDir: {}, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + // Empty tar (two 512-byte zero blocks) has no entries + registry.nock.get(`/-/stage/${stageId}/tarball`) + .reply(200, Buffer.alloc(1024), { 'content-type': 'application/octet-stream' }) + + mockGlobals(t, { 'process.cwd': () => prefix }) + + await t.rejects(npm.exec('stage', ['download', stageId]), { + message: /Could not read package.json from tarball/, + }) +}) diff --git a/deps/npm/test/lib/commands/stage/index.js b/deps/npm/test/lib/commands/stage/index.js new file mode 100644 index 00000000000000..bd81c7aab5e42e --- /dev/null +++ b/deps/npm/test/lib/commands/stage/index.js @@ -0,0 +1,332 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm') +const MockRegistry = require('@npmcli/mock-registry') +const path = require('node:path') +const fs = require('node:fs') + +const pkg = '@npmcli/test-package' +const token = 'test-auth-token' +const authConfig = { '//registry.npmjs.org/:_authToken': token } + +const pkgJson = { + name: pkg, + description: 'npm test package', + version: '1.0.0', +} + +t.test('stages a package from cwd', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /\+ @npmcli\/test-package@1\.0\.0 \(staged\)/) +}) + +t.test('stages with --dry-run', async t => { + const { npm, joinedOutput, logs } = await loadMockNpm(t, { + config: { ...authConfig, 'dry-run': true }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + await npm.exec('stage', ['publish']) + t.ok(logs.notice.some(n => /Staging to .* \(dry-run\)/.test(n))) + t.match(joinedOutput(), /\+ @npmcli\/test-package@1\.0\.0 \(staged\)/) +}) + +t.test('stages with --json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, json: true }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + const out = JSON.parse(joinedOutput()) + t.equal(out[pkg].name, pkg) + t.equal(out[pkg].version, '1.0.0') +}) + +t.test('stages with --json includes stageId', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, json: true }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + const stageId = 'f8e7a45b-7a5f-4f31-8e6d-9dd1c6ef38c0' + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, { stageId }) + await npm.exec('stage', ['publish']) + const out = JSON.parse(joinedOutput()) + t.equal(out[pkg].name, pkg) + t.equal(out[pkg].stageId, stageId) +}) + +t.test('completion returns subcommands', async t => { + const Stage = require('../../../../lib/commands/stage/index.js') + const res = await Stage.completion({ conf: { argv: { remain: ['npm', 'stage'] } } }) + t.strictSame(res, ['publish', 'list', 'view', 'approve', 'reject', 'download']) +}) + +t.test('completion returns empty for subcommand', async t => { + const Stage = require('../../../../lib/commands/stage/index.js') + const res = await Stage.completion({ conf: { argv: { remain: ['npm', 'stage', 'publish'] } } }) + t.strictSame(res, []) +}) + +t.test('throws on invalid semver tag', async t => { + const { npm } = await loadMockNpm(t, { + config: { ...authConfig, tag: '1.0.0' }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + await t.rejects(npm.exec('stage', ['publish']), { + message: /Tag name must not be a valid SemVer range/, + }) +}) + +t.test('throws ENEEDAUTH with no credentials', async t => { + const { npm } = await loadMockNpm(t, { + config: {}, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + await t.rejects(npm.exec('stage', ['publish']), { + code: 'ENEEDAUTH', + }) +}) + +t.test('warns on --dry-run with no credentials', async t => { + const { npm, logs } = await loadMockNpm(t, { + config: { 'dry-run': true }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + await npm.exec('stage', ['publish']) + t.match(logs.warn, [/requires you to be logged in.*\(dry-run\)/]) +}) + +t.test('stages a package with positional arg', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish', '.']) + t.match(joinedOutput(), /\+ @npmcli\/test-package@1\.0\.0 \(staged\)/) +}) + +t.test('respects ignore-scripts', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, 'ignore-scripts': true }, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + scripts: { + prepublishOnly: 'exit 1', + publish: 'exit 1', + postpublish: 'exit 1', + }, + }), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /\(staged\)/) +}) + +t.test('foreground-scripts can be set to false', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, 'foreground-scripts': false }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /\(staged\)/) +}) + +t.test('runs lifecycle scripts', async t => { + const { npm, prefix } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + scripts: { + prepublishOnly: 'touch scripts-prepublishonly', + publish: 'touch scripts-publish', + postpublish: 'touch scripts-postpublish', + }, + }), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.equal(fs.existsSync(path.join(prefix, 'scripts-prepublishonly')), true) + t.equal(fs.existsSync(path.join(prefix, 'scripts-publish')), true) + t.equal(fs.existsSync(path.join(prefix, 'scripts-postpublish')), true) +}) + +t.test('respects publishConfig', async t => { + const alternateRegistry = 'https://other.registry.npmjs.org' + const { npm, joinedOutput, logs } = await loadMockNpm(t, { + config: { + [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + }, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + publishConfig: { + registry: alternateRegistry, + other: 'unknown-key', + }, + }), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: alternateRegistry, + authorization: 'test-other-token', + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /\(staged\)/) + t.match(logs.warn, [/Unknown publishConfig/]) +}) + +t.test('warns about auto-corrected package.json errors', async t => { + const { npm, logs } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify({ + name: pkg, + version: '1.0.0', + repository: 'github:user/repo', + }), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.ok(logs.warn.some(w => /auto-corrected/.test(w))) + t.ok(logs.warn.some(w => /errors corrected/.test(w))) +}) + +t.test('stages with basic auth (username)', async t => { + const basic = Buffer.from('test-user:test-password').toString('base64') + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { + '//registry.npmjs.org/:username': 'test-user', + '//registry.npmjs.org/:_password': Buffer.from('test-password').toString('base64'), + }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + basic, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /\(staged\)/) +}) + +t.test('stages with cert auth', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { + '//registry.npmjs.org/:certfile': '/path/to/cert', + '//registry.npmjs.org/:keyfile': '/path/to/key', + }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, {}) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /\(staged\)/) +}) + +t.test('throws EPRIVATE for private packages', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify({ ...pkgJson, private: true }), + }, + }) + await t.rejects(npm.exec('stage', ['publish']), { + code: 'EPRIVATE', + }) +}) + +t.test('outputs stageId when returned', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: authConfig, + prefixDir: { + 'package.json': JSON.stringify(pkgJson), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.post('/-/stage/package/@npmcli%2ftest-package').reply(201, { stageId: 'abc-123' }) + await npm.exec('stage', ['publish']) + t.match(joinedOutput(), /staged with id abc-123/) +}) diff --git a/deps/npm/test/lib/commands/stage/list.js b/deps/npm/test/lib/commands/stage/list.js new file mode 100644 index 00000000000000..e66680db8277f2 --- /dev/null +++ b/deps/npm/test/lib/commands/stage/list.js @@ -0,0 +1,147 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm.js') +const MockRegistry = require('@npmcli/mock-registry') + +const token = 'test-auth-token' +const authConfig = { '//registry.npmjs.org/:_authToken': token } + +const stageItems = [ + { + id: '1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f', + packageName: '@npmcli/example-package', + version: '1.2.3', + tag: 'latest', + createdAt: '2026-03-16T09:00:00.000Z', + actor: 'octocat', + actorType: 'user', + shasum: '4f7f5f1d5bcf2f72f6e4d6c4f3b2812d8a2f6c19', + }, + { + id: 'f8e7a45b-7a5f-4f31-8e6d-9dd1c6ef38c0', + packageName: 'example-lib', + version: '0.4.0', + tag: 'next', + createdAt: '2026-03-15T18:22:11.000Z', + actor: 'npm-bot', + actorType: 'trusted automation', + shasum: '8eb3b4e9b6e3d0d2c86be1e6d4f43f4be62e80ad', + }, +] + +t.test('lists all staged packages', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get('/-/stage?page=0&perPage=100') + .reply(200, { items: stageItems, page: 0, perPage: 100, total: 2 }) + await npm.exec('stage', ['list']) + const out = joinedOutput() + t.match(out, 'package name: @npmcli/example-package') + t.match(out, 'package name: example-lib') + t.match(out, 'version: 1.2.3') + t.match(out, 'version: 0.4.0') +}) + +t.test('lists with package filter', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get('/-/stage?page=0&perPage=100&package=%40npmcli%2Fexample-package') + .reply(200, { items: [stageItems[0]], page: 0, perPage: 100, total: 1 }) + await npm.exec('stage', ['list', '@npmcli/example-package']) + const out = joinedOutput() + t.match(out, '@npmcli/example-package') + t.notMatch(out, 'example-lib') +}) + +t.test('lists with --json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, json: true }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get('/-/stage?page=0&perPage=100') + .reply(200, { items: stageItems, page: 0, perPage: 100, total: 2 }) + await npm.exec('stage', ['list']) + const out = JSON.parse(joinedOutput()) + t.equal(out.length, 2) + t.equal(out[0].packageName, '@npmcli/example-package') + t.equal(out[0].id, '1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f', 'uuid id is not redacted') +}) + +t.test('shows message when no packages', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get('/-/stage?page=0&perPage=100') + .reply(200, { items: [], page: 0, perPage: 100, total: 0 }) + await npm.exec('stage', ['list']) + t.match(joinedOutput(), 'No staged packages found.') +}) + +t.test('shows filtered message when no packages with filter', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get('/-/stage?page=0&perPage=100&package=nonexistent') + .reply(200, { items: [], page: 0, perPage: 100, total: 0 }) + await npm.exec('stage', ['list', 'nonexistent']) + t.match(joinedOutput(), 'No staged versions of package name "nonexistent".') +}) + +t.test('throws on version specifier', async t => { + const { npm } = await loadMockNpm(t, { + config: { ...authConfig }, + }) + await t.rejects(npm.exec('stage', ['list', 'area@1.0.0']), { + code: 'EUSAGE', + }) +}) + +t.test('paginates through multiple pages', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + // page 0: 100 items, total 101 + const page0Items = Array.from({ length: 100 }, (_, i) => ({ + ...stageItems[0], + id: `id-${i}`, + version: `1.0.${i}`, + })) + registry.nock.get('/-/stage?page=0&perPage=100') + .reply(200, { items: page0Items, page: 0, perPage: 100, total: 101 }) + registry.nock.get('/-/stage?page=1&perPage=100') + .reply(200, { items: [stageItems[1]], page: 1, perPage: 100, total: 101 }) + await npm.exec('stage', ['list']) + const out = joinedOutput() + t.match(out, 'version: 1.0.0') + t.match(out, 'version: 0.4.0') +}) diff --git a/deps/npm/test/lib/commands/stage/reject.js b/deps/npm/test/lib/commands/stage/reject.js new file mode 100644 index 00000000000000..d021075a245230 --- /dev/null +++ b/deps/npm/test/lib/commands/stage/reject.js @@ -0,0 +1,72 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm.js') +const MockRegistry = require('@npmcli/mock-registry') + +const token = 'test-auth-token' +const authConfig = { '//registry.npmjs.org/:_authToken': token } +const stageId = '1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f' + +t.test('rejects a staged package', async t => { + const { npm, joinedOutput, logs } = await loadMockNpm(t, { + config: { ...authConfig, otp: '123456' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.delete(`/-/stage/${stageId}`).reply(204) + await npm.exec('stage', ['reject', stageId]) + t.match(joinedOutput(), /has been rejected/) + t.match(logs.warn, [/permanently delete/]) +}) + +t.test('throws usageError without stage-id', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['reject']), { + code: 'EUSAGE', + }) +}) + +t.test('throws on invalid uuid', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['reject', 'not-a-uuid']), { + message: /stage-id must be a valid UUID/, + }) +}) + +t.test('throws on 404 (stage-id not found)', async t => { + const { npm } = await loadMockNpm(t, { + config: { ...authConfig, otp: '123456' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.delete(`/-/stage/${stageId}`) + .reply(404, { error: 'Not found' }) + await t.rejects(npm.exec('stage', ['reject', stageId]), { + statusCode: 404, + }) +}) + +t.test('throws on 403 (not an owner)', async t => { + const { npm } = await loadMockNpm(t, { + config: { ...authConfig, otp: '123456' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.delete(`/-/stage/${stageId}`) + .reply(403, { error: 'You do not have permission to reject this package' }) + await t.rejects(npm.exec('stage', ['reject', stageId]), { + statusCode: 403, + }) +}) diff --git a/deps/npm/test/lib/commands/stage/view.js b/deps/npm/test/lib/commands/stage/view.js new file mode 100644 index 00000000000000..604caf98fb23b0 --- /dev/null +++ b/deps/npm/test/lib/commands/stage/view.js @@ -0,0 +1,100 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm.js') +const MockRegistry = require('@npmcli/mock-registry') + +const token = 'test-auth-token' +const authConfig = { '//registry.npmjs.org/:_authToken': token } + +const stageItem = { + id: '1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f', + packageName: '@npmcli/example-package', + version: '1.2.3', + tag: 'latest', + createdAt: '2026-03-16T09:00:00.000Z', + actor: 'octocat', + actorType: 'user', + shasum: '4f7f5f1d5bcf2f72f6e4d6c4f3b2812d8a2f6c19', +} + +t.test('views a staged package', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: authConfig, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageItem.id}`).reply(200, stageItem) + await npm.exec('stage', ['view', stageItem.id]) + const out = joinedOutput() + t.match(out, /id:/) + t.match(out, 'package name: @npmcli/example-package') + t.match(out, 'version: 1.2.3') +}) + +t.test('views with --json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { ...authConfig, json: true }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageItem.id}`).reply(200, stageItem) + await npm.exec('stage', ['view', stageItem.id]) + const out = JSON.parse(joinedOutput()) + t.ok(out.id) + t.equal(out.packageName, '@npmcli/example-package') +}) + +t.test('throws usageError without stage-id', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['view']), { + code: 'EUSAGE', + }) +}) + +t.test('throws on invalid uuid', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + await t.rejects(npm.exec('stage', ['view', 'not-a-uuid']), { + message: /stage-id must be a valid UUID/, + }) +}) + +t.test('throws on 404 (stage-id not found)', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageItem.id}`) + .reply(404, { error: 'Not found' }) + await t.rejects(npm.exec('stage', ['view', stageItem.id]), { + statusCode: 404, + }) +}) + +t.test('throws on 403 (not an owner)', async t => { + const { npm } = await loadMockNpm(t, { + config: authConfig, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.nock.get(`/-/stage/${stageItem.id}`) + .reply(403, { error: 'You do not have permission to view this package' }) + await t.rejects(npm.exec('stage', ['view', stageItem.id]), { + statusCode: 403, + }) +}) diff --git a/deps/npm/test/lib/commands/trust/circleci.js b/deps/npm/test/lib/commands/trust/circleci.js index 1ceec9a6e5845f..51211785c57484 100644 --- a/deps/npm/test/lib/commands/trust/circleci.js +++ b/deps/npm/test/lib/commands/trust/circleci.js @@ -44,6 +44,7 @@ t.test('circleci with all options provided', async t => { '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', '--context-id', '123e4567-e89b-12d3-a456-426614174000', + '--allow-publish', ]) }) @@ -85,6 +86,7 @@ t.test('circleci without optional context-id', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]) }) @@ -128,6 +130,7 @@ t.test('circleci with multiple context-ids', async t => { '--vcs-origin', 'github.com/owner/repo', '--context-id', '123e4567-e89b-12d3-a456-426614174000', '--context-id', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + '--allow-publish', ]) }) @@ -152,6 +155,7 @@ t.test('circleci missing required org-id', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /org-id is required/ } ) @@ -178,6 +182,7 @@ t.test('circleci missing required project-id', async t => { '--org-id', '550e8400-e29b-41d4-a716-446655440000', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /project-id is required/ } ) @@ -204,6 +209,7 @@ t.test('circleci missing required pipeline-definition-id', async t => { '--org-id', '550e8400-e29b-41d4-a716-446655440000', '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /pipeline-definition-id is required/ } ) @@ -230,6 +236,7 @@ t.test('circleci missing required vcs-origin', async t => { '--org-id', '550e8400-e29b-41d4-a716-446655440000', '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--allow-publish', ]), { message: /vcs-origin is required/ } ) @@ -257,6 +264,7 @@ t.test('circleci with invalid org-id uuid format', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /org-id must be a valid UUID/ } ) @@ -284,6 +292,7 @@ t.test('circleci with invalid vcs-origin format', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'invalid-format', + '--allow-publish', ]), { message: /vcs-origin must be in format 'provider\/owner\/repo'/ } ) @@ -311,6 +320,7 @@ t.test('circleci with vcs-origin containing scheme prefix', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'https://github.com/owner/repo', + '--allow-publish', ]), { message: /vcs-origin must not include a scheme/ } ) @@ -336,6 +346,7 @@ t.test('circleci missing package name', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /Package name must be specified either as an argument or in package.json file/ } ) diff --git a/deps/npm/test/lib/commands/trust/github.js b/deps/npm/test/lib/commands/trust/github.js index a2b16d272bde15..6cc65bff354d00 100644 --- a/deps/npm/test/lib/commands/trust/github.js +++ b/deps/npm/test/lib/commands/trust/github.js @@ -35,7 +35,7 @@ t.test('github with all options provided', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--environment', 'production']) + await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--environment', 'production', '--allow-publish']) }) t.test('github with invalid repository format', async t => { @@ -61,7 +61,7 @@ t.test('github with invalid repository format', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'invalid']), + npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'invalid', '--allow-publish']), { message: /must be specified in the format owner\/repository/ } ) }) @@ -89,7 +89,7 @@ t.test('github with file as path', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--yes', '--file', '.github/workflows/ci.yml', '--repository', 'owner/repo']), + npm.exec('trust', ['github', packageName, '--yes', '--file', '.github/workflows/ci.yml', '--repository', 'owner/repo', '--allow-publish']), { message: /must be just a file not a path/ } ) }) @@ -124,7 +124,7 @@ t.test('github without environment', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo']) + await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--allow-publish']) }) t.test('bodyToOptions with all fields', t => { diff --git a/deps/npm/test/lib/commands/trust/gitlab.js b/deps/npm/test/lib/commands/trust/gitlab.js index 0b60196830c5f7..16f3804f978962 100644 --- a/deps/npm/test/lib/commands/trust/gitlab.js +++ b/deps/npm/test/lib/commands/trust/gitlab.js @@ -35,7 +35,7 @@ t.test('gitlab with all options provided', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/subgroup/repo', '--environment', 'production']) + await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/subgroup/repo', '--environment', 'production', '--allow-publish']) }) t.test('gitlab with invalid project format', async t => { @@ -61,7 +61,7 @@ t.test('gitlab with invalid project format', async t => { }) await t.rejects( - npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'invalid']), + npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'invalid', '--allow-publish']), { message: /must be specified in the format/ } ) }) @@ -89,7 +89,7 @@ t.test('gitlab with file as path', async t => { }) await t.rejects( - npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab/ci.yml', '--project', 'group/repo']), + npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab/ci.yml', '--project', 'group/repo', '--allow-publish']), { message: /must be just a file not a path/ } ) }) @@ -124,7 +124,7 @@ t.test('gitlab without environment', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/repo']) + await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/repo', '--allow-publish']) }) t.test('bodyToOptions with all fields', t => { diff --git a/deps/npm/test/lib/commands/update.js b/deps/npm/test/lib/commands/update.js index a8c68bd65bb361..68067b8af8168f 100644 --- a/deps/npm/test/lib/commands/update.js +++ b/deps/npm/test/lib/commands/update.js @@ -95,3 +95,33 @@ t.test('completion', async t => { const res = await update.completion({ conf: { argv: { remain: ['npm', 'update'] } } }) t.type(res, Array) }) + +t.test('update threads allowScripts policy through to arborist', async t => { + // The reify step uses the resolved policy. The advisory warning is + // emitted from reifyFinish (already covered by install.js tests), + // so here we verify the call site populates opts.allowScripts. + let capturedOpts + const FakeArborist = function (opts) { + capturedOpts = opts + this.options = opts + this.actualTree = { inventory: new Map() } + } + FakeArborist.prototype.reify = async function () {} + + const mock = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + allowScripts: { canvas: true }, + }), + }, + mocks: { + '@npmcli/arborist': FakeArborist, + '{LIB}/utils/reify-finish.js': async () => {}, + }, + }) + await mock.npm.exec('update', []) + t.strictSame(capturedOpts.allowScripts, { canvas: true }, + 'opts.allowScripts populated from package.json') +}) diff --git a/deps/npm/test/lib/trust-cmd.js b/deps/npm/test/lib/trust-cmd.js index f0c52aadbd2c4a..57d70702818a52 100644 --- a/deps/npm/test/lib/trust-cmd.js +++ b/deps/npm/test/lib/trust-cmd.js @@ -32,7 +32,7 @@ t.test('trust-cmd via trust github with read function called', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) }) t.test('trust-cmd via trust github with all options', async t => { @@ -57,7 +57,77 @@ t.test('trust-cmd via trust github with all options', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli', '--environment', 'production']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli', '--environment', 'production']) +}) + +t.test('trust-cmd via trust github with --allow-stage-publish', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + yes: true, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', ['github', '--allow-stage-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) +}) + +t.test('trust-cmd via trust github with --allow-staged-publish alias', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + yes: true, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', ['github', '--allow-staged-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) +}) + +t.test('trust-cmd via trust github missing permissions', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + yes: true, + }, + }) + + await t.rejects( + npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + { message: /At least one permission flag is required/ } + ) }) t.test('trust-cmd via trust github infers from package.json', async t => { @@ -91,7 +161,7 @@ t.test('trust-cmd via trust github infers from package.json', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', '--yes', '--file', 'workflow.yml']) + await npm.exec('trust', ['github', '--allow-publish', '--yes', '--file', 'workflow.yml']) }) t.test('trust-cmd via trust github with dry-run', async t => { @@ -108,7 +178,7 @@ t.test('trust-cmd via trust github with dry-run', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) t.ok(joinedOutput().includes('Establishing trust'), 'shows notice about establishing trust') }) @@ -122,7 +192,7 @@ t.test('trust-cmd via trust github missing package name', async t => { }) await t.rejects( - npm.exec('trust', ['github', '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: /Package name must be specified/ }, 'throws when no package name' ) @@ -141,7 +211,7 @@ t.test('trust-cmd via trust github missing file', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--repository', 'npm/cli']), { message: /must be specified with the file option/ }, 'throws when no file' ) @@ -160,7 +230,7 @@ t.test('trust-cmd via trust github invalid file extension', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.txt', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.txt', '--repository', 'npm/cli']), { message: /must end in \.yml or \.yaml/ }, 'throws when file has wrong extension' ) @@ -179,7 +249,7 @@ t.test('trust-cmd via trust github missing repository', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml']), { message: /must be specified with repository option/ }, 'throws when no repository' ) @@ -200,7 +270,7 @@ t.test('trust-cmd via trust github with custom registry warning', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) t.ok(logs.warn.some(l => l.includes('may not support trusted publishing')), 'warns about custom registry') }) @@ -220,7 +290,7 @@ t.test('trust-cmd via trust github with --json', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) const output = joinedOutput() t.ok(output.includes(packageName), 'JSON output includes package name') @@ -250,7 +320,7 @@ t.test('trust-cmd via trust github with user confirmation no', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when user declines' ) @@ -271,7 +341,7 @@ t.test('trust-cmd via trust github with --no-yes', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when --no-yes flag is set' ) @@ -300,7 +370,7 @@ t.test('trust-cmd via trust github with invalid answer', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when user gives invalid answer' ) @@ -336,7 +406,7 @@ t.test('trust-cmd via trust github with user confirmation Y uppercase', async t registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) }) t.test('trust-cmd via trust github with user enters empty string', async t => { @@ -362,7 +432,7 @@ t.test('trust-cmd via trust github with user enters empty string', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when user enters empty string' ) @@ -383,7 +453,7 @@ t.test('trust-cmd via trust github with mismatched repo type', async t => { }) await t.rejects( - npm.exec('trust', ['github', '--file', 'workflow.yml']), + npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml']), { message: /Repository in package.json is not a GitHub repository/ }, 'throws when repository type does not match provider' ) @@ -404,7 +474,7 @@ t.test('trust-cmd via trust github with mismatched repo type but flag provided', }, }) - await npm.exec('trust', ['github', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) + await npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) t.ok(logs.warn.some(l => l.includes('Repository in package.json is not a GitHub repository')), 'warns about repository type mismatch') }) @@ -424,7 +494,7 @@ t.test('trust-cmd via trust github with different repo in package.json', async t }, }) - await npm.exec('trust', ['github', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) + await npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) t.ok(logs.warn.some(l => l.includes('differs from provided')), 'warns about repository mismatch') }) @@ -459,7 +529,7 @@ t.test('trust-cmd via trust github with user confirmation yes spelled out', asyn registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) }) t.test('trust-cmd via trust github showing response with id and type', async t => { @@ -500,7 +570,7 @@ t.test('trust-cmd via trust github showing response with id and type', async t = }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) const output = joinedOutput() t.ok(output.includes('type:'), 'output shows type field') @@ -520,7 +590,7 @@ t.test('trust-cmd via trust github missing repository when package name differs' }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml']), { message: /must be specified with repository option/ }, 'throws when no repository and package name differs' ) @@ -619,7 +689,7 @@ t.test('trust-cmd via trust github showing fromPackageJson indicator', async t = }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml']) const output = joinedOutput() t.ok(output.includes('from package.json'), 'output shows fromPackageJson indicator') @@ -663,7 +733,7 @@ t.test('trust-cmd via trust github showing URLs for fields', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) const output = joinedOutput() t.match(output, /https:\/\/github\.com\/npm\/cli\b/, 'output shows repository URL') @@ -684,7 +754,7 @@ t.test('trust-cmd via trust github with yes=false flag', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: /User cancelled operation/ }, 'throws when yes is explicitly false' ) @@ -846,3 +916,28 @@ t.test('TrustCommand - logOptions with urls but all values are null', async t => t.ok(output.includes('file'), 'shows file field') t.notOk(output.includes('Links to verify manually'), 'does not show links header when all urls are null') }) + +t.test('formatPermissions with unknown permission falls back to raw value', t => { + const result = TrustCommand.formatPermissions(['unknownPermission']) + t.equal(result, 'unknownPermission', 'returns raw value for unknown permission') + t.end() +}) + +t.test('displayResponseBody with empty body', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + class TestTrustCmd extends TrustCommand { + static name = 'test' + static description = 'Test command' + } + + const cmd = new TestTrustCmd(npm) + cmd.displayResponseBody({ body: [], packageName: '@npmcli/test-package' }) + + const output = joinedOutput() + t.match(output, /No trust configurations found/, 'shows no configurations message') +}) diff --git a/deps/npm/test/lib/utils/allow-scripts-writer.js b/deps/npm/test/lib/utils/allow-scripts-writer.js new file mode 100644 index 00000000000000..56314f8eb5a521 --- /dev/null +++ b/deps/npm/test/lib/utils/allow-scripts-writer.js @@ -0,0 +1,637 @@ +const t = require('tap') +const path = require('node:path') +const { + applyApprovalForPackage, + applyDenyForPackage, + nameKeyFor, + versionedKeyFor, + isSingleVersionPin, +} = require('../../../lib/utils/allow-scripts-writer.js') + +const node = (overrides = {}) => { + const name = overrides.name ?? overrides.packageName ?? 'pkg' + const packageName = overrides.packageName ?? name + const version = overrides.version ?? '1.0.0' + const urlPkg = packageName + return { + name, + packageName, + version, + resolved: overrides.resolved + ?? `https://registry.npmjs.org/${urlPkg}/-/${urlPkg}-${version}.tgz`, + location: overrides.location ?? `node_modules/${name}`, + isRegistryDependency: overrides.isRegistryDependency ?? true, + } +} + +t.test('nameKeyFor / versionedKeyFor — registry', async t => { + const n = node({ name: 'canvas', version: '2.11.0' }) + t.equal(nameKeyFor(n), 'canvas') + t.equal(versionedKeyFor(n), 'canvas@2.11.0') +}) + +t.test('nameKeyFor / versionedKeyFor — git', async t => { + const n = node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#deadbeefcafebabe1234567890abcdef12345678', + }) + t.equal(nameKeyFor(n), 'github:foo/bar') + t.equal(versionedKeyFor(n), 'github:foo/bar#deadbeefcafebabe1234567890abcdef12345678') +}) + +t.test('nameKeyFor / versionedKeyFor — file', async t => { + const n = node({ name: 'local', resolved: 'file:../local' }) + t.equal(nameKeyFor(n), 'file:../local') + t.equal(versionedKeyFor(n), 'file:../local') +}) + +t.test('isSingleVersionPin', async t => { + t.ok(isSingleVersionPin('pkg@1.2.3')) + t.notOk(isSingleVersionPin('pkg')) + t.notOk(isSingleVersionPin('pkg@^1')) + t.notOk(isSingleVersionPin('pkg@1.2.3 || 2.0.0')) + t.notOk(isSingleVersionPin('@@@bad')) +}) + +t.test('applyApprovalForPackage — empty allowScripts, --pin', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + {}, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(changes, [{ key: 'canvas@2.11.0', change: 'added' }]) +}) + +t.test('applyApprovalForPackage — empty allowScripts, --no-pin', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + {}, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) + t.strictSame(changes, [{ key: 'canvas', change: 'added' }]) +}) + +t.test('applyApprovalForPackage — stale pin rewritten to new installed version', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { 'canvas@2.10.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.match(changes, [ + { key: 'canvas@2.10.0', change: 'removed-stale' }, + { key: 'canvas@2.11.0', change: 'added' }, + ]) +}) + +t.test('applyApprovalForPackage — multi-version disjunction is preserved', async t => { + const { allowScripts } = applyApprovalForPackage( + { 'canvas@2.10.0 || 2.11.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { + 'canvas@2.10.0 || 2.11.0': true, + 'canvas@2.11.0': true, + }) +}) + +t.test('applyApprovalForPackage — already-allowed exact version is a no-op', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { 'canvas@2.11.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — existing deny wins, returns warning', async t => { + const { allowScripts, changes, warning } = applyApprovalForPackage( + { canvas: false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { canvas: false }) + t.strictSame(changes, []) + t.match(warning, /canvas is denied/) +}) + +t.test('applyApprovalForPackage — versioned deny wins too', async t => { + const { changes, warning } = applyApprovalForPackage( + { 'canvas@2.11.0': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(changes, []) + t.match(warning, /denied|versioned deny/) +}) + +t.test('applyApprovalForPackage — name-only existing, --no-pin no-op', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { canvas: true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — --no-pin downgrades pinned entry to name-only', async t => { + const { allowScripts } = applyApprovalForPackage( + { 'canvas@2.10.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) +}) + +t.test('applyApprovalForPackage — multiple installed versions write multiple pins', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [ + node({ name: 'lodash', version: '4.17.21' }), + node({ name: 'lodash', version: '3.10.1' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) +}) + +t.test('applyApprovalForPackage — keeps existing pin matching one installed, adds pin for other', async t => { + const { allowScripts } = applyApprovalForPackage( + { 'lodash@4.17.21': true }, + [ + node({ name: 'lodash', version: '4.17.21' }), + node({ name: 'lodash', version: '3.10.1' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) +}) + +t.test('applyDenyForPackage — empty allowScripts adds name-only false', async t => { + const { allowScripts, changes } = applyDenyForPackage( + {}, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(allowScripts, { 'core-js': false }) + t.strictSame(changes, [{ key: 'core-js', change: 'added' }]) +}) + +t.test('applyDenyForPackage — pinned allow is replaced by name-only deny', async t => { + const { allowScripts } = applyDenyForPackage( + { 'core-js@3.0.0': true }, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(allowScripts, { 'core-js': false }) +}) + +t.test('applyDenyForPackage — already-denied is a no-op', async t => { + const { changes } = applyDenyForPackage( + { 'core-js': false }, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(changes, []) +}) + +t.test('applyDenyForPackage — name-only true is replaced by name-only false', async t => { + const { allowScripts } = applyDenyForPackage( + { 'core-js': true }, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(allowScripts, { 'core-js': false }) +}) + +t.test('applyApprovalForPackage — preserves unrelated entries', async t => { + const { allowScripts } = applyApprovalForPackage( + { other: true, 'unrelated@1.0.0': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { + other: true, + 'unrelated@1.0.0': false, + 'canvas@2.11.0': true, + }) +}) + +t.test('applyApprovalForPackage — git node writes hosted shortcut with commit', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#deadbeefcafebabe1234567890abcdef12345678', + })], + { pin: true } + ) + t.strictSame(allowScripts, { + 'github:foo/bar#deadbeefcafebabe1234567890abcdef12345678': true, + }) +}) + +t.test('applyApprovalForPackage — git node --no-pin writes hosted shortcut without commit', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#deadbeef', + })], + { pin: false } + ) + t.strictSame(allowScripts, { 'github:foo/bar': true }) +}) + +t.test('applyApprovalForPackage — file dep uses resolved as both keys', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ name: 'local', resolved: 'file:../local' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'file:../local': true }) +}) + +t.test('applyApprovalForPackage — empty nodes returns unchanged', async t => { + const { allowScripts, changes } = applyApprovalForPackage({ x: true }, [], { pin: true }) + t.strictSame(allowScripts, { x: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — name-only entry is replaced by pin (RFC table)', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { canvas: true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + // Per RFC table: pkg: true + --pin must upgrade to pkg@x.y.z: true. + // Both entries left behind would be wrong. + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.match(changes, [ + { key: 'canvas@2.11.0', change: 'added' }, + { key: 'canvas', change: 'replaced-by-pin' }, + ]) +}) + +t.test('applyApprovalForPackage — name-only + multi-version installs replaces with all pins', async t => { + const { allowScripts } = applyApprovalForPackage( + { lodash: true }, + [ + node({ name: 'lodash', version: '4.17.21' }), + node({ name: 'lodash', version: '3.10.1' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) +}) + +t.test('applyApprovalForPackage — name-only is preserved when --no-pin', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { canvas: true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — name-only NOT dropped when no pinning could happen', async t => { + // Node has no version, so installedKeys is empty. The name-only entry + // must NOT be dropped or we silently lose the policy. + const noVersion = { name: 'pkg', packageName: 'pkg', version: undefined, resolved: 'https://registry.npmjs.org/pkg/-/pkg-1.tgz' } + const { allowScripts } = applyApprovalForPackage( + { pkg: true }, + [noVersion], + { pin: true } + ) + t.strictSame(allowScripts, { pkg: true }) +}) + +t.test('applyApprovalForPackage — convergent: running twice gives the same result', async t => { + // Start with stale state including a name-only entry. + const start = { canvas: true, 'canvas@2.10.0': true } + const nodes = [node({ name: 'canvas', version: '2.11.0' })] + + const run1 = applyApprovalForPackage(start, nodes, { pin: true }) + const run2 = applyApprovalForPackage(run1.allowScripts, nodes, { pin: true }) + + t.strictSame(run1.allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(run2.allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(run2.changes, [], 'second run is a no-op') +}) + +t.test('applyApprovalForPackage — deny still wins even when name-only is upgraded', async t => { + const { allowScripts, warning } = applyApprovalForPackage( + { canvas: true, 'canvas@2.11.0': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + // Existing deny on the version blocks the approval. + t.strictSame(allowScripts, { canvas: true, 'canvas@2.11.0': false }) + t.match(warning, /denied|versioned deny/) +}) + +t.test('keyTargetsNode — unparseable key returns false (via applyApproval)', async t => { + // An unparseable key in the existing object should be ignored. + const { allowScripts } = applyApprovalForPackage( + { '@@@invalid': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.equal(allowScripts['canvas@2.11.0'], true) + t.equal(allowScripts['@@@invalid'], true) +}) + +t.test('applyDenyForPackage — empty nodes array returns unchanged', async t => { + const { allowScripts, changes } = applyDenyForPackage({ existing: true }, []) + t.strictSame(allowScripts, { existing: true }) + t.strictSame(changes, []) +}) + +t.test('applyDenyForPackage — node with no nameable identity is a no-op', async t => { + // A node whose resolved field is unparseable as a git URL and has no + // version/name produces a null name; the writer must short-circuit. + const weird = { name: '', packageName: '', version: undefined, resolved: undefined } + const { allowScripts, changes } = applyDenyForPackage({}, [weird]) + t.strictSame(allowScripts, {}) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — file dep with deny entry blocks approval', async t => { + const { warning } = applyApprovalForPackage( + { 'file:../local': false }, + [node({ name: 'local', resolved: 'file:../local' })], + { pin: true } + ) + t.match(warning, /denied|versioned deny/) +}) + +t.test('applyApprovalForPackage — remote tarball deny blocks approval', async t => { + const remote = { name: 'pkg', packageName: 'pkg', version: '1.0.0', resolved: 'https://example.com/pkg.tgz' } + const { warning } = applyApprovalForPackage( + { 'https://example.com/pkg.tgz': false }, + [remote], + { pin: true } + ) + t.match(warning, /denied|versioned deny/) +}) + +t.test('applyApprovalForPackage — no-pin with no name produces no-op', async t => { + const weird = { name: '', packageName: '', resolved: 'git+ssh://no.parse' } + const { allowScripts, changes } = applyApprovalForPackage({}, [weird], { pin: false }) + t.strictSame(allowScripts, {}) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — pin with no versioned key is a no-op', async t => { + const noVersion = { name: 'pkg', packageName: 'pkg', version: undefined, resolved: 'https://registry.npmjs.org/pkg/-/pkg-1.tgz' } + const { allowScripts, changes } = applyApprovalForPackage({}, [noVersion], { pin: true }) + t.strictSame(allowScripts, {}) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — pin with no versioned key and existing name-only is no-op', async t => { + const noVersion = { name: 'pkg', packageName: 'pkg', version: undefined, resolved: 'https://registry.npmjs.org/pkg/-/pkg-1.tgz' } + const { changes } = applyApprovalForPackage({ pkg: true }, [noVersion], { pin: true }) + t.strictSame(changes, []) +}) + +t.test('keyTargetsNode handles file with directory-typed key', async t => { + // A "directory" spec for a relative path. + const dirNode = { name: 'local', packageName: 'local', resolved: 'file:./local-dir' } + const { allowScripts } = applyApprovalForPackage( + {}, + [dirNode], + { pin: true } + ) + t.equal(allowScripts['file:./local-dir'], true) +}) + +t.test('nameKeyFor / versionedKeyFor — null node', async t => { + t.equal(nameKeyFor(null), null) + t.equal(versionedKeyFor(null), null) +}) + +t.test('nameKeyFor / versionedKeyFor — non-hosted git url returns null', async t => { + const n = { name: 'pkg', packageName: 'pkg', resolved: 'git+https://example.invalid/foo/bar.git#abc' } + t.equal(nameKeyFor(n), null) + t.equal(versionedKeyFor(n), null) +}) + +t.test('versionedKeyFor — absolute path resolved field', async t => { + const n = { name: 'pkg', packageName: 'pkg', resolved: '/abs/path/local' } + t.equal(versionedKeyFor(n), '/abs/path/local') + t.equal(nameKeyFor(n), '/abs/path/local') +}) + +t.test('applyApprovalForPackage — node.resolved parse error in keyTargetsNode is safe', async t => { + // An existing git-style key for a package whose own resolved field + // doesn't parse: the key just doesn't target anything. + const gitNode = node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#abc', + }) + // Add an explicit unparseable existing entry. + const { allowScripts } = applyApprovalForPackage( + { 'github:other/other': true }, + [gitNode], + { pin: true } + ) + // Existing entry unchanged; new git entry added. + t.equal(allowScripts['github:other/other'], true) + t.equal(allowScripts['github:foo/bar#abc'], true) +}) + +t.test('keyTargetsNode — alias key does not target anything (via writer)', async t => { + // Alias-typed key falls through the switch default. + const { allowScripts } = applyApprovalForPackage( + { 'foo@npm:bar@1.0.0': true }, + [node({ name: 'foo', packageName: 'foo', version: '1.0.0' })], + { pin: true } + ) + // Alias entry untouched, new pin added separately. + t.equal(allowScripts['foo@npm:bar@1.0.0'], true) + t.equal(allowScripts['foo@1.0.0'], true) +}) +t.test('keyTargetsNode handles tag-type key', async t => { + // 'canvas@latest' parses as type='tag'. The writer should treat it like + // a name-only match (any installed version of canvas). + const { allowScripts } = applyApprovalForPackage( + { 'canvas@latest': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + // The tag key targets the canvas node (same package name), so the + // 'canvas@2.11.0' pin gets added; tag key is preserved. + t.equal(allowScripts['canvas@latest'], true) + t.equal(allowScripts['canvas@2.11.0'], true) +}) + +t.test('keyTargetsNode handles file-type tarball key matching saveSpec', async t => { + // 'file:pkg.tgz' parses as type='file' with saveSpec='file:pkg.tgz'. + const tarballNode = { + name: 'pkg', + packageName: 'pkg', + version: '1.0.0', + resolved: 'file:pkg.tgz', + } + const { allowScripts } = applyApprovalForPackage( + { 'file:pkg.tgz': false }, + [tarballNode], + { pin: true } + ) + // saveSpec match: deny wins, no pin added. + t.equal(allowScripts['file:pkg.tgz'], false) +}) + +t.test('keyTargetsNode handles file-type tarball key matching fetchSpec', async t => { + // When node.resolved is an absolute path matching parsed.fetchSpec. + // Use path.resolve so the absolute path is platform-correct (npa + // parses POSIX-style `/abs/...` as a directory on Windows). + const absTgz = path.resolve('pkg.tgz') + const tarballNode = { + name: 'pkg', + packageName: 'pkg', + version: '1.0.0', + resolved: absTgz, + } + const { allowScripts, warning } = applyApprovalForPackage( + { './pkg.tgz': false }, + [tarballNode], + { pin: true } + ) + t.equal(allowScripts['./pkg.tgz'], false) + t.match(warning, /denied|versioned deny/) +}) + +t.test('versionedKeyFor — git node without committish', async t => { + // versionedKeyFor's ternary takes the "no committish" branch. + t.equal( + versionedKeyFor({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git', + }), + 'github:foo/bar' + ) +}) + +t.test('versionedKeyFor / nameKeyFor — absolute path resolved field', async t => { + // Hits the `resolved.startsWith('/')` branch in both helpers. + const n = { name: 'pkg', packageName: 'pkg', resolved: '/abs/local-dir' } + t.equal(versionedKeyFor(n), '/abs/local-dir') + t.equal(nameKeyFor(n), '/abs/local-dir') +}) + +t.test('keyTargetsNode — git key against a node with no resolved field', async t => { + // Defensive: if existing has a git-shaped key and the installed node + // has no resolved field, keyTargetsNode bails out and no policy entry + // can be derived from untrusted sources. + const noResolved = { name: 'bar', packageName: 'bar', resolved: undefined } + const { allowScripts } = applyApprovalForPackage( + { 'github:foo/bar': true }, + [noResolved], + { pin: false } + ) + // Existing entry untouched. No new key written: nameKeyFor returns + // null for a node with no trusted identity source. + t.equal(allowScripts['github:foo/bar'], true) + t.notOk('bar' in allowScripts, 'no entry written under attacker-controlled node.name') +}) + +t.test('applyApprovalForPackage — default args (no options object)', async t => { + // Hits the `{ pin = true } = {}` default-arg branch. + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ name: 'canvas', version: '2.11.0' })] + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) +}) + +t.test('applyApprovalForPackage — deny-wins warning when node has no name', async t => { + // Hits the `name || 'this package'` fallback in the warning message. + const noName = { name: '', packageName: '', resolved: 'git+ssh://no.parse' } + const { warning } = applyApprovalForPackage( + { 'github:foo/bar': false }, + [noName], + { pin: true } + ) + // No keys target this node (its resolved doesn't parse to a hosted URL), + // so deny-wins doesn't trigger. Result is no warning. + t.notOk(warning) +}) + +t.test('denyWarning branches on key shape per RFC §approve-scripts', async t => { + // Name-only deny: only remedy is to remove the entry. + const nameOnly = applyApprovalForPackage( + { canvas: false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.match(nameOnly.warning, /remove the entry from allowScripts/) + t.notMatch(nameOnly.warning, /widen the deny/) + + // Pinned deny on a different version: suggest both widen and remove. + const pinned = applyApprovalForPackage( + { 'canvas@2.10.0': false }, + [node({ name: 'canvas', version: '2.10.0' })], + { pin: true } + ) + t.match(pinned.warning, /versioned deny/) + t.match(pinned.warning, /npm deny-scripts canvas/) + t.match(pinned.warning, /widen the deny to all versions/) + t.match(pinned.warning, /remove the entry/) + + // Multi-version deny disjunction: same as pinned (versioned). + const multi = applyApprovalForPackage( + { 'canvas@2.10.0 || 2.11.0': false }, + [node({ name: 'canvas', version: '2.10.0' })], + { pin: true } + ) + t.match(multi.warning, /versioned deny/) + t.match(multi.warning, /npm deny-scripts canvas/) +}) + +t.test('denyWarning: tag-type key (pkg@latest: false) is name-only', async t => { + // `canvas@latest` parses as type='tag'. Treat the same as a bare name. + const { warning } = applyApprovalForPackage( + { 'canvas@latest': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.match(warning, /remove the entry/) + t.notMatch(warning, /versioned deny/) +}) + +t.test('applyApprovalForPackage — multi-version entry + --pin=false adds name-only alongside', async t => { + // RFC table: existing `pkg@a.b.c || d.e.f: true` + installed `pkg@x.y.z` + // + --pin=false adds `pkg: true`. The multi-version disjunction stays + // (it captures intent the command can't infer), and the name-only + // entry is added. + const { allowScripts } = applyApprovalForPackage( + { 'canvas@1.0.0 || 2.0.0': true }, + [node({ name: 'canvas', version: '3.0.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { + 'canvas@1.0.0 || 2.0.0': true, + canvas: true, + }) +}) + +t.test('versionedKeyFor — registry resolved that versionFromTgz cannot parse returns null', async t => { + // Private-registry mirror / alternate CDN URL shape that doesn't match + // the standard `/-/name-version.tgz` pattern. Exercises the log.silly + // breadcrumb path in versionedKeyFor, including each fallback branch + // of the `node.path || node.name || ''` label expression. + const resolved = 'https://private-mirror.example.com/blobs/abc123' + t.equal(versionedKeyFor({ + path: '/fake/mystery', name: 'mystery', resolved, isRegistryDependency: true, + }), null, 'falls back when node has a path') + t.equal(versionedKeyFor({ + name: 'mystery', resolved, isRegistryDependency: true, + }), null, 'falls back when node has only a name') + t.equal(versionedKeyFor({ + resolved, isRegistryDependency: true, + }), null, 'falls back when node has neither path nor name') +}) diff --git a/deps/npm/test/lib/utils/check-allow-scripts.js b/deps/npm/test/lib/utils/check-allow-scripts.js new file mode 100644 index 00000000000000..8dea9674375df4 --- /dev/null +++ b/deps/npm/test/lib/utils/check-allow-scripts.js @@ -0,0 +1,263 @@ +const t = require('tap') + +const mockCheck = (t, mocks = {}) => + t.mock('../../../lib/utils/check-allow-scripts.js', mocks) + +// Build a minimal "arborist tree" fixture for the walker. +const arb = ({ nodes, allowScripts = null, ignoreScripts = false } = {}) => ({ + options: { allowScripts, ignoreScripts }, + actualTree: { + inventory: new Map(nodes.map((n, i) => [`node_modules/${n.name || `n${i}`}`, n])), + }, +}) + +const node = ({ + name = 'pkg', + packageName, + version = '1.0.0', + resolved, + scripts = {}, + gypfile, + path: nodePath = `/fake/${name}`, + isProjectRoot = false, + isWorkspace = false, + isLink = false, + isRegistryDependency, +} = {}) => { + const pkgName = packageName ?? name + const resolvedUrl = resolved + ?? `https://registry.npmjs.org/${pkgName}/-/${pkgName}-${version}.tgz` + // Default isRegistryDependency to match the shape of resolved: registry + // tarballs are registry, anything else (git, file, remote) is not. + const isReg = isRegistryDependency ?? /^https?:\/\/[^/]+\/.+\/-\/[^/]+-\d/.test(resolvedUrl) + return { + name, + packageName: pkgName, + version, + resolved: resolvedUrl, + location: `node_modules/${name}`, + isRegistryDependency: isReg, + path: nodePath, + isProjectRoot, + isWorkspace, + isLink, + package: { scripts, ...(gypfile !== undefined ? { gypfile } : {}) }, + } +} + +t.test('returns [] when ignoreScripts is set', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ scripts: { install: 'do-stuff' } })], + ignoreScripts: true, + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('returns [] when dangerouslyAllowAllScripts is set', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ nodes: [node({ scripts: { install: 'do-stuff' } })] }), + npm: { flatOptions: { dangerouslyAllowAllScripts: true } }, + }) + t.strictSame(result, []) +}) + +t.test('skips project root, workspace, and linked nodes', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'root', scripts: { install: 'x' }, isProjectRoot: true }), + node({ name: 'ws', scripts: { install: 'x' }, isWorkspace: true }), + node({ name: 'linked', scripts: { install: 'x' }, isLink: true }), + ], + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('skips nodes with no install-relevant scripts', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ scripts: { test: 'jest' } })], + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('includes nodes with preinstall/install/postinstall', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'a', scripts: { preinstall: 'pre' } }), + node({ name: 'b', scripts: { install: 'inst' } }), + node({ name: 'c', scripts: { postinstall: 'post' } }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 3) + t.strictSame(result[0].scripts, { preinstall: 'pre' }) + t.strictSame(result[1].scripts, { install: 'inst' }) + t.strictSame(result[2].scripts, { postinstall: 'post' }) +}) + +t.test('prepare counts for non-registry sources only', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + // registry: prepare ignored + node({ + name: 'registry-pkg', + resolved: 'https://registry.npmjs.org/registry-pkg/-/registry-pkg-1.0.0.tgz', + scripts: { prepare: 'do' }, + }), + // git: prepare counts + node({ + name: 'git-pkg', + resolved: 'git+ssh://git@github.com/foo/bar.git#abcdef0123456789', + scripts: { prepare: 'do' }, + }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1) + t.equal(result[0].node.name, 'git-pkg') +}) + +t.test('detects synthetic node-gyp via binding.gyp runtime check', async t => { + const checkAllowScripts = mockCheck(t, { + '@npmcli/arborist/lib/install-scripts.js': async (n) => { + if (n.path === '/has-bindings') { + return { install: 'node-gyp rebuild' } + } + return {} + }, + }) + + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'native', path: '/has-bindings' }), + node({ name: 'pure-js', path: '/no-bindings' }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1) + t.equal(result[0].node.name, 'native') + t.strictSame(result[0].scripts, { install: 'node-gyp rebuild' }) +}) + +t.test('skips node-gyp detection when gypfile is explicitly false', async t => { + // Mock returns no scripts to simulate the gypfile:false short-circuit + // inside getInstallScripts. + const checkAllowScripts = mockCheck(t, { + '@npmcli/arborist/lib/install-scripts.js': async () => ({}), + }) + + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ name: 'opt-out', gypfile: false })], + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('skips approved nodes', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ name: 'allowed', scripts: { install: 'x' } })], + allowScripts: { allowed: true }, + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('skips denied nodes (false counts as reviewed)', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ name: 'denied', scripts: { install: 'x' } })], + allowScripts: { denied: false }, + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('includes unreviewed nodes when policy is set but does not cover them', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'allowed', scripts: { install: 'x' } }), + node({ name: 'unreviewed', scripts: { install: 'y' } }), + ], + allowScripts: { allowed: true }, + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1) + t.equal(result[0].node.name, 'unreviewed') +}) + +t.test('reports every install-script node when no policy is set', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'a', scripts: { install: 'x' } }), + node({ name: 'b', scripts: { postinstall: 'y' } }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 2) +}) + +t.test('survives missing actualTree', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: { options: {} }, + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('bundled dep with install scripts is reported as unreviewed regardless of policy', async t => { + const checkAllowScripts = mockCheck(t) + const bundled = node({ + name: 'bundled-pkg', + version: '1.0.0', + resolved: undefined, + scripts: { install: 'do-stuff' }, + }) + bundled.inBundle = true + + const result = await checkAllowScripts({ + arb: arb({ + nodes: [bundled], + // Policy explicitly allows the bundled name — the matcher should + // still return null and the walker should still flag the bundled + // dep as unreviewed. + allowScripts: { 'bundled-pkg': true }, + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1, 'bundled dep flagged despite explicit allow entry') + t.equal(result[0].node, bundled) +}) diff --git a/deps/npm/test/lib/utils/display.js b/deps/npm/test/lib/utils/display.js index 89c4a855028490..b33ab69a365944 100644 --- a/deps/npm/test/lib/utils/display.js +++ b/deps/npm/test/lib/utils/display.js @@ -271,6 +271,35 @@ t.test('Display.clean', async (t) => { } }) +t.test('json output redacts by default', async t => { + // Do not set redact: false globally in the json flush path of display.js. + // If a command needs unredacted output (e.g. UUIDs), pass + // { [META]: true, redact: false } at the call site via output.standard(). + const { META } = require('proc-log') + const { output, outputs, clearOutput } = await mockDisplay(t) + + output.buffer({ + url: 'https://registry.npmjs.org/', + id: '550e8400-e29b-41d4-a716-446655440000', + }) + output.flush({ [META]: true, json: true }) + + t.equal(outputs.length, 1, 'one output') + const parsed = JSON.parse(outputs[0]) + t.equal(parsed.id, '***', 'uuid values are redacted in json output') + + // commands that need unredacted output should use output.standard + // with redact: false at the call site instead of disabling globally + clearOutput() + output.standard( + JSON.stringify({ id: '550e8400-e29b-41d4-a716-446655440000' }, null, 2), + { [META]: true, redact: false } + ) + const inlineParsed = JSON.parse(outputs[0]) + t.equal(inlineParsed.id, '550e8400-e29b-41d4-a716-446655440000', + 'inline redact: false preserves uuid values') +}) + t.test('prompt functionality', async t => { t.test('regular prompt completion works', async t => { const { input } = await mockDisplay(t) diff --git a/deps/npm/test/lib/utils/key-values.js b/deps/npm/test/lib/utils/key-values.js new file mode 100644 index 00000000000000..5e61f9e55fe596 --- /dev/null +++ b/deps/npm/test/lib/utils/key-values.js @@ -0,0 +1,84 @@ +const t = require('tap') +const { logObject, logStageItem, defaultPredicate } = require('../../../lib/utils/key-values.js') +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') + +t.test('defaultPredicate skips null and undefined', t => { + const chalk = { green: v => v } + t.equal(defaultPredicate('k', null, chalk), null) + t.equal(defaultPredicate('k', undefined, chalk), null) + t.equal(defaultPredicate('k', 'val', chalk), 'val') + t.end() +}) + +t.test('logObject json mode', async t => { + const { joinedOutput } = await loadMockNpm(t) + const chalk = { cyan: v => v, green: v => v } + logObject({ a: 1, b: 2 }, { chalk, json: true }) + const out = JSON.parse(joinedOutput()) + t.same(out, { a: 1, b: 2 }) +}) + +t.test('logObject skips null values with default predicate', async t => { + const { joinedOutput } = await loadMockNpm(t) + const chalk = { cyan: v => v, green: v => v } + logObject({ a: 'yes', b: null, c: 'also' }, { chalk }) + const out = joinedOutput() + t.match(out, /a: yes/) + t.match(out, /c: also/) + t.notMatch(out, /b:/) +}) + +t.test('logStageItem includes extra properties', async t => { + const { joinedOutput } = await loadMockNpm(t) + const chalk = { cyan: v => v, green: v => v } + logStageItem({ + id: 'abc', + packageName: 'pkg', + version: '1.0.0', + tag: 'latest', + createdAt: '2026-01-01', + actor: 'user', + actorType: 'human', + shasum: 'sha1', + extra: 'bonus', + }, { chalk }) + const out = joinedOutput() + t.match(out, /extra: bonus/) + t.match(out, /package name: pkg/) +}) + +t.test('logObject with custom predicate', async t => { + const { joinedOutput } = await loadMockNpm(t) + const chalk = { cyan: v => v, green: v => v } + logObject({ a: 'one', b: 'two' }, { + chalk, + predicate: (key, value) => `[${value}]`, + }) + const out = joinedOutput() + t.match(out, /a: \[one\]/) + t.match(out, /b: \[two\]/) +}) + +t.test('logStageItem without actorType shows actor alone', async t => { + const { joinedOutput } = await loadMockNpm(t) + const chalk = { cyan: v => v, green: v => v } + logStageItem({ + id: 'abc', + packageName: 'pkg', + version: '1.0.0', + tag: 'latest', + createdAt: '2026-01-01', + actor: 'user', + shasum: 'sha1', + }, { chalk }) + const out = joinedOutput() + t.match(out, /staged by: user/) + t.notMatch(out, /\(/) +}) + +t.test('logObject with all values skipped produces no output', async t => { + const { joinedOutput } = await loadMockNpm(t) + const chalk = { cyan: v => v, green: v => v } + logObject({ a: null, b: undefined }, { chalk }) + t.equal(joinedOutput(), '') +}) diff --git a/deps/npm/test/lib/utils/reify-output.js b/deps/npm/test/lib/utils/reify-output.js index 134951e40aabd1..b1bc92b1c77aed 100644 --- a/deps/npm/test/lib/utils/reify-output.js +++ b/deps/npm/test/lib/utils/reify-output.js @@ -448,3 +448,114 @@ t.test('prints dedupe difference on long', async t => { t.matchSnapshot(out, 'diff table') }) + +t.test('prints unreviewed install scripts summary', async t => { + const mockReifyWithExtras = async (t, reify, extras, { command, ...config } = {}) => { + const mock = await mockNpm(t, { command, config }) + Object.defineProperty(mock.npm, 'command', { + get () { + return command + }, + enumerable: true, + }) + reifyOutput(mock.npm, reify, extras) + mock.npm.finish() + return mock + } + + const baseReify = { + actualTree: { name: 'host', inventory: { has: () => false } }, + diff: { children: [] }, + } + + const unreviewedScripts = [ + { + node: { packageName: 'canvas', name: 'canvas', version: '2.11.0', path: '/x/canvas' }, + scripts: { install: 'node-gyp rebuild' }, + }, + { + node: { packageName: 'sharp', name: 'sharp', version: '0.33.2', path: '/x/sharp' }, + scripts: { preinstall: 'pre', postinstall: 'post' }, + }, + ] + + const mock = await mockReifyWithExtras(t, baseReify, { unreviewedScripts }) + const warn = mock.logs.warn.byTitle('allow-scripts').join('\n') + t.match(warn, /2 packages have install scripts not yet covered/) + t.match(warn, /canvas@2\.11\.0 \(install: node-gyp rebuild\)/) + t.match(warn, /sharp@0\.33\.2 \(preinstall: pre; postinstall: post\)/) + t.match(warn, /npm approve-scripts --allow-scripts-pending/) +}) + +t.test('single unreviewed script uses singular wording', async t => { + const mockReifyWithExtras = async (t, reify, extras) => { + const mock = await mockNpm(t, {}) + reifyOutput(mock.npm, reify, extras) + mock.npm.finish() + return mock + } + + const mock = await mockReifyWithExtras( + t, + { actualTree: { inventory: { has: () => false } }, diff: { children: [] } }, + { + unreviewedScripts: [{ + node: { packageName: 'one', name: 'one', version: '1.0.0', path: '/x' }, + scripts: { install: 'do' }, + }], + } + ) + t.match(mock.logs.warn.byTitle('allow-scripts').join('\n'), /1 package has install scripts/) +}) + +t.test('json output includes unreviewedScripts', async t => { + const mock = await mockNpm(t, { config: { json: true } }) + reifyOutput(mock.npm, { + actualTree: { inventory: { size: 0 } }, + diff: null, + }, { + unreviewedScripts: [{ + node: { packageName: 'pkg', name: 'pkg', version: '1.0.0', path: '/x' }, + scripts: { install: 'cmd' }, + }], + }) + mock.npm.finish() + const parsed = JSON.parse(mock.joinedOutput()) + t.match(parsed.unreviewedScripts, [{ + name: 'pkg', + version: '1.0.0', + path: '/x', + scripts: { install: 'cmd' }, + }]) +}) + +t.test('unreviewed script with node.name only (no packageName) still renders', async t => { + const mock = await mockNpm(t, {}) + reifyOutput(mock.npm, { + actualTree: { inventory: { has: () => false } }, + diff: { children: [] }, + }, { + unreviewedScripts: [{ + node: { name: 'fallback', path: '/x' }, // no packageName, no version + scripts: { install: 'cmd' }, + }], + }) + mock.npm.finish() + t.match(mock.logs.warn.byTitle('allow-scripts').join('\n'), / fallback \(install: cmd\)/) +}) + +t.test('json output includes node.name when packageName is missing', async t => { + const mock = await mockNpm(t, { config: { json: true } }) + reifyOutput(mock.npm, { + actualTree: { inventory: { size: 0 } }, + diff: null, + }, { + unreviewedScripts: [{ + node: { name: 'fallback', path: '/x' }, + scripts: { install: 'cmd' }, + }], + }) + mock.npm.finish() + const parsed = JSON.parse(mock.joinedOutput()) + t.equal(parsed.unreviewedScripts[0].name, 'fallback') +}) diff --git a/deps/npm/test/lib/utils/resolve-allow-scripts.js b/deps/npm/test/lib/utils/resolve-allow-scripts.js new file mode 100644 index 00000000000000..0d6cdb8c040ac9 --- /dev/null +++ b/deps/npm/test/lib/utils/resolve-allow-scripts.js @@ -0,0 +1,347 @@ +const t = require('tap') +const mockNpm = require('../../fixtures/mock-npm') +const tmock = require('../../fixtures/tmock') + +const loadResolver = (t) => tmock(t, '{LIB}/utils/resolve-allow-scripts.js') + +// Helper that simulates config layering. `cliConfig` sets the value at +// the 'cli' source; `npmrcConfig` sets it at the 'user' source. mockNpm +// puts all `config` keys into the 'cli' source by default, so for npmrc +// tests we use an .npmrc file instead. + +t.test('returns null when no policy is set anywhere', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { 'package.json': JSON.stringify({ name: 'p' }) }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.strictSame(result, { policy: null, source: null }) +}) + +t.test('global install: skips package.json but still consults CLI', async t => { + const { npm } = await mockNpm(t, { + config: { global: true, 'allow-scripts': 'canvas' }, + prefixDir: { 'package.json': JSON.stringify({ name: 'p', allowScripts: { sharp: true } }) }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('global install: skips package.json but still consults .npmrc', async t => { + const { npm } = await mockNpm(t, { + config: { global: true }, + homeDir: { '.npmrc': 'allow-scripts = canvas' }, + prefixDir: { + 'package.json': JSON.stringify({ name: 'p', allowScripts: { sharp: true } }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('global install with no CLI or .npmrc returns null', async t => { + const { npm } = await mockNpm(t, { + config: { global: true }, + prefixDir: { 'package.json': JSON.stringify({ name: 'p', allowScripts: { x: true } }) }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.strictSame(result, { policy: null, source: null }) +}) + +t.test('reads from package.json when only package.json is set', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { canvas: true, 'core-js': false, 'sharp@0.33.2': true }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { canvas: true, 'core-js': false, 'sharp@0.33.2': true }) +}) + +t.test('--allow-scripts CLI flag is rejected in project-scoped installs', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + }, + // mock-npm puts all config keys at the 'cli' source. + config: { 'allow-scripts': 'canvas' }, + }) + const resolveAllowScripts = loadResolver(t) + await t.rejects( + resolveAllowScripts(mock.npm), + { code: 'EALLOWSCRIPTS', message: /--allow-scripts is not allowed/ } + ) +}) + +t.test('--allow-scripts CLI flag is accepted in global installs (RFC layer 1 wins)', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + }, + config: { 'allow-scripts': 'canvas', global: true }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('package.json wins over .npmrc setting (RFC layer 2 > layer 3)', async t => { + // Put the allow-scripts setting in an .npmrc file so it loads at the + // 'user' source, not 'cli'. + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { sharp: true }) + t.match( + mock.logs.warn.byTitle('allow-scripts'), + [/\.npmrc allow-scripts setting is being ignored because package.json/] + ) +}) + +t.test('.npmrc setting is used when nothing higher is set', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p' }), + '.npmrc': 'allow-scripts = canvas, sharp', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true, sharp: true }) +}) + +t.test('--allow-scripts CLI flag is accepted via skipProjectConfig (npm exec)', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p' }), + '.npmrc': 'allow-scripts = canvas', + }, + config: { 'allow-scripts': 'sharp' }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm, { skipProjectConfig: true }) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { sharp: true }) + t.match( + mock.logs.warn.byTitle('allow-scripts'), + [/\.npmrc allow-scripts setting is being ignored because --allow-scripts/] + ) +}) + +t.test('empty allowScripts object in package.json falls through to .npmrc', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p', allowScripts: {} }), + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('missing package.json with .npmrc setting uses .npmrc', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('reads from npm.prefix, not cwd, so workspace sub-installs find root policy', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'root', + workspaces: ['packages/*'], + allowScripts: { sharp: true }, + }), + packages: { + sub: { 'package.json': JSON.stringify({ name: 'sub' }) }, + }, + }, + chdir: ({ prefix }) => require('node:path').join(prefix, 'packages', 'sub'), + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { sharp: true }) +}) + +t.test('drops package.json entries with forbidden semver ranges and warns', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { + 'sharp@^0.33.0': true, // forbidden: caret range + 'canvas@~2.11.0': true, // forbidden: tilde range + 'core-js@>=3.0.0': true, // forbidden: gte range + 'good@1.2.3': true, // OK: exact pin + 'also-good': true, // OK: bare name + 'disjunction@1.0.0 || 2.0.0': true, // OK: exact disjunction + }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { + 'good@1.2.3': true, + 'also-good': true, + 'disjunction@1.0.0 || 2.0.0': true, + }) + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.equal(warnings.filter(m => /semver ranges/.test(m)).length, 3) +}) + +t.test('drops package.json entries with dist-tag specs and warns', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { + 'sharp@latest': true, // forbidden: dist-tag + 'canvas@next': true, // forbidden: dist-tag + 'good@1.2.3': true, // OK: exact pin + }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { 'good@1.2.3': true }) + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.equal(warnings.filter(m => /dist-tag specs/.test(m)).length, 2) +}) + +t.test('drops .npmrc forbidden ranges (and warns) but keeps valid entries', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p' }), + '.npmrc': 'allow-scripts = canvas, sharp@^0.33.0, lodash@4.17.21', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true, 'lodash@4.17.21': true }) + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.ok(warnings.some(m => /sharp@\^0\.33\.0/.test(m) && /semver ranges/.test(m))) +}) + +t.test('drops package.json entries that fail npa parse', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { + '@@@invalid@@@': true, + good: true, + }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { good: true }) + t.ok(mock.logs.warn.byTitle('allow-scripts').some(m => /unparseable/.test(m))) +}) + +t.test('returns null when all package.json entries are dropped as invalid', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { 'sharp@^0.33.0': true }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.strictSame(result, { policy: null, source: null }) +}) + +t.test('skipProjectConfig: ignores package.json even when present', async t => { + // Per RFC line 299, exec/npx consults only user/global .npmrc. + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm, { skipProjectConfig: true }) + // package.json is skipped, falls through to .npmrc. + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('skipProjectConfig: CLI still wins over .npmrc', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + config: { 'allow-scripts': 'lodash' }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm, { skipProjectConfig: true }) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { lodash: true }) +}) + +t.test('skipProjectConfig: returns null when only package.json is set', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm, { skipProjectConfig: true }) + t.strictSame(result, { policy: null, source: null }) +}) diff --git a/deps/npm/test/lib/utils/sbom-cyclonedx.js b/deps/npm/test/lib/utils/sbom-cyclonedx.js index fc30130f1fae72..ea569d41c57d8b 100644 --- a/deps/npm/test/lib/utils/sbom-cyclonedx.js +++ b/deps/npm/test/lib/utils/sbom-cyclonedx.js @@ -291,6 +291,25 @@ t.test('node - with duplicate deps', t => { t.end() }) +t.test('node - with duplicate edges to same dep', t => { + // A node can have multiple outgoing edges resolving to the same + // `name@version` (e.g. a direct `dep1: ^1` plus an alias + // `dep1-aliased: npm:dep1@^1`). The resulting `dependsOn` array must + // still contain each ref at most once, since CycloneDX 1.5 requires + // unique items. + const node = { + ...root, + edgesOut: [ + { to: dep1 }, + { to: dep1 }, + ], + } + const res = cyclonedxOutput({ npm, nodes: [node, dep1] }) + t.same(res.dependencies[0].dependsOn, ['dep1@0.0.1']) + t.matchSnapshot(JSON.stringify(res)) + t.end() +}) + // Check that all of the generated test snapshots validate against the CycloneDX schema t.test('schema validation', t => { // Load schemas diff --git a/deps/npm/test/lib/utils/sbom-spdx.js b/deps/npm/test/lib/utils/sbom-spdx.js index cdeb68218ee332..d2599b0824510c 100644 --- a/deps/npm/test/lib/utils/sbom-spdx.js +++ b/deps/npm/test/lib/utils/sbom-spdx.js @@ -256,6 +256,27 @@ t.test('node - with duplicate deps', t => { t.end() }) +t.test('node - with duplicate edges to same dep', t => { + // A node can have multiple outgoing edges resolving to the same + // `name@version` of the same edge type (e.g. a direct `dep1: ^1` plus an + // alias `dep1-aliased: npm:dep1@^1`). The resulting relationships must + // still be unique per (source, target, type) triple. + const node = { ...root, + edgesOut: [ + { to: dep1 }, + { to: dep1 }, + ] } + const res = spdxOutput({ npm, nodes: [node, dep1] }) + const depRels = res.relationships.filter( + r => r.spdxElementId === 'SPDXRef-Package-dep1-0.0.1' + && r.relatedSpdxElement === 'SPDXRef-Package-root-1.0.0' + && r.relationshipType === 'DEPENDENCY_OF' + ) + t.equal(depRels.length, 1) + t.matchSnapshot(JSON.stringify(res)) + t.end() +}) + // Check that all of the generated test snapshots validate against the SPDX schema t.test('schema validation', t => { const ajv = new Ajv() diff --git a/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js b/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js new file mode 100644 index 00000000000000..e246c68998c451 --- /dev/null +++ b/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js @@ -0,0 +1,191 @@ +const t = require('tap') + +const preflight = require('../../../lib/utils/strict-allow-scripts-preflight.js') + +// Build a node fixture that checkAllowScripts will pick up as "unreviewed": +// registry-resolved, hasInstallScript true, not project root / workspace / +// link, and no allowScripts entry covering it. +const node = ({ + name = 'pkg', + version = '1.0.0', + scripts = { install: 'node-gyp rebuild' }, +} = {}) => ({ + name, + resolved: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`, + hasInstallScript: !!Object.keys(scripts).length, + path: `/fake/${name}`, + isProjectRoot: false, + isWorkspace: false, + isLink: false, + package: { name, version, scripts }, +}) + +const tree = (nodes) => ({ + inventory: new Map(nodes.map((n, i) => [`node_modules/${n.name}-${i}`, n])), +}) + +const makeArb = ({ ideal, actual, allowScripts = null } = {}) => { + const arb = { + options: { allowScripts, ignoreScripts: false }, + idealTree: ideal ?? null, + actualTree: actual ?? null, + } + arb.buildIdealTree = async () => arb.idealTree + return arb +} + +t.test('no-op when strictAllowScripts is not set', async t => { + const arb = makeArb({ ideal: tree([node()]) }) + await preflight({ arb, npm: { flatOptions: {} }, idealTreeOpts: {} }) + t.pass('returned without throwing') +}) + +t.test('no-op when dangerouslyAllowAllScripts overrides', async t => { + const arb = makeArb({ ideal: tree([node()]) }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true, dangerouslyAllowAllScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('returned without throwing') +}) + +t.test('no-op when ignoreScripts overrides', async t => { + const arb = makeArb({ ideal: tree([node()]) }) + arb.options.ignoreScripts = true + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('returned without throwing') +}) + +t.test('throws when unreviewed install scripts exist (idealTree path)', async t => { + const arb = makeArb({ ideal: tree([node({ name: 'canvas' }), node({ name: 'sharp' })]) }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }), + { + code: 'ESTRICTALLOWSCRIPTS', + message: /2 package\(s\) have install scripts not covered/, + } + ) +}) + +t.test('passes when all install-script nodes are explicitly approved', async t => { + const arb = makeArb({ + ideal: tree([node({ name: 'canvas' })]), + allowScripts: { canvas: true }, + }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('no error thrown') +}) + +t.test('passes when all install-script nodes are explicitly denied', async t => { + const arb = makeArb({ + ideal: tree([node({ name: 'canvas' })]), + allowScripts: { canvas: false }, + }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('no error thrown') +}) + +t.test('skips buildIdealTree when arb.idealTree already exists (npm ci path)', async t => { + // `npm ci` builds the ideal tree before calling the preflight. The + // helper must not rebuild it. + const ideal = tree([node({ name: 'pre-built' })]) + const arb = makeArb({ ideal, allowScripts: { 'pre-built': true } }) + let buildCalls = 0 + arb.buildIdealTree = async () => { + buildCalls++ + return arb.idealTree + } + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.equal(buildCalls, 0, 'buildIdealTree was not called a second time') +}) + +t.test('builds the ideal tree when arb.idealTree is empty (npm install path)', async t => { + // `npm install` does not pre-build the ideal tree. The helper must + // build it so checkAllowScripts has something to walk. + const arb = makeArb({ allowScripts: { 'fresh-pkg': true } }) + let buildCalls = 0 + arb.buildIdealTree = async () => { + buildCalls++ + arb.idealTree = tree([node({ name: 'fresh-pkg' })]) + } + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.equal(buildCalls, 1, 'buildIdealTree was called once') +}) + +t.test('uses actualTree when idealTreeOpts is not provided (rebuild path)', async t => { + const arb = makeArb({ actual: tree([node({ name: 'rebuild-pkg' })]) }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + }), + { + code: 'ESTRICTALLOWSCRIPTS', + message: /rebuild-pkg@1\.0\.0/, + } + ) +}) + +t.test('error message includes script bodies', async t => { + const arb = makeArb({ + ideal: tree([node({ name: 'canvas', version: '2.11.0', scripts: { install: 'node-gyp rebuild' } })]), + }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }), + { message: /canvas@2\.11\.0 \(install: node-gyp rebuild\)/ } + ) +}) + +t.test('error label falls back to node.name when package.version is missing', async t => { + // Exercises the `version ? '${name}@${version}' : name` branch in the + // error formatter when a node has no package.version (and the name + // falls back to node.name via `node.package?.name || node.name`). + const bare = { + name: 'no-version-pkg', + resolved: 'https://registry.npmjs.org/no-version-pkg/-/no-version-pkg-1.0.0.tgz', + hasInstallScript: true, + path: '/fake/no-version-pkg', + isProjectRoot: false, + isWorkspace: false, + isLink: false, + package: { scripts: { install: 'node-gyp rebuild' } }, + } + const arb = makeArb({ ideal: tree([bare]) }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }), + { message: /no-version-pkg \(install: node-gyp rebuild\)/ } + ) +}) diff --git a/deps/npm/test/lib/utils/tar.js b/deps/npm/test/lib/utils/tar.js index 85a95e57766855..3eff023ccec999 100644 --- a/deps/npm/test/lib/utils/tar.js +++ b/deps/npm/test/lib/utils/tar.js @@ -109,6 +109,20 @@ t.test('should log tarball contents with unicode', async (t) => { t.end() }) +t.test('logTar with json and no key emits bare tarball object', async (t) => { + const buffered = [] + const logTar = tmock(t, '{LIB}/utils/tar.js', { + 'proc-log': { + log: { notice: () => {} }, + output: { buffer: (data) => buffered.push(data) }, + }, + }).logTar + + const tarball = { name: 'my-pkg', version: '1.0.0' } + logTar(tarball, { json: true }) + t.strictSame(buffered, [tarball], 'buffers the bare tarball when key is omitted') +}) + t.test('should getContents of a tarball with only a package.json', async (t) => { const testDir = t.testdir({ 'package.json': JSON.stringify({ diff --git a/deps/npm/test/lib/utils/warn-workspace-allow-scripts.js b/deps/npm/test/lib/utils/warn-workspace-allow-scripts.js new file mode 100644 index 00000000000000..c9a5727157c21e --- /dev/null +++ b/deps/npm/test/lib/utils/warn-workspace-allow-scripts.js @@ -0,0 +1,108 @@ +const t = require('tap') +const { + findWorkspaceAllowScripts, + warnWorkspaceAllowScripts, +} = require('../../../lib/utils/warn-workspace-allow-scripts.js') + +const node = ({ + name = 'pkg', + packageName, + isWorkspace = false, + isProjectRoot = false, + allowScripts, + path = `/fake/${name}`, +} = {}) => ({ + name, + packageName: packageName ?? name, + path, + isWorkspace, + isProjectRoot, + package: allowScripts !== undefined ? { allowScripts } : {}, +}) + +const tree = (nodes) => ({ + inventory: new Map(nodes.map((n, i) => [`node_modules/${n.name || `n${i}`}`, n])), +}) + +t.test('returns [] for empty tree', async t => { + t.strictSame(findWorkspaceAllowScripts(tree([])), []) +}) + +t.test('returns [] for missing tree', async t => { + t.strictSame(findWorkspaceAllowScripts(null), []) + t.strictSame(findWorkspaceAllowScripts(undefined), []) +}) + +t.test('ignores project root with allowScripts', async t => { + const t1 = tree([ + node({ name: 'root', isProjectRoot: true, isWorkspace: true, allowScripts: { x: true } }), + ]) + t.strictSame(findWorkspaceAllowScripts(t1), []) +}) + +t.test('ignores non-workspace dep with allowScripts', async t => { + const t1 = tree([ + node({ name: 'dep', allowScripts: { x: true } }), + ]) + t.strictSame(findWorkspaceAllowScripts(t1), []) +}) + +t.test('finds non-root workspace with allowScripts', async t => { + const ws = node({ name: 'ws', isWorkspace: true, allowScripts: { x: true } }) + const t1 = tree([ + node({ name: 'root', isProjectRoot: true, isWorkspace: true }), + ws, + ]) + t.equal(findWorkspaceAllowScripts(t1).length, 1) + t.equal(findWorkspaceAllowScripts(t1)[0], ws) +}) + +t.test('finds workspace with empty allowScripts object too', async t => { + const ws = node({ name: 'ws', isWorkspace: true, allowScripts: {} }) + t.equal(findWorkspaceAllowScripts(tree([ws])).length, 1) +}) + +t.test('warnWorkspaceAllowScripts emits one log.warn per offender', async t => { + const warnings = [] + const listener = (level, ...args) => { + if (level === 'warn') { + warnings.push(args) + } + } + process.on('log', listener) + t.teardown(() => process.off('log', listener)) + + const t1 = tree([ + node({ name: 'root', isProjectRoot: true, isWorkspace: true }), + node({ name: 'a', isWorkspace: true, allowScripts: { x: true } }), + node({ name: 'b', isWorkspace: true, allowScripts: { y: false } }), + node({ name: 'c', isWorkspace: true }), // no allowScripts; no warning + ]) + warnWorkspaceAllowScripts(t1) + + t.equal(warnings.length, 2) + t.match(warnings[0][1], /allowScripts in workspace a/) + t.match(warnings[1][1], /allowScripts in workspace b/) +}) + +t.test('warnWorkspaceAllowScripts uses node.name when packageName missing', async t => { + const warnings = [] + const listener = (level, ...args) => { + if (level === 'warn') { + warnings.push(args) + } + } + process.on('log', listener) + t.teardown(() => process.off('log', listener)) + + // packageName undefined, name set + const ws = { + name: 'fallback-name', + path: '/x', + isWorkspace: true, + isProjectRoot: false, + package: { allowScripts: { x: true } }, + } + warnWorkspaceAllowScripts({ inventory: new Map([['node_modules/ws', ws]]) }) + t.match(warnings[0][1], /workspace fallback-name/) +}) diff --git a/deps/sqlite/sqlite3.c b/deps/sqlite/sqlite3.c index dfd557adeda581..0c83f247e89464 100644 --- a/deps/sqlite/sqlite3.c +++ b/deps/sqlite/sqlite3.c @@ -238388,7 +238388,7 @@ static int sessionApplyOneOp( for(i=0; rc==SQLITE_OK && iabPK[i] || (bPatchset==0 && pOld) ){ + if( pOld && (p->abPK[i] || bPatchset==0) ){ rc = sessionBindValue(pUp, i*2+2, pOld); } if( rc==SQLITE_OK && pNew ){ diff --git a/deps/uv/uv.gyp b/deps/uv/uv.gyp index 540445f1f3249b..760a3cdb0019d1 100644 --- a/deps/uv/uv.gyp +++ b/deps/uv/uv.gyp @@ -62,7 +62,6 @@ 'uv_sources_win': [ 'include/uv/win.h', 'src/win/async.c', - 'src/win/atomicops-inl.h', 'src/win/core.c', 'src/win/detect-wakeup.c', 'src/win/dl.c', diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 9e6eabb77caa1e..a9f9a148fdcc11 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -881,7 +881,7 @@ _may contain sensitive data_. Use [`buf.fill(0)`][`buf.fill()`] to initialize such `Buffer` instances with zeroes. When using [`Buffer.allocUnsafe()`][] to allocate new `Buffer` instances, -allocations less than `Buffer.poolSize >>> 1` (4KiB when default poolSize is used) are sliced +allocations less than `Buffer.poolSize >>> 1` (32KiB when default poolSize is used) are sliced from a single pre-allocated `Buffer`. This allows applications to avoid the garbage collection overhead of creating many individually allocated `Buffer` instances. This approach improves both performance and memory usage by @@ -1513,9 +1513,13 @@ console.log(Buffer.isEncoding('')); -* Type: {integer} **Default:** `8192` +* Type: {integer} **Default:** `65536` This is the size (in bytes) of pre-allocated internal `Buffer` instances used for pooling. This value may be modified. diff --git a/doc/api/debugger.md b/doc/api/debugger.md index 5eeffd43e57f5f..6b4a095af88939 100644 --- a/doc/api/debugger.md +++ b/doc/api/debugger.md @@ -236,6 +236,10 @@ debug> added: - v26.1.0 changes: + - version: v26.3.0 + pr-url: https://github.com/nodejs/node/pull/63437 + description: Add `probe_failure` terminal `error` event for inspector-side mid-session + failures, and `error.details` for additional context on per-hit and terminal errors. - version: v26.2.0 pr-url: https://github.com/nodejs/node/pull/63286 description: JSON report schema bumped to v2. Probe `target` is now @@ -382,7 +386,10 @@ $ node inspect --json --probe cli.js:5 --expr 'rss' cli.js "value": 55443456, "description": "55443456" } - // If the expression throws, "error" is present instead of "result". + // If the probe expression throws, fails, or never completes, the entry + // carries an `error` field instead of `result` with the shape + // `{ message: string, details?: object }`. The `message` and `details` + // content is informational only and may change between releases. }, { "probe": 0, @@ -414,24 +421,40 @@ $ node inspect --json --probe cli.js:5 --expr 'rss' cli.js // "error": { // "code": "probe_target_exit", // "exitCode": 1, - // "stderr": "[Error: boom]", + // "stderr": "Error: boom", // "message": "Target exited with code 1 before probes: app.js:10" // } // } + // 5. { + // "event": "error", + // "pending": [1], + // "error": { + // "code": "probe_failure", + // "probe": 0, + // "stderr": "...", + // "message": "Target process exited during probe evaluation before probes: app.js:12. If the failure repeats, review the probe expression.", + // "details": { "lastCdpMethod": "Debugger.evaluateOnCallFrame" } + // } + // } } ] } ``` -### Output and exit codes from the probed process +### Output and exit codes Probe mode only prints the final probe report to stdout, and otherwise silences stdout/stderr from the child process. When the probing session ends, -`node inspect` typically exits with code `0` and prints a final report to -stdout. If the child process exits with a non-zero code before the -probe session ends, the final report records a terminal `error` event along -with the exit code and captured child stderr. The probing process itself -still exits with code `0` in this case. +the probing process typically exits with code `0` and prints a final report to +stdout. If the child process exits with a non-zero code before the probe +session ends, or the probe session cannot complete for another reason, the +final report records a terminal `error` event. + +When `error.code` is `'probe_failure'` or `'probe_timeout'`, the probing process +exits with a non-zero code, indicating recorded hits may be incomplete. +In this case, `error.message` will contain recovery hints, and `error.probe`, +when present, is an index into the report's `probes` array that identifies +the possible culprit probe on a best-effort basis to help guide debugging. Invalid arguments and fatal launch or connect failures may cause the probing process to exit with a non-zero code and print an error message diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index ce5ef73708f70e..b5b2940b91a484 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -810,7 +810,7 @@ changes: Type: Revoked The [`events.listenerCount(emitter, eventName)`][] API was deprecated, as it -provided identical fuctionality to [`emitter.listenerCount(eventName)`][]. The +provided identical functionality to [`emitter.listenerCount(eventName)`][]. The deprecation was revoked because this function has been repurposed to also accept {EventTarget} arguments. diff --git a/doc/api/errors.md b/doc/api/errors.md index e8c87ebb502fdc..e03eff915ca1cd 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3102,7 +3102,7 @@ The context must be a `SecureContext`. ### `ERR_TLS_INVALID_PROTOCOL_METHOD` -The specified `secureProtocol` method is invalid. It is either unknown, or +The specified `secureProtocol` method is invalid. It is either unknown, or disabled because it is insecure. diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 37ccf2a719bc1a..01abdc0b6bbdb1 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -124,18 +124,18 @@ such as `0` and `1`; JavaScript `true` and `false` are not accepted. Functions and callbacks are described with signature objects. -Supported fields: +Signature objects may contain the following properties, both of which are +optional: -* `result`, `return`, or `returns` for the return type. -* `parameters` or `arguments` for the parameter type list. +* `return` {string} A [type name][type names] specifying the return type of the + function or callback. **Default:** `'void'`. +* `arguments` {string\[]} An array of [type names][] specifying the argument + type list of the function or callback. **Default:** `[]`. -Only one return-type field and one parameter-list field may be present in a -single signature object. - -```cjs +```js const signature = { - result: 'i32', - parameters: ['i32', 'i32'], + return: 'i32', + arguments: ['i32', 'i32'], }; ``` @@ -193,7 +193,7 @@ import { dlopen } from 'node:ffi'; { using handle = dlopen('./mylib.so', { - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, }); console.log(handle.functions.add_i32(20, 22)); } // handle.lib.close() is invoked automatically here. @@ -203,8 +203,8 @@ import { dlopen } from 'node:ffi'; import { dlopen } from 'node:ffi'; const { lib, functions } = dlopen('./mylib.so', { - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, - string_length: { parameters: ['pointer'], result: 'u64' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, + string_length: { arguments: ['pointer'], return: 'u64' }, }); console.log(functions.add_i32(20, 22)); @@ -214,8 +214,8 @@ console.log(functions.add_i32(20, 22)); const { dlopen } = require('node:ffi'); const { lib, functions } = dlopen('./mylib.so', { - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, - string_length: { parameters: ['pointer'], result: 'u64' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, + string_length: { arguments: ['pointer'], return: 'u64' }, }); console.log(functions.add_i32(20, 22)); @@ -356,8 +356,8 @@ const { DynamicLibrary } = require('node:ffi'); const lib = new DynamicLibrary('./mylib.so'); const add = lib.getFunction('add_i32', { - parameters: ['i32', 'i32'], - result: 'i32', + arguments: ['i32', 'i32'], + return: 'i32', }); console.log(add(20, 22)); @@ -407,7 +407,7 @@ const { DynamicLibrary } = require('node:ffi'); const lib = new DynamicLibrary('./mylib.so'); const callback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, (value) => value * 2, ); ``` @@ -417,7 +417,7 @@ Callbacks are subject to the following restrictions: * They must be invoked on the same system thread where they were created. * They must not throw exceptions. * They must not return promises. -* They must return a value compatible with the declared result type. +* They must return a value compatible with the declared return type. * They must not call `library.close()` on their owning library while running. * They must not unregister themselves while running. @@ -465,7 +465,7 @@ JavaScript `number` values that match the declared type. For 64-bit integer types (`i64` and `u64`), pass JavaScript `bigint` values. -For pointer-like parameters: +For pointer-like arguments: * `null` and `undefined` are passed as null pointers. * `string` values are copied to temporary NUL-terminated UTF-8 strings for the @@ -727,3 +727,4 @@ and keep callback and pointer lifetimes explicit on the native side. [`--allow-ffi`]: cli.md#--allow-ffi [`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy [`using`]: https://tc39.es/proposal-explicit-resource-management/#sec-using-declarations +[type names]: #type-names diff --git a/doc/api/fs.md b/doc/api/fs.md index 53f78b38774f18..baf65c936003bf 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -7989,7 +7989,26 @@ added: * Type: {number|bigint} -Free blocks available to unprivileged users. +Free blocks available to unprivileged users. Multiply by [`statfs.bsize`][] +to get the number of available bytes. + +```mjs +import { statfs } from 'node:fs/promises'; + +const stats = await statfs('/'); +const availableBytes = stats.bsize * stats.bavail; +console.log(`Available space: ${availableBytes} bytes`); +``` + +```cjs +const { statfs } = require('node:fs/promises'); + +(async () => { + const stats = await statfs('/'); + const availableBytes = stats.bsize * stats.bavail; + console.log(`Available space: ${availableBytes} bytes`); +})(); +``` #### `statfs.bfree` @@ -8001,7 +8020,26 @@ added: * Type: {number|bigint} -Free blocks in file system. +Free blocks in file system. Multiply by [`statfs.bsize`][] to get the number +of free bytes. + +```mjs +import { statfs } from 'node:fs/promises'; + +const stats = await statfs('/'); +const freeBytes = stats.bsize * stats.bfree; +console.log(`Free space: ${freeBytes} bytes`); +``` + +```cjs +const { statfs } = require('node:fs/promises'); + +(async () => { + const stats = await statfs('/'); + const freeBytes = stats.bsize * stats.bfree; + console.log(`Free space: ${freeBytes} bytes`); +})(); +``` #### `statfs.blocks` @@ -8013,7 +8051,26 @@ added: * Type: {number|bigint} -Total data blocks in file system. +Total data blocks in file system. Multiply by [`statfs.bsize`][] to get the +total size in bytes. + +```mjs +import { statfs } from 'node:fs/promises'; + +const stats = await statfs('/'); +const totalBytes = stats.bsize * stats.blocks; +console.log(`Total space: ${totalBytes} bytes`); +``` + +```cjs +const { statfs } = require('node:fs/promises'); + +(async () => { + const stats = await statfs('/'); + const totalBytes = stats.bsize * stats.blocks; + console.log(`Total space: ${totalBytes} bytes`); +})(); +``` #### `statfs.bsize` @@ -8025,7 +8082,7 @@ added: * Type: {number|bigint} -Optimal transfer block size. +Optimal transfer block size in bytes. #### `statfs.frsize` @@ -8071,7 +8128,11 @@ added: * Type: {number|bigint} -Type of file system. +Type of file system. A platform-specific numeric identifier for the type of +file system. This value corresponds to the `f_type` field returned by +`statfs(2)` on POSIX systems (for example, `0xEF53` for ext4 on Linux). Its +meaning is OS-dependent and is not guaranteed to be consistent across +platforms. ### Class: `fs.Utf8Stream` @@ -9124,6 +9185,7 @@ the file contents. [`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 [`minimatch`]: https://github.com/isaacs/minimatch [`node:stream/iter`]: stream_iter.md +[`statfs.bsize`]: #statfsbsize [`stream/iter pipeTo()`]: stream_iter.md#pipetosource-transforms-writer [`stream/iter pull()`]: stream_iter.md#pullsource-transforms-options [`stream/iter pullSync()`]: stream_iter.md#pullsyncsource-transforms diff --git a/doc/api/http.md b/doc/api/http.md index 5a6986c775cc7c..f51e1afc8a93f6 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3662,6 +3662,9 @@ Found'`. diff --git a/doc/api/permissions.md b/doc/api/permissions.md index 5af6fbb398f53a..3f2a6411d3f678 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -78,7 +78,7 @@ flag. For WASI, use the [`--allow-wasi`][] flag. For FFI, use the When enabling the Permission Model through the [`--permission`][] flag a new property `permission` is added to the `process` object. -This property contains one function: +This property contains the following functions: ##### `permission.has(scope[, reference])` @@ -92,6 +92,41 @@ process.permission.has('fs.read'); // true process.permission.has('fs.read', '/home/rafaelgss/protected-folder'); // false ``` +##### `permission.drop(scope[, reference])` + +API call to drop permissions at runtime. This operation is **irreversible**. + +When called without a reference, the entire scope is dropped. When called +with a reference, only the permission for that specific resource is revoked. +Dropping a permission only affects future access checks. It does not close or +revoke access to resources that are already open, such as file descriptors, +network sockets, child processes, or worker threads. Applications are +responsible for closing or terminating those resources when they are no longer +needed. + +You can only drop the exact resource that was explicitly granted. The +reference passed to `drop()` must match the original grant. If a permission +was granted using a wildcard (`*`), only the entire scope can be dropped +(by calling `drop()` without a reference). If a directory was granted +(e.g. `--allow-fs-read=/my/folder`), you cannot drop individual files +inside it - you must drop the same directory that was originally granted. + +```js +const fs = require('node:fs'); + +// Read config at startup while we still have permission +const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); + +// Drop read access to /etc/myapp after initialization +process.permission.drop('fs.read', '/etc/myapp'); + +// This will now throw ERR_ACCESS_DENIED +process.permission.has('fs.read', '/etc/myapp/config.json'); // false + +// Drop child process permission entirely +process.permission.drop('child'); +``` + #### File System Permissions The Permission Model, by default, restricts access to the file system through the `node:fs` module. diff --git a/doc/api/process.md b/doc/api/process.md index b1a273a40b3f57..bd0d670ea73683 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -3167,6 +3167,65 @@ process.permission.has('fs.read', './README.md'); process.permission.has('fs.read'); ``` +### `process.permission.drop(scope[, reference])` + + + +> Stability: 1.1 - Active Development + +* `scope` {string} +* `reference` {string} + +Drops the specified permission from the current process. This operation is +**irreversible** — once a permission is dropped, it cannot be restored through +any Node.js API. + +If no reference is provided, the entire scope is dropped. For example, +`process.permission.drop('fs.read')` will revoke ALL file system read +permissions. + +When a reference is provided, only the permission for that specific resource +is dropped. For example, `process.permission.drop('fs.read', '/etc/myapp')` +will revoke read access to that directory while keeping other read +permissions intact. + +**Important:** You can only drop the exact resource that was explicitly +granted. The reference passed to `drop()` must match the original grant: + +* If a permission was granted using a wildcard (`*`), such as + `--allow-fs-read=*`, individual paths cannot be dropped - only the entire + scope can be dropped (by calling `drop()` without a reference). +* If a directory was granted (e.g. `--allow-fs-read=/my/folder`), you cannot + drop access to individual files inside it. You must drop the same directory + that was granted. Any remaining grants continue to apply. + +The available scopes are the same as [`process.permission.has()`][]: + +* `fs` - All File System (drops both read and write) +* `fs.read` - File System read operations +* `fs.write` - File System write operations +* `child` - Child process spawning operations +* `worker` - Worker thread spawning operation +* `net` - Network operations +* `inspector` - Inspector operations +* `wasi` - WASI operations +* `addon` - Native addon operations + +```js +const fs = require('node:fs'); + +// Read configuration during startup +const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); + +// Drop read access to the config directory after initialization +process.permission.drop('fs.read', '/etc/myapp'); + +// This will now throw ERR_ACCESS_DENIED +fs.readFileSync('/etc/myapp/config.json'); +``` + ## `process.pid` -* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only. +* Type: {bigint} The total number of retry packets sent by this endpoint. Read only. + +### `endpointStats.retryRateLimited` + +* Type: {bigint} The total number of retry packets dropped by the global rate + limiter. Read only. A non-zero value indicates the endpoint is under retry + flood pressure. ### `endpointStats.versionNegotiationCount` @@ -716,7 +851,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only. +* Type: {bigint} The total number of version negotiation packets sent by this + endpoint. Read only. + +### `endpointStats.versionNegotiationRateLimited` + +* Type: {bigint} The total number of version negotiation packets dropped by + the global rate limiter. Read only. ### `endpointStats.statelessResetCount` @@ -724,7 +865,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only. +* Type: {bigint} The total number of stateless reset packets sent by this + endpoint. Read only. + +### `endpointStats.statelessResetRateLimited` + +* Type: {bigint} The total number of stateless reset packets dropped by the + global rate limiter. Read only. ### `endpointStats.immediateCloseCount` @@ -732,7 +879,24 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only. +* Type: {bigint} The total number of immediate connection close packets sent + by this endpoint. Read only. + +### `endpointStats.immediateCloseRateLimited` + +* Type: {bigint} The total number of immediate connection close packets + dropped by the global rate limiter. Read only. + +### `endpointStats.sessionCreationRateLimited` + +* Type: {bigint} The total number of session creation attempts dropped by the + per-host rate limiter. Read only. A non-zero value indicates one or more + remote addresses are creating sessions faster than the configured rate allows. + +### `endpointStats.packetsBlocked` + +* Type: {bigint} The total number of incoming packets dropped by the + block list filter. Read only. ## Class: `QuicSession` @@ -742,6 +906,18 @@ added: v23.8.0 A `QuicSession` represents the local side of a QUIC connection. +### `session.applicationOptions` + + + +* Type: {quic.ApplicationOptions} + +The current application-level options for this session. These include settings +that are specific to the negotiated application protocol (e.g. HTTP/3) and may +be negotiated separately from the transport parameters. Read only. + ### `session.close([options])` + +* Type: {quic.TransportParams|null} + +The transport parameters advertised by the local endpoint during the handshake. +Returns `null` if the session has been destroyed. Read only. + ### `session.endpoint` + +* Type: {quic.TransportParams|null|undefined} + +The transport parameters advertised by the remote peer during the handshake. +Returns `null` if the session has been destroyed, `undefined` if the handshake +has not yet completed and the remote parameters are not yet available. Read +only. + ### `session.sendDatagram(datagram[, encoding])` + +* Type: {bigint} + +The current number of bytes sitting in the stream's receive accumulation +buffer, awaiting delivery to the application. A value near zero indicates +the reader is keeping up with incoming data. A value near the stream's +flow control window indicates the application is not consuming data fast +enough. + ### `streamStats.bytesReceived` + +* Type: {bigint} + +The peak number of bytes that were accumulated in the stream's receive +buffer at any point during the stream's lifetime. This value only +increases monotonically. It is useful for diagnosing whether a stream +experienced backpressure episodes and whether the accumulation buffer +sizing is appropriate for the workload. + ### `streamStats.maxOffset` + +* Type: {Object} + +The application specific options. + +#### `applicationOptions.maxHeaderPairs` + +* Type: {bigint|number} + +Maximum number of header name-value pairs accepted per header block. +Headers beyond this limit are silently dropped. **Default:** `128` + +#### `applicationOptions.maxHeaderLength` + +* Type: {bigint|number} + +Maximum total byte length of all header names and values combined per header +block. Headers that would push the total over this limit are silently +dropped. **Default:** `8192` + +#### `applicationOptions.maxFieldSectionSize` + +* Type: {bigint|number} + +Maximum size of a compressed header field section (QPACK). `0` means +unlimited. **Default:** `0` + +#### `applicationOptions.qpackMaxDTableCapacity` + +* Type: {bigint|number} + +QPACK dynamic table capacity in bytes. Set to `0` to disable the dynamic +table. **Default:** `4096` + +#### `applicationOptions.qpackEncoderMaxDTableCapacity` + +* Type: {bigint|number} + +QPACK encoder maximum dynamic table capacity. **Default:** `4096` + +#### `applicationOptions.qpackBlockedStreams` + +* Type: {bigint|number} + +Maximum number of streams that can e blocked waiting for QPACK dynamic table +updates. **Default:** `100` + +#### `applicationOptions.enableConnectProtocol` + +* Type: {boolean} + +Enable the extended CONNECT protocol (RFC 9220). **Default:** `false` + +#### `applicationOptions.enableDatagrams` + +* Type: {boolean} + +Enable HTTP/3 datagrams (RFC 9297). **Default:** `false` + ### Type: `EndpointOptions` + +* Type: {boolean} +* Default: `false` + +When `true`, allows multiple endpoints (across separate processes) to bind to +the same address and port. The kernel will load-balance incoming UDP datagrams +across all sockets bound with this option. This enables horizontal scaling of +QUIC servers by running multiple Node.js processes on the same port. + +Supported on Linux 3.9+ and DragonFlyBSD 3.6+. On unsupported platforms, the +bind will fail with an error. + #### `endpointOptions.maxConnectionsPerHost` +* Type: {number} +* **Default:** `100` -* Type: {bigint|number} +The maximum number of QUIC retry packets the endpoint will send per second. +This is a global rate limit (not per-host) that caps the total server-wide +retry response rate, preventing spoofed-source floods from consuming unbounded +resources. + +#### `endpointOptions.retryBurst` -Specifies the maximum number of QUIC retry attempts allowed per remote peer address. +* Type: {number} +* **Default:** `200` -#### `endpointOptions.maxStatelessResetsPerHost` +The maximum burst of retry packets allowed before rate limiting takes effect. - +#### `endpointOptions.statelessResetRate` -* Type: {bigint|number} +* Type: {number} +* **Default:** `100` + +The maximum number of stateless reset packets the endpoint will send per second. + +#### `endpointOptions.statelessResetBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of stateless reset packets allowed before rate limiting +takes effect. + +#### `endpointOptions.versionNegotiationRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of version negotiation packets the endpoint will send per +second. + +#### `endpointOptions.versionNegotiationBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of version negotiation packets allowed before rate limiting +takes effect. + +#### `endpointOptions.immediateCloseRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of immediate connection close packets the endpoint will +send per second. + +#### `endpointOptions.immediateCloseBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of immediate connection close packets allowed before rate +limiting takes effect. -Specifies the maximum number of stateless resets that are allowed per remote peer address. +#### `endpointOptions.sessionCreationRate` + +* Type: {number} +* **Default:** `50` + +The maximum number of new sessions that a single remote address can create per +second. This is a per-host rate limit tracked in the address validation LRU +cache. It prevents a validated remote address from churning through sessions +(rapidly opening and abandoning connections) faster than the server can handle. +For benchmarking where traffic comes from a single source, set this to a high +value. + +#### `endpointOptions.sessionCreationBurst` + +* Type: {number} +* **Default:** `100` + +The maximum burst of new session creations allowed from a single remote address +before rate limiting takes effect. #### `endpointOptions.retryTokenExpiration` @@ -2423,30 +2829,9 @@ Default: `'h3'` added: v26.2.0 --> -* Type: {Object} +* Type: {quic.ApplicationOptions} -HTTP/3 application-specific options. These only apply when the negotiated -ALPN selects the HTTP/3 application (`'h3'`). - -* `maxHeaderPairs` {number} Maximum number of header name-value pairs - accepted per header block. Headers beyond this limit are silently - dropped. **Default:** `128` -* `maxHeaderLength` {number} Maximum total byte length of all header - names and values combined per header block. Headers that would push - the total over this limit are silently dropped. **Default:** `8192` -* `maxFieldSectionSize` {number} Maximum size of a compressed header - field section (QPACK). `0` means unlimited. **Default:** `0` -* `qpackMaxDTableCapacity` {number} QPACK dynamic table capacity in - bytes. Set to `0` to disable the dynamic table. **Default:** `4096` -* `qpackEncoderMaxDTableCapacity` {number} QPACK encoder maximum - dynamic table capacity. **Default:** `4096` -* `qpackBlockedStreams` {number} Maximum number of streams that can - be blocked waiting for QPACK dynamic table updates. - **Default:** `100` -* `enableConnectProtocol` {boolean} Enable the extended CONNECT - protocol (RFC 9220). **Default:** `false` -* `enableDatagrams` {boolean} Enable HTTP/3 datagrams (RFC 9297). - **Default:** `false` +Application-specific options. ```mjs const { listen } = await import('node:quic'); @@ -2616,9 +3001,15 @@ added: v23.8.0 --> * Type: {string} One of `'use'`, `'ignore'`, or `'default'`. +* **Default:** `'ignore'` When the remote peer advertises a preferred address, this option specifies whether -to use it or ignore it. +to use it or ignore it. The default is `'ignore'` because honoring a server's +preferred address causes the client to migrate its connection to a different IP +address, which can be exploited for data exfiltration attacks that are +indistinguishable from legitimate QUIC connection migration at the network level. +Set to `'use'` only when connecting to trusted servers that require preferred +address migration. #### `sessionOptions.qlog` @@ -2658,6 +3049,23 @@ reported as lost via the `ondatagramstatus` callback. This option is immutable after session creation. +#### `sessionOptions.streamIdleTimeout` + +* Type: {bigint|number} +* **Default:** `30000` (30 seconds) + +The maximum time in milliseconds that a peer-initiated stream can be idle +(no data received) before it is automatically destroyed. This protects +against slowloris-style attacks where a remote peer opens streams but never +sends data, holding server resources indefinitely. Only peer-initiated +streams are checked — locally-initiated streams are the application's +responsibility. Set to `0` to disable. + +The idle check runs as part of the normal send processing loop, so it adds +no additional timers or event loop overhead. The +`session.stats.streamsIdleTimedOut` counter tracks how many streams have been +destroyed by this mechanism. + #### `sessionOptions.maxDatagramSendAttempts` * Type: {number} @@ -2696,6 +3104,23 @@ added: v23.8.0 Specifies the maximum number of milliseconds a TLS handshake is permitted to take to complete before timing out. +#### `sessionOptions.initialRtt` + + + +* Type: {bigint|number} +* **Default:** `0` (use ngtcp2 default of 333ms) + +Specifies the initial round-trip time estimate in milliseconds. This value is +used for probe timeout (PTO) computation, initial pacing, and early loss +detection before the first actual RTT sample is collected from the connection. +The default of 333ms is appropriate for the general internet. For low-latency +environments such as loopback or same-rack deployments, setting a value closer +to the actual RTT (e.g., `1`) avoids unnecessarily conservative initial +behavior. + #### `sessionOptions.keepAlive` +The `TransportParams` type represents the QUIC transport parameters that are +negotiated during session establishment. These parameters are used when +creating a session. The negotiated values can be observed via the +`session.localTransportParams` and `session.remoteTransportParams` properties. + +#### `transportParams.initialSCID` + + + +* Type: {string} + +The initial source connection ID (SCID) specified. This field is ignored on +creation of the session and is provided for informational purposes only when +available in the `session.localTransportParams` and +`session.remoteTransportParams` properties. + +#### `transportParams.originalDCID` + + + +* Type: {string} + +The original destination connection ID (DCID) specified. This field is +ignored on creation of the session and is provided for informational +purposes only when available in the `session.localTransportParams` and +`session.remoteTransportParams` properties. + #### `transportParams.preferredAddressIpv4` + +* Type: {string} + +The retry connection ID specified. This field is ignored on creation +of the session and is provided for informational purposes only when +available in the `session.localTransportParams` and +`session.remoteTransportParams` properties. + ## Callbacks ### Callback error handling @@ -3870,8 +4365,10 @@ throughput issues caused by flow control. [JSON-SEQ]: https://www.rfc-editor.org/rfc/rfc7464 [NSS Key Log Format]: https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format [Permission Model]: permissions.md#permission-model +[RFC 8879]: https://www.rfc-editor.org/rfc/rfc8879 [RFC 8999]: https://www.rfc-editor.org/rfc/rfc8999 [RFC 9000]: https://www.rfc-editor.org/rfc/rfc9000 +[RFC 9000 Section 8.1]: https://www.rfc-editor.org/rfc/rfc9000#section-8.1 [RFC 9001]: https://www.rfc-editor.org/rfc/rfc9001 [RFC 9002]: https://www.rfc-editor.org/rfc/rfc9002 [RFC 9114]: https://www.rfc-editor.org/rfc/rfc9114 @@ -3894,11 +4391,25 @@ throughput issues caused by flow control. [`application.enableConnectProtocol`]: #sessionoptionsapplication [`application.enableDatagrams`]: #sessionoptionsapplication [`application.qpackMaxDTableCapacity`]: #sessionoptionsapplication +[`endpoint.busy`]: #endpointbusy [`endpoint.maxConnectionsPerHost`]: #endpointmaxconnectionsperhost [`endpoint.maxConnectionsTotal`]: #endpointmaxconnectionstotal +[`endpointOptions.blockListPolicy`]: #endpointoptionsblocklistpolicy +[`endpointOptions.blockList`]: #endpointoptionsblocklist +[`endpointOptions.immediateCloseBurst`]: #endpointoptionsimmediatecloseburst +[`endpointOptions.immediateCloseRate`]: #endpointoptionsimmediatecloserate +[`endpointOptions.retryBurst`]: #endpointoptionsretryburst +[`endpointOptions.retryRate`]: #endpointoptionsretryrate +[`endpointOptions.sessionCreationBurst`]: #endpointoptionssessioncreationburst +[`endpointOptions.sessionCreationRate`]: #endpointoptionssessioncreationrate +[`endpointOptions.statelessResetBurst`]: #endpointoptionsstatelessresetburst +[`endpointOptions.statelessResetRate`]: #endpointoptionsstatelessresetrate +[`endpointOptions.versionNegotiationBurst`]: #endpointoptionsversionnegotiationburst +[`endpointOptions.versionNegotiationRate`]: #endpointoptionsversionnegotiationrate [`error.errorCode`]: #errorerrorcode [`fs.promises.open(path, 'r')`]: fs.md#fspromisesopenpath-flags-mode [`maxDatagramFrameSize`]: #transportparamsmaxdatagramframesize +[`net.BlockList`]: net.md#class-netblocklist [`quic.connect()`]: #quicconnectaddress-options [`quic.listen()`]: #quiclistenonsession-options [`session.close()`]: #sessioncloseoptions diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 98c9c00442dfc8..483212a8b4edb2 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -812,8 +812,12 @@ added: * `changeset` {Uint8Array} A binary changeset or patchset. * `options` {Object} The configuration options for how the changes will be applied. - * `filter` {Function} Skip changes that, when targeted table name is supplied to this function, return a truthy value. - By default, all changes are attempted. + * `filter` {Function} for each table affected by at least + one change in the changeset, the `filter` callback is invoked with the + table name as the first argument. If the return value is falsy, then no + attempt is made to apply any changes to the table. + Otherwise, if the return value is truthy or no `filter` callback is provided, + all changes related to the table are attempted. * `onConflict` {Function} A function that determines how to handle conflicts. The function receives one argument, which can be one of the following values: diff --git a/doc/api/test.md b/doc/api/test.md index b3fbc06ba83f2e..0a3e271d54b0d4 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3435,6 +3435,9 @@ added: - v18.9.0 - v16.19.0 changes: + - version: v26.3.0 + pr-url: https://github.com/nodejs/node/pull/63435 + description: Added `parentId` to test events that carry a `testId`. - version: - v20.0.0 - v19.9.0 @@ -3522,6 +3525,9 @@ Emitted when code coverage is enabled and all tests have completed. `undefined` if the test was run through the REPL. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. + * `parentId` {number|undefined} The `testId` of the enclosing test, or + `undefined` for top-level tests. Lets custom reporters track lineage + when concurrent siblings at the same nesting level interleave. * `tags` {string\[]} The flattened lowercased tags declared on the test and its ancestor suites, in declaration order. Empty for untagged tests. See [Test tags][]. @@ -3548,6 +3554,9 @@ The corresponding declaration ordered events are `'test:pass'` and `'test:fail'` `undefined` if the test was run through the REPL. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. + * `parentId` {number|undefined} The `testId` of the enclosing test, or + `undefined` for top-level tests. Lets custom reporters track lineage + when concurrent siblings at the same nesting level interleave. * `tags` {string\[]} The flattened lowercased tags declared on the test and its ancestor suites, in declaration order. Empty for untagged tests. See [Test tags][]. @@ -3592,6 +3601,9 @@ defined. `undefined` if the test was run through the REPL. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. + * `parentId` {number|undefined} The `testId` of the enclosing test, or + `undefined` for top-level tests. Lets custom reporters track lineage + when concurrent siblings at the same nesting level interleave. * `tags` {string\[]} The flattened lowercased tags declared on the test and its ancestor suites, in declaration order. Empty for untagged tests. See [Test tags][]. @@ -3621,6 +3633,9 @@ Emitted when a test is enqueued for execution. `undefined` if the test was run through the REPL. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. + * `parentId` {number|undefined} The `testId` of the enclosing test, or + `undefined` for top-level tests. Lets custom reporters track lineage + when concurrent siblings at the same nesting level interleave. * `tags` {string\[]} The flattened lowercased tags declared on the test and its ancestor suites, in declaration order. Empty for untagged tests. See [Test tags][]. @@ -3681,6 +3696,9 @@ since the parent runner only knows about file-level tests. When using `undefined` if the test was run through the REPL. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. + * `parentId` {number|undefined} The `testId` of the enclosing test, or + `undefined` for top-level tests. Lets custom reporters track lineage + when concurrent siblings at the same nesting level interleave. * `tags` {string\[]} The flattened lowercased tags declared on the test and its ancestor suites, in declaration order. Empty for untagged tests. See [Test tags][]. @@ -3723,6 +3741,9 @@ defined. `undefined` if the test was run through the REPL. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. + * `parentId` {number|undefined} The `testId` of the enclosing test, or + `undefined` for top-level tests. Lets custom reporters track lineage + when concurrent siblings at the same nesting level interleave. * `tags` {string\[]} The flattened lowercased tags declared on the test and its ancestor suites, in declaration order. Empty for untagged tests. See [Test tags][]. diff --git a/doc/api/tls.md b/doc/api/tls.md index 5d11e2c53eada0..a1a8c98d987784 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -2199,7 +2199,7 @@ changes: Creates a new [`tls.Server`][]. The `secureConnectionListener`, if provided, is automatically set as a listener for the [`'secureConnection'`][] event. -The `ticketKeys` options is automatically shared between `node:cluster` module +The `ticketKeys` option is automatically shared between `node:cluster` module workers. The following illustrates a simple echo server: diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index f225d62a54f0fb..c2bdaeac9c37c1 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -849,7 +849,7 @@ The algorithms currently supported include: * `'ML-KEM-768'`[^modern-algos] * `'ML-KEM-1024'`[^modern-algos] -### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` +### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, keyUsages)` -* `unwrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} -* `unwrappedKeyAlgo` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams|KmacImportParams} +* `unwrapAlgorithm` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} +* `unwrappedKeyAlgorithm` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams|KmacImportParams} @@ -1448,8 +1448,8 @@ In cryptography, "wrapping a key" refers to exporting and then encrypting the keying material. This method attempts to decrypt a wrapped key and create a {CryptoKey} instance. It is equivalent to calling [`subtle.decrypt()`][] first on the encrypted key data (using the `wrappedKey`, -`unwrapAlgo`, and `unwrappingKey` arguments as input) then passing the results -to the [`subtle.importKey()`][] method using the `unwrappedKeyAlgo`, +`unwrapAlgorithm`, and `unwrappingKey` arguments as input) then passing the results +to the [`subtle.importKey()`][] method using the `unwrappedKeyAlgorithm`, `extractable`, and `keyUsages` arguments as inputs. If successful, the returned promise is resolved with a {CryptoKey} object. @@ -1537,7 +1537,7 @@ The algorithms currently supported include: * `'RSA-PSS'` * `'RSASSA-PKCS1-v1_5'` -### `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` +### `subtle.wrapKey(format, key, wrappingKey, wrapAlgorithm)` @@ -1564,10 +1564,10 @@ changes: In cryptography, "wrapping a key" refers to exporting and then encrypting the keying material. This method exports the keying material into the format identified by `format`, then encrypts it using the method and -parameters specified by `wrapAlgo` and the keying material provided by +parameters specified by `wrapAlgorithm` and the keying material provided by `wrappingKey`. It is the equivalent to calling [`subtle.exportKey()`][] using `format` and `key` as the arguments, then passing the result to the -[`subtle.encrypt()`][] method using `wrappingKey` and `wrapAlgo` as inputs. If +[`subtle.encrypt()`][] method using `wrappingKey` and `wrapAlgorithm` as inputs. If successful, the returned promise will be resolved with an {ArrayBuffer} containing the encrypted key data. @@ -2795,19 +2795,19 @@ added: v25.9.0 [Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/ [`SubtleCrypto.supports()`]: #static-method-subtlecryptosupportsoperation-algorithm-lengthoradditionalalgorithm [`subtle.decapsulateBits()`]: #subtledecapsulatebitsdecapsulationalgorithm-decapsulationkey-ciphertext -[`subtle.decapsulateKey()`]: #subtledecapsulatekeydecapsulationalgorithm-decapsulationkey-ciphertext-sharedkeyalgorithm-extractable-usages +[`subtle.decapsulateKey()`]: #subtledecapsulatekeydecapsulationalgorithm-decapsulationkey-ciphertext-sharedkeyalgorithm-extractable-keyusages [`subtle.decrypt()`]: #subtledecryptalgorithm-key-data [`subtle.deriveBits()`]: #subtlederivebitsalgorithm-basekey-length -[`subtle.deriveKey()`]: #subtlederivekeyalgorithm-basekey-derivedkeyalgorithm-extractable-keyusages +[`subtle.deriveKey()`]: #subtlederivekeyalgorithm-basekey-derivedkeytype-extractable-keyusages [`subtle.digest()`]: #subtledigestalgorithm-data [`subtle.encapsulateBits()`]: #subtleencapsulatebitsencapsulationalgorithm-encapsulationkey -[`subtle.encapsulateKey()`]: #subtleencapsulatekeyencapsulationalgorithm-encapsulationkey-sharedkeyalgorithm-extractable-usages +[`subtle.encapsulateKey()`]: #subtleencapsulatekeyencapsulationalgorithm-encapsulationkey-sharedkeyalgorithm-extractable-keyusages [`subtle.encrypt()`]: #subtleencryptalgorithm-key-data [`subtle.exportKey()`]: #subtleexportkeyformat-key [`subtle.generateKey()`]: #subtlegeneratekeyalgorithm-extractable-keyusages [`subtle.getPublicKey()`]: #subtlegetpublickeykey-keyusages [`subtle.importKey()`]: #subtleimportkeyformat-keydata-algorithm-extractable-keyusages [`subtle.sign()`]: #subtlesignalgorithm-key-data -[`subtle.unwrapKey()`]: #subtleunwrapkeyformat-wrappedkey-unwrappingkey-unwrapalgo-unwrappedkeyalgo-extractable-keyusages +[`subtle.unwrapKey()`]: #subtleunwrapkeyformat-wrappedkey-unwrappingkey-unwrapalgorithm-unwrappedkeyalgorithm-extractable-keyusages [`subtle.verify()`]: #subtleverifyalgorithm-key-signature-data -[`subtle.wrapKey()`]: #subtlewrapkeyformat-key-wrappingkey-wrapalgo +[`subtle.wrapKey()`]: #subtlewrapkeyformat-key-wrappingkey-wrapalgorithm diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 8f4b152bc26a68..720c5640d2065a 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -1369,17 +1369,14 @@ port2.postMessage(new Foo()); // Prints: { c: 3 } ``` -This limitation extends to many built-in objects, such as the global `URL` -object: +Some built-in objects cannot be cloned at all. For example, posting a +`URL` object throws a `DataCloneError`: ```js const { port1, port2 } = new MessageChannel(); -port1.onmessage = ({ data }) => console.log(data); - port2.postMessage(new URL('https://example.org')); - -// Prints: { } +// Throws DataCloneError: Cannot clone object of unsupported type. ``` ### `port.hasRef()` diff --git a/doc/changelogs/CHANGELOG_V26.md b/doc/changelogs/CHANGELOG_V26.md index 56812f4d072e2d..7bf48443bb33b0 100644 --- a/doc/changelogs/CHANGELOG_V26.md +++ b/doc/changelogs/CHANGELOG_V26.md @@ -8,6 +8,7 @@
                                            CircleCI context UUID to match
                                            --allow-publishfalseBooleanAllow npm publish for this trusted publisher configuration
                                            --allow-stage-publish, --allow-staged-publishfalseBooleanAllow npm stage publish for this trusted publisher configuration
                                            --dry-run false Boolean
                                            +26.3.0
                                            26.2.0
                                            26.1.0
                                            26.0.0
                                            @@ -43,6 +44,176 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2026-06-01, Version 26.3.0 (Current), @aduh95 + +### Notable Changes + +#### Potential changes to macOS Universal Binary availability + +With Apple and its ecosystem progressively dropping support for Intel-based +architectures, it has become apparent that the Node.js project may not be able +to maintain the universal binaries we currently distribute for the full lifetime +of Node.js 26. This change serves to communicate that risk. At present, our +intention remains to continue shipping universal binaries supporting both Apple +Silicon and Intel-based Macs for as long as practical. + +Contributed by Antoine du Hamel in [#63055](https://github.com/nodejs/node/pull/63055). + +#### Other notable changes + +* \[[`a2a4b33dd8`](https://github.com/nodejs/node/commit/a2a4b33dd8)] - **(SEMVER-MINOR)** **buffer**: increase `Buffer.poolSize` default to 64 KiB (Matteo Collina) [#63597](https://github.com/nodejs/node/pull/63597) +* \[[`051a2152f7`](https://github.com/nodejs/node/commit/051a2152f7)] - **crypto**: update root certificates to NSS 3.123.1 (Node.js GitHub Bot) [#63527](https://github.com/nodejs/node/pull/63527) +* \[[`49462eca37`](https://github.com/nodejs/node/commit/49462eca37)] - **(SEMVER-MINOR)** **http**: add `httpValidation` option to configure header value validation (RajeshKumar11) [#61597](https://github.com/nodejs/node/pull/61597) +* \[[`97b7ab19bd`](https://github.com/nodejs/node/commit/97b7ab19bd)] - **(SEMVER-MINOR)** **inspector**: expose precise coverage start to JS runtime (sangwook) [#63079](https://github.com/nodejs/node/pull/63079) +* \[[`cfb80a2103`](https://github.com/nodejs/node/commit/cfb80a2103)] - **(SEMVER-MINOR)** **lib,permission**: add `permission.drop` (Rafael Gonzaga) [#62672](https://github.com/nodejs/node/pull/62672) + +### Commits + +* \[[`a2a4b33dd8`](https://github.com/nodejs/node/commit/a2a4b33dd8)] - **(SEMVER-MINOR)** **buffer**: increase Buffer.poolSize default to 64 KiB (Matteo Collina) [#63597](https://github.com/nodejs/node/pull/63597) +* \[[`0eff3e23b9`](https://github.com/nodejs/node/commit/0eff3e23b9)] - **build**: def `NODE_USE_NODE_CODE_CACHE` only used in node\_mksnapshot (Chengzhong Wu) [#63588](https://github.com/nodejs/node/pull/63588) +* \[[`447ab2d252`](https://github.com/nodejs/node/commit/447ab2d252)] - **build,win**: fix VS2022 arm64 PGO build (Stefan Stojanovic) [#63413](https://github.com/nodejs/node/pull/63413) +* \[[`86032758e4`](https://github.com/nodejs/node/commit/86032758e4)] - **build,win**: replace LTCG with Thin LTO for releases (Stefan Stojanovic) [#63114](https://github.com/nodejs/node/pull/63114) +* \[[`5f4d794052`](https://github.com/nodejs/node/commit/5f4d794052)] - **build,win**: add Rust toolchain automated configuration Windows (Mike McCready) [#63381](https://github.com/nodejs/node/pull/63381) +* \[[`051a2152f7`](https://github.com/nodejs/node/commit/051a2152f7)] - **crypto**: update root certificates to NSS 3.123.1 (Node.js GitHub Bot) [#63527](https://github.com/nodejs/node/pull/63527) +* \[[`d0f65e3579`](https://github.com/nodejs/node/commit/d0f65e3579)] - **crypto**: coerce -0 keylen to +0 in pbkdf2 and scrypt (Jordan Harband) [#63531](https://github.com/nodejs/node/pull/63531) +* \[[`e3ddb326c9`](https://github.com/nodejs/node/commit/e3ddb326c9)] - **crypto**: harden WebCrypto against prototype pollution (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`e04cd17dc0`](https://github.com/nodejs/node/commit/e04cd17dc0)] - **crypto**: pass CryptoKey handles to KDF jobs (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`64ba74d847`](https://github.com/nodejs/node/commit/64ba74d847)] - **crypto**: remove async from WebCrypto methods (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`bd230418b4`](https://github.com/nodejs/node/commit/bd230418b4)] - **crypto**: add WebCrypto CryptoJob mode (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`1a4090a83d`](https://github.com/nodejs/node/commit/1a4090a83d)] - **debugger**: surface inspector failures in probe mode (Joyee Cheung) [#63437](https://github.com/nodejs/node/pull/63437) +* \[[`dbc78ff825`](https://github.com/nodejs/node/commit/dbc78ff825)] - **debugger,test**: deflake resume failure test and add debug logs (Joyee Cheung) [#63524](https://github.com/nodejs/node/pull/63524) +* \[[`4da442f432`](https://github.com/nodejs/node/commit/4da442f432)] - **deps**: upgrade npm to 11.16.0 (npm team) [#63602](https://github.com/nodejs/node/pull/63602) +* \[[`63372cfa87`](https://github.com/nodejs/node/commit/63372cfa87)] - **deps**: SQLite: cherry-pick b869ed6b067d623cb1383549f2a18aa35508385d (Junsu Han) [#63525](https://github.com/nodejs/node/pull/63525) +* \[[`e286fa170d`](https://github.com/nodejs/node/commit/e286fa170d)] - **deps**: upgrade npm to 11.15.0 (npm team) [#63463](https://github.com/nodejs/node/pull/63463) +* \[[`de996437a5`](https://github.com/nodejs/node/commit/de996437a5)] - **doc**: downgrade macOS x64 to Tier 2 (Antoine du Hamel) [#63055](https://github.com/nodejs/node/pull/63055) +* \[[`22ac78750c`](https://github.com/nodejs/node/commit/22ac78750c)] - **doc**: remove duplicated sentences in large-pull-requests.md (Joyee Cheung) [#63650](https://github.com/nodejs/node/pull/63650) +* \[[`532f7f2085`](https://github.com/nodejs/node/commit/532f7f2085)] - **doc**: update `git node land` instructions for security releases (Antoine du Hamel) [#63586](https://github.com/nodejs/node/pull/63586) +* \[[`c61f90dfb9`](https://github.com/nodejs/node/commit/c61f90dfb9)] - **doc**: drop --experimental from --permission (Rafael Gonzaga) [#63583](https://github.com/nodejs/node/pull/63583) +* \[[`fd69d7b16a`](https://github.com/nodejs/node/commit/fd69d7b16a)] - **doc**: improve `fs.StatFs` properties descriptions (aymanxdev) [#62578](https://github.com/nodejs/node/pull/62578) +* \[[`693257782c`](https://github.com/nodejs/node/commit/693257782c)] - **doc**: generate llms.txt (Guilherme Araújo) [#62027](https://github.com/nodejs/node/pull/62027) +* \[[`55a57beb26`](https://github.com/nodejs/node/commit/55a57beb26)] - **doc**: explicitly ask for reproducible in JS (Rafael Gonzaga) [#63479](https://github.com/nodejs/node/pull/63479) +* \[[`4895c2babc`](https://github.com/nodejs/node/commit/4895c2babc)] - **doc**: fix URL postMessage example in worker\_threads (Kit Dallege) [#62203](https://github.com/nodejs/node/pull/62203) +* \[[`0355c36e37`](https://github.com/nodejs/node/commit/0355c36e37)] - **doc**: clarify `filter` option of `sqlite.database.applyChangeset` (Antoine du Hamel) [#63515](https://github.com/nodejs/node/pull/63515) +* \[[`c85ee22df6`](https://github.com/nodejs/node/commit/c85ee22df6)] - **doc**: fix double spaces in ERR\_TLS\_INVALID\_PROTOCOL\_METHOD (Daijiro Wachi) [#63511](https://github.com/nodejs/node/pull/63511) +* \[[`62947192f6`](https://github.com/nodejs/node/commit/62947192f6)] - **doc**: move hyperlinks outside of text blocks (Aviv Keller) [#63493](https://github.com/nodejs/node/pull/63493) +* \[[`9849690a1d`](https://github.com/nodejs/node/commit/9849690a1d)] - **doc**: edit Rust toolchain general install instructions (Antoine du Hamel) [#63488](https://github.com/nodejs/node/pull/63488) +* \[[`885d2462e9`](https://github.com/nodejs/node/commit/885d2462e9)] - **doc**: fix double space in modules.md (Daijiro Wachi) [#63512](https://github.com/nodejs/node/pull/63512) +* \[[`42fbb48bc6`](https://github.com/nodejs/node/commit/42fbb48bc6)] - **doc**: fix "options" to "option" in tls.createServer (Daijiro Wachi) [#63453](https://github.com/nodejs/node/pull/63453) +* \[[`05a7b0a301`](https://github.com/nodejs/node/commit/05a7b0a301)] - **doc**: add Rust toolchain general install instructions (Mike McCready) [#63426](https://github.com/nodejs/node/pull/63426) +* \[[`e13dfd7ed0`](https://github.com/nodejs/node/commit/e13dfd7ed0)] - **doc**: update toolchain for official releases (Richard Lau) [#63441](https://github.com/nodejs/node/pull/63441) +* \[[`82306881cc`](https://github.com/nodejs/node/commit/82306881cc)] - **doc**: fix typo in deprecations (Daijiro Wachi) [#63434](https://github.com/nodejs/node/pull/63434) +* \[[`eeb77d217c`](https://github.com/nodejs/node/commit/eeb77d217c)] - **doc,lib**: align WebCrypto names with spec (Filip Skokan) [#63518](https://github.com/nodejs/node/pull/63518) +* \[[`679e13c57f`](https://github.com/nodejs/node/commit/679e13c57f)] - **errors**: handle V8 warnings in DisallowJavascriptExecutionScope (Divyanshu Sharma) [#63491](https://github.com/nodejs/node/pull/63491) +* \[[`7f41f5d803`](https://github.com/nodejs/node/commit/7f41f5d803)] - **ffi**: validate 'void' as parameter type in getFunction and getFunctions (Anshika Jain) [#63504](https://github.com/nodejs/node/pull/63504) +* \[[`972cd227cb`](https://github.com/nodejs/node/commit/972cd227cb)] - **ffi**: remove function signature property aliases (René) [#63482](https://github.com/nodejs/node/pull/63482) +* \[[`5d7805e433`](https://github.com/nodejs/node/commit/5d7805e433)] - **ffi**: move DynamicLibrary disposer to native layer (René) [#63459](https://github.com/nodejs/node/pull/63459) +* \[[`5a0b32dc24`](https://github.com/nodejs/node/commit/5a0b32dc24)] - **gyp**: update deps gypfiles (Nad Alaba) [#63117](https://github.com/nodejs/node/pull/63117) +* \[[`49462eca37`](https://github.com/nodejs/node/commit/49462eca37)] - **(SEMVER-MINOR)** **http**: add httpValidation option to configure header value validation (RajeshKumar11) [#61597](https://github.com/nodejs/node/pull/61597) +* \[[`e3c6629ee3`](https://github.com/nodejs/node/commit/e3c6629ee3)] - **http2**: emit session close before stream close (Matteo Collina) [#63414](https://github.com/nodejs/node/pull/63414) +* \[[`97b7ab19bd`](https://github.com/nodejs/node/commit/97b7ab19bd)] - **(SEMVER-MINOR)** **inspector**: expose precise coverage start to JS runtime (sangwook) [#63079](https://github.com/nodejs/node/pull/63079) +* \[[`6bef10e7b7`](https://github.com/nodejs/node/commit/6bef10e7b7)] - **lib**: cleanup stateless diffiehellman key handling (Filip Skokan) [#62645](https://github.com/nodejs/node/pull/62645) +* \[[`fdc0b3d49c`](https://github.com/nodejs/node/commit/fdc0b3d49c)] - **lib**: define `kEnumerableProperty` atomically (Antoine du Hamel) [#63609](https://github.com/nodejs/node/pull/63609) +* \[[`99baf27aeb`](https://github.com/nodejs/node/commit/99baf27aeb)] - **lib**: fix typos in esm loader comments (RonGamzu) [#63465](https://github.com/nodejs/node/pull/63465) +* \[[`cfb80a2103`](https://github.com/nodejs/node/commit/cfb80a2103)] - **(SEMVER-MINOR)** **lib,permission**: add permission.drop (Rafael Gonzaga) [#62672](https://github.com/nodejs/node/pull/62672) +* \[[`8e75efb9bc`](https://github.com/nodejs/node/commit/8e75efb9bc)] - **meta**: flip mcollina emails in .mailmap (Matteo Collina) [#63621](https://github.com/nodejs/node/pull/63621) +* \[[`a4ae97045f`](https://github.com/nodejs/node/commit/a4ae97045f)] - **meta**: label "source maps" PRs (Chengzhong Wu) [#63591](https://github.com/nodejs/node/pull/63591) +* \[[`3455a48ae1`](https://github.com/nodejs/node/commit/3455a48ae1)] - **meta**: add `vfs` subsystem label (René) [#62331](https://github.com/nodejs/node/pull/62331) +* \[[`01bfcdfc20`](https://github.com/nodejs/node/commit/01bfcdfc20)] - **meta**: skip scheduled workflows on forks (Jamie Magee) [#63565](https://github.com/nodejs/node/pull/63565) +* \[[`bc4c457eae`](https://github.com/nodejs/node/commit/bc4c457eae)] - **meta**: add additional gitignore entries (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`e1d65d9509`](https://github.com/nodejs/node/commit/e1d65d9509)] - **module**: load ESM helpers eagerly in the snapshot (Joyee Cheung) [#63550](https://github.com/nodejs/node/pull/63550) +* \[[`6a97b0932a`](https://github.com/nodejs/node/commit/6a97b0932a)] - **quic**: add proper error codes & messages for QUIC failures (Tim Perry) [#63198](https://github.com/nodejs/node/pull/63198) +* \[[`5989f4a6e1`](https://github.com/nodejs/node/commit/5989f4a6e1)] - **quic**: support hostname verification (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`b4d30e7a78`](https://github.com/nodejs/node/commit/b4d30e7a78)] - **quic**: add stream idle timeout (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`8a1017f774`](https://github.com/nodejs/node/commit/8a1017f774)] - **quic**: add block list support for endpoints (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`5a3ab93c49`](https://github.com/nodejs/node/commit/5a3ab93c49)] - **quic**: improve peer cert verification (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`9701a82a78`](https://github.com/nodejs/node/commit/9701a82a78)] - **quic**: handle h3 max header size option (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`71788a2048`](https://github.com/nodejs/node/commit/71788a2048)] - **quic**: add rate limiting docs (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`309bd49906`](https://github.com/nodejs/node/commit/309bd49906)] - **quic**: cache timestamp for address lru cache (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`2ce5588d51`](https://github.com/nodejs/node/commit/2ce5588d51)] - **quic**: add session creation rate limiting (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`98808baed1`](https://github.com/nodejs/node/commit/98808baed1)] - **quic**: refine rate limiting (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`75a4176b32`](https://github.com/nodejs/node/commit/75a4176b32)] - **quic**: flip preferred address policy default to 'ignore' (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`8b6b03d60c`](https://github.com/nodejs/node/commit/8b6b03d60c)] - **quic**: add doc note about certificate size limitations (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`30eff873e0`](https://github.com/nodejs/node/commit/30eff873e0)] - **quic**: add applicationOptions to session (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`4303daa43c`](https://github.com/nodejs/node/commit/4303daa43c)] - **quic**: add getters for local and remote transport parameters (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`e1b1bb5465`](https://github.com/nodejs/node/commit/e1b1bb5465)] - **quic**: improve recv coalescing test sizes (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`25a416f457`](https://github.com/nodejs/node/commit/25a416f457)] - **quic**: add initial RTT option to session options (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`22e91c357f`](https://github.com/nodejs/node/commit/22e91c357f)] - **quic**: enable recvmmsg batching in Endpoint (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`c96d8a9d9b`](https://github.com/nodejs/node/commit/c96d8a9d9b)] - **quic**: improve stream header collection performance (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`409460f2ce`](https://github.com/nodejs/node/commit/409460f2ce)] - **quic**: add reusePort option to QuicEndpoint (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`9a2afffec9`](https://github.com/nodejs/node/commit/9a2afffec9)] - **quic**: coalesce received data into fewer buffers (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`f9a6a2f558`](https://github.com/nodejs/node/commit/f9a6a2f558)] - **quic**: apply multiple additional minor improvements (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`ea5f3724ee`](https://github.com/nodejs/node/commit/ea5f3724ee)] - **quic**: fix tests that are missing serverEndpoint close (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`6cffc931fc`](https://github.com/nodejs/node/commit/6cffc931fc)] - **quic**: fixup some v8:: qualifiers (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`9bc875e522`](https://github.com/nodejs/node/commit/9bc875e522)] - **quic**: fix premature unref of endpoint when listening (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`f940d6b1be`](https://github.com/nodejs/node/commit/f940d6b1be)] - **quic**: fixup UAFs in bindingdata, streams, and app (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`fd00e0acb0`](https://github.com/nodejs/node/commit/fd00e0acb0)] - **quic**: fix UAF in Application::OnTimeout() (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`378dbf00e9`](https://github.com/nodejs/node/commit/378dbf00e9)] - **quic**: improve the quic js structure (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`0045dc30b6`](https://github.com/nodejs/node/commit/0045dc30b6)] - **quic**: improve internal structure of QuicStream (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`5e38946b26`](https://github.com/nodejs/node/commit/5e38946b26)] - **quic**: add aliased struct arenas (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`95430437a0`](https://github.com/nodejs/node/commit/95430437a0)] - **quic**: add handshake timeout and default connection limits (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`5622701429`](https://github.com/nodejs/node/commit/5622701429)] - **quic**: implement rate limiting for version nego and immediate close (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`b171f391cd`](https://github.com/nodejs/node/commit/b171f391cd)] - **quic**: fixup linting issue after other changes (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`24e9f4f177`](https://github.com/nodejs/node/commit/24e9f4f177)] - **quic**: fix crash in early handshake failure (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`5025e85d0a`](https://github.com/nodejs/node/commit/5025e85d0a)] - **quic**: eliminate per-received datagram allocation (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`aec1e17ec5`](https://github.com/nodejs/node/commit/aec1e17ec5)] - **quic**: cache the timestamp on send and receive (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`9560084560`](https://github.com/nodejs/node/commit/9560084560)] - **quic**: add support for future ECN marking (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`2b3ff8ada2`](https://github.com/nodejs/node/commit/2b3ff8ada2)] - **quic**: improve batching of packet sending (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`fe3639a4d6`](https://github.com/nodejs/node/commit/fe3639a4d6)] - **quic**: improve backend quic packet processing (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`f043013d9a`](https://github.com/nodejs/node/commit/f043013d9a)] - **src**: remove TOCTOU race condition when encoding SAB-backed `Buffer`s (Antoine du Hamel) [#63517](https://github.com/nodejs/node/pull/63517) +* \[[`343958224d`](https://github.com/nodejs/node/commit/343958224d)] - **src**: skip duplicate UTF-8 validation in TextDecoder fatal path (Mert Can Altin) [#63231](https://github.com/nodejs/node/pull/63231) +* \[[`2906fa833d`](https://github.com/nodejs/node/commit/2906fa833d)] - **src**: dispatch ToV8Value(string\_view) via StringBytes::Encode (Mert Can Altin) [#63370](https://github.com/nodejs/node/pull/63370) +* \[[`860f9d8d4b`](https://github.com/nodejs/node/commit/860f9d8d4b)] - **src**: fix ContextifyContext property definer interception result (Chengzhong Wu) [#63549](https://github.com/nodejs/node/pull/63549) +* \[[`fcccffcbe6`](https://github.com/nodejs/node/commit/fcccffcbe6)] - **src**: fix crash when reading length on Storage.prototype (Mohamed Sayed) [#63529](https://github.com/nodejs/node/pull/63529) +* \[[`55f65f9fb6`](https://github.com/nodejs/node/commit/55f65f9fb6)] - **src**: improve token return value check (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`7a36ca46cd`](https://github.com/nodejs/node/commit/7a36ca46cd)] - **src**: expose `node::RegisterContext` to make a node managed context (Chengzhong Wu) [#62322](https://github.com/nodejs/node/pull/62322) +* \[[`9bda92963c`](https://github.com/nodejs/node/commit/9bda92963c)] - **src,sqlite**: only pass `xFilter` when user provided a callback (Antoine du Hamel) [#63516](https://github.com/nodejs/node/pull/63516) +* \[[`563db50f38`](https://github.com/nodejs/node/commit/563db50f38)] - **stream**: switch to internal `sleep` binding (Antoine du Hamel) [#63611](https://github.com/nodejs/node/pull/63611) +* \[[`a6e2322ee6`](https://github.com/nodejs/node/commit/a6e2322ee6)] - **stream**: use data listener for compose forwarding (Trivikram Kamat) [#63593](https://github.com/nodejs/node/pull/63593) +* \[[`7198895c6b`](https://github.com/nodejs/node/commit/7198895c6b)] - **stream**: serialize concurrent share consumer reads (Trivikram Kamat) [#63478](https://github.com/nodejs/node/pull/63478) +* \[[`70ba8be1d7`](https://github.com/nodejs/node/commit/70ba8be1d7)] - **stream**: fix lint error (Antoine du Hamel) [#63598](https://github.com/nodejs/node/pull/63598) +* \[[`1608d905a7`](https://github.com/nodejs/node/commit/1608d905a7)] - **stream**: reject pending reads on iterator throw (Trivikram Kamat) [#63555](https://github.com/nodejs/node/pull/63555) +* \[[`dc12b730d8`](https://github.com/nodejs/node/commit/dc12b730d8)] - **stream**: wait for push writer end fallback to drain (Trivikram Kamat) [#63503](https://github.com/nodejs/node/pull/63503) +* \[[`4f40a85a1a`](https://github.com/nodejs/node/commit/4f40a85a1a)] - **stream**: flush each fused stateless transform (Trivikram Kamat) [#63468](https://github.com/nodejs/node/pull/63468) +* \[[`526e0fc427`](https://github.com/nodejs/node/commit/526e0fc427)] - **stream**: avoid duplicate writes in toWritable (Trivikram Kamat) [#63360](https://github.com/nodejs/node/pull/63360) +* \[[`0008d01f9c`](https://github.com/nodejs/node/commit/0008d01f9c)] - **stream**: propagate abort reason in share and broadcast (Trivikram Kamat) [#63358](https://github.com/nodejs/node/pull/63358) +* \[[`217338e18b`](https://github.com/nodejs/node/commit/217338e18b)] - **stream**: fix Writable.toWeb() hang on synchronous drain (sangwook) [#61197](https://github.com/nodejs/node/pull/61197) +* \[[`381f4b1b10`](https://github.com/nodejs/node/commit/381f4b1b10)] - **stream**: disallow writing string chunk with 'buffer' encoding (René) [#63062](https://github.com/nodejs/node/pull/63062) +* \[[`cbee0de1cb`](https://github.com/nodejs/node/commit/cbee0de1cb)] - **stream**: align `Readable.toWeb` termination with eos (ikeyan) [#62394](https://github.com/nodejs/node/pull/62394) +* \[[`be91f0a927`](https://github.com/nodejs/node/commit/be91f0a927)] - **test**: shorten path in net pipe connect errors (Matteo Collina) [#63405](https://github.com/nodejs/node/pull/63405) +* \[[`83cada8bcc`](https://github.com/nodejs/node/commit/83cada8bcc)] - **test**: deflake test-debugger-probe-timeout (Joyee Cheung) [#63547](https://github.com/nodejs/node/pull/63547) +* \[[`3560b96a10`](https://github.com/nodejs/node/commit/3560b96a10)] - **test**: deflake test-webcrypto-crypto-job-mode (Filip Skokan) [#63543](https://github.com/nodejs/node/pull/63543) +* \[[`0c9c52373a`](https://github.com/nodejs/node/commit/0c9c52373a)] - **test**: remove test-node-output-v8-warning (Joyee Cheung) [#63469](https://github.com/nodejs/node/pull/63469) +* \[[`12052dbe14`](https://github.com/nodejs/node/commit/12052dbe14)] - **test**: cover webcrypto prototype pollution systematically (Filip Skokan) [#63520](https://github.com/nodejs/node/pull/63520) +* \[[`8c479f274a`](https://github.com/nodejs/node/commit/8c479f274a)] - **test**: update test426-fixtures to 9b9e225b5a63139e9a95cdd1bf874a8f0b9d131 (Node.js GitHub Bot) [#63373](https://github.com/nodejs/node/pull/63373) +* \[[`2ca32a5ee8`](https://github.com/nodejs/node/commit/2ca32a5ee8)] - **test**: update WPT for url to e4a4672e9e (Node.js GitHub Bot) [#63372](https://github.com/nodejs/node/pull/63372) +* \[[`1bf875bd21`](https://github.com/nodejs/node/commit/1bf875bd21)] - **test**: deflake async-hooks statwatcher test (Trivikram Kamat) [#63396](https://github.com/nodejs/node/pull/63396) +* \[[`97dbfa09f7`](https://github.com/nodejs/node/commit/97dbfa09f7)] - **test**: avoid test\_runner watch restart in spec snapshot (Trivikram Kamat) [#63392](https://github.com/nodejs/node/pull/63392) +* \[[`8b038d7b33`](https://github.com/nodejs/node/commit/8b038d7b33)] - **test**: reduce watch mode restart flakiness (Trivikram Kamat) [#63390](https://github.com/nodejs/node/pull/63390) +* \[[`f504c01d66`](https://github.com/nodejs/node/commit/f504c01d66)] - **test**: get rid of unnecessary `AbortController` instanciations (Antoine du Hamel) [#63489](https://github.com/nodejs/node/pull/63489) +* \[[`170585ff90`](https://github.com/nodejs/node/commit/170585ff90)] - **test**: isolate rerun-failures state file under tmpdir (Chemi Atlow) [#63449](https://github.com/nodejs/node/pull/63449) +* \[[`935468a49e`](https://github.com/nodejs/node/commit/935468a49e)] - **test**: fixup quic tests (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`fbbdfdcfc7`](https://github.com/nodejs/node/commit/fbbdfdcfc7)] - **test**: wait for ok before initial break after restart (Yuya Inoue) [#62807](https://github.com/nodejs/node/pull/62807) +* \[[`db808ad77d`](https://github.com/nodejs/node/commit/db808ad77d)] - **test**: unskip snapshot reproducibility test (Joyee Cheung) [#63307](https://github.com/nodejs/node/pull/63307) +* \[[`259d8b3dce`](https://github.com/nodejs/node/commit/259d8b3dce)] - **test**: update WPT for WebCryptoAPI to 97bbc7247a (Node.js GitHub Bot) [#63417](https://github.com/nodejs/node/pull/63417) +* \[[`d56c6cd708`](https://github.com/nodejs/node/commit/d56c6cd708)] - **test\_runner**: ignore erased TS lines in coverage (Matteo Collina) [#63510](https://github.com/nodejs/node/pull/63510) +* \[[`16015f1565`](https://github.com/nodejs/node/commit/16015f1565)] - **test\_runner**: fix suite diagnostic chanel end (Moshe Atlow) [#63533](https://github.com/nodejs/node/pull/63533) +* \[[`003b9ccbe9`](https://github.com/nodejs/node/commit/003b9ccbe9)] - **test\_runner**: dont buffer unordered events in process isolation mode (Moshe Atlow) [#63432](https://github.com/nodejs/node/pull/63432) +* \[[`fdc4b5aed4`](https://github.com/nodejs/node/commit/fdc4b5aed4)] - **test\_runner**: fix --test-rerun-failures swallowing failures on retry (Chemi Atlow) [#63431](https://github.com/nodejs/node/pull/63431) +* \[[`6a0bd2f329`](https://github.com/nodejs/node/commit/6a0bd2f329)] - **test\_runner**: add parentId to test events with testId (Moshe Atlow) [#63435](https://github.com/nodejs/node/pull/63435) +* \[[`a646c93254`](https://github.com/nodejs/node/commit/a646c93254)] - **test\_runner**: show replayed-from-attempt hint in spec reporter (Moshe Atlow) [#63429](https://github.com/nodejs/node/pull/63429) +* \[[`b1fa59cbb6`](https://github.com/nodejs/node/commit/b1fa59cbb6)] - **test\_runner**: preserve run duration when using test-rerun (Moshe Atlow) [#63429](https://github.com/nodejs/node/pull/63429) +* \[[`6ac7ff24ac`](https://github.com/nodejs/node/commit/6ac7ff24ac)] - **tools**: refine `v8.nix` source definition (Antoine du Hamel) [#63625](https://github.com/nodejs/node/pull/63625) +* \[[`59c01b959f`](https://github.com/nodejs/node/commit/59c01b959f)] - **tools**: add lint rule for aborted AbortController (Trivikram Kamat) [#63541](https://github.com/nodejs/node/pull/63541) +* \[[`2ab034f6f9`](https://github.com/nodejs/node/commit/2ab034f6f9)] - **tools**: bump @node-core/doc-kit in /tools/doc in the doc group (dependabot\[bot]) [#63494](https://github.com/nodejs/node/pull/63494) +* \[[`a6af903e0a`](https://github.com/nodejs/node/commit/a6af903e0a)] - **tools**: bump brace-expansion from 5.0.5 to 5.0.6 in /tools/eslint (dependabot\[bot]) [#63415](https://github.com/nodejs/node/pull/63415) +* \[[`215cd543dd`](https://github.com/nodejs/node/commit/215cd543dd)] - **tools**: skip commit-lint on backport pull requests (Marco) [#63378](https://github.com/nodejs/node/pull/63378) +* \[[`0479f28e95`](https://github.com/nodejs/node/commit/0479f28e95)] - **tools**: fix skip of `test-internet` on forks (Antoine du Hamel) [#63492](https://github.com/nodejs/node/pull/63492) +* \[[`69dfadf785`](https://github.com/nodejs/node/commit/69dfadf785)] - **tools**: mock some Python utils in `v8.nix` to reuse builds (Antoine du Hamel) [#63454](https://github.com/nodejs/node/pull/63454) +* \[[`7b3e222cda`](https://github.com/nodejs/node/commit/7b3e222cda)] - **util**: remove unused functions (Antoine du Hamel) [#63612](https://github.com/nodejs/node/pull/63612) +* \[[`5a1f67c27b`](https://github.com/nodejs/node/commit/5a1f67c27b)] - **util**: create hex style cache and fast path (Guilherme Araújo) [#62999](https://github.com/nodejs/node/pull/62999) + ## 2026-05-20, Version 26.2.0 (Current), @aduh95 diff --git a/doc/contributing/large-pull-requests.md b/doc/contributing/large-pull-requests.md index 00ceb8452e2abc..8920ffdcbb3785 100644 --- a/doc/contributing/large-pull-requests.md +++ b/doc/contributing/large-pull-requests.md @@ -14,10 +14,6 @@ ## Overview -Large pull requests are difficult to review or sometimes impossible to review in the GitHub UI. They are likely to sit -for a long time without receiving adequate review, and when they do get reviewed, -the quality of that review is often lower due to reviewer fatigue. Contributors -should avoid creating large pull requests except in those cases where it is Large pull requests are difficult to review or sometimes impossible to review in the GitHub UI. They are likely to sit for a long time without receiving adequate review, and when they do get reviewed, the quality of that review is diff --git a/doc/contributing/releases.md b/doc/contributing/releases.md index 5299b0026298e3..e2eba8880b1db9 100644 --- a/doc/contributing/releases.md +++ b/doc/contributing/releases.md @@ -272,11 +272,12 @@ $ git reset --hard upstream/vN.x The list of patches to include should be listed in the "Next Security Release" issue in `nodejs-private`. Ask the security release steward if you're unsure. -The `git node land` tool does not work with the `nodejs-private` -organization. To land a PR in Node.js private, use `git cherry-pick` to apply -each commit from the PR. You will also need to manually apply the PR -metadata (`PR-URL`, `Reviewed-by`, etc.) by amending the commit messages. If +To use the `git node land` tool to land Pull Requests in the `nodejs-private` +organization, you need to specify the full URL to the Pull Request and make sure +you provide a GitHub token with read permission to the private repository. If known, additionally include `CVE-ID: CVE-XXXX-XXXXX` in the commit metadata. +Make sure to sign and push to resulting commit to the private repository and not +the public one. **Note**: Do not run CI on the PRs in `nodejs-private` until CI is locked down. You can integrate the PRs into the proposal without running full CI. diff --git a/lib/_http_client.js b/lib/_http_client.js index b7f0aa759b0643..73d7b84c17a8fd 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -46,7 +46,7 @@ const { freeParser, parsers, HTTPParser, - isLenient, + calculateLenientFlags, prepareError, kSkipPendingData, } = require('_http_common'); @@ -74,6 +74,7 @@ const { codes: { ERR_HTTP_HEADERS_SENT, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_HTTP_TOKEN, ERR_INVALID_PROTOCOL, ERR_UNESCAPED_CHARACTERS, @@ -82,6 +83,7 @@ const { const { validateInteger, validateBoolean, + validateOneOf, validateString, } = require('internal/validators'); const { getTimerDuration } = require('internal/timers'); @@ -119,9 +121,6 @@ const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; const kError = Symbol('kError'); const kPath = Symbol('kPath'); -const kLenientAll = HTTPParser.kLenientAll | 0; -const kLenientNone = HTTPParser.kLenientNone | 0; - const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request'; function validateHost(host, name) { @@ -299,6 +298,21 @@ function ClientRequest(input, options, cb) { this.insecureHTTPParser = insecureHTTPParser; + const httpValidation = options.httpValidation; + if (httpValidation !== undefined) { + validateOneOf(httpValidation, 'options.httpValidation', + ['strict', 'relaxed', 'insecure']); + if (insecureHTTPParser !== undefined) { + throw new ERR_INVALID_ARG_VALUE( + 'options.httpValidation', + httpValidation, + 'cannot be used together with options.insecureHTTPParser', + ); + } + } + + this.httpValidation = httpValidation; + if (options.joinDuplicateHeaders !== undefined) { validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders'); } @@ -907,12 +921,11 @@ function emitFreeNT(req) { function tickOnSocket(req, socket) { const parser = parsers.alloc(); req.socket = socket; - const lenient = req.insecureHTTPParser === undefined ? - isLenient() : req.insecureHTTPParser; + const lenientFlags = calculateLenientFlags(req.httpValidation, req.insecureHTTPParser); parser.initialize(HTTPParser.RESPONSE, new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req), req.maxHeaderSize || 0, - lenient ? kLenientAll : kLenientNone); + lenientFlags); parser.socket = socket; parser.outgoing = req; req.parser = parser; diff --git a/lib/_http_common.js b/lib/_http_common.js index 3c389ba054decc..d5e7bdedee39fb 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -256,17 +256,31 @@ function checkIsHttpToken(val) { return true; } -const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +// Strict header value regex per RFC 7230 (original/default behavior): +// field-value = *( field-content / obs-fold ) +// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] +// field-vchar = VCHAR / obs-text +// This rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f). +const strictHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + +// Lenient header value regex per Fetch spec (https://fetch.spec.whatwg.org/#header-value): +// - Must contain no 0x00 (NUL) or HTTP newline bytes (0x0a LF, 0x0d CR) +// - Must be byte sequences (0x00-0xff), not arbitrary unicode +// This allows most control characters except NUL, CR, and LF. +// eslint-disable-next-line no-control-regex +const lenientHeaderCharRegex = /[\x00\x0a\x0d]|[^\x00-\xff]/; + /** - * True if val contains an invalid field-vchar - * field-value = *( field-content / obs-fold ) - * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] - * field-vchar = VCHAR / obs-text + * True if val contains an invalid header value character. + * By default uses strict validation per RFC 7230. + * When lenient=true, uses relaxed validation per Fetch spec. * @param {string} val + * @param {boolean} [lenient] - Use lenient validation (Fetch spec rules) * @returns {boolean} */ -function checkInvalidHeaderChar(val) { - return headerCharRegex.test(val); +function checkInvalidHeaderChar(val, lenient = false) { + const regex = lenient ? lenientHeaderCharRegex : strictHeaderCharRegex; + return regex.test(val); } function cleanParser(parser) { @@ -300,6 +314,19 @@ function isLenient() { return insecureHTTPParser; } +function calculateLenientFlags(httpValidation, insecureHTTPParserOption) { + if (httpValidation === 'strict') { + return HTTPParser.kLenientNone | 0; + } else if (httpValidation === 'relaxed') { + return HTTPParser.kLenientHeaderValueRelaxed | 0; + } else if (httpValidation === 'insecure') { + return HTTPParser.kLenientAll | 0; + } + const lenient = insecureHTTPParserOption === undefined ? + isLenient() : insecureHTTPParserOption; + return lenient ? HTTPParser.kLenientAll | 0 : HTTPParser.kLenientNone | 0; +} + module.exports = { _checkInvalidHeaderChar: checkInvalidHeaderChar, _checkIsHttpToken: checkIsHttpToken, @@ -312,6 +339,7 @@ module.exports = { kIncomingMessage, HTTPParser, isLenient, + calculateLenientFlags, prepareError, kSkipPendingData, }; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 0d012c77cbffd9..b07cfc9b3419d9 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -44,6 +44,7 @@ const { _checkIsHttpToken: checkIsHttpToken, _checkInvalidHeaderChar: checkInvalidHeaderChar, chunkExpression: RE_TE_CHUNKED, + isLenient, } = require('_http_common'); const { defaultTriggerAsyncIdScope, @@ -158,6 +159,33 @@ function OutgoingMessage(options) { ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype); ObjectSetPrototypeOf(OutgoingMessage, Stream); +// Check if lenient header validation should be used. +// For ClientRequest: checks this.httpValidation or this.insecureHTTPParser +// For ServerResponse: checks the server's httpValidation or insecureHTTPParser +// Falls back to global --insecure-http-parser flag. +OutgoingMessage.prototype._isLenientHeaderValidation = function() { + // New httpValidation option takes priority (ClientRequest case) + if (this.httpValidation !== undefined) { + return this.httpValidation !== 'strict'; + } + // ServerResponse: check server's httpValidation option + const serverHttpValidation = this.req?.socket?.server?.httpValidation; + if (serverHttpValidation !== undefined) { + return serverHttpValidation !== 'strict'; + } + // Legacy insecureHTTPParser - ClientRequest has it directly + if (typeof this.insecureHTTPParser === 'boolean') { + return this.insecureHTTPParser; + } + // ServerResponse can access via req.socket.server + const serverOption = this.req?.socket?.server?.insecureHTTPParser; + if (typeof serverOption === 'boolean') { + return serverOption; + } + // Fall back to global option + return isLenient(); +}; + ObjectDefineProperty(OutgoingMessage.prototype, 'errored', { __proto__: null, get() { @@ -411,18 +439,19 @@ function _storeHeader(firstLine, headers) { trailer: false, header: firstLine, }; + const lenient = this._isLenientHeaderValidation(); if (headers) { if (headers === this[kOutHeaders]) { for (const key in headers) { const entry = headers[key]; - processHeader(this, state, entry[0], entry[1], false); + processHeader(this, state, entry[0], entry[1], false, lenient); } } else if (ArrayIsArray(headers)) { if (headers.length && ArrayIsArray(headers[0])) { for (let i = 0; i < headers.length; i++) { const entry = headers[i]; - processHeader(this, state, entry[0], entry[1], true); + processHeader(this, state, entry[0], entry[1], true, lenient); } } else { if (headers.length % 2 !== 0) { @@ -430,13 +459,13 @@ function _storeHeader(firstLine, headers) { } for (let n = 0; n < headers.length; n += 2) { - processHeader(this, state, headers[n + 0], headers[n + 1], true); + processHeader(this, state, headers[n + 0], headers[n + 1], true, lenient); } } } else { for (const key in headers) { if (ObjectHasOwn(headers, key)) { - processHeader(this, state, key, headers[key], true); + processHeader(this, state, key, headers[key], true, lenient); } } } @@ -535,7 +564,7 @@ function _storeHeader(firstLine, headers) { if (state.expect) this._send(''); } -function processHeader(self, state, key, value, validate) { +function processHeader(self, state, key, value, validate, lenient) { if (validate) validateHeaderName(key); @@ -562,17 +591,17 @@ function processHeader(self, state, key, value, validate) { // Retain for(;;) loop for performance reasons // Refs: https://github.com/nodejs/node/pull/30958 for (let i = 0; i < value.length; i++) - storeHeader(self, state, key, value[i], validate); + storeHeader(self, state, key, value[i], validate, lenient); return; } value = value.join('; '); } - storeHeader(self, state, key, value, validate); + storeHeader(self, state, key, value, validate, lenient); } -function storeHeader(self, state, key, value, validate) { +function storeHeader(self, state, key, value, validate, lenient) { if (validate) - validateHeaderValue(key, value); + validateHeaderValue(key, value, lenient); state.header += key + ': ' + value + '\r\n'; matchHeader(self, state, key, value); } @@ -618,11 +647,11 @@ const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFra } })); -const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value) => { +const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => { if (value === undefined) { throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name); } - if (checkInvalidHeaderChar(value)) { + if (checkInvalidHeaderChar(value, lenient)) { debug('Header "%s" contains invalid characters', name); throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name); } @@ -647,7 +676,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) { throw new ERR_HTTP_HEADERS_SENT('set'); } validateHeaderName(name); - validateHeaderValue(name, value); + if (value === undefined) { + throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); + } + if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) { + debug('Header "%s" contains invalid characters', name); + throw new ERR_INVALID_CHAR('header content', name); + } let headers = this[kOutHeaders]; if (headers === null) @@ -705,7 +740,13 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) { throw new ERR_HTTP_HEADERS_SENT('append'); } validateHeaderName(name); - validateHeaderValue(name, value); + if (value === undefined) { + throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); + } + if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) { + debug('Header "%s" contains invalid characters', name); + throw new ERR_INVALID_CHAR('header content', name); + } const field = name.toLowerCase(); const headers = this[kOutHeaders]; @@ -1001,12 +1042,13 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { // Check if the field must be sent several times const isArrayValue = ArrayIsArray(value); + const lenient = this._isLenientHeaderValidation(); if ( isArrayValue && value.length > 1 && (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase())) ) { for (let j = 0, l = value.length; j < l; j++) { - if (checkInvalidHeaderChar(value[j])) { + if (checkInvalidHeaderChar(value[j], lenient)) { debug('Trailer "%s"[%d] contains invalid characters', field, j); throw new ERR_INVALID_CHAR('trailer content', field); } @@ -1017,7 +1059,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { value = value.join('; '); } - if (checkInvalidHeaderChar(value)) { + if (checkInvalidHeaderChar(value, lenient)) { debug('Trailer "%s" contains invalid characters', field); throw new ERR_INVALID_CHAR('trailer content', field); } diff --git a/lib/_http_server.js b/lib/_http_server.js index 8b0887a4eac42f..f772ebc3724cca 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -46,7 +46,7 @@ const { kIncomingMessage, kSocket, HTTPParser, - isLenient, + calculateLenientFlags, _checkInvalidHeaderChar: checkInvalidHeaderChar, prepareError, } = require('_http_common'); @@ -92,6 +92,7 @@ const { const { validateInteger, validateBoolean, + validateOneOf, validateLinkHeaderValue, validateObject, validateFunction, @@ -187,8 +188,7 @@ const STATUS_CODES = { const kOnExecute = HTTPParser.kOnExecute | 0; const kOnTimeout = HTTPParser.kOnTimeout | 0; -const kLenientAll = HTTPParser.kLenientAll | 0; -const kLenientNone = HTTPParser.kLenientNone | 0; + const kConnections = Symbol('http.server.connections'); const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval'); @@ -325,19 +325,20 @@ ServerResponse.prototype.writeInformation = function writeInformation( const statusMessage = STATUS_CODES[statusCode] || 'unknown'; let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`; + const lenient = this._isLenientHeaderValidation(); if (headers !== undefined && headers !== null) { if (ArrayIsArray(headers)) { if (headers.length && ArrayIsArray(headers[0])) { for (let i = 0; i < headers.length; i++) { const entry = headers[i]; - head += processInformationHeader(entry[0], entry[1]); + head += processInformationHeader(entry[0], entry[1], lenient); } } else { if (headers.length % 2 !== 0) { throw new ERR_INVALID_ARG_VALUE('headers', headers); } for (let i = 0; i < headers.length; i += 2) { - head += processInformationHeader(headers[i], headers[i + 1]); + head += processInformationHeader(headers[i], headers[i + 1], lenient); } } } else { @@ -345,7 +346,7 @@ ServerResponse.prototype.writeInformation = function writeInformation( const keys = ObjectKeys(headers); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - head += processInformationHeader(key, headers[key]); + head += processInformationHeader(key, headers[key], lenient); } } } @@ -355,9 +356,9 @@ ServerResponse.prototype.writeInformation = function writeInformation( return this._writeRaw(head, 'ascii', cb); }; -function processInformationHeader(name, value) { +function processInformationHeader(name, value, lenient) { validateHeaderName(name); - validateHeaderValue(name, value); + validateHeaderValue(name, value, lenient); return `${name}: ${value}\r\n`; } @@ -516,6 +517,20 @@ function storeHTTPOptions(options) { validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser'); this.insecureHTTPParser = insecureHTTPParser; + const httpValidation = options.httpValidation; + if (httpValidation !== undefined) { + validateOneOf(httpValidation, 'options.httpValidation', + ['strict', 'relaxed', 'insecure']); + if (insecureHTTPParser !== undefined) { + throw new ERR_INVALID_ARG_VALUE( + 'options.httpValidation', + httpValidation, + 'cannot be used together with options.insecureHTTPParser', + ); + } + } + this.httpValidation = httpValidation; + const requestTimeout = options.requestTimeout; if (requestTimeout !== undefined) { validateInteger(requestTimeout, 'requestTimeout', 0); @@ -761,8 +776,7 @@ function connectionListenerInternal(server, socket) { const parser = parsers.alloc(); - const lenient = server.insecureHTTPParser === undefined ? - isLenient() : server.insecureHTTPParser; + const lenientFlags = calculateLenientFlags(server.httpValidation, server.insecureHTTPParser); // TODO(addaleax): This doesn't play well with the // `async_hooks.currentResource()` proposal, see @@ -771,7 +785,7 @@ function connectionListenerInternal(server, socket) { HTTPParser.REQUEST, new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket), server.maxHeaderSize || 0, - lenient ? kLenientAll : kLenientNone, + lenientFlags, server[kConnections], ); parser.socket = socket; diff --git a/lib/buffer.js b/lib/buffer.js index 5c983b5a240108..4377b53d865f65 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -169,7 +169,7 @@ const constants = ObjectDefineProperties({}, { }, }); -Buffer.poolSize = 8 * 1024; +Buffer.poolSize = 64 * 1024; let poolSize, poolOffset, allocPool, allocBuffer; function createPool() { diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index d4c3fd688314c2..843854ce22f866 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -23,7 +23,7 @@ const noRestrictedSyntax = [ message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead", }, { - selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuotaExceededError)$/])', + selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError|QuotaExceededError)$/])', message: "Use an error exported by 'internal/errors' instead.", }, { diff --git a/lib/ffi.js b/lib/ffi.js index b3a1563b520dcc..5dc1d80ba5c700 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -56,10 +56,6 @@ const { require('internal/ffi-shared-buffer'); -DynamicLibrary.prototype[SymbolDispose] = function() { - this.close(); -}; - function checkFFIPermission() { if (!permission.isEnabled()) { return; diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js index 552819405a1a60..776b510dfd3bc6 100644 --- a/lib/internal/blocklist.js +++ b/lib/internal/blocklist.js @@ -296,4 +296,5 @@ ObjectSetPrototypeOf(InternalBlockList.prototype, BlockList.prototype); module.exports = { BlockList, InternalBlockList, + kHandle, }; diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 73fdde03d73ba8..981502c51700be 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -7,7 +7,7 @@ const { const { AESCipherJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyVariantAES_CTR_128, kKeyVariantAES_CBC_128, kKeyVariantAES_GCM_128, @@ -107,7 +107,7 @@ function getVariant(name, length) { function asyncAesCtrCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -118,7 +118,7 @@ function asyncAesCtrCipher(mode, key, data, algorithm) { function asyncAesCbcCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -128,7 +128,7 @@ function asyncAesCbcCipher(mode, key, data, algorithm) { function asyncAesKwCipher(mode, key, data) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -140,7 +140,7 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { const tagByteLength = tagLength / 8; return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -155,7 +155,7 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { const tagByteLength = tagLength / 8; return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -175,27 +175,31 @@ function aesCipher(mode, key, data, algorithm) { } } -async function aesGenerateKey(algorithm, extractable, keyUsages) { +function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; if (name !== 'AES-KW') ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( 'Unsupported key usage for an AES key', 'SyntaxError'); } + if (usagesSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length }, getUsagesMask(usagesSet), - extractable); + extractable)); } function aesImportKey( @@ -203,13 +207,13 @@ function aesImportKey( format, keyData, extractable, - keyUsages) { + usages) { const { name } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; if (name !== 'AES-KW') ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( 'Unsupported key usage for an AES key', diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index 6110c55c16dfb8..6d9f9e462d01fe 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -3,8 +3,6 @@ const { FunctionPrototypeCall, MathPow, - StringPrototypeToLowerCase, - TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; @@ -14,6 +12,7 @@ const { Argon2Job, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, kTypeArgon2d, kTypeArgon2i, kTypeArgon2id, @@ -21,7 +20,6 @@ const { const { lazyDOMException, - promisify, } = require('internal/util'); const { @@ -30,6 +28,7 @@ const { const { getArrayBufferOrView, + jobPromise, } = require('internal/crypto/util'); const { @@ -143,20 +142,12 @@ function check(algorithm, parameters) { validateString(algorithm, 'algorithm'); validateOneOf(algorithm, 'algorithm', ['argon2d', 'argon2i', 'argon2id']); - let type; - switch (algorithm) { - case 'argon2d': - type = kTypeArgon2d; - break; - case 'argon2i': - type = kTypeArgon2i; - break; - case 'argon2id': - type = kTypeArgon2id; - break; - default: // unreachable - throw new ERR_CRYPTO_ARGON2_NOT_SUPPORTED(); - } + const type = { + '__proto__': null, + 'argon2d': kTypeArgon2d, + 'argon2i': kTypeArgon2i, + 'argon2id': kTypeArgon2id, + }[algorithm]; validateObject(parameters, 'parameters'); @@ -193,7 +184,6 @@ function check(algorithm, parameters) { return { message, nonce, secret, associatedData, tagLength, passes, parallelism, memory, type }; } -const argon2Promise = promisify(argon2); function validateArgon2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -211,32 +201,27 @@ function validateArgon2DeriveBitsLength(length) { } } -async function argon2DeriveBits(algorithm, baseKey, length) { +function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); - let result; - try { - result = await argon2Promise( - StringPrototypeToLowerCase(algorithm.name), - { - // TODO(panva): call the job directly without needing to re-export the handle - message: getCryptoKeyHandle(baseKey).export(), - nonce: algorithm.nonce, - parallelism: algorithm.parallelism, - tagLength: length / 8, - memory: algorithm.memory, - passes: algorithm.passes, - secret: algorithm.secretValue, - associatedData: algorithm.associatedData, - }, - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - - return TypedArrayPrototypeGetBuffer(result); + const type = { + '__proto__': null, + 'Argon2d': kTypeArgon2d, + 'Argon2i': kTypeArgon2i, + 'Argon2id': kTypeArgon2id, + }[algorithm.name]; + + return jobPromise(() => new Argon2Job( + kCryptoJobWebCrypto, + getCryptoKeyHandle(baseKey), + algorithm.nonce, + algorithm.parallelism, + length / 8, + algorithm.memory, + algorithm.passes, + algorithm.secretValue === undefined ? new Uint8Array() : algorithm.secretValue, + algorithm.associatedData === undefined ? new Uint8Array() : algorithm.associatedData, + type)); } module.exports = { diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 8d26a2888200ff..3e6152b1f55501 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -8,7 +8,7 @@ const { const { SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, kSignJobModeSign, @@ -73,10 +73,10 @@ function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { } } -async function cfrgGenerateKey(algorithm, extractable, keyUsages) { +function cfrgGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'Ed25519': // Fall through @@ -97,23 +97,13 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { } break; } - let nid; - switch (name) { - case 'Ed25519': - nid = EVP_PKEY_ED25519; - break; - case 'Ed448': - nid = EVP_PKEY_ED448; - break; - case 'X25519': - nid = EVP_PKEY_X25519; - break; - case 'X448': - nid = EVP_PKEY_X448; - break; - } - - const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); + const nid = { + '__proto__': null, + 'Ed25519': EVP_PKEY_ED25519, + 'Ed448': EVP_PKEY_ED448, + 'X25519': EVP_PKEY_X25519, + 'X448': EVP_PKEY_X448, + }[name]; let publicUsages; let privateUsages; @@ -134,21 +124,19 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - getUsagesMask(publicUsages), - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - getUsagesMask(privateUsages), - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function cfrgExportKey(key, format) { @@ -182,11 +170,11 @@ function cfrgImportKey( keyData, algorithm, extractable, - keyUsages) { + usages) { const { name } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableCfrgKeyUse( @@ -243,15 +231,15 @@ function cfrgImportKey( extractable); } -async function eddsaSignVerify(key, data, algorithm, signature) { +function eddsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), undefined, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 1bd173cab36191..689cab59f3fbf2 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -7,7 +7,7 @@ const { const { ChaCha20Poly1305CipherJob, SecretKeyGenJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -39,7 +39,7 @@ function validateKeyLength(length) { function c20pCipher(mode, key, data, algorithm) { return jobPromise(() => new ChaCha20Poly1305CipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -47,25 +47,29 @@ function c20pCipher(mode, key, data, algorithm) { algorithm.additionalData)); } -async function c20pGenerateKey(algorithm, extractable, keyUsages) { +function c20pGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( `Unsupported key usage for a ${algorithm.name} key`, 'SyntaxError'); } + if (usagesSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, 256)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + 256, { name }, getUsagesMask(usagesSet), - extractable); + extractable)); } function c20pImportKey( @@ -73,11 +77,11 @@ function c20pImportKey( format, keyData, extractable, - keyUsages) { + usages) { const { name } = algorithm; const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( `Unsupported key usage for a ${algorithm.name} key`, diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index c1c097f627b408..f491c50f021a6a 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -20,6 +20,7 @@ const { ECDHConvertKey: _ECDHConvertKey, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -64,6 +65,7 @@ const { const { getArrayBufferOrView, jobPromise, + jobPromiseThen, toBuf, kHandle, } = require('internal/crypto/util'); @@ -367,7 +369,7 @@ function diffieHellman(options, callback) { let masks; // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. -async function ecdhDeriveBits(algorithm, baseKey, length) { +function ecdhDeriveBits(algorithm, baseKey, length) { const { 'public': key } = algorithm; if (getCryptoKeyType(baseKey) !== 'private') { @@ -390,8 +392,8 @@ async function ecdhDeriveBits(algorithm, baseKey, length) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } - const bits = await jobPromise(() => new DHBitsJob( - kCryptoJobAsync, + const bits = jobPromise(() => new DHBitsJob( + kCryptoJobWebCrypto, getCryptoKeyHandle(key), undefined, undefined, @@ -407,27 +409,29 @@ async function ecdhDeriveBits(algorithm, baseKey, length) { if (length === null) return bits; - // If the length is not a multiple of 8 the nearest ceiled - // multiple of 8 is sliced. - const sliceLength = MathCeil(length / 8); + return jobPromiseThen(bits, (bits) => { + // If the length is not a multiple of 8 the nearest ceiled + // multiple of 8 is sliced. + const sliceLength = MathCeil(length / 8); - const { byteLength } = bits; - // If the length is larger than the derived secret, throw. - if (byteLength < sliceLength) - throw lazyDOMException('derived bit length is too small', 'OperationError'); + const { byteLength } = bits; + // If the length is larger than the derived secret, throw. + if (byteLength < sliceLength) + throw lazyDOMException('derived bit length is too small', 'OperationError'); - const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); + const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); - const mod = length % 8; - if (mod === 0) - return slice; + const mod = length % 8; + if (mod === 0) + return slice; - // eslint-disable-next-line no-sparse-arrays - masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; + // eslint-disable-next-line no-sparse-arrays + masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; - const masked = new Uint8Array(slice); - masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; - return TypedArrayPrototypeGetBuffer(masked); + const masked = new Uint8Array(slice); + masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; + return TypedArrayPrototypeGetBuffer(masked); + }); } module.exports = { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 983bfde2e8efa6..d102b3fe05a29c 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -10,7 +10,7 @@ const { EcKeyPairGenJob, KeyObjectHandle, SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, kKeyTypePublic, @@ -77,10 +77,10 @@ function verifyAcceptableEcKeyUse(name, isPublic, usages) { } } -async function ecGenerateKey(algorithm, extractable, keyUsages) { +function ecGenerateKey(algorithm, extractable, usages) { const { name, namedCurve } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'ECDSA': if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { @@ -98,9 +98,6 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { // Fall through } - const handles = await jobPromise(() => new EcKeyPairGenJob( - kCryptoJobAsync, namedCurve)); - let publicUsages; let privateUsages; switch (name) { @@ -116,21 +113,20 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { const keyAlgorithm = { name, namedCurve }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - getUsagesMask(publicUsages), - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - getUsagesMask(privateUsages), - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, publicKey, privateKey }; + return jobPromise(() => new EcKeyPairGenJob( + kCryptoJobWebCrypto, + namedCurve, + undefined, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function ecExportKey(key, format) { @@ -182,12 +178,12 @@ function ecImportKey( keyData, algorithm, extractable, - keyUsages, + usages, ) { const { name, namedCurve } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableEcKeyUse( @@ -264,17 +260,15 @@ function ecImportKey( extractable); } -async function ecdsaSignVerify(key, data, { name, hash }, signature) { +function ecdsaSignVerify(key, data, { name, hash }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - const hashname = normalizeHashName(hash.name); - - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), undefined, @@ -282,7 +276,7 @@ async function ecdsaSignVerify(key, data, { name, hash }, signature) { undefined, undefined, data, - hashname, + normalizeHashName(hash.name), undefined, // Salt length, not used with ECDSA undefined, // PSS Padding, not used with ECDSA kSigEncP1363, diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 857753c2b39f9c..5aec1614cb92e9 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -12,7 +12,7 @@ const { Hash: _Hash, HashJob, Hmac: _Hmac, - kCryptoJobAsync, + kCryptoJobWebCrypto, oneShotDigest, TurboShakeJob, KangarooTwelveJob, @@ -200,7 +200,7 @@ Hmac.prototype._transform = Hash.prototype._transform; // Implementation for WebCrypto subtle.digest() -async function asyncDigest(algorithm, data) { +function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); switch (algorithm.name) { @@ -221,16 +221,16 @@ async function asyncDigest(algorithm, data) { case 'cSHAKE128': // Fall through case 'cSHAKE256': - return await jobPromise(() => new HashJob( - kCryptoJobAsync, + return jobPromise(() => new HashJob( + kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, algorithm.outputLength)); case 'TurboSHAKE128': // Fall through case 'TurboSHAKE256': - return await jobPromise(() => new TurboShakeJob( - kCryptoJobAsync, + return jobPromise(() => new TurboShakeJob( + kCryptoJobWebCrypto, algorithm.name, algorithm.domainSeparation ?? 0x1f, algorithm.outputLength / 8, @@ -238,8 +238,8 @@ async function asyncDigest(algorithm, data) { case 'KT128': // Fall through case 'KT256': - return await jobPromise(() => new KangarooTwelveJob( - kCryptoJobAsync, + return jobPromise(() => new KangarooTwelveJob( + kCryptoJobWebCrypto, algorithm.name, algorithm.customization, algorithm.outputLength / 8, diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 424c56fd894961..73b16da6923024 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -3,12 +3,14 @@ const { ArrayBuffer, FunctionPrototypeCall, + PromiseResolve, } = primordials; const { HKDFJob, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -27,10 +29,9 @@ const { } = require('internal/crypto/util'); const { - createSecretKey, getCryptoKeyHandle, - getKeyObjectHandle, isKeyObject, + prepareSecretKey, } = require('internal/crypto/keys'); const { @@ -76,10 +77,10 @@ const validateParameters = hideStackFrames((hash, key, salt, info, length) => { function prepareKey(key) { if (isKeyObject(key)) - return getKeyObjectHandle(key); + return prepareSecretKey(key); if (isAnyArrayBuffer(key)) - return getKeyObjectHandle(createSecretKey(key)); + return key; key = toBuf(key); @@ -97,7 +98,7 @@ function prepareKey(key) { key); } - return getKeyObjectHandle(createSecretKey(key)); + return key; } function hkdf(hash, key, salt, info, length, callback) { @@ -148,26 +149,20 @@ function validateHkdfDeriveBitsLength(length) { } } -async function hkdfDeriveBits(algorithm, baseKey, length) { +function hkdfDeriveBits(algorithm, baseKey, length) { validateHkdfDeriveBitsLength(length); const { hash, salt, info } = algorithm; if (length === 0) - return new ArrayBuffer(0); - - try { - return await jobPromise(() => new HKDFJob( - kCryptoJobAsync, - normalizeHashName(hash.name), - getCryptoKeyHandle(baseKey), - salt, - info, - length / 8)); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + return PromiseResolve(new ArrayBuffer(0)); + + return jobPromise(() => new HKDFJob( + kCryptoJobWebCrypto, + normalizeHashName(hash.name), + getCryptoKeyHandle(baseKey), + salt, + info, + length / 8)); } module.exports = { diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 57576f729b7b41..724b2104d4b8c8 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -8,7 +8,7 @@ const { const { HmacJob, KmacJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kSignJobModeSign, kSignJobModeVerify, SecretKeyGenJob, @@ -40,30 +40,34 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -async function hmacGenerateKey(algorithm, extractable, keyUsages) { +function hmacGenerateKey(algorithm, extractable, usages) { const { hash, name, length = getBlockSize(hash.name), } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { throw lazyDOMException( 'Unsupported key usage for an HMAC key', 'SyntaxError'); } + if (usageSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length, hash }, getUsagesMask(usageSet), - extractable); + extractable)); } -async function kmacGenerateKey(algorithm, extractable, keyUsages) { +function kmacGenerateKey(algorithm, extractable, usages) { const { name, length = { @@ -73,20 +77,24 @@ async function kmacGenerateKey(algorithm, extractable, keyUsages) { }[name], } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError'); } + if (usageSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length }, getUsagesMask(usageSet), - extractable); + extractable)); } function macImportKey( @@ -94,10 +102,10 @@ function macImportKey( keyData, algorithm, extractable, - keyUsages, + usages, ) { const isHmac = algorithm.name === 'HMAC'; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${algorithm.name} key`, @@ -168,7 +176,7 @@ function macImportKey( function hmacSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new HmacJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, normalizeHashName(getCryptoKeyAlgorithm(key).hash.name), getCryptoKeyHandle(key), @@ -179,7 +187,7 @@ function hmacSignVerify(key, data, algorithm, signature) { function kmacSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new KmacJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), algorithm.name, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index bd93327f93aa5f..e2497a2b722b97 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -9,7 +9,7 @@ const { const { SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, @@ -59,50 +59,41 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { } } -async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { +function mlDsaGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for an ${name} key`, 'SyntaxError'); } - let nid; - switch (name) { - case 'ML-DSA-44': - nid = EVP_PKEY_ML_DSA_44; - break; - case 'ML-DSA-65': - nid = EVP_PKEY_ML_DSA_65; - break; - case 'ML-DSA-87': - nid = EVP_PKEY_ML_DSA_87; - break; - } + const nid = { + '__proto__': null, + 'ML-DSA-44': EVP_PKEY_ML_DSA_44, + 'ML-DSA-65': EVP_PKEY_ML_DSA_65, + 'ML-DSA-87': EVP_PKEY_ML_DSA_87, + }[name]; - const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); - const publicUsagesMask = getUsagesMask(getUsagesUnion(usageSet, 'verify')); - const privateUsagesMask = getUsagesMask(getUsagesUnion(usageSet, 'sign')); + const publicUsages = getUsagesUnion(usageSet, 'verify'); + const privateUsages = getUsagesUnion(usageSet, 'sign'); const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - publicUsagesMask, - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - privateUsagesMask, - extractable); - - return { __proto__: null, privateKey, publicKey }; + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function mlDsaExportKey(key, format) { @@ -145,11 +136,11 @@ function mlDsaImportKey( keyData, algorithm, extractable, - keyUsages) { + usages) { const { name } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableMlDsaKeyUse( @@ -214,15 +205,15 @@ function mlDsaImportKey( extractable); } -async function mlDsaSignVerify(key, data, algorithm, signature) { +function mlDsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), undefined, diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 99367290ea22cd..2dea4d00af052f 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -1,7 +1,6 @@ 'use strict'; const { - PromiseWithResolvers, SafeSet, StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, @@ -9,7 +8,7 @@ const { } = primordials; const { - kCryptoJobAsync, + kCryptoJobWebCrypto, KEMDecapsulateJob, KEMEncapsulateJob, kKeyFormatDER, @@ -50,10 +49,10 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -async function mlKemGenerateKey(algorithm, extractable, keyUsages) { +function mlKemGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) { throw lazyDOMException( `Unsupported key usage for an ${name} key`, @@ -67,29 +66,26 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { 'ML-KEM-1024': EVP_PKEY_ML_KEM_1024, }[name]; - const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); - const publicUsagesMask = getUsagesMask( - getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits')); - const privateUsagesMask = getUsagesMask( - getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits')); + const publicUsages = + getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); + const privateUsages = + getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - publicUsagesMask, - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - privateUsagesMask, - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function mlKemExportKey(key, format) { @@ -141,11 +137,11 @@ function mlKemImportKey( keyData, algorithm, extractable, - keyUsages) { + usages) { const { name } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableMlKemKeyUse( @@ -215,33 +211,13 @@ function mlKemEncapsulate(encapsulationKey) { throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError'); } - const { promise, resolve, reject } = PromiseWithResolvers(); - - const job = new KEMEncapsulateJob( - kCryptoJobAsync, + return jobPromise(() => new KEMEncapsulateJob( + kCryptoJobWebCrypto, getCryptoKeyHandle(encapsulationKey), undefined, undefined, undefined, - undefined); - - job.ondone = (error, result) => { - if (error) { - reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: error })); - } else { - const { 0: sharedKey, 1: ciphertext } = result; - - resolve({ - sharedKey: TypedArrayPrototypeGetBuffer(sharedKey), - ciphertext: TypedArrayPrototypeGetBuffer(ciphertext), - }); - } - }; - job.run(); - - return promise; + undefined)); } function mlKemDecapsulate(decapsulationKey, ciphertext) { @@ -249,29 +225,14 @@ function mlKemDecapsulate(decapsulationKey, ciphertext) { throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError'); } - const { promise, resolve, reject } = PromiseWithResolvers(); - - const job = new KEMDecapsulateJob( - kCryptoJobAsync, + return jobPromise(() => new KEMDecapsulateJob( + kCryptoJobWebCrypto, getCryptoKeyHandle(decapsulationKey), undefined, undefined, undefined, undefined, - ciphertext); - - job.ondone = (error, result) => { - if (error) { - reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: error })); - } else { - resolve(TypedArrayPrototypeGetBuffer(result)); - } - }; - job.run(); - - return promise; + ciphertext)); } module.exports = { diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 7f0fa0e1855efe..f42ce3bbd133d5 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -3,7 +3,7 @@ const { ArrayBuffer, FunctionPrototypeCall, - TypedArrayPrototypeGetBuffer, + PromiseResolve, } = primordials; const { Buffer } = require('buffer'); @@ -12,6 +12,7 @@ const { PBKDF2Job, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -23,11 +24,11 @@ const { const { getArrayBufferOrView, normalizeHashName, + jobPromise, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { @@ -91,11 +92,12 @@ function check(password, salt, iterations, keylen, digest) { // to the 31-bit range here (which is plenty). validateInt32(iterations, 'iterations', 1); validateInt32(keylen, 'keylen', 0); + // Coerce -0 to +0. + keylen += 0; return { password, salt, iterations, keylen, digest }; } -const pbkdf2Promise = promisify(pbkdf2); function validatePbkdf2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -107,26 +109,20 @@ function validatePbkdf2DeriveBitsLength(length) { } } -async function pbkdf2DeriveBits(algorithm, baseKey, length) { +function pbkdf2DeriveBits(algorithm, baseKey, length) { validatePbkdf2DeriveBitsLength(length); const { iterations, hash, salt } = algorithm; if (length === 0) - return new ArrayBuffer(0); - - let result; - try { - // TODO(panva): call the job directly without needing to re-export the handle - result = await pbkdf2Promise( - getCryptoKeyHandle(baseKey).export(), salt, iterations, length / 8, normalizeHashName(hash.name), - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + return PromiseResolve(new ArrayBuffer(0)); - return TypedArrayPrototypeGetBuffer(result); + return jobPromise(() => new PBKDF2Job( + kCryptoJobWebCrypto, + getCryptoKeyHandle(baseKey), + salt, + iterations, + length / 8, + normalizeHashName(hash.name))); } module.exports = { diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 6034ed64e69514..a09dd7b9f0fda9 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -10,7 +10,7 @@ const { const { RSACipherJob, SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kSignJobModeSign, kSignJobModeVerify, @@ -85,7 +85,7 @@ function validateRsaOaepAlgorithm(algorithm) { } } -async function rsaOaepCipher(mode, key, data, algorithm) { +function rsaOaepCipher(mode, key, data, algorithm) { validateRsaOaepAlgorithm(algorithm); const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private'; @@ -95,8 +95,8 @@ async function rsaOaepCipher(mode, key, data, algorithm) { 'InvalidAccessError'); } - return await jobPromise(() => new RSACipherJob( - kCryptoJobAsync, + return jobPromise(() => new RSACipherJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -105,10 +105,10 @@ async function rsaOaepCipher(mode, key, data, algorithm) { algorithm.label)); } -async function rsaKeyGenerate( +function rsaKeyGenerate( algorithm, extractable, - keyUsages, + usages, ) { const publicExponentConverted = bigIntArrayToUnsignedInt(algorithm.publicExponent); if (publicExponentConverted === undefined) { @@ -123,7 +123,7 @@ async function rsaKeyGenerate( hash, } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'RSA-OAEP': @@ -142,12 +142,6 @@ async function rsaKeyGenerate( } } - const handles = await jobPromise(() => new RsaKeyPairGenJob( - kCryptoJobAsync, - kKeyVariantRSA_SSA_PKCS1_v1_5, - modulusLength, - publicExponentConverted)); - const keyAlgorithm = { name, modulusLength, @@ -155,6 +149,12 @@ async function rsaKeyGenerate( hash, }; + if (publicExponentConverted < 3 || publicExponentConverted % 2 === 0) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError'); + } + let publicUsages; let privateUsages; switch (name) { @@ -170,21 +170,21 @@ async function rsaKeyGenerate( } } - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - getUsagesMask(publicUsages), - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - getUsagesMask(privateUsages), - extractable); - - return { __proto__: null, publicKey, privateKey }; + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + + return jobPromise(() => new RsaKeyPairGenJob( + kCryptoJobWebCrypto, + kKeyVariantRSA_SSA_PKCS1_v1_5, + modulusLength, + publicExponentConverted, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function rsaExportKey(key, format) { @@ -213,8 +213,8 @@ function rsaImportKey( keyData, algorithm, extractable, - keyUsages) { - const usagesSet = new SafeSet(keyUsages); + usages) { + const usagesSet = new SafeSet(usages); let handle; switch (format) { case 'KeyObject': { @@ -276,39 +276,44 @@ function rsaImportKey( }, getUsagesMask(usagesSet), extractable); } -async function rsaSignVerify(key, data, { saltLength }, signature) { +function rsaSignVerify(key, data, { saltLength }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => { - const algorithm = getCryptoKeyAlgorithm(key); - if (algorithm.name === 'RSA-PSS') { + const algorithm = getCryptoKeyAlgorithm(key); + if (algorithm.name === 'RSA-PSS') { + try { validateInt32( saltLength, 'algorithm.saltLength', 0, - MathCeil((algorithm.modulusLength - 1) / 8) - getDigestSizeInBytes(algorithm.hash.name) - 2); + MathCeil((algorithm.modulusLength - 1) / 8) - + getDigestSizeInBytes(algorithm.hash.name) - 2); + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); } + } - return new SignJob( - kCryptoJobAsync, - signature === undefined ? kSignJobModeSign : kSignJobModeVerify, - getCryptoKeyHandle(key), - undefined, - undefined, - undefined, - undefined, - data, - normalizeHashName(algorithm.hash.name), - saltLength, - algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, - undefined, - undefined, - signature); - }); + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, + signature === undefined ? kSignJobModeSign : kSignJobModeVerify, + getCryptoKeyHandle(key), + undefined, + undefined, + undefined, + undefined, + data, + normalizeHashName(algorithm.hash.name), + saltLength, + algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, + undefined, + undefined, + signature)); } diff --git a/lib/internal/crypto/scrypt.js b/lib/internal/crypto/scrypt.js index ce170dff3b8344..14e5f0ac68ce74 100644 --- a/lib/internal/crypto/scrypt.js +++ b/lib/internal/crypto/scrypt.js @@ -83,6 +83,8 @@ function check(password, salt, keylen, options) { password = getArrayBufferOrView(password, 'password'); salt = getArrayBufferOrView(salt, 'salt'); validateInt32(keylen, 'keylen', 0); + // Coerce -0 to +0. + keylen += 0; let { N, r, p, maxmem } = defaults; if (options && options !== defaults) { diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 663375b9e155d2..74d86de3f1b9e1 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -9,12 +9,13 @@ const { DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, - FunctionPrototypeBind, Number, ObjectDefineProperty, ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, + PromisePrototypeThen, + PromiseReject, PromiseWithResolvers, SafeMap, SafeSet, @@ -78,6 +79,7 @@ const { emitExperimentalWarning, filterDuplicateStrings, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -91,6 +93,7 @@ const { isDataView, isArrayBufferView, isAnyArrayBuffer, + isPromise, } = require('internal/util/types'); const kHandle = Symbol('kHandle'); @@ -665,25 +668,80 @@ const validateByteSource = hideStackFrames((val, name) => { val); }); -function onDone(resolve, reject, err, result) { - if (err) { - return reject(lazyDOMException( +// CryptoJob constructors can synchronously throw while running their native +// AdditionalConfig hook. WebCrypto needs those operation-specific setup +// failures to reject with an OperationError. +function jobPromise(getJob) { + try { + return getJob().run(); + } catch (err) { + return PromiseReject(lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err })); } - resolve(result); } -function jobPromise(getJob) { - const { promise, resolve, reject } = PromiseWithResolvers(); +// Temporarily shadow inherited then accessors on WebCrypto result objects. +// Promise resolution reads "then" synchronously for thenable assimilation. +// Returning an own undefined data property keeps that lookup from reaching +// user-mutated prototypes. +function prepareWebCryptoResult(value) { + if ((value === null || typeof value !== 'object') && + typeof value !== 'function') { + return false; + } + if (isPromise(value) || ObjectPrototypeHasOwnProperty(value, 'then')) + return false; + setOwnProperty(value, 'then', undefined); + return true; +} + +// Remove the temporary then property installed by prepareWebCryptoResult(). +function cleanupWebCryptoResult(value) { + delete value.then; +} + +// Resolve a WebCrypto promise while inherited then accessors are shadowed. +function resolveWebCryptoResult(resolve, value) { + const shouldCleanupResult = prepareWebCryptoResult(value); + try { + resolve(value); + } finally { + if (shouldCleanupResult) + cleanupWebCryptoResult(value); + } +} + +// Run a WebCrypto promise reaction and settle the outer promise. +function settleJobPromise(handler, resolve, reject, value, isRejected) { try { - const job = getJob(); - job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); - job.run(); + if (typeof handler === 'function') { + resolveWebCryptoResult(resolve, handler(value)); + } else if (isRejected) { + reject(value); + } else { + resolveWebCryptoResult(resolve, value); + } } catch (err) { - onDone(resolve, reject, err); + reject(err); } - return promise; +} + +// Promise.prototype.then gets promise.constructor to determine the result +// promise's species. These promises are internal WebCrypto intermediates, so +// make that lookup stay on the promise itself instead of user-mutated state. +function jobPromiseThen(promise, onFulfilled, onRejected) { + const { + promise: resultPromise, + resolve, + reject, + } = PromiseWithResolvers(); + setOwnProperty(promise, 'constructor', undefined); + PromisePrototypeThen( + promise, + (value) => settleJobPromise(onFulfilled, resolve, reject, value, false), + (value) => settleJobPromise(onRejected, resolve, reject, value, true)); + return resultPromise; } // In WebCrypto, the publicExponent option in RSA is represented as a @@ -902,12 +960,16 @@ module.exports = { toBuf, kNamedCurveAliases, + kSupportedAlgorithms, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, validateByteSource, validateKeyOps, jobPromise, + jobPromiseThen, + cleanupWebCryptoResult, + prepareWebCryptoResult, validateMaxBufferLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 1d351ab90bc7c4..05c337d3262229 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1,16 +1,23 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeSlice, FunctionPrototypeCall, JSONParse, JSONStringify, ObjectDefineProperties, + ObjectKeys, + ObjectSetPrototypeOf, + PromiseReject, + PromiseResolve, ReflectApply, ReflectConstruct, + SafeArrayIterator, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeStartsWith, + SymbolIterator, SymbolToStringTag, TypedArrayPrototypeGetBuffer, } = primordials; @@ -23,7 +30,10 @@ const { kWebCryptoCipherDecrypt, } = internalBinding('crypto'); -const { TextDecoder, TextEncoder } = require('internal/encoding'); +const { + decodeUTF8, + encodeUtf8String, +} = internalBinding('encoding_binding'); const { codes: { @@ -51,9 +61,12 @@ const { } = require('internal/crypto/hash'); const { + cleanupWebCryptoResult, getBlockSize, + jobPromiseThen, normalizeAlgorithm, normalizeHashName, + prepareWebCryptoResult, validateMaxBufferLength, } = require('internal/crypto/util'); @@ -61,6 +74,7 @@ const { emitExperimentalWarning, kEnumerableProperty, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -68,9 +82,38 @@ const { randomUUID: _randomUUID, } = require('internal/crypto/random'); +const { + isPromise, +} = require('internal/util/types'); + let webidl; -async function digest(algorithm, data) { +// WebCrypto methods return promises, including for synchronous validation +// failures. Keep that conversion in one place so method bodies stay readable. +function callSubtleCryptoMethod(fn, receiver, args) { + try { + const result = ReflectApply(fn, receiver, args); + if (isPromise(result)) + return result; + // PromiseResolve() performs thenable assimilation for object results. + // Shadow inherited then accessors while it resolves synchronous results. + const shouldCleanupResult = prepareWebCryptoResult(result); + try { + return PromiseResolve(result); + } finally { + if (shouldCleanupResult) + cleanupWebCryptoResult(result); + } + } catch (err) { + return PromiseReject(err); + } +} + +function digest(algorithm, data) { + return callSubtleCryptoMethod(digestImpl, this, arguments); +} + +function digestImpl(algorithm, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -85,9 +128,9 @@ async function digest(algorithm, data) { context: '2nd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'digest'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); - return await FunctionPrototypeCall(asyncDigest, this, algorithm, data); + return FunctionPrototypeCall(asyncDigest, this, normalizedAlgorithm, data); } function randomUUID() { @@ -95,7 +138,14 @@ function randomUUID() { return _randomUUID(); } -async function generateKey( +function generateKey( + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(generateKeyImpl, this, arguments); +} + +function generateKeyImpl( algorithm, extractable, keyUsages) { @@ -112,24 +162,20 @@ async function generateKey( prefix, context: '2nd argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'generateKey'); - let result; - let resultType; - switch (algorithm.name) { + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'generateKey'); + switch (normalizedAlgorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/rsa') - .rsaKeyGenerate(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/rsa') + .rsaKeyGenerate(normalizedAlgorithm, extractable, usages); case 'Ed25519': // Fall through case 'Ed448': @@ -137,22 +183,16 @@ async function generateKey( case 'X25519': // Fall through case 'X448': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/cfrg') - .cfrgGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/cfrg') + .cfrgGenerateKey(normalizedAlgorithm, extractable, usages); case 'ECDSA': // Fall through case 'ECDH': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ec') - .ecGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/ec') + .ecGenerateKey(normalizedAlgorithm, extractable, usages); case 'HMAC': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') - .hmacGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/mac') + .hmacGenerateKey(normalizedAlgorithm, extractable, usages); case 'AES-CTR': // Fall through case 'AES-CBC': @@ -162,62 +202,40 @@ async function generateKey( case 'AES-OCB': // Fall through case 'AES-KW': - resultType = 'CryptoKey'; - result = await require('internal/crypto/aes') - .aesGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/aes') + .aesGenerateKey(normalizedAlgorithm, extractable, usages); case 'ChaCha20-Poly1305': - resultType = 'CryptoKey'; - result = await require('internal/crypto/chacha20_poly1305') - .c20pGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/chacha20_poly1305') + .c20pGenerateKey(normalizedAlgorithm, extractable, usages); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ml_dsa') - .mlDsaGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/ml_dsa') + .mlDsaGenerateKey(normalizedAlgorithm, extractable, usages); case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ml_kem') - .mlKemGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/ml_kem') + .mlKemGenerateKey(normalizedAlgorithm, extractable, usages); case 'KMAC128': // Fall through case 'KMAC256': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') - .kmacGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/mac') + .kmacGenerateKey(normalizedAlgorithm, extractable, usages); default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } +} - if (resultType === 'CryptoKey') { - const type = getCryptoKeyType(result); - if ((type === 'secret' || type === 'private') && - getCryptoKeyUsagesMask(result) === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } - } else if (getCryptoKeyUsagesMask(result.privateKey) === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } - - return result; +function deriveBits(algorithm, baseKey, length = null) { + return callSubtleCryptoMethod(deriveBitsImpl, this, arguments); } -async function deriveBits(algorithm, baseKey, length = null) { +function deriveBitsImpl(algorithm, baseKey, length = null) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -238,35 +256,35 @@ async function deriveBits(algorithm, baseKey, length = null) { }); } - algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); if (!hasCryptoKeyUsage(baseKey, 'deriveBits')) { throw lazyDOMException( 'baseKey does not have deriveBits usage', 'InvalidAccessError'); } - if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== normalizedAlgorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - switch (algorithm.name) { + switch (normalizedAlgorithm.name) { case 'X25519': // Fall through case 'X448': // Fall through case 'ECDH': - return await require('internal/crypto/diffiehellman') - .ecdhDeriveBits(algorithm, baseKey, length); + return require('internal/crypto/diffiehellman') + .ecdhDeriveBits(normalizedAlgorithm, baseKey, length); case 'HKDF': - return await require('internal/crypto/hkdf') - .hkdfDeriveBits(algorithm, baseKey, length); + return require('internal/crypto/hkdf') + .hkdfDeriveBits(normalizedAlgorithm, baseKey, length); case 'PBKDF2': - return await require('internal/crypto/pbkdf2') - .pbkdf2DeriveBits(algorithm, baseKey, length); + return require('internal/crypto/pbkdf2') + .pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length); case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': - return await require('internal/crypto/argon2') - .argon2DeriveBits(algorithm, baseKey, length); + return require('internal/crypto/argon2') + .argon2DeriveBits(normalizedAlgorithm, baseKey, length); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } @@ -310,10 +328,19 @@ function getKeyLength({ name, length, hash }) { } } -async function deriveKey( +function deriveKey( algorithm, baseKey, - derivedKeyAlgorithm, + derivedKeyType, + extractable, + keyUsages) { + return callSubtleCryptoMethod(deriveKeyImpl, this, arguments); +} + +function deriveKeyImpl( + algorithm, + baseKey, + derivedKeyType, extractable, keyUsages) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -329,7 +356,7 @@ async function deriveKey( prefix, context: '2nd argument', }); - derivedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(derivedKeyAlgorithm, { + derivedKeyType = webidl.converters.AlgorithmIdentifier(derivedKeyType, { prefix, context: '3rd argument', }); @@ -337,60 +364,67 @@ async function deriveKey( prefix, context: '4th argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '5th argument', }); - algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); - derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); + const normalizedDerivedKeyAlgorithmImport = + normalizeAlgorithm(derivedKeyType, 'importKey'); + const normalizedDerivedKeyAlgorithmLength = + normalizeAlgorithm(derivedKeyType, 'get key length'); if (!hasCryptoKeyUsage(baseKey, 'deriveKey')) { throw lazyDOMException( 'baseKey does not have deriveKey usage', 'InvalidAccessError'); } - if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== normalizedAlgorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length')); - let bits; - switch (algorithm.name) { + const length = getKeyLength(normalizedDerivedKeyAlgorithmLength); + let secret; + switch (normalizedAlgorithm.name) { case 'X25519': // Fall through case 'X448': // Fall through case 'ECDH': - bits = await require('internal/crypto/diffiehellman') - .ecdhDeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/diffiehellman') + .ecdhDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'HKDF': - bits = await require('internal/crypto/hkdf') - .hkdfDeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/hkdf') + .hkdfDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'PBKDF2': - bits = await require('internal/crypto/pbkdf2') - .pbkdf2DeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/pbkdf2') + .pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length); break; case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': - bits = await require('internal/crypto/argon2') - .argon2DeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/argon2') + .argon2DeriveBits(normalizedAlgorithm, baseKey, length); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return FunctionPrototypeCall( + return jobPromiseThen(secret, (secret) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages, - ); + 'raw-secret', + secret, + normalizedDerivedKeyAlgorithmImport, + extractable, + usages, + )); } -async function exportKeySpki(key) { +function exportKeySpki(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through @@ -432,7 +466,7 @@ async function exportKeySpki(key) { } } -async function exportKeyPkcs8(key) { +function exportKeyPkcs8(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through @@ -474,7 +508,7 @@ async function exportKeyPkcs8(key) { } } -async function exportKeyRawPublic(key, format) { +function exportKeyRawPublic(key, format) { switch (getCryptoKeyAlgorithm(key).name) { case 'ECDSA': // Fall through @@ -519,7 +553,7 @@ async function exportKeyRawPublic(key, format) { } } -async function exportKeyRawSeed(key) { +function exportKeyRawSeed(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'ML-DSA-44': // Fall through @@ -540,7 +574,7 @@ async function exportKeyRawSeed(key) { } } -async function exportKeyRawSecret(key, format) { +function exportKeyRawSecret(key, format) { switch (getCryptoKeyAlgorithm(key).name) { case 'AES-CTR': // Fall through @@ -568,32 +602,26 @@ async function exportKeyRawSecret(key, format) { } } -async function exportKeyJWK(key) { +function exportKeyJWK(key) { const algorithm = getCryptoKeyAlgorithm(key); - const parameters = { - key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), - ext: getCryptoKeyExtractable(key), - }; + let alg; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkRsa); - if (alg) parameters.alg = alg; break; } case 'RSA-PSS': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkRsaPss); - if (alg) parameters.alg = alg; break; } case 'RSA-OAEP': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkRsaOaep); - if (alg) parameters.alg = alg; break; } case 'ECDSA': @@ -619,7 +647,7 @@ async function exportKeyJWK(key) { case 'Ed25519': // Fall through case 'Ed448': - parameters.alg = algorithm.name; + alg = algorithm.name; break; case 'AES-CTR': // Fall through @@ -630,48 +658,43 @@ async function exportKeyJWK(key) { case 'AES-OCB': // Fall through case 'AES-KW': - parameters.alg = require('internal/crypto/aes') + alg = require('internal/crypto/aes') .getAlgorithmName(algorithm.name, algorithm.length); break; case 'ChaCha20-Poly1305': - parameters.alg = 'C20P'; + alg = 'C20P'; break; case 'HMAC': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkHmac); - if (alg) parameters.alg = alg; break; } case 'KMAC128': - parameters.alg = 'K128'; + alg = 'K128'; break; case 'KMAC256': { - parameters.alg = 'K256'; + alg = 'K256'; break; } default: return undefined; } + // Keep `alg` in the object literal so an inherited setter cannot capture + // `parameters` before native export populates key material. Delete it for + // algorithms without a JWK alg value to keep the expected shape. + const parameters = { + key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), + ext: getCryptoKeyExtractable(key), + alg, + }; + if (alg === undefined) delete parameters.alg; + return getCryptoKeyHandle(key).exportJwk(parameters, true); } -async function exportKey(format, key) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - +function exportKeySync(format, key) { const algorithm = getCryptoKeyAlgorithm(key); try { normalizeAlgorithm(algorithm, 'exportKey'); @@ -688,43 +711,43 @@ async function exportKey(format, key) { switch (format) { case 'spki': { if (type === 'public') { - result = await exportKeySpki(key); + result = exportKeySpki(key); } break; } case 'pkcs8': { if (type === 'private') { - result = await exportKeyPkcs8(key); + result = exportKeyPkcs8(key); } break; } case 'jwk': { - result = await exportKeyJWK(key); + result = exportKeyJWK(key); break; } case 'raw-secret': { if (type === 'secret') { - result = await exportKeyRawSecret(key, format); + result = exportKeyRawSecret(key, format); } break; } case 'raw-public': { if (type === 'public') { - result = await exportKeyRawPublic(key, format); + result = exportKeyRawPublic(key, format); } break; } case 'raw-seed': { if (type === 'private') { - result = await exportKeyRawSeed(key); + result = exportKeyRawSeed(key); } break; } case 'raw': { if (type === 'secret') { - result = await exportKeyRawSecret(key, format); + result = exportKeyRawSecret(key, format); } else if (type === 'public') { - result = await exportKeyRawPublic(key, format); + result = exportKeyRawPublic(key, format); } break; } @@ -739,6 +762,82 @@ async function exportKey(format, key) { return result; } +function exportKey(format, key) { + return callSubtleCryptoMethod(exportKeyImpl, this, arguments); +} + +function exportKeyImpl(format, key) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); + + webidl ??= require('internal/crypto/webidl'); + const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + format = webidl.converters.KeyFormat(format, { + prefix, + context: '1st argument', + }); + key = webidl.converters.CryptoKey(key, { + prefix, + context: '2nd argument', + }); + + return exportKeySync(format, key); +} + +// Parsed JWK arrays are detached from Array.prototype but still need to pass +// WebIDL sequence conversion, which reads @@iterator from the value. +function safeArrayIterator() { + return new SafeArrayIterator(this); +} + +// The WebCrypto spec parses and stringifies JWKs in a fresh global object. +// Detach internal JSON values from the current global's mutable prototypes to +// approximate those fresh-realm semantics without creating a new realm. +function detachFromUserPrototypes(value) { + if (value === null || typeof value !== 'object') + return; + + ObjectSetPrototypeOf(value, null); + + if (ArrayIsArray(value)) { + setOwnProperty(value, SymbolIterator, safeArrayIterator); + for (let n = 0; n < value.length; n++) + detachFromUserPrototypes(value[n]); + return; + } + + const keys = ObjectKeys(value); + for (let n = 0; n < keys.length; n++) + detachFromUserPrototypes(value[keys[n]]); +} + +// Parse wrapped JWK bytes according to WebCrypto's "parse a JWK" procedure. +function parseJwk(data) { + let key; + try { + // WebCrypto parses JWKs in a fresh global. Detach parsed JSON values + // from user-mutated prototypes before WebIDL dictionary conversion. + // Wrapped JWKs may be produced outside WebCrypto, so parse using the + // spec-required UTF-8. + const json = decodeUTF8(data, false, true); + const result = JSONParse(json); + detachFromUserPrototypes(result); + key = webidl.converters.JsonWebKey(result); + } catch (err) { + throw lazyDOMException( + 'Invalid wrapped JWK key', + { name: 'DataError', cause: err }); + } + + if (key.kty === undefined) { + throw lazyDOMException( + 'Invalid wrapped JWK key', + 'DataError'); + } + + return key; +} + function aliasKeyFormat(format) { switch (format) { case 'raw-public': @@ -749,7 +848,7 @@ function aliasKeyFormat(format) { } } -function importKeySync(format, keyData, algorithm, extractable, keyUsages) { +function importKeySync(format, keyData, algorithm, extractable, usages) { let result; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': @@ -759,14 +858,24 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { case 'RSA-OAEP': format = aliasKeyFormat(format); result = require('internal/crypto/rsa') - .rsaImportKey(format, keyData, algorithm, extractable, keyUsages); + .rsaImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'ECDSA': // Fall through case 'ECDH': format = aliasKeyFormat(format); result = require('internal/crypto/ec') - .ecImportKey(format, keyData, algorithm, extractable, keyUsages); + .ecImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'Ed25519': // Fall through @@ -777,7 +886,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { case 'X448': format = aliasKeyFormat(format); result = require('internal/crypto/cfrg') - .cfrgImportKey(format, keyData, algorithm, extractable, keyUsages); + .cfrgImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'HMAC': // Fall through @@ -785,7 +899,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'KMAC256': result = require('internal/crypto/mac') - .macImportKey(format, keyData, algorithm, extractable, keyUsages); + .macImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'AES-CTR': // Fall through @@ -797,11 +916,21 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'AES-OCB': result = require('internal/crypto/aes') - .aesImportKey(algorithm, format, keyData, extractable, keyUsages); + .aesImportKey( + algorithm, + format, + keyData, + extractable, + usages); break; case 'ChaCha20-Poly1305': result = require('internal/crypto/chacha20_poly1305') - .c20pImportKey(algorithm, format, keyData, extractable, keyUsages); + .c20pImportKey( + algorithm, + format, + keyData, + extractable, + usages); break; case 'HKDF': // Fall through @@ -812,7 +941,7 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { format, keyData, extractable, - keyUsages); + usages); break; case 'Argon2d': // Fall through @@ -825,7 +954,7 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { format, keyData, extractable, - keyUsages); + usages); } break; case 'ML-DSA-44': @@ -834,7 +963,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'ML-DSA-87': result = require('internal/crypto/ml_dsa') - .mlDsaImportKey(format, keyData, algorithm, extractable, keyUsages); + .mlDsaImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'ML-KEM-512': // Fall through @@ -842,7 +976,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'ML-KEM-1024': result = require('internal/crypto/ml_kem') - .mlKemImportKey(format, keyData, algorithm, extractable, keyUsages); + .mlKemImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; } @@ -862,7 +1001,16 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { return result; } -async function importKey( +function importKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(importKeyImpl, this, arguments); +} + +function importKeyImpl( format, keyData, algorithm, @@ -890,23 +1038,31 @@ async function importKey( prefix, context: '4th argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '5th argument', }); - algorithm = normalizeAlgorithm(algorithm, 'importKey'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); return FunctionPrototypeCall( importKeySync, this, - format, keyData, algorithm, extractable, keyUsages, + format, + keyData, + normalizedAlgorithm, + extractable, + usages, ); } // subtle.wrapKey() is essentially a subtle.exportKey() followed // by a subtle.encrypt(). -async function wrapKey(format, key, wrappingKey, algorithm) { +function wrapKey(format, key, wrappingKey, wrapAlgorithm) { + return callSubtleCryptoMethod(wrapKeyImpl, this, arguments); +} + +function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -924,55 +1080,74 @@ async function wrapKey(format, key, wrappingKey, algorithm) { prefix, context: '3rd argument', }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + const algorithm = webidl.converters.AlgorithmIdentifier(wrapAlgorithm, { prefix, context: '4th argument', }); + let normalizedAlgorithm; try { - algorithm = normalizeAlgorithm(algorithm, 'wrapKey'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'wrapKey'); } catch { - algorithm = normalizeAlgorithm(algorithm, 'encrypt'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); } - if (algorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(wrappingKey, 'wrapKey')) throw lazyDOMException( 'Unable to use this key to wrapKey', 'InvalidAccessError'); - let keyData = await FunctionPrototypeCall(exportKey, this, format, key); + const exportedKey = exportKeySync(format, key); + let bytes = exportedKey; if (format === 'jwk') { - const ec = new TextEncoder(); - const raw = JSONStringify(keyData); + // The WebCrypto spec stringifies JWKs in a new global object. Rather + // than create a new realm here, detach this internally generated JWK from + // user-mutated prototypes so JSON.stringify cannot read inherited toJSON + // hooks from the current global. + detachFromUserPrototypes(exportedKey); + const json = JSONStringify(exportedKey); // As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey // we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes // in length - if (algorithm.name === 'AES-KW' && raw.length % 8 !== 0) { - keyData = ec.encode(raw + StringPrototypeRepeat(' ', 8 - (raw.length % 8))); + // The spec then UTF-8 encodes json. + if (normalizedAlgorithm.name === 'AES-KW' && json.length % 8 !== 0) { + bytes = encodeUtf8String( + json + StringPrototypeRepeat(' ', 8 - (json.length % 8))); } else { - keyData = ec.encode(raw); + bytes = encodeUtf8String(json); } } - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, - algorithm, + normalizedAlgorithm, wrappingKey, - keyData, + bytes, 'wrapKey'); } // subtle.unwrapKey() is essentially a subtle.decrypt() followed // by a subtle.importKey(). -async function unwrapKey( +function unwrapKey( format, wrappedKey, unwrappingKey, - unwrapAlgo, - unwrappedKeyAlgo, + unwrapAlgorithm, + unwrappedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(unwrapKeyImpl, this, arguments); +} + +function unwrapKeyImpl( + format, + wrappedKey, + unwrappingKey, + unwrapAlgorithm, + unwrappedKeyAlgorithm, extractable, keyUsages) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -992,12 +1167,12 @@ async function unwrapKey( prefix, context: '3rd argument', }); - unwrapAlgo = webidl.converters.AlgorithmIdentifier(unwrapAlgo, { + const algorithm = webidl.converters.AlgorithmIdentifier(unwrapAlgorithm, { prefix, context: '4th argument', }); - unwrappedKeyAlgo = webidl.converters.AlgorithmIdentifier( - unwrappedKeyAlgo, + unwrappedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( + unwrappedKeyAlgorithm, { prefix, context: '5th argument', @@ -1007,98 +1182,103 @@ async function unwrapKey( prefix, context: '6th argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '7th argument', }); + let normalizedAlgorithm; try { - unwrapAlgo = normalizeAlgorithm(unwrapAlgo, 'unwrapKey'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'unwrapKey'); } catch { - unwrapAlgo = normalizeAlgorithm(unwrapAlgo, 'decrypt'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); } - unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey'); + const normalizedKeyAlgorithm = + normalizeAlgorithm(unwrappedKeyAlgorithm, 'importKey'); - if (unwrapAlgo.name !== getCryptoKeyAlgorithm(unwrappingKey).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(unwrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(unwrappingKey, 'unwrapKey')) throw lazyDOMException( 'Unable to use this key to unwrapKey', 'InvalidAccessError'); - let keyData = await cipherOrWrap( + const bytes = cipherOrWrap( kWebCryptoCipherDecrypt, - unwrapAlgo, + normalizedAlgorithm, unwrappingKey, wrappedKey, 'unwrapKey'); - if (format === 'jwk') { - // The fatal: true option is only supported in builds that have ICU. - const options = process.versions.icu !== undefined ? - { fatal: true } : undefined; - const dec = new TextDecoder('utf-8', options); - try { - keyData = JSONParse(dec.decode(keyData)); - } catch { - throw lazyDOMException('Invalid wrapped JWK key', 'DataError'); + return jobPromiseThen(bytes, (bytes) => { + let keyData = bytes; + if (format === 'jwk') { + keyData = parseJwk(bytes); } - } - return FunctionPrototypeCall( - importKeySync, - this, - format, keyData, unwrappedKeyAlgo, extractable, keyUsages, - ); + return FunctionPrototypeCall( + importKeySync, + this, + format, + keyData, + normalizedKeyAlgorithm, + extractable, + usages, + ); + }); } -async function signVerify(algorithm, key, data, signature) { - const op = signature !== undefined ? 'verify' : 'sign'; // This is also usage - algorithm = normalizeAlgorithm(algorithm, op); +function signVerify(algorithm, key, data, signature) { + const operation = signature !== undefined ? 'verify' : 'sign'; // This is also usage + const normalizedAlgorithm = normalizeAlgorithm(algorithm, operation); - if (algorithm.name !== getCryptoKeyAlgorithm(key).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!hasCryptoKeyUsage(key, op)) + if (!hasCryptoKeyUsage(key, operation)) throw lazyDOMException( - `Unable to use this key to ${op}`, 'InvalidAccessError'); + `Unable to use this key to ${operation}`, 'InvalidAccessError'); - switch (algorithm.name) { + switch (normalizedAlgorithm.name) { case 'RSA-PSS': // Fall through case 'RSASSA-PKCS1-v1_5': - return await require('internal/crypto/rsa') - .rsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/rsa') + .rsaSignVerify(key, data, normalizedAlgorithm, signature); case 'ECDSA': - return await require('internal/crypto/ec') - .ecdsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/ec') + .ecdsaSignVerify(key, data, normalizedAlgorithm, signature); case 'Ed25519': // Fall through case 'Ed448': // Fall through - return await require('internal/crypto/cfrg') - .eddsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/cfrg') + .eddsaSignVerify(key, data, normalizedAlgorithm, signature); case 'HMAC': - return await require('internal/crypto/mac') - .hmacSignVerify(key, data, algorithm, signature); + return require('internal/crypto/mac') + .hmacSignVerify(key, data, normalizedAlgorithm, signature); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - return await require('internal/crypto/ml_dsa') - .mlDsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/ml_dsa') + .mlDsaSignVerify(key, data, normalizedAlgorithm, signature); case 'KMAC128': // Fall through case 'KMAC256': - return await require('internal/crypto/mac') - .kmacSignVerify(key, data, algorithm, signature); + return require('internal/crypto/mac') + .kmacSignVerify(key, data, normalizedAlgorithm, signature); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function sign(algorithm, key, data) { +function sign(algorithm, key, data) { + return callSubtleCryptoMethod(signImpl, this, arguments); +} + +function signImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1117,10 +1297,14 @@ async function sign(algorithm, key, data) { context: '3rd argument', }); - return await signVerify(algorithm, key, data); + return signVerify(algorithm, key, data); } -async function verify(algorithm, key, signature, data) { +function verify(algorithm, key, signature, data) { + return callSubtleCryptoMethod(verifyImpl, this, arguments); +} + +function verifyImpl(algorithm, key, signature, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1143,19 +1327,19 @@ async function verify(algorithm, key, signature, data) { context: '4th argument', }); - return await signVerify(algorithm, key, data, signature); + return signVerify(algorithm, key, data, signature); } -async function cipherOrWrap(mode, algorithm, key, data, op) { +function cipherOrWrap(mode, normalizedAlgorithm, key, data, operation) { // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. validateMaxBufferLength(data, 'data'); - switch (algorithm.name) { + switch (normalizedAlgorithm.name) { case 'RSA-OAEP': - return await require('internal/crypto/rsa') - .rsaCipher(mode, key, data, algorithm); + return require('internal/crypto/rsa') + .rsaCipher(mode, key, data, normalizedAlgorithm); case 'AES-CTR': // Fall through case 'AES-CBC': @@ -1163,21 +1347,25 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-GCM': // Fall through case 'AES-OCB': - return await require('internal/crypto/aes') - .aesCipher(mode, key, data, algorithm); + return require('internal/crypto/aes') + .aesCipher(mode, key, data, normalizedAlgorithm); case 'ChaCha20-Poly1305': - return await require('internal/crypto/chacha20_poly1305') - .c20pCipher(mode, key, data, algorithm); + return require('internal/crypto/chacha20_poly1305') + .c20pCipher(mode, key, data, normalizedAlgorithm); case 'AES-KW': - if (op === 'wrapKey' || op === 'unwrapKey') { - return await require('internal/crypto/aes') - .aesCipher(mode, key, data, algorithm); + if (operation === 'wrapKey' || operation === 'unwrapKey') { + return require('internal/crypto/aes') + .aesCipher(mode, key, data, normalizedAlgorithm); } } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function encrypt(algorithm, key, data) { +function encrypt(algorithm, key, data) { + return callSubtleCryptoMethod(encryptImpl, this, arguments); +} + +function encryptImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1196,25 +1384,29 @@ async function encrypt(algorithm, key, data) { context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'encrypt'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); - if (algorithm.name !== getCryptoKeyAlgorithm(key).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(key, 'encrypt')) throw lazyDOMException( 'Unable to use this key to encrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, - algorithm, + normalizedAlgorithm, key, data, 'encrypt', ); } -async function decrypt(algorithm, key, data) { +function decrypt(algorithm, key, data) { + return callSubtleCryptoMethod(decryptImpl, this, arguments); +} + +function decryptImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1233,18 +1425,18 @@ async function decrypt(algorithm, key, data) { context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'decrypt'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); - if (algorithm.name !== getCryptoKeyAlgorithm(key).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(key, 'decrypt')) throw lazyDOMException( 'Unable to use this key to decrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherDecrypt, - algorithm, + normalizedAlgorithm, key, data, 'decrypt', @@ -1252,7 +1444,11 @@ async function decrypt(algorithm, key, data) { } // Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-getPublicKey -async function getPublicKey(key, keyUsages) { +function getPublicKey(key, keyUsages) { + return callSubtleCryptoMethod(getPublicKeyImpl, this, arguments); +} + +function getPublicKeyImpl(key, keyUsages) { emitExperimentalWarning('The getPublicKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1263,7 +1459,7 @@ async function getPublicKey(key, keyUsages) { prefix, context: '1st argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '2nd argument', }); @@ -1273,30 +1469,37 @@ async function getPublicKey(key, keyUsages) { throw lazyDOMException('key must be a private key', type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); - // TODO(panva): this is by no means a hot path, but let's still follow up to get // rid of this awkwardness const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key))); - return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, keyUsages); + return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, usages); +} + +function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { + return callSubtleCryptoMethod(encapsulateBitsImpl, this, arguments); } -async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { +function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { emitExperimentalWarning('The encapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'encapsulateBits' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 2, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(encapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + encapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { prefix, context: '2nd argument', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1315,43 +1518,65 @@ async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - return await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages) { +function encapsulateKey( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(encapsulateKeyImpl, this, arguments); +} + +function encapsulateKeyImpl( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + keyUsages) { emitExperimentalWarning('The encapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'encapsulateKey' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 5, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(encapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + encapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { prefix, context: '2nd argument', }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(sharedKeyAlgorithm, { - prefix, - context: '3rd argument', - }); + sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( + sharedKeyAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); extractable = webidl.converters.boolean(extractable, { prefix, context: '4th argument', }); - usages = webidl.converters['sequence'](usages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '5th argument', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); - const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedSharedKeyAlgorithm = + normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1366,43 +1591,54 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe 'InvalidAccessError'); } - let encapsulateBits; + let encapsulatedBits; switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - encapsulateBits = await require('internal/crypto/ml_kem') + encapsulatedBits = require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - const sharedKey = FunctionPrototypeCall( - importKeySync, - this, - 'raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, extractable, usages, - ); - - const encapsulatedKey = { - ciphertext: encapsulateBits.ciphertext, - sharedKey, - }; + return jobPromiseThen(encapsulatedBits, (encapsulatedBits) => { + const sharedKey = FunctionPrototypeCall( + importKeySync, + this, + 'raw-secret', + encapsulatedBits.sharedKey, + normalizedSharedKeyAlgorithm, + extractable, + usages, + ); + + return { + ciphertext: encapsulatedBits.ciphertext, + sharedKey, + }; + }); +} - return encapsulatedKey; +function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { + return callSubtleCryptoMethod(decapsulateBitsImpl, this, arguments); } -async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { +function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertext) { emitExperimentalWarning('The decapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'decapsulateBits' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 3, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(decapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + decapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { prefix, context: '2nd argument', @@ -1412,7 +1648,8 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert context: '3rd argument', }); - const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const normalizedDecapsulationAlgorithm = + normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1431,26 +1668,43 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - return await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function decapsulateKey( - decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages, -) { +function decapsulateKey( + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(decapsulateKeyImpl, this, arguments); +} + +function decapsulateKeyImpl( + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + keyUsages) { emitExperimentalWarning('The decapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'decapsulateKey' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 6, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(decapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + decapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { prefix, context: '2nd argument', @@ -1459,21 +1713,26 @@ async function decapsulateKey( prefix, context: '3rd argument', }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(sharedKeyAlgorithm, { - prefix, - context: '4th argument', - }); + sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( + sharedKeyAlgorithm, + { + prefix, + context: '4th argument', + }, + ); extractable = webidl.converters.boolean(extractable, { prefix, context: '5th argument', }); - usages = webidl.converters['sequence'](usages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '6th argument', }); - const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); - const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const normalizedDecapsulationAlgorithm = + normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const normalizedSharedKeyAlgorithm = + normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1493,18 +1752,22 @@ async function decapsulateKey( case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - decapsulatedBits = await require('internal/crypto/ml_kem') + decapsulatedBits = require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return FunctionPrototypeCall( + return jobPromiseThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, usages, - ); + 'raw-secret', + decapsulatedBits, + normalizedSharedKeyAlgorithm, + extractable, + usages, + )); } // The SubtleCrypto and Crypto classes are defined as part of the @@ -1558,10 +1821,13 @@ class SubtleCrypto { let length; let additionalAlgorithm; if (operation === 'deriveKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); if (!check('importKey', additionalAlgorithm)) { return false; @@ -1575,19 +1841,25 @@ class SubtleCrypto { operation = 'deriveBits'; } else if (operation === 'wrapKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); if (!check('exportKey', additionalAlgorithm)) { return false; } } else if (operation === 'unwrapKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); if (!check('importKey', additionalAlgorithm)) { return false; @@ -1621,10 +1893,13 @@ class SubtleCrypto { return false; } } else if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); let normalizedAdditionalAlgorithm; try { @@ -1649,7 +1924,8 @@ class SubtleCrypto { case 'HMAC': case 'KMAC128': case 'KMAC256': - if (normalizedAdditionalAlgorithm.length === undefined || normalizedAdditionalAlgorithm.length === 256) { + if (normalizedAdditionalAlgorithm.length === undefined || + normalizedAdditionalAlgorithm.length === 256) { break; } return false; diff --git a/lib/internal/debugger/inspect_probe.js b/lib/internal/debugger/inspect_probe.js index 4600d8eb10ee3b..fc9f3056f52341 100644 --- a/lib/internal/debugger/inspect_probe.js +++ b/lib/internal/debugger/inspect_probe.js @@ -12,7 +12,7 @@ const { NumberIsNaN, NumberParseInt, ObjectEntries, - Promise, + PromiseWithResolvers, RegExpPrototypeExec, RegExpPrototypeSymbolSplit, SafeMap, @@ -20,10 +20,12 @@ const { StringPrototypeIncludes, StringPrototypeSlice, StringPrototypeStartsWith, + Symbol, } = primordials; const { clearTimeout, setTimeout } = require('timers'); const { SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util'); +const debug = require('internal/util/debuglog').debuglog('inspect_probe'); const InspectClient = require('internal/debugger/inspect_client'); const { @@ -42,6 +44,18 @@ const { const kProbeDefaultTimeout = 30000; const kProbeVersion = 2; const kProbeDisconnectSentinel = 'Waiting for the debugger to disconnect...'; +const kProbeAttachedSentinel = 'Debugger attached.'; +const kProbeListeningPrefix = 'Debugger listening on ws://'; +const kProbeEndingPrefix = 'Debugger ending on ws://'; +const kProbeHelpLine = + 'For help, see: https://nodejs.org/learn/getting-started/debugging'; +// Thrown by `callCdp` after `recordInspectorFailure` has handled the failure, +// so callers can short-circuit without recording duplicate events. +const kInspectorFailedSentinel = Symbol('probe.inspectorFailed'); +const kStartupTeardownAdvice = + 'The target startup may have torn down the inspector. If startup does ' + + 'not touch the inspector, this is likely a Node.js bug. Please file an issue.'; +const kReviewProbeExprAdvice = 'If the failure repeats, review the probe expression.'; const kDigitsRegex = /^\d+$/; const kInspectPortRegex = /^--inspect-port=(\d+)$/; @@ -56,7 +70,7 @@ const kInspectPortRegex = /^--inspect-port=(\d+)$/; /** * Location where the probe was evaluated, serialized into the public report. * @typedef {object} Location - * @property {string} url V8-reported script URL. + * @property {string} [url] V8-reported script URL, if known. * @property {number} line 1-based line number. * @property {number} column 1-based column number. */ @@ -118,17 +132,7 @@ function formatPendingProbeLocations(probes, pending) { return ArrayPrototypeJoin(ArrayFrom(seen), ', '); } -function formatTargetExitMessage(probes, pending, exitCode, signal) { - const status = signal === null ? - `Target exited with code ${exitCode}` : - `Target exited with signal ${signal}`; - if (pending.length === 0) { - return `${status} before target completion`; - } - return `${status} before probes: ${formatPendingProbeLocations(probes, pending)}`; -} - -// Trim the "Waiting for the debugger to disconnect..." message from stderr for reporting child errors. +// Trim inspector-side noise lines from stderr for reporting child errors. function trimProbeChildStderr(stderr) { const lines = RegExpPrototypeSymbolSplit(/\r\n|\r|\n/g, stderr); const kept = []; @@ -136,6 +140,10 @@ function trimProbeChildStderr(stderr) { const line = lines[i]; if (line === '' && i === lines.length - 1) { continue; } if (line === kProbeDisconnectSentinel) { continue; } + if (line === kProbeAttachedSentinel) { continue; } + if (line === kProbeHelpLine) { continue; } + if (StringPrototypeStartsWith(line, kProbeListeningPrefix)) { continue; } + if (StringPrototypeStartsWith(line, kProbeEndingPrefix)) { continue; } ArrayPrototypePush(kept, line); } return ArrayPrototypeJoin(kept, '\n'); @@ -251,9 +259,7 @@ function buildProbeTextReport(report) { formatHitLocation(result.location) : formatTargetText(probe.target); ArrayPrototypePush(lines, `Hit ${result.hit} at ${locText}`); if (result.error !== undefined) { - ArrayPrototypePush(lines, - ` [error] ${probe.expr} = ` + - `${formatRemoteObject(result.error)}`); + ArrayPrototypePush(lines, ` [error] ${probe.expr} = ${result.error.message}`); } else { ArrayPrototypePush(lines, ` ${probe.expr} = ` + @@ -428,18 +434,29 @@ class ProbeInspectorSession { this.childStderr = ''; this.disconnectRequested = false; this.finished = false; + // True once the inspector WebSocket connects. Event handlers ignore + // pre-connect exits/closes so launch/connect failures report through run(). + this.connected = false; + // True once breakpoints are bound and the target is released from --inspect-brk + // via `Runtime.runIfWaitingForDebugger`. + // Distinguishes "exited before user code ran" from "exited during the live session". this.started = false; - this.stderrBuffer = ''; + // A sliding buffer of at most kProbeDisconnectSentinel.length to detect disconnect. + this.disconnectSentinelBuffer = ''; /** @type {Map} keyed by V8 breakpointId. */ this.breakpointDefinitions = new SafeMap(); /** @type {Map} scriptId -> URL. */ this.scriptIdToUrl = new SafeMap(); this.results = []; this.timeout = null; - this.resolveCompletion = null; - this.completionPromise = new Promise((resolve) => { - this.resolveCompletion = resolve; - }); + // The currently-awaited CDP request, or null when no request is in flight. + /** @type {{ method: string, probe: { index: number, location: Location } | null } | null} */ + this.inFlight = null; + // Most recently completed probe, used for `error.probe` when a non-evaluate CDP call rejects. + this.lastProbeIndex = null; + const { promise, resolve } = PromiseWithResolvers(); + this.completionPromise = promise; + this.resolveCompletion = resolve; /** @type {Probe[]} */ this.probes = ArrayPrototypeMap(options.probes, ({ expr, target }) => ({ expr, target, hits: 0 })); this.onChildOutput = FunctionPrototypeBind(this.onChildOutput, this); @@ -449,81 +466,134 @@ class ProbeInspectorSession { this.onScriptParsed = FunctionPrototypeBind(this.onScriptParsed, this); } - finish(state) { + // Marking the probing process to exit with 0, recorded hits are trustworthy. + finishWithTrustedResult(terminal) { + this.finish(kNoFailure, terminal); + } + + // Marking the probing process to exit with 1, recorded hits are best-effort. + finishWithUnreliableResult(terminal) { + this.finish(kGenericUserError, terminal); + } + + finish(exitCode, terminal) { if (this.finished) { return; } + debug('finish: exitCode=%d, terminal=%s', exitCode, terminal?.event); this.finished = true; if (this.timeout !== null) { clearTimeout(this.timeout); this.timeout = null; } - this.resolveCompletion(state); + this.resolveCompletion({ exitCode, terminal }); + } + + getProbeTargetExitEvent(exitCode, signal) { + const pending = this.getPendingProbeIndices(); + const how = signal !== null ? `with signal ${signal}` : `with code ${exitCode}`; + const status = pending.length === 0 ? + 'target completion' : `probes: ${formatPendingProbeLocations(this.probes, pending)}`; + const error = { + __proto__: null, + code: 'probe_target_exit', + message: `Target exited ${how} before ${status}`, + }; + if (exitCode !== null) { error.exitCode = exitCode; } + if (signal !== null) { error.signal = signal; } + error.stderr = trimProbeChildStderr(this.childStderr); + return { event: 'error', pending, error }; } onChildOutput(text, which) { if (which !== 'stderr') { return; } - if (this.started) { - this.childStderr += text; - } + this.childStderr += text; - const combined = this.stderrBuffer + text; - if (this.started && + const combined = this.disconnectSentinelBuffer + text; + // Detect the disconnect sentinel. + if (this.connected && StringPrototypeIncludes(combined, kProbeDisconnectSentinel)) { this.disconnectRequested = true; this.client.reset(); } - if (combined.length > kProbeDisconnectSentinel.length) { - this.stderrBuffer = StringPrototypeSlice(combined, - combined.length - - kProbeDisconnectSentinel.length); + if (combined.length > kProbeDisconnectSentinel.length) { // Slide the buffer. + this.disconnectSentinelBuffer = StringPrototypeSlice(combined, + combined.length - kProbeDisconnectSentinel.length); } else { - this.stderrBuffer = combined; + this.disconnectSentinelBuffer = combined; } } onChildExit(code, signal) { - if (this.started) { - if (code !== 0 || signal !== null) { - this.finish({ - __proto__: null, - event: 'error', - exitCode: code, - signal, - stderr: trimProbeChildStderr(this.childStderr), - }); - } else { - this.finish('complete'); - } + debug('child exit: code=%s signal=%s connected=%s started=%s finished=%s inFlight=%j', + code, signal, this.connected, this.started, this.finished, this.inFlight); + // Pre-connect exits are deliberately silent: the target never reached + // a state where probes could be set, so any report would be empty. + if (!this.connected) { return; } + if (this.finished) { return; } + if (this.inFlight !== null && this.inFlight.probe !== null) { + this.recordInspectorFailure({ + reason: 'Target process exited during probe evaluation', + advice: kReviewProbeExprAdvice, + }); + return; } + if (this.started && code === 0 && signal === null) { + const pending = this.getPendingProbeIndices(); + this.finishWithTrustedResult(pending.length === 0 ? { event: 'completed' } : { event: 'miss', pending }); + return; + } + this.finishWithTrustedResult(this.getProbeTargetExitEvent(code, signal)); } onClientClose() { - if (!this.started || this.child === null) { return; } - - // TODO(joyeecheung): Surface mid-probe inspector disconnects as terminal probe errors - // instead of deferring to timeout or miss classification. + debug('client close: disconnectRequested=%s finished=%s inFlight=%j', + this.disconnectRequested, this.finished, this.inFlight); + if (!this.connected) { return; } if (this.disconnectRequested) { return; } + if (this.finished) { return; } if (this.child.exitCode !== null || this.child.signalCode !== null) { this.onChildExit(this.child.exitCode, this.child.signalCode); + return; } + if (!this.started) { + this.recordInspectorFailure({ + reason: 'Inspector connection lost before probes started', + advice: kStartupTeardownAdvice, + }); + return; + } + if (this.inFlight !== null) { + if (this.inFlight.probe !== null || this.lastProbeIndex !== null) { + this.recordInspectorFailure({ + reason: 'Inspector connection lost during probe activity', + advice: 'A probe expression may have caused the disconnection. ' + + 'If the failure repeats, review the probe expressions.', + }); + } else { + this.recordInspectorFailure({ + reason: 'Inspector connection lost during inspector activity', + advice: 'This is likely a Node.js bug. Please file an issue.', + }); + } + return; + } + this.recordInspectorFailure({ + reason: 'Inspector connection lost', + advice: 'The target was likely terminated externally. If the failure ' + + 'persists, check the target\'s process environment.', + }); } onPaused(params) { - // TODO(joyeecheung): Preserve evaluation and resume failures as terminal probe errors - // instead of collapsing them into a synthetic completion. this.handlePaused(params).catch((error) => { - if (!this.finished) { - if (error?.code === 'ERR_DEBUGGER_ERROR') { - if (this.child !== null && - (this.child.exitCode !== null || this.child.signalCode !== null)) { - this.onChildExit(this.child.exitCode, this.child.signalCode); - } - return; - } - this.finish('complete'); - } + if (error === kInspectorFailedSentinel) { return; } + this.recordInspectorFailure({ + reason: 'Probe mode encountered an unexpected internal failure', + advice: 'This is likely a Node.js bug. Please file an issue.', + internalError: error, + }); }); } @@ -561,6 +631,7 @@ class ProbeInspectorSession { // Evaluate the expressions in the order they appear on the command line. for (const probeIndex of definition.probeIndices) { await this.evaluateProbe(callFrameId, probeIndex, location); + if (this.finished) { break; } } } @@ -568,35 +639,193 @@ class ProbeInspectorSession { } async evaluateProbe(callFrameId, probeIndex, location) { + if (this.finished) { return; } const probe = this.probes[probeIndex]; - const evaluation = await this.client.callMethod('Debugger.evaluateOnCallFrame', { - callFrameId, - expression: probe.expr, - generatePreview: true, - }); + const evaluation = await this.callCdp( + 'Debugger.evaluateOnCallFrame', + { callFrameId, expression: probe.expr, generatePreview: true }, + { __proto__: null, index: probeIndex, location }, + ); + this.lastProbeIndex = probeIndex; probe.hits++; const result = { probe: probeIndex, event: 'hit', hit: probe.hits, location }; if (evaluation.exceptionDetails !== undefined) { - result.error = evaluation.result === undefined ? { - type: 'object', - subtype: 'error', - description: 'Probe expression failed', - } : trimRemoteObject(evaluation.result); + result.error = { + __proto__: null, + message: evaluation.result?.description ?? 'Probe expression failed', + details: { __proto__: null, exception: trimRemoteObject(evaluation.exceptionDetails) }, + }; } else { result.result = trimRemoteObject(evaluation.result); } - ArrayPrototypePush(this.results, result); } async resume() { if (this.finished) { return; } - await this.client.callMethod('Debugger.resume'); + await this.callCdp('Debugger.resume'); + } + + async callCdp(method, params, probe = null) { + if (this.finished) { throw kInspectorFailedSentinel; } + this.inFlight = { __proto__: null, method, probe }; + debug('CDP -> %s%s', method, probe !== null ? `, probe=${probe.index}` : ''); + try { + const result = await this.client.callMethod(method, params); + // A timeout or process exit can finish the report while the CDP request + // is still outstanding. Ignore the late reply in that case. + if (this.finished) { + debug('CDP <- %s discarded (already finished)', method); + throw kInspectorFailedSentinel; + } + debug('CDP <- %s (success)', method); + return result; + } catch (err) { + if (err !== kInspectorFailedSentinel) { // Already handled. + debug('CDP <- %s error: %s', method, err?.code); + } + if (this.disconnectRequested) { + // Only the in-flight evaluation gets attribution. Other rejections + // under disconnect are downstream noise. + if (probe !== null) { + this.recordInspectorFailure({ + reason: 'Target process exited during probe evaluation', + advice: kReviewProbeExprAdvice, + }); + } + throw kInspectorFailedSentinel; + } + // Another event handler already recorded the terminal event. + if (this.finished) { throw kInspectorFailedSentinel; } + if (!this.started) { + this.recordInspectorFailure({ + reason: 'Probe mode failed before user code ran', + advice: kStartupTeardownAdvice, + cdpError: err, + }); + } else if (method === 'Debugger.evaluateOnCallFrame') { + this.recordInspectorFailure({ + reason: 'The inspector could not evaluate a probe expression', + advice: `The rejection details are recorded on the probe hit. ${kReviewProbeExprAdvice}`, + cdpError: err, + }); + } else if (this.lastProbeIndex !== null) { + this.recordInspectorFailure({ + reason: 'Probe session failed after a probe evaluation', + advice: 'If the failure repeats, review the most-recently-evaluated probe expression.', + cdpError: err, + }); + } else { + this.recordInspectorFailure({ + reason: 'Probe session failed during inspector activity', + advice: 'This is likely a Node.js bug. Please file an issue.', + cdpError: err, + }); + } + throw kInspectorFailedSentinel; + } finally { + this.inFlight = null; + } + } + + // Records the first inspector-side terminal for the session, later callers are ignored. + recordInspectorFailure({ reason, advice, cdpError, internalError }) { + if (this.finished) { return; } + debug('recordInspectorFailure "%s": inFlight=%j, lastProbeIndex=%s, cdpError=%j', + reason, this.inFlight, this.lastProbeIndex, cdpError); + const child = this.child; + const exitedAbnormally = child !== null && + (child.signalCode !== null || (child.exitCode !== null && child.exitCode !== 0)); + const inFlightProbe = this.inFlight === null ? null : this.inFlight.probe; + // This normally emits `probe_failure`, but yields to `probe_target_exit` when the child + // has already exited abnormally and there is no in-flight probe to attribute to. + if (exitedAbnormally && inFlightProbe === null) { + this.finishWithTrustedResult(this.getProbeTargetExitEvent(child.exitCode, child.signalCode)); + return; + } + + const failedCdpMethod = this.inFlight === null ? null : this.inFlight.method; + let protocolError = null; + // // `ERR_DEBUGGER_ERROR` is a Node-internal code, not a CDP-level protocol code + if (cdpError !== undefined && cdpError.code !== 'ERR_DEBUGGER_ERROR') { + protocolError = { __proto__: null, message: cdpError.message, code: cdpError.code }; + } + const protocolErrorGoesOnHit = (protocolError !== null) && (failedCdpMethod === 'Debugger.evaluateOnCallFrame'); + + let attribution = null; + if (inFlightProbe !== null) { + const { index, location } = inFlightProbe; + const error = { __proto__: null }; + if (protocolErrorGoesOnHit) { + error.message = 'Probe evaluation failed at the protocol layer'; + error.details = { __proto__: null, protocolError }; + } else { + error.message = 'Probe evaluation did not complete'; + } + this.probes[index].hits++; + ArrayPrototypePush(this.results, { + probe: index, event: 'hit', hit: this.probes[index].hits, location, error, + }); + attribution = index; + } else if (failedCdpMethod !== null && this.lastProbeIndex !== null) { + attribution = this.lastProbeIndex; + } + // When there is no in-flight CDP call (e.g. `onClientClose` after all probes hit), ignore + // `lastProbeIndex` since it can't be attributed to a specific probe. + + const pending = this.getPendingProbeIndices(); + const suffix = pending.length === 0 ? + '' : ` before probes: ${formatPendingProbeLocations(this.probes, pending)}`; + const error = { + __proto__: null, + code: 'probe_failure', + message: `${reason}${suffix}. ${advice}`, + }; + if (attribution !== null) { error.probe = attribution; } + error.stderr = trimProbeChildStderr(this.childStderr); + + let details; + if (failedCdpMethod !== null) { + details = { __proto__: null, lastCdpMethod: failedCdpMethod }; + if (protocolError !== null && !protocolErrorGoesOnHit) { details.protocolError = protocolError; } + } + if (internalError !== undefined) { + details ??= { __proto__: null }; + details.internalError = { __proto__: null, message: internalError?.message, stack: internalError?.stack }; + } + if (details !== undefined) { error.details = details; } + + this.finishWithUnreliableResult({ event: 'error', pending, error }); } startTimeout() { - this.timeout = setTimeout(() => { this.finish('timeout'); }, this.options.timeout); + this.timeout = setTimeout(() => { + debug('timeout fired: finished=%s, inFlight=%j, lastProbeIndex=%s', + this.finished, this.inFlight, this.lastProbeIndex); + if (this.finished) { return; } + if (this.inFlight !== null) { + const hasProbeAttribution = + this.inFlight.probe !== null || this.lastProbeIndex !== null; + this.recordInspectorFailure({ + reason: 'Probe session timed out', + advice: hasProbeAttribution ? + ('The probe expression may be slow, hanging, or interfering with the inspector connection. ' + + 'Try increasing `--timeout`; if the failure persists, review the probe expressions.') : + 'Try increasing `--timeout`; if the failure persists, please file an issue.', + }); + return; + } + const pending = this.getPendingProbeIndices(); + const message = `Timed out after ${this.options.timeout}ms waiting for ` + + (pending.length === 0 ? 'target completion' : + `probes: ${formatPendingProbeLocations(this.probes, pending)}`); + this.finishWithUnreliableResult({ + event: 'timeout', + pending, + error: { code: 'probe_timeout', message }, + }); + }, this.options.timeout); this.timeout.unref(); } @@ -649,7 +878,7 @@ class ProbeInspectorSession { params.columnNumber = target.column - 1; } - const result = await this.client.callMethod('Debugger.setBreakpointByUrl', params); + const result = await this.callCdp('Debugger.setBreakpointByUrl', params); this.breakpointDefinitions.set(result.breakpointId, { probeIndices }); } } @@ -664,53 +893,17 @@ class ProbeInspectorSession { return pending; } - buildReport(state) { - const pending = this.getPendingProbeIndices(); - const report = { - v: kProbeVersion, - probes: ArrayPrototypeMap(this.probes, ({ expr, target }) => ({ expr, target })), - results: ArrayPrototypeSlice(this.results), + buildReport({ exitCode, terminal }) { + const results = ArrayPrototypeSlice(this.results); + ArrayPrototypePush(results, terminal); + return { + code: exitCode, + report: { + v: kProbeVersion, + probes: ArrayPrototypeMap(this.probes, ({ expr, target }) => ({ expr, target })), + results, + }, }; - - if (state === 'timeout') { - ArrayPrototypePush(report.results, { - event: 'timeout', - pending, - error: { - code: 'probe_timeout', - message: pending.length === 0 ? - `Timed out after ${this.options.timeout}ms waiting for target completion` : - `Timed out after ${this.options.timeout}ms waiting for probes: ` + - `${formatPendingProbeLocations(this.probes, pending)}`, - }, - }); - return { code: kGenericUserError, report }; - } - - if (state?.event === 'error') { - const error = { - __proto__: null, - code: 'probe_target_exit', - message: formatTargetExitMessage(this.probes, pending, state.exitCode, state.signal), - }; - if (state.exitCode !== null) { - error.exitCode = state.exitCode; - } - if (state.signal !== null) { - error.signal = state.signal; - } - error.stderr = state.stderr; - ArrayPrototypePush(report.results, { event: 'error', pending, error }); - return { code: kNoFailure, report }; - } - - if (pending.length === 0) { - ArrayPrototypePush(report.results, { event: 'completed' }); - } else { - ArrayPrototypePush(report.results, { event: 'miss', pending }); - } - - return { code: kNoFailure, report }; } async cleanup() { @@ -746,13 +939,18 @@ class ProbeInspectorSession { this.attachListeners(); await this.client.connect(actualPort, actualHost); - await this.client.callMethod('Runtime.enable'); - await this.client.callMethod('Debugger.enable'); - await this.bindBreakpoints(); - this.started = true; - this.startTimeout(); - - await this.client.callMethod('Runtime.runIfWaitingForDebugger'); + this.connected = true; + + try { + await this.callCdp('Runtime.enable'); + await this.callCdp('Debugger.enable'); + await this.bindBreakpoints(); + this.started = true; + this.startTimeout(); + await this.callCdp('Runtime.runIfWaitingForDebugger'); + } catch (err) { + if (err !== kInspectorFailedSentinel) { throw err; } + } const state = await this.completionPromise; return this.buildReport(state); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index f989bc8fe60a6e..ba632359fbc185 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1689,14 +1689,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP', E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); E('ERR_PROXY_INVALID_CONFIG', '%s', Error); E('ERR_PROXY_TUNNEL', '%s', Error); -E('ERR_QUIC_APPLICATION_ERROR', 'A QUIC application error occurred. %d [%s]', Error); E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error); E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error); E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error); E('ERR_QUIC_STREAM_ABORTED', '%s', Error); E('ERR_QUIC_STREAM_RESET', 'The QUIC stream was reset by the peer with error code %d', Error); -E('ERR_QUIC_TRANSPORT_ERROR', 'A QUIC transport error occurred. %d [%s]', Error); E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error); E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) { let message = 'require() cannot be used on an ESM ' + diff --git a/lib/internal/ffi-shared-buffer.js b/lib/internal/ffi-shared-buffer.js index bce51fd79959dd..c5a769c394f11a 100644 --- a/lib/internal/ffi-shared-buffer.js +++ b/lib/internal/ffi-shared-buffer.js @@ -31,18 +31,14 @@ const { TypeError, } = primordials; -const { - codes: { - ERR_INTERNAL_ASSERTION, - }, -} = require('internal/errors'); +const assert = require('internal/assert'); const { DynamicLibrary, charIsSigned, + kSbArguments, kSbInvokeSlow, - kSbParams, - kSbResult, + kSbReturn, kSbSharedBuffer, uintptrMax, } = internalBinding('ffi'); @@ -159,11 +155,9 @@ function writeNumericArg(view, info, offset, arg, index) { return; } - /* c8 ignore start */ // Unreachable: caller filters out non-numeric kinds. - throw new ERR_INTERNAL_ASSERTION( - `FFI: writeNumericArg reached with unexpected kind="${kind}"`); - /* c8 ignore stop */ + /* c8 ignore next */ + assert.fail(`FFI: writeNumericArg reached with unexpected kind="${kind}"`); } // Returns true on fast-path success, false when the caller must fall back @@ -208,51 +202,46 @@ function inheritMetadata(wrapper, rawFn, nargs) { // arguments out of it into invocation-local storage before `ffi_call` and // reads the return value back only after, so nested/reentrant calls into // the same function are safe. -function wrapWithSharedBuffer(rawFn, parameters, resultType) { - if (rawFn === undefined || rawFn === null) return rawFn; +function wrapWithSharedBuffer(rawFn, signature) { + if (rawFn == null) return rawFn; const buffer = rawFn[kSbSharedBuffer]; if (buffer === undefined) return rawFn; // Callers without explicit signature info (the `functions` accessor - // patch below) rely on the `kSbParams` / `kSbResult` metadata attached + // patch below) rely on the `kSbArguments` / `kSbReturn` metadata attached // by the native `CreateFunction`. - if (parameters === undefined) parameters = rawFn[kSbParams]; - if (resultType === undefined) resultType = rawFn[kSbResult]; - // `CreateFunction` always attaches these for SB-eligible functions. - // Missing here means the native side and this wrapper are out of sync. - /* c8 ignore start */ - if (parameters === undefined || resultType === undefined) { - throw new ERR_INTERNAL_ASSERTION( - 'FFI: shared-buffer raw function is missing kSbParams or kSbResult'); + let argumentTypes, returnType; + if (signature === undefined) { + argumentTypes = rawFn[kSbArguments]; + returnType = rawFn[kSbReturn]; + + // `CreateFunction` always attaches these for SB-eligible functions. + // Missing here means the native side and this wrapper are out of sync. + assert(argumentTypes !== undefined && returnType !== undefined, + 'FFI: shared-buffer raw function is missing kSbArguments or kSbReturn'); + } else { + argumentTypes = signature.arguments ?? []; + returnType = signature.return ?? 'void'; } - /* c8 ignore stop */ const slowInvoke = rawFn[kSbInvokeSlow]; const view = new DataView(buffer); let retGetter = null; - if (resultType !== 'void') { - const retInfo = sbTypeInfo[resultType]; - /* c8 ignore start */ - if (retInfo === undefined) { - throw new ERR_INTERNAL_ASSERTION( - `FFI: shared-buffer type table missing entry for result type "${resultType}"`); - } - /* c8 ignore stop */ + if (returnType !== 'void') { + const retInfo = sbTypeInfo[returnType]; + assert(retInfo !== undefined, + `FFI: shared-buffer type table missing entry for return type "${returnType}"`); retGetter = retInfo.get; } - const nargs = parameters.length; + const nargs = argumentTypes.length; const argInfos = []; const argOffsets = []; let anyPointer = false; for (let i = 0; i < nargs; i++) { - const info = sbTypeInfo[parameters[i]]; - /* c8 ignore start */ - if (info === undefined) { - throw new ERR_INTERNAL_ASSERTION( - `FFI: shared-buffer type table missing entry for parameter type "${parameters[i]}"`); - } - /* c8 ignore stop */ + const info = sbTypeInfo[argumentTypes[i]]; + assert(info !== undefined, + `FFI: shared-buffer type table missing entry for argument type "${argumentTypes[i]}"`); // Push the `sbTypeInfo` entry directly (entries with the same `kind` // share a shape, keeping `writeNumericArg`'s call sites // low-polymorphism) and store offsets in a parallel array to avoid @@ -267,13 +256,8 @@ function wrapWithSharedBuffer(rawFn, parameters, resultType) { // Pointer signatures need a per-arg runtime type check and fall back // to the native slow-path invoker for non-BigInt pointer arguments, // so arity specialization wouldn't buy much here. - /* c8 ignore start */ - if (slowInvoke === undefined) { - throw new ERR_INTERNAL_ASSERTION( - 'FFI: shared-buffer raw function with pointer arguments is ' + - 'missing kSbInvokeSlow'); - } - /* c8 ignore stop */ + assert(slowInvoke !== undefined, + 'FFI: shared-buffer raw function with pointer arguments is missing kSbInvokeSlow'); wrapper = function(...args) { if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); @@ -542,19 +526,6 @@ function buildNumericWrapper( }; } -// Accept-set mirrors the native `ParseFunctionSignature` in -// `src/ffi/types.cc`. `ParseFunctionSignature` additionally throws when -// multiple aliases are set at once. The wrapper runs before the native -// call, so those conflicts still surface from the native side regardless -// of which alias we happen to read here. -function sigParams(sig) { - return sig.parameters ?? sig.arguments ?? []; -} - -function sigResult(sig) { - return sig.result ?? sig.return ?? sig.returns ?? 'void'; -} - // The native invoker for SB-eligible symbols is `InvokeFunctionSB`, which // reads arguments from the shared buffer populated by // `wrapWithSharedBuffer`. These patches make sure every path that surfaces @@ -563,11 +534,11 @@ function sigResult(sig) { const rawGetFunction = DynamicLibrary.prototype.getFunction; const rawGetFunctions = DynamicLibrary.prototype.getFunctions; -DynamicLibrary.prototype.getFunction = function getFunction(name, sig) { - // Native `DynamicLibrary::GetFunction` validates `sig`, so by the time - // we have `raw` we know `sig` is a valid object. - const raw = FunctionPrototypeCall(rawGetFunction, this, name, sig); - return wrapWithSharedBuffer(raw, sigParams(sig), sigResult(sig)); +DynamicLibrary.prototype.getFunction = function getFunction(name, signature) { + // Native `DynamicLibrary::GetFunction` validates `signature`, so by the time + // we have `raw` we know `signature` is a valid object. + const raw = FunctionPrototypeCall(rawGetFunction, this, name, signature); + return wrapWithSharedBuffer(raw, signature); }; DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { @@ -583,13 +554,12 @@ DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { for (let i = 0; i < keys.length; i++) { const name = keys[i]; // No `definitions`: native side returned every cached function, so we - // wrap using each function's own `kSbParams` / `kSbResult` metadata + // wrap using each function's own `kSbArguments` / `kSbReturn` metadata // (same fallback as the `functions` accessor). if (definitions === undefined) { out[name] = wrapWithSharedBuffer(raw[name]); } else { - const sig = definitions[name]; - out[name] = wrapWithSharedBuffer(raw[name], sigParams(sig), sigResult(sig)); + out[name] = wrapWithSharedBuffer(raw[name], definitions[name]); } } return out; @@ -602,16 +572,12 @@ DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { // uninitialized buffer. const functionsDescriptor = ObjectGetOwnPropertyDescriptor(DynamicLibrary.prototype, 'functions'); - /* c8 ignore start */ - if (functionsDescriptor === undefined || !functionsDescriptor.get) { - // Missing getter means the native and JS sides are out of sync; silently - // skipping the patch would expose the fast-path-against-uninitialized-buffer - // footgun this whole block exists to prevent. - throw new ERR_INTERNAL_ASSERTION( - 'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter'); - } - /* c8 ignore stop */ - const origGetter = functionsDescriptor.get; + const origGetter = functionsDescriptor?.get; + // Missing getter means the native and JS sides are out of sync; silently + // skipping the patch would expose the fast-path-against-uninitialized-buffer + // footgun this whole block exists to prevent. + assert(origGetter !== undefined, + 'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter'); ObjectDefineProperty(DynamicLibrary.prototype, 'functions', { __proto__: null, configurable: true, diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 720bd1319b381f..0aa01d9b39dc3e 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -178,6 +178,12 @@ function handleErrorFromBinding(error) { } class FileHandle extends EventEmitter { + #brandCheck = undefined; + + static isFileHandle(value) { + return (value != null && typeof value === 'object' && #brandCheck in value); + } + /** * @param {InternalFSBinding.FileHandle | undefined} filehandle */ diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 273ddd15414b51..1c6edd65cae8f0 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -838,6 +838,10 @@ function requestOnConnect(headersList, options) { } } +function requestOnError(error) { + this.destroy(error); +} + // Validates that priority options are correct, specifically: // 1. options.weight must be a number // 2. options.parent must be a positive number @@ -1153,7 +1157,7 @@ function setupHandle(socket, type, options) { process.nextTick(emit, this, 'connect', this, socket); } -// Emits a close event followed by an error event if err is truthy. Used +// Emits an error event followed by a close event if err is truthy. Used // by Http2Session.prototype.destroy() function emitClose(self, error) { if (error) @@ -1224,6 +1228,9 @@ function closeSession(session, code, error) { session.setTimeout(0); session.removeAllListeners('timeout'); + const socket = session[kSocket]; + const handle = session[kHandle]; + // Destroy any pending and open streams if (state.pendingStreams.size > 0 || state.streams.size > 0) { const cancel = new ERR_HTTP2_STREAM_CANCEL(error); @@ -1231,10 +1238,6 @@ function closeSession(session, code, error) { state.streams.forEach((stream) => stream.destroy(error)); } - // Disassociate from the socket and server. - const socket = session[kSocket]; - const handle = session[kHandle]; - // Destroy the handle if it exists at this point. if (handle !== undefined) { handle.ondone = finishSessionClose.bind(null, session, error); @@ -1809,11 +1812,15 @@ class ClientHttp2Session extends Http2Session { request(headersParam, options) { debugSessionObj(this, 'initiating request'); - if (this.destroyed) - throw new ERR_HTTP2_INVALID_SESSION(); - - if (this.closed) - throw new ERR_HTTP2_GOAWAY_SESSION(); + // Keep argument validation synchronous, but defer session-state failures + // to the returned stream so request retries from stream callbacks do not + // throw before session lifecycle handlers run. + let requestError; + if (this.destroyed) { + requestError = new ERR_HTTP2_INVALID_SESSION(); + } else if (this.closed) { + requestError = new ERR_HTTP2_GOAWAY_SESSION(); + } this[kUpdateTimer](); @@ -1899,19 +1906,24 @@ class ClientHttp2Session extends Http2Session { } } - const onConnect = reqAsync.bind(requestOnConnect.bind(stream, headersList, options)); - if (this.connecting) { - if (this[kPendingRequestCalls] !== null) { - this[kPendingRequestCalls].push(onConnect); + if (requestError) { + process.nextTick(reqAsync.bind(requestOnError.bind(stream, requestError))); + } else { + const onConnect = reqAsync.bind( + requestOnConnect.bind(stream, headersList, options)); + if (this.connecting) { + if (this[kPendingRequestCalls] !== null) { + this[kPendingRequestCalls].push(onConnect); + } else { + this[kPendingRequestCalls] = [onConnect]; + this.once('connect', () => { + this[kPendingRequestCalls].forEach((f) => f()); + this[kPendingRequestCalls] = null; + }); + } } else { - this[kPendingRequestCalls] = [onConnect]; - this.once('connect', () => { - this[kPendingRequestCalls].forEach((f) => f()); - this[kPendingRequestCalls] = null; - }); + onConnect(); } - } else { - onConnect(); } if (onClientStreamCreatedChannel.hasSubscribers) { diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 34a9393ac18f01..5099bc44f8a207 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -14,7 +14,8 @@ const { hardenRegExp, } = primordials; - +const { LoadCache, ResolveCache } = require('internal/modules/esm/module_map'); +const { ModuleJob, ModuleJobSync } = require('internal/modules/esm/module_job'); // This is needed to avoid cycles in esm/resolve <-> cjs/loader const { kIsExecuting, @@ -85,24 +86,6 @@ const { isPromise } = require('internal/util/types'); * @typedef {import('url').URL} URL */ -/** - * Lazy loads the module_map module and returns a new instance of ResolveCache. - * @returns {import('./module_map.js').ResolveCache} - */ -function newResolveCache() { - const { ResolveCache } = require('internal/modules/esm/module_map'); - return new ResolveCache(); -} - -/** - * Generate a load cache (to store the final result of a load-chain for a particular module). - * @returns {import('./module_map.js').LoadCache} - */ -function newLoadCache() { - const { LoadCache } = require('internal/modules/esm/module_map'); - return new LoadCache(); -} - const { translators } = require('internal/modules/esm/translators'); const { defaultResolve } = require('internal/modules/esm/resolve'); const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/esm/load'); @@ -161,12 +144,12 @@ class ModuleLoader { /** * Registry of resolved specifiers */ - #resolveCache = newResolveCache(); + #resolveCache = new ResolveCache(); /** * Registry of loaded modules, akin to `require.cache` */ - loadCache = newLoadCache(); + loadCache = new LoadCache(); /** * @see {AsyncLoaderHooks.isForAsyncLoaderHookWorker} @@ -238,7 +221,6 @@ class ModuleLoader { * @returns {Promise} The module object. */ async executeModuleJob(url, wrap, isEntryPoint = false) { - const { ModuleJob } = require('internal/modules/esm/module_job'); const module = await onImport.tracePromise(async () => { const job = new ModuleJob(this, url, undefined, wrap, kEvaluationPhase, false, false, kImportInImportedESM); this.loadCache.set(url, undefined, job); @@ -289,8 +271,8 @@ class ModuleLoader { let job = this.loadCache.get(url, kImplicitTypeAttribute); // This module job is already created: // 1. If it was loaded by `require()` before, at this point the instantiation - // is already completed and we can check the whether it is in a cycle - // (in that case the module status is kEvaluaing), and whether the + // is already completed and we can check whether it is in a cycle + // (in that case the module status is kEvaluating), and whether the // required graph is synchronous. // 2. If it was loaded by `import` before, only allow it if it's already evaluated // to forbid cycles. @@ -298,7 +280,7 @@ class ModuleLoader { // synchronously so that any previously imported synchronous graph is already // evaluated at this point. // TODO(joyeecheung): add something similar to CJS loader's requireStack to help - // debugging the the problematic links in the graph for import. + // debugging the problematic links in the graph for import. debug('importSyncForRequire', parent?.filename, '->', filename, job); if (job !== undefined) { mod[kRequiredModuleSymbol] = job.module; @@ -357,7 +339,6 @@ class ModuleLoader { const wrap = compileSourceTextModule(url, source, kUser); const inspectBrk = (isMain && getOptionValue('--inspect-brk')); - const { ModuleJobSync } = require('internal/modules/esm/module_job'); job = new ModuleJobSync(this, url, kEmptyObject, wrap, kEvaluationPhase, isMain, inspectBrk, kImportInRequiredESM); this.loadCache.set(url, kImplicitTypeAttribute, job); @@ -587,7 +568,6 @@ class ModuleLoader { assert(moduleOrModulePromise instanceof ModuleWrap, `Expected ModuleWrap for loading ${url}`); } - const { ModuleJob, ModuleJobSync } = require('internal/modules/esm/module_job'); // TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too. const ModuleJobCtor = (requestType === kImportInRequiredESM ? ModuleJobSync : ModuleJob); const isMain = (parentURL === undefined); diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 8d2a97259704a1..86ef400ca7559e 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -145,6 +145,11 @@ function processTypeScriptCode(code, options) { return transformedCode; } +function stripTypeScriptTypesForCoverage(code) { + validateString(code, 'code'); + return processTypeScriptCode(code, { mode: 'strip-only' }); +} + /** * Performs type-stripping to TypeScript source code internally. @@ -205,4 +210,5 @@ function addSourceMap(code, sourceMap) { module.exports = { stripTypeScriptModuleTypes, stripTypeScriptTypes, + stripTypeScriptTypesForCoverage, }; diff --git a/lib/internal/process/permission.js b/lib/internal/process/permission.js index b5da69d08c455e..78e10e6e15fd0d 100644 --- a/lib/internal/process/permission.js +++ b/lib/internal/process/permission.js @@ -43,6 +43,18 @@ module.exports = ObjectFreeze({ return permission.has(scope, reference); }, + drop(scope, reference) { + validateString(scope, 'scope'); + if (reference != null) { + if (isBuffer(reference)) { + validateBuffer(reference, 'reference'); + } else { + validateString(reference, 'reference'); + } + } + + permission.drop(scope, reference); + }, availableFlags() { if (_ffi === undefined) { const { getOptionValue } = require('internal/options'); diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 16a80c2d4f410f..076ae226f4f997 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -660,7 +660,7 @@ function initializePermission() { }; // Guarantee path module isn't monkey-patched to bypass permission model ObjectFreeze(require('path')); - const { has } = require('internal/process/permission'); + const { has, drop } = require('internal/process/permission'); const warnFlags = [ '--allow-addons', '--allow-child-process', @@ -712,6 +712,7 @@ function initializePermission() { configurable: false, value: { has, + drop, }, }); } else { diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index a5ce3d8c14fb23..d237adccd448cc 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -9,6 +9,7 @@ const { ArrayPrototypePush, BigInt, DataViewPrototypeGetByteLength, + ErrorCaptureStackTrace, FunctionPrototypeBind, Number, ObjectDefineProperties, @@ -36,6 +37,10 @@ if (!process.features.quic || !getOptionValue('--experimental-quic')) { } const { inspect } = require('internal/util/inspect'); +const { + BlockList, + kHandle: kBlockListHandle, +} = require('internal/blocklist'); let debug = require('internal/util/debuglog').debuglog('quic', (fn) => { debug = fn; @@ -104,13 +109,11 @@ const { ERR_INVALID_THIS, ERR_MISSING_ARGS, ERR_OUT_OF_RANGE, - ERR_QUIC_APPLICATION_ERROR, ERR_QUIC_CONNECTION_FAILED, ERR_QUIC_ENDPOINT_CLOSED, ERR_QUIC_OPEN_STREAM_FAILED, ERR_QUIC_STREAM_ABORTED, ERR_QUIC_STREAM_RESET, - ERR_QUIC_TRANSPORT_ERROR, ERR_QUIC_VERSION_NEGOTIATION_ERROR, }, } = require('internal/errors'); @@ -183,6 +186,7 @@ const { kGoaway, kHandshake, kHandshakeCompleted, + kVerifyPeer, kHeaders, kOwner, kRemoveSession, @@ -203,14 +207,13 @@ const { kTrailers, kVersionNegotiation, kInspect, - kWantsHeaders, - kWantsTrailers, } = require('internal/quic/symbols'); const { QuicEndpointStats, QuicStreamStats, QuicSessionStats, + kCreateDisconnected, } = require('internal/quic/stats'); const { @@ -305,10 +308,21 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [disableStatelessReset] When true, the endpoint will not send stateless resets * @property {bigint|number} [idleTimeout] The default idle timeout for sessions on this endpoint * @property {boolean} [ipv6Only] Use IPv6 only + * @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing * @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections - * @property {bigint|number} [maxRetries] The maximum number of retries - * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host + * @property {number} [retryRate] Global rate limit for retry packets (per second) + * @property {number} [retryBurst] Burst capacity for retry rate limiter + * @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second) + * @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter + * @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second) + * @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter + * @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second) + * @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter + * @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second) + * @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter + * @property {net.BlockList} [blockList] Block list for filtering incoming packets by source address + * @property {'deny'|'allow'} [blockListPolicy='deny'] How to interpret the block list * @property {ArrayBufferView} [resetTokenSecret] The reset token secret * @property {bigint|number} [retryTokenExpiration] The retry token expiration * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) @@ -374,6 +388,7 @@ const endpointRegistry = new SafeSet(); * @property {number} [version] The QUIC version * @property {number} [minVersion] The minimum acceptable QUIC version * @property {'use'|'ignore'|'default'} [preferredAddressPolicy] The preferred address policy + * @property {'strict'|'auto'|'manual'} [verifyPeer='auto'] Peer certificate verification policy (client only) * @property {ApplicationOptions} [application] The application options * @property {TransportParams} [transportParams] The transport parameters * @property {string} [servername] The server name identifier (client only) @@ -404,6 +419,9 @@ const endpointRegistry = new SafeSet(); * @property {ArrayBufferView} [token] An opaque address validation token * previously received from the server via `onnewtoken` (client only). * @property {bigint|number} [handshakeTimeout] The handshake timeout + * @property {bigint|number} [initialRtt] The initial round-trip time estimate in milliseconds. + * Used for PTO computation and initial pacing before the first RTT sample. Default uses + * ngtcp2's built-in default of 333ms. Set lower for low-latency environments. * @property {bigint|number} [keepAlive] The keep-alive timeout in milliseconds. When set, * PING frames will be sent automatically to prevent idle timeout. * @property {bigint|number} [maxStreamWindow] The maximum stream window @@ -417,6 +435,7 @@ const endpointRegistry = new SafeSet(); * @property {number} [drainingPeriodMultiplier] Multiplier applied to the * draining period (3 * PTO) used by ngtcp2. Range `3..255`. * **Default:** `3`. + * @property {bigint|number} [streamIdleTimeout] Time in ms before idle peer-initiated streams are destroyed * @property {number} [maxDatagramSendAttempts] Maximum number of times a * datagram is retried before being abandoned. Range `1..255`. * **Default:** `5`. @@ -464,6 +483,51 @@ const endpointRegistry = new SafeSet(); * @property {string} [validationErrorCode] The error code for the validation failure (if any) */ +/** + * @typedef {object} QuicStreamDestroyOptions + * @property {bigint|number} [code] An explicit application + * error code to send on the resulting `RESET_STREAM` / + * `STOP_SENDING` frames. Numbers are coerced to `BigInt`. When + * omitted, the code is derived from `error` per the precedence + * above. + * @property {string} [reason] Optional human-readable reason. + * Accepted for symmetry with `session.close()` / + * `session.destroy()`; QUIC `RESET_STREAM` and `STOP_SENDING` + * frames do not themselves carry a reason field over the wire. + */ + +/** + * @typedef {object} SendHeadersOptions + * @property {boolean} [terminal] When true, indicates that no body data will be + * sent after these headers. + */ + +/** + * @typedef {object} StreamPriority + * @property {'default' | 'low' | 'high'} level The priority level of the stream. + * @property {boolean} incremental Whether to interleave data with same-priority streams. + */ + +/** + * @typedef {object} QuicSessionPath + * @property {SocketAddress} local The local address for this path + * @property {SocketAddress} remote The remote address for this path + */ + +/** + * @typedef {object} SNIContextOptions + * @property {boolean} [replace] When `true`, the provided SNI context will replace + * the default context for the session. When `false` (default), the provided + * context will be merged with the default context, with precedence given to + * the provided context on any overlapping options. + */ + +/** + * @typedef {object} ProcessSessionOptions + * @property {boolean} forServer true if processing options for a server session + * @property {string} addressFamily the address family to use for validating + */ + /** * Called when the Endpoint receives a new server-side Session. * @callback OnSessionCallback @@ -658,7 +722,7 @@ setCallbacks({ }, /** * Called when the QuicEndpoint C++ handle receives a new server-side session - * @param {*} session The QuicSession C++ handle + * @param {object} session The QuicSession C++ handle */ onSessionNew(session) { debug('new server session callback', this[kOwner], session); @@ -673,10 +737,12 @@ setCallbacks({ * @param {number} errorType * @param {number} code * @param {string} [reason] + * @param {string} [errorName] Decoded TLS alert name when `code` is a + * CRYPTO_ERROR; otherwise undefined. */ - onSessionClose(errorType, code, reason) { - debug('session close callback', errorType, code, reason); - this[kOwner][kFinishClose](errorType, code, reason); + onSessionClose(errorType, code, reason, errorName) { + debug('session close callback', errorType, code, reason, errorName); + this[kOwner][kFinishClose](errorType, code, reason, errorName); }, /** @@ -792,9 +858,9 @@ setCallbacks({ /** * Called when the session receives a session version negotiation request - * @param {*} version - * @param {*} requestedVersions - * @param {*} supportedVersions + * @param {number} version + * @param {number[]} requestedVersions + * @param {number[]} supportedVersions */ onSessionVersionNegotiation(version, requestedVersions, @@ -858,6 +924,21 @@ setCallbacks({ // from QuicError::ToV8Value. Convert to a proper Node.js Error. if (error !== undefined) { error = convertQuicError(error); + } else if (this[kOwner] && !this[kOwner].destroyed) { + // The stream is closing cleanly, but it may have been reset by the + // peer (ReceiveStreamReset) or locally (resetStream). The C++ side + // records the reset code in state.resetCode. If set, surface the + // reset as the close error so stream.closed rejects -- the reset + // was an abnormal termination even if the session closed cleanly. + const resetCode = getQuicStreamState(this[kOwner]).resetCode; + if (resetCode !== undefined && resetCode > 0n) { + error = makeQuicError( + 'ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', + resetCode, + `stream reset with code ${resetCode}`); + } } debug(`stream ${this[kOwner].id} closed callback with error: ${error}`); this[kOwner][kFinishClose](error); @@ -885,6 +966,12 @@ setCallbacks({ }, }); +function assertPrivateSymbol(privateSymbol) { + if (privateSymbol !== kPrivateConstructor) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } +} + // QUIC error codes are 62-bit varints (RFC 9000 section 16). The // maximum representable code is 2**62 - 1. const kMaxQuicErrorCode = (1n << 62n) - 1n; @@ -911,6 +998,10 @@ class QuicError extends Error { /** @type {'transport' | 'application'} */ #type; + static isQuicError(val) { + return val != null && typeof val === 'object' && #errorCode in val; + } + /** * @param {string} message * @param {object} options @@ -968,21 +1059,50 @@ class QuicError extends Error { } } -// Converts a raw QuicError array [type, code, reason] from C++ into a -// proper Node.js Error object. +// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or +// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for +// the wire code when known: either the OpenSSL-decoded TLS alert +// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes +// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined. +// `reason` is the peer-supplied UTF-8 reason string from the +// CONNECTION_CLOSE / RESET_STREAM frame, often empty. +function quicErrorMessage(prefix, errorCode, reason, errorName) { + let msg = `${prefix} `; + msg += errorName ? `${errorName} (${errorCode})` : `${errorCode}`; + if (reason) msg += `: ${reason}`; + return msg; +} + +function makeQuicError(code, prefix, type, errorCode, reason, errorName) { + const err = new QuicError( + quicErrorMessage(prefix, errorCode, reason, errorName), + { errorCode, code, type }); + ErrorCaptureStackTrace(err, makeQuicError); + if (reason) err.reason = reason; + if (errorName) err.errorName = errorName; + return err; +} + function convertQuicError(error) { const type = error[0]; const code = error[1]; const reason = error[2]; + const errorName = error[3]; switch (type) { case 'transport': - return new ERR_QUIC_TRANSPORT_ERROR(code, reason); + return makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName); case 'application': - return new ERR_QUIC_APPLICATION_ERROR(code, reason); + return makeQuicError('ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', code, reason, errorName); case 'version_negotiation': return new ERR_QUIC_VERSION_NEGOTIATION_ERROR(); default: - return new ERR_QUIC_TRANSPORT_ERROR(code, reason); + return makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName); } } @@ -1089,7 +1209,7 @@ function validateBody(body) { // FileHandle -- lock it and pass the C++ handle to GetDataQueueFromSource // which creates an fd-backed DataQueue entry from the file path. - if (body instanceof FileHandle) { + if (FileHandle.isFileHandle(body)) { if (body[kFileLocked]) { throw new ERR_INVALID_STATE('FileHandle is locked'); } @@ -1166,7 +1286,7 @@ function applyCallbacks(session, cbs) { * type and calls the appropriate C++ method. * @param {object} handle The C++ stream handle * @param {QuicStream} stream The JS stream object - * @param {*} body The body source + * @param {any} body The body source */ const kDefaultHighWaterMark = 65536; const kDefaultMaxPendingDatagrams = 128; @@ -1208,7 +1328,7 @@ function configureOutbound(handle, stream, body) { // DataQueue entry from the file path. The FileHandle is locked to // prevent concurrent use and closed automatically when the stream // finishes. - if (body instanceof FileHandle) { + if (FileHandle.isFileHandle(body)) { if (body[kFileLocked]) { throw new ERR_INVALID_STATE('FileHandle is locked'); } @@ -1363,8 +1483,14 @@ let getQuicStreamState; let getQuicSessionState; let getQuicEndpointState; let assertIsQuicEndpoint; +let assertIsQuicStream; +let assertIsQuicSession; +let assertHeadersSupported; let assertEndpointNotClosedOrClosing; let assertEndpointIsNotBusy; +let isQuicStream; +let isQuicSession; +let isQuicEndpoint; function maybeGetCloseError(context, status, pendingError) { switch (context) { @@ -1391,70 +1517,54 @@ function maybeGetCloseError(context, status, pendingError) { } class QuicStream { - /** @type {object} */ #handle; - /** - * Flag set at the top of `destroy()` to make the method safely - * re-entrant. Distinct from `#handle === undefined` (which signals - * "fully destroyed" and is set inside `[kFinishClose]`) so that - * `[kFinishClose]`'s own destroyed-guard does not bail before the - * cleanup work runs. - * @type {boolean} - */ - #destroying = false; - /** @type {QuicSession} */ - #session; - /** @type {QuicStreamStats} */ - #stats; - /** @type {QuicStreamState} */ - #state; - /** @type {number} */ - #direction = undefined; - /** @type {Function|undefined} */ - #onerror = undefined; - /** @type {OnBlockedCallback|undefined} */ - #onblocked = undefined; - /** @type {OnStreamErrorCallback|undefined} */ - #onreset = undefined; - /** @type {Function|undefined} */ - #onheaders = undefined; - /** @type {Function|undefined} */ - #ontrailers = undefined; - /** @type {Function|undefined} */ - #oninfo = undefined; - /** @type {Function|undefined} */ - #onwanttrailers = undefined; - /** @type {object|undefined} */ - #headers = undefined; - /** @type {object|undefined} */ - #pendingTrailers = undefined; - /** @type {Promise} */ - #pendingClose = PromiseWithResolvers(); - #reader; - #iteratorLocked = false; - #writer = undefined; - #outboundSet = false; - /** @type {FileHandle|undefined} */ - #fileHandle = undefined; + #inner = { + __proto__: null, + session: undefined, + direction: undefined, + isLocal: false, + state: undefined, + stats: undefined, + pendingClose: undefined, + reader: undefined, + destroying: false, + iteratorLocked: false, + outboundSet: false, + writer: undefined, + fileHandle: undefined, + headers: undefined, + pendingTrailers: undefined, + onerror: undefined, + onblocked: undefined, + onreset: undefined, + onheaders: undefined, + ontrailers: undefined, + oninfo: undefined, + onwanttrailers: undefined, + }; static { - getQuicStreamState = function(stream) { - QuicStream.#assertIsQuicStream(stream); - return stream.#state; + isQuicStream = function(val) { + return val != null && typeof val === 'object' && #handle in val; }; - } - static #assertIsQuicStream(val) { - if (val == null || !(#handle in val)) { - throw new ERR_INVALID_THIS('QuicStream'); - } - } + assertIsQuicStream = function(val) { + if (!isQuicStream(val)) { + throw new ERR_INVALID_THIS('QuicStream'); + } + }; - #assertHeadersSupported() { - if (getQuicSessionState(this.#session).headersSupported === 2) { - throw new ERR_INVALID_STATE( - 'The negotiated QUIC application protocol does not support headers'); - } + assertHeadersSupported = function(session) { + if (getQuicSessionState(session).headersSupported === 2) { + throw new ERR_INVALID_STATE( + 'The negotiated QUIC application protocol does not support headers'); + } + }; + + getQuicStreamState = function(stream) { + assertIsQuicStream(stream); + return stream.#inner.state; + }; } /** @@ -1462,29 +1572,25 @@ class QuicStream { * @param {object} handle * @param {QuicSession} session * @param {number} direction + * @param {boolean} [isLocal] */ - constructor(privateSymbol, handle, session, direction) { - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } + constructor(privateSymbol, handle, session, direction, isLocal) { + assertPrivateSymbol(privateSymbol); this.#handle = handle; - this.#handle[kOwner] = this; - this.#session = session; - this.#direction = direction; - this.#stats = new QuicStreamStats(kPrivateConstructor, this.#handle.stats); - this.#state = new QuicStreamState(kPrivateConstructor, this.#handle.state); - this.#reader = this.#handle.getReader(); + handle[kOwner] = this; + const inner = this.#inner; + inner.session = session; + inner.direction = direction; + inner.isLocal = isLocal; + inner.state = new QuicStreamState( + kPrivateConstructor, handle.state, handle.stateByteOffset); if (hasObserver('quic')) { startPerf(this, kPerfEntry, { type: 'quic', name: 'QuicStream' }); } - if (this.pending) { - debug(`pending ${this.direction} stream created`); - } else { - debug(`${this.direction} stream ${this.id} created`); - } + debug('stream created'); } get [kValidatedSource]() { return true; } @@ -1496,16 +1602,18 @@ class QuicStream { * @yields {Uint8Array[]} */ async *[SymbolAsyncIterator]() { - QuicStream.#assertIsQuicStream(this); - if (this.#iteratorLocked) { + assertIsQuicStream(this); + const inner = this.#inner; + if (inner.iteratorLocked) { throw new ERR_INVALID_STATE('Stream is already being read'); } - this.#iteratorLocked = true; + inner.iteratorLocked = true; + inner.reader ??= this.#handle?.getReader(); // Non-readable stream (outbound-only unidirectional, or closed) - if (!this.#reader) return; + if (!inner.reader) return; - yield* createBlobReaderIterable(this.#reader, { + yield* createBlobReaderIterable(inner.reader, { getReadError: () => { // The read side ends for one of three reasons: // * Clean FIN received from the peer (state.finReceived @@ -1519,8 +1627,8 @@ class QuicStream { // stream.stopSending(). Both paths run EndReadable in // C++, setting state.readEnded without setting // state.finReceived. There is no peer code to surface. - if (this.#state.readEnded && !this.#state.finReceived) { - const peerResetCode = this.#state.resetCode; + if (inner.state.readEnded && !inner.state.finReceived) { + const peerResetCode = inner.state.resetCode; if (peerResetCode !== undefined && peerResetCode > 0n) { return new ERR_QUIC_STREAM_RESET(Number(peerResetCode)); } @@ -1538,8 +1646,8 @@ class QuicStream { * @type {boolean} */ get pending() { - QuicStream.#assertIsQuicStream(this); - return this.#state.pending; + assertIsQuicStream(this); + return this.#inner.state.pending; } /** @@ -1549,8 +1657,8 @@ class QuicStream { * @type {boolean} */ get early() { - QuicStream.#assertIsQuicStream(this); - return this.#state.early; + assertIsQuicStream(this); + return this.#inner.state.early; } /** @@ -1560,143 +1668,153 @@ class QuicStream { * @type {number} */ get highWaterMark() { - QuicStream.#assertIsQuicStream(this); - return this.#state.highWaterMark; + assertIsQuicStream(this); + return this.#inner.state.highWaterMark; } set highWaterMark(val) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); validateInteger(val, 'highWaterMark', 0, 0xFFFFFFFF); - this.#state.highWaterMark = val; + const inner = this.#inner; + inner.state.highWaterMark = val; // If writeDesiredSize hasn't been set yet (still 0 from initialization), // initialize it to the highWaterMark so the first write can proceed. - if (this.#state.writeDesiredSize === 0 && val > 0) { - this.#state.writeDesiredSize = val; + if (inner.state.writeDesiredSize === 0 && val > 0) { + inner.state.writeDesiredSize = val; } } /** @type {Function|undefined} */ get onerror() { - QuicStream.#assertIsQuicStream(this); - return this.#onerror; + assertIsQuicStream(this); + return this.#inner.onerror; } set onerror(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#onerror = undefined; + inner.onerror = undefined; } else { validateFunction(fn, 'onerror'); - this.#onerror = FunctionPrototypeBind(fn, this); - markPromiseAsHandled(this.#pendingClose.promise); + inner.onerror = FunctionPrototypeBind(fn, this); + // Lazily create the close promise so it can be marked handled. + inner.pendingClose ??= PromiseWithResolvers(); + markPromiseAsHandled(inner.pendingClose.promise); } } /** @type {OnBlockedCallback} */ get onblocked() { - QuicStream.#assertIsQuicStream(this); - return this.#onblocked; + assertIsQuicStream(this); + return this.#inner.onblocked; } set onblocked(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#onblocked = undefined; - this.#state.wantsBlock = false; + inner.onblocked = undefined; + inner.state.wantsBlock = false; } else { validateFunction(fn, 'onblocked'); - this.#onblocked = FunctionPrototypeBind(fn, this); - this.#state.wantsBlock = true; + inner.onblocked = FunctionPrototypeBind(fn, this); + inner.state.wantsBlock = true; } } /** @type {OnStreamErrorCallback} */ get onreset() { - QuicStream.#assertIsQuicStream(this); - return this.#onreset; + assertIsQuicStream(this); + return this.#inner.onreset; } set onreset(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#onreset = undefined; - this.#state.wantsReset = false; + inner.onreset = undefined; + inner.state.wantsReset = false; } else { validateFunction(fn, 'onreset'); - this.#onreset = FunctionPrototypeBind(fn, this); - this.#state.wantsReset = true; + inner.onreset = FunctionPrototypeBind(fn, this); + inner.state.wantsReset = true; } } /** @type {OnHeadersCallback} */ get onheaders() { - QuicStream.#assertIsQuicStream(this); - return this.#onheaders; + assertIsQuicStream(this); + return this.#inner.onheaders; } set onheaders(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#onheaders = undefined; - this.#state[kWantsHeaders] = false; + inner.onheaders = undefined; + inner.state.wantsHeaders = false; } else { - this.#assertHeadersSupported(); validateFunction(fn, 'onheaders'); - this.#onheaders = FunctionPrototypeBind(fn, this); - this.#state[kWantsHeaders] = true; + assertHeadersSupported(inner.session); + inner.onheaders = FunctionPrototypeBind(fn, this); + inner.state.wantsHeaders = true; } } /** @type {Function|undefined} */ get oninfo() { - QuicStream.#assertIsQuicStream(this); - return this.#oninfo; + assertIsQuicStream(this); + return this.#inner.oninfo; } set oninfo(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#oninfo = undefined; + inner.oninfo = undefined; } else { - this.#assertHeadersSupported(); validateFunction(fn, 'oninfo'); - this.#oninfo = FunctionPrototypeBind(fn, this); + assertHeadersSupported(inner.session); + inner.oninfo = FunctionPrototypeBind(fn, this); } } /** @type {Function|undefined} */ get ontrailers() { - QuicStream.#assertIsQuicStream(this); - return this.#ontrailers; + assertIsQuicStream(this); + return this.#inner.ontrailers; } set ontrailers(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#ontrailers = undefined; + inner.ontrailers = undefined; } else { - this.#assertHeadersSupported(); validateFunction(fn, 'ontrailers'); - this.#ontrailers = FunctionPrototypeBind(fn, this); + assertHeadersSupported(inner.session); + inner.ontrailers = FunctionPrototypeBind(fn, this); } } /** @type {Function|undefined} */ get onwanttrailers() { - QuicStream.#assertIsQuicStream(this); - return this.#onwanttrailers; + assertIsQuicStream(this); + return this.#inner.onwanttrailers; } set onwanttrailers(fn) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; if (fn === undefined) { - this.#onwanttrailers = undefined; - this.#state[kWantsTrailers] = false; + inner.onwanttrailers = undefined; + inner.state.wantsTrailers = false; } else { - this.#assertHeadersSupported(); validateFunction(fn, 'onwanttrailers'); - this.#onwanttrailers = FunctionPrototypeBind(fn, this); - this.#state[kWantsTrailers] = true; + assertHeadersSupported(inner.session); + inner.onwanttrailers = FunctionPrototypeBind(fn, this); + inner.state.wantsTrailers = true; } } @@ -1707,8 +1825,8 @@ class QuicStream { * @type {object|undefined} */ get headers() { - QuicStream.#assertIsQuicStream(this); - return this.#headers; + assertIsQuicStream(this); + return this.#inner.headers; } /** @@ -1716,22 +1834,20 @@ class QuicStream { * @type {object|undefined} */ get pendingTrailers() { - QuicStream.#assertIsQuicStream(this); - return this.#pendingTrailers; + assertIsQuicStream(this); + return this.#inner.pendingTrailers; } set pendingTrailers(headers) { - QuicStream.#assertIsQuicStream(this); + const inner = this.#inner; + assertIsQuicStream(this); + assertHeadersSupported(inner.session); if (headers === undefined) { - this.#pendingTrailers = undefined; + inner.pendingTrailers = undefined; return; } - if (getQuicSessionState(this.#session).headersSupported === 2) { - throw new ERR_INVALID_STATE( - 'The negotiated QUIC application protocol does not support headers'); - } validateObject(headers, 'headers'); - this.#pendingTrailers = headers; + inner.pendingTrailers = headers; } /** @@ -1739,8 +1855,12 @@ class QuicStream { * @type {QuicStreamStats} */ get stats() { - QuicStream.#assertIsQuicStream(this); - return this.#stats; + assertIsQuicStream(this); + const inner = this.#inner; + const handle = this.#handle; + return inner.stats ??= (handle == null) ? + QuicStreamStats[kCreateDisconnected]() : + new QuicStreamStats(kPrivateConstructor, handle.stats, handle.statsByteOffset); } /** @@ -1749,31 +1869,28 @@ class QuicStream { * @type {QuicSession | null} */ get session() { - QuicStream.#assertIsQuicStream(this); - if (this.destroyed) return null; - return this.#session; + assertIsQuicStream(this); + return this.#inner.session; } /** - * Returns the id for this stream. If the stream is destroyed or still pending, + * Returns the id for this stream. If the stream is still pending, * `null` will be returned. * @type {bigint | null} */ get id() { - QuicStream.#assertIsQuicStream(this); - if (this.destroyed || this.pending) return null; - return this.#state.id; + assertIsQuicStream(this); + if (this.pending) return null; + return this.#inner.state.id; } /** - * Returns the directionality of this stream. If the stream is destroyed - * or still pending, `null` will be returned. - * @type {'bidi'|'uni'|null} + * Returns the directionality of this stream. + * @type {'bidi'|'uni'} */ get direction() { - QuicStream.#assertIsQuicStream(this); - if (this.destroyed || this.pending) return null; - return this.#direction === kStreamDirectionBidirectional ? 'bidi' : 'uni'; + assertIsQuicStream(this); + return this.#inner.direction === kStreamDirectionBidirectional ? 'bidi' : 'uni'; } /** @@ -1781,7 +1898,7 @@ class QuicStream { * @type {boolean} */ get destroyed() { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); return this.#handle === undefined; } @@ -1790,8 +1907,9 @@ class QuicStream { * @type {Promise} */ get closed() { - QuicStream.#assertIsQuicStream(this); - return this.#pendingClose.promise; + assertIsQuicStream(this); + this.#inner.pendingClose ??= PromiseWithResolvers(); + return this.#inner.pendingClose.promise; } /** @@ -1805,19 +1923,11 @@ class QuicStream { * `QuicError`) -> the negotiated application's "internal error" * code from `QuicSessionState.internalErrorCode`. * @param {any} error - * @param {object} [options] - * @param {bigint|number} [options.code] An explicit application - * error code to send on the resulting `RESET_STREAM` / - * `STOP_SENDING` frames. Numbers are coerced to `BigInt`. When - * omitted, the code is derived from `error` per the precedence - * above. - * @param {string} [options.reason] Optional human-readable reason. - * Accepted for symmetry with `session.close()` / - * `session.destroy()`; QUIC `RESET_STREAM` and `STOP_SENDING` - * frames do not themselves carry a reason field over the wire. + * @param {QuicStreamDestroyOptions} [options] */ destroy(error, options = kEmptyObject) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); + const inner = this.#inner; // Two distinct guards: // * `#destroying` flips synchronously here so any re-entrant call // from inside this method's user callbacks hits the guard and @@ -1827,7 +1937,7 @@ class QuicStream { // `onStreamClose -> [kFinishClose]` path - which does NOT go // through `destroy()` and therefore never sets `#destroying`. // `[kFinishClose]` clears `#handle` at the end of its work. - if (this.#destroying || this.destroyed) return; + if (inner.destroying || this.destroyed) return; // Validate options up front so a malformed `options` argument // throws before any side effects (mutating `#destroying`, // emitting wire frames, invoking `onerror`, settling the closed @@ -1843,30 +1953,25 @@ class QuicStream { if (reason !== undefined) { validateString(reason, 'options.reason'); } - this.#destroying = true; + inner.destroying = true; // Resolve the wire error code for any RESET_STREAM / STOP_SENDING // frames emitted below. let abortCode; if (optionCode !== undefined) { abortCode = BigInt(optionCode); } else if (error !== undefined) { - abortCode = error instanceof QuicError ? + abortCode = QuicError.isQuicError(error) ? error.errorCode : - getQuicSessionState(this.#session).internalErrorCode; + getQuicSessionState(inner.session).internalErrorCode; } // When destroying with an error, ensure the peer stops sending // data we are about to discard by emitting STOP_SENDING. The // condition gates the emission to error-path destroys with a - // still-open readable side. Direction model for the readable - // side: - // * bidi: always has a readable side. - // * uni + #reader !== undefined: remote-initiated, read-only. - // * uni + #reader === undefined: locally-initiated, write-only; - // no readable side to stop. + // still-open readable side. The C++ state.readEnded flag is + // authoritative -- it is set for locally-initiated uni streams + // (which have no readable side) and when reading completes. if (abortCode !== undefined && - !this.#state.readEnded && - (this.#direction === kStreamDirectionBidirectional || - this.#reader !== undefined)) { + !inner.state.readEnded) { this.#handle.stopSending(abortCode); } // When destroying with an error, ensure the peer learns about @@ -1875,22 +1980,14 @@ class QuicStream { // streams that destroy without ever accessing stream.writer // (e.g. used setBody or never wrote at all) need an explicit // RESET_STREAM here so the write side does not dangle on the - // wire. The condition gates the emission to error-path destroys - // with a still-open writable side. - // Direction model for the writable side: - // * bidi: always has a writable side. - // * uni + #reader === undefined: locally-initiated, write-only. - // * uni + #reader !== undefined: remote-initiated, read-only; - // no writable side to reset. + // wire. The C++ state.writeEnded flag is authoritative. if (abortCode !== undefined && - this.#writer === undefined && - !this.#state.writeEnded && - (this.#direction === kStreamDirectionBidirectional || - this.#reader === undefined)) { + inner.writer === undefined && + !inner.state.writeEnded) { this.#handle.resetStream(abortCode); } - if (error !== undefined && typeof this.#onerror === 'function') { - invokeOnerror(this.#onerror, error); + if (error !== undefined && typeof inner.onerror === 'function') { + invokeOnerror(inner.onerror, error); } const handle = this.#handle; this[kFinishClose](error); @@ -1906,34 +2003,32 @@ class QuicStream { * @param {ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob} outbound */ setOutbound(outbound) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) { throw new ERR_INVALID_STATE('Stream is destroyed'); } - if (this.#state.hasOutbound) { + if (this.#inner.state.hasOutbound) { throw new ERR_INVALID_STATE('Stream already has an outbound data source'); } this.#handle.attachSource(validateBody(outbound)); } /** - * Send initial or response headers on this stream. Throws if the - * application does not support headers. * @param {object} headers - * @param {{ terminal?: boolean }} [options] + * @param {SendHeadersOptions} [options] * @returns {boolean} */ sendHeaders(headers, options = kEmptyObject) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) return false; - if (getQuicSessionState(this.#session).headersSupported === 2) { + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { throw new ERR_INVALID_STATE( 'The negotiated QUIC application protocol does not support headers'); } validateObject(headers, 'headers'); const { terminal = false } = options; const headerString = buildNgHeaderString( - headers, assertValidPseudoHeader, true); + headers, assertValidPseudoHeader, true /* strictSingleValueFields */); const flags = terminal ? kHeadersFlagsTerminal : kHeadersFlagsNone; return this.#handle.sendHeaders(kHeadersKindInitial, headerString, flags); } @@ -1945,9 +2040,9 @@ class QuicStream { * @returns {boolean} */ sendInformationalHeaders(headers) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) return false; - if (getQuicSessionState(this.#session).headersSupported === 2) { + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { throw new ERR_INVALID_STATE( 'The negotiated QUIC application protocol does not support headers'); } @@ -1966,9 +2061,9 @@ class QuicStream { * @returns {boolean} */ sendTrailers(headers) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) return false; - if (getQuicSessionState(this.#session).headersSupported === 2) { + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { throw new ERR_INVALID_STATE( 'The negotiated QUIC application protocol does not support headers'); } @@ -1985,9 +2080,10 @@ class QuicStream { * @type {object} */ get writer() { - QuicStream.#assertIsQuicStream(this); - if (this.#writer !== undefined) return this.#writer; - if (this.#outboundSet) { + assertIsQuicStream(this); + const inner = this.#inner; + if (inner.writer !== undefined) return inner.writer; + if (inner.outboundSet) { throw new ERR_INVALID_STATE( 'Stream outbound already configured with a body source'); } @@ -2018,7 +2114,7 @@ class QuicStream { // more data. Refuse the sync write. // If a drain is already pending, another operation is waiting // for capacity. Refuse the sync write. - if (closed || errored || stream.#state.writeEnded || drainWakeup != null) { + if (closed || errored || stream.#inner.state.writeEnded || drainWakeup != null) { return false; } chunk = toUint8Array(chunk); @@ -2032,7 +2128,7 @@ class QuicStream { // at which point the standard drain mechanism takes over. // This follows the Web Streams model where writes beyond the HWM // succeed and backpressure applies to *subsequent* writes. - if (stream.#state.writeDesiredSize === 0) return false; + if (stream.#inner.state.writeDesiredSize === 0) return false; const result = handle.write([chunk]); if (result === undefined) return false; totalBytesWritten += len; @@ -2047,7 +2143,7 @@ class QuicStream { signal.throwIfAborted(); } if (errored) throw error; - if (closed || stream.#state.writeEnded) { + if (closed || stream.#inner.state.writeEnded) { throw new ERR_INVALID_STATE('Writer is closed'); } // If a drain is already pending, another operation is waiting @@ -2064,14 +2160,14 @@ class QuicStream { } function writevSync(chunks) { - if (closed || errored || stream.#state.writeEnded || drainWakeup != null) { + if (closed || errored || stream.#inner.state.writeEnded || drainWakeup != null) { return false; } chunks = convertChunks(chunks); let len = 0; for (const c of chunks) len += TypedArrayPrototypeGetByteLength(c); if (len === 0) return true; - if (stream.#state.writeDesiredSize === 0) return false; + if (stream.#inner.state.writeDesiredSize === 0) return false; const result = handle.write(chunks); if (result === undefined) return false; totalBytesWritten += len; @@ -2087,7 +2183,7 @@ class QuicStream { } if (errored) throw error; - if (closed || stream.#state.writeEnded) { + if (closed || stream.#inner.state.writeEnded) { throw new ERR_INVALID_STATE('Writer is closed'); } @@ -2178,9 +2274,9 @@ class QuicStream { // `H3_INTERNAL_ERROR` (0x102); for raw QUIC applications // it falls back to the QUIC transport-layer // `INTERNAL_ERROR` (0x1). - const code = error instanceof QuicError ? + const code = QuicError.isQuicError(error) ? error.errorCode : - getQuicSessionState(stream.#session).internalErrorCode; + getQuicSessionState(stream.#inner.session).internalErrorCode; handle.resetStream(code); if (drainWakeup != null) { drainWakeup.reject(error); @@ -2191,8 +2287,8 @@ class QuicStream { const writer = { __proto__: null, get desiredSize() { - if (closed || errored || stream.#state.writeEnded) return null; - return stream.#state.writeDesiredSize; + if (closed || errored || stream.#inner.state.writeEnded) return null; + return stream.#inner.state.writeDesiredSize; }, writeSync, write, @@ -2205,7 +2301,7 @@ class QuicStream { if (closed || errored) return null; // If a drain is already pending, return the existing promise. if (drainWakeup != null) return drainWakeup.promise; - if (stream.#state.writeDesiredSize > 0) return null; + if (stream.#inner.state.writeDesiredSize > 0) return null; drainWakeup = PromiseWithResolvers(); return drainWakeup.promise; }, @@ -2219,45 +2315,46 @@ class QuicStream { }; // Non-writable stream - return a pre-closed writer. - // A readable unidirectional stream is a remote uni (read-only). - if (!handle || this.destroyed || this.#state.writeEnded || - (this.#direction === kStreamDirectionUnidirectional && - this.#reader !== undefined)) { + // A remote unidirectional stream is read-only and has no writable + // side. isLocal distinguishes locally-initiated (writable) from + // remotely-initiated (read-only) uni streams. + if (!handle || this.destroyed || inner.state.writeEnded || + (inner.direction === kStreamDirectionUnidirectional && + !inner.isLocal)) { closed = true; - this.#writer = writer; - return this.#writer; + return inner.writer = writer; } // Initialize the outbound DataQueue for streaming writes handle.initStreamingSource(); initStreamingBackpressure(this); - this.#writer = writer; - return this.#writer; + return inner.writer = writer; } /** * Sets the outbound body source for this stream. Accepts all body * source types (string, TypedArray, Blob, AsyncIterable, Promise, null). * Can only be called once. Mutually exclusive with stream.writer. - * @param {*} body + * @param {any} body */ setBody(body) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) { throw new ERR_INVALID_STATE('Stream is destroyed'); } - if (this.#outboundSet) { + const inner = this.#inner; + if (inner.outboundSet) { throw new ERR_INVALID_STATE('Stream outbound already configured'); } - if (this.#writer !== undefined) { + if (inner.writer !== undefined) { throw new ERR_INVALID_STATE('Stream writer already accessed'); } - this.#outboundSet = true; + inner.outboundSet = true; // If the body is a FileHandle, store it so it is closed // automatically when the stream finishes. - if (body instanceof FileHandle) { - this.#fileHandle = body; + if (FileHandle.isFileHandle(body)) { + inner.fileHandle = body; } configureOutbound(this.#handle, this, body); } @@ -2269,7 +2366,7 @@ class QuicStream { * @param {FileHandle} fh */ [kAttachFileHandle](fh) { - this.#fileHandle = fh; + this.#inner.fileHandle = fh; } /** @@ -2280,7 +2377,7 @@ class QuicStream { * @param {number|bigint} code */ stopSending(code = 0n) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) return; this.#handle.stopSending(BigInt(code)); } @@ -2293,7 +2390,7 @@ class QuicStream { * @param {number|bigint} code */ resetStream(code = 0n) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) return; this.#handle.resetStream(BigInt(code)); } @@ -2302,12 +2399,12 @@ class QuicStream { * The priority of the stream. If the stream is destroyed or if * the session does not support priority, `null` will be * returned. - * @type {{ level: 'default' | 'low' | 'high', incremental: boolean } | null} + * @type {StreamPriority | null} */ get priority() { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed || - !getQuicSessionState(this.#session).isPrioritySupported) return null; + !getQuicSessionState(this.#inner.session).isPrioritySupported) return null; const packed = this.#handle.getPriority(); const urgency = packed >> 1; const incremental = !!(packed & 1); @@ -2317,15 +2414,12 @@ class QuicStream { /** * Sets the priority of the stream. - * @param {{ - * level?: 'default' | 'low' | 'high', - * incremental?: boolean, - * }} options + * @param {StreamPriority} [options] */ setPriority(options = kEmptyObject) { - QuicStream.#assertIsQuicStream(this); + assertIsQuicStream(this); if (this.destroyed) return; - if (!getQuicSessionState(this.#session).isPrioritySupported) { + if (!getQuicSessionState(this.#inner.session).isPrioritySupported) { throw new ERR_INVALID_STATE( 'The session does not support stream priority'); } @@ -2355,7 +2449,7 @@ class QuicStream { [kSendHeaders](headers, kind = kHeadersKindInitial, flags = kHeadersFlagsTerminal) { validateObject(headers, 'headers'); - if (getQuicSessionState(this.#session).headersSupported === 2) { + if (getQuicSessionState(this.#inner.session).headersSupported === 2) { throw new ERR_INVALID_STATE( 'The negotiated QUIC application protocol does not support headers'); } @@ -2373,27 +2467,22 @@ class QuicStream { } [kFinishClose](error) { - if (this.destroyed) return this.#pendingClose.promise; + const inner = this.#inner; + inner.pendingClose ??= PromiseWithResolvers(); + if (this.destroyed) { + return inner.pendingClose.promise; + } if (error !== undefined) { - if (this.pending) { - debug(`destroying pending stream with error: ${error}`); - } else { - debug(`destroying stream ${this.id} with error: ${error}`); - } - this.#pendingClose.reject(error); + inner.pendingClose.reject(error); } else { - if (this.pending) { - debug('destroying pending stream with no error'); - } else { - debug(`destroying stream ${this.id} with no error`); - } - this.#pendingClose.resolve(); + inner.pendingClose.resolve(); } + debug('stream closed'); if (onStreamClosedChannel.hasSubscribers) { onStreamClosedChannel.publish({ __proto__: null, stream: this, - session: this.#session, + session: inner.session, error, stats: this.stats, }); @@ -2406,46 +2495,45 @@ class QuicStream { }, }); } - this.#stats[kFinishClose](); - this.#state[kFinishClose](); - this.#session[kRemoveStream](this); - if (this.#writer !== undefined) { - this.#writer.fail(error); - } - this.#session = undefined; - this.#pendingClose.reject = undefined; - this.#pendingClose.resolve = undefined; - this.#onblocked = undefined; - this.#onreset = undefined; - this.#onheaders = undefined; - this.#onerror = undefined; - this.#ontrailers = undefined; - this.#oninfo = undefined; - this.#onwanttrailers = undefined; - this.#headers = undefined; - this.#pendingTrailers = undefined; + inner.stats?.[kFinishClose](); + inner.state?.[kFinishClose](); + inner.session[kRemoveStream](this); + inner.writer?.fail(error); + inner.session = undefined; + inner.pendingClose.reject = undefined; + inner.pendingClose.resolve = undefined; + inner.onblocked = undefined; + inner.onreset = undefined; + inner.onheaders = undefined; + inner.onerror = undefined; + inner.ontrailers = undefined; + inner.oninfo = undefined; + inner.onwanttrailers = undefined; + inner.headers = undefined; + inner.pendingTrailers = undefined; this.#handle = undefined; - if (this.#fileHandle !== undefined) { + if (inner.fileHandle !== undefined) { // Close the FileHandle that was used as a body source. The close // may fail if the user already closed it -- that's expected and // harmless, so mark the promise as handled. - markPromiseAsHandled(this.#fileHandle.close()); - this.#fileHandle = undefined; + markPromiseAsHandled(this.#inner.fileHandle.close()); + inner.fileHandle = undefined; } } [kBlocked]() { + const inner = this.#inner; // The blocked event should only be called if the stream was created with // an onblocked callback. The callback should always exist here. - assert(this.#onblocked, 'Unexpected stream blocked event'); + assert(inner.onblocked, 'Unexpected stream blocked event'); if (onStreamBlockedChannel.hasSubscribers) { onStreamBlockedChannel.publish({ __proto__: null, stream: this, - session: this.#session, + session: inner.session, }); } - safeCallbackInvoke(this.#onblocked, this); + safeCallbackInvoke(inner.onblocked, this); } [kDrain]() { @@ -2454,81 +2542,85 @@ class QuicStream { } [kReset](error) { + const inner = this.#inner; // The reset event should only be called if the stream was created with // an onreset callback. The callback should always exist here. - assert(this.#onreset, 'Unexpected stream reset event'); + assert(inner.onreset, 'Unexpected stream reset event'); if (onStreamResetChannel.hasSubscribers) { onStreamResetChannel.publish({ __proto__: null, stream: this, - session: this.#session, + session: inner.session, error, }); } - safeCallbackInvoke(this.#onreset, this, error); + safeCallbackInvoke(inner.onreset, this, error); } [kHeaders](headers, kind) { const block = parseHeaderPairs(headers); const kindName = kHeadersKindName[kind] ?? kind; + const inner = this.#inner; switch (kindName) { case 'initial': - assert(this.#onheaders, 'Unexpected stream headers event'); - if (this.#headers === undefined) this.#headers = block; + assert(inner.onheaders, 'Unexpected stream headers event'); + inner.headers ??= block; if (onStreamHeadersChannel.hasSubscribers) { onStreamHeadersChannel.publish({ __proto__: null, stream: this, - session: this.#session, + session: inner.session, headers: block, }); } - safeCallbackInvoke(this.#onheaders, this, block); + safeCallbackInvoke(inner.onheaders, this, block); break; case 'trailing': if (onStreamTrailersChannel.hasSubscribers) { onStreamTrailersChannel.publish({ __proto__: null, stream: this, - session: this.#session, + session: inner.session, trailers: block, }); } - if (this.#ontrailers) - safeCallbackInvoke(this.#ontrailers, this, block); + if (inner.ontrailers) + safeCallbackInvoke(inner.ontrailers, this, block); break; case 'hints': if (onStreamInfoChannel.hasSubscribers) { onStreamInfoChannel.publish({ __proto__: null, stream: this, - session: this.#session, + session: inner.session, headers: block, }); } - if (this.#oninfo) - safeCallbackInvoke(this.#oninfo, this, block); + if (typeof inner.oninfo === 'function') + safeCallbackInvoke(inner.oninfo, this, block); break; } } [kTrailers]() { if (this.destroyed) return; + const inner = this.#inner; // nghttp3 is asking us to provide trailers to send. // Check for pre-set pendingTrailers first, then the callback. - if (this.#pendingTrailers) { - this.sendTrailers(this.#pendingTrailers); - this.#pendingTrailers = undefined; - } else if (this.#onwanttrailers) { - safeCallbackInvoke(this.#onwanttrailers, this); + if (inner.pendingTrailers) { + this.sendTrailers(inner.pendingTrailers); + inner.pendingTrailers = undefined; + } else if (typeof inner.onwanttrailers === 'function') { + safeCallbackInvoke(inner.onwanttrailers, this); } } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicStream { }'; + } const opts = { __proto__: null, @@ -2536,105 +2628,90 @@ class QuicStream { depth: options.depth == null ? null : options.depth - 1, }; + const { + id, + direction, + pending, + stats, + session, + } = this; + return `QuicStream ${inspect({ __proto__: null, - id: this.id, - direction: this.direction, - pending: this.pending, - stats: this.stats, - state: this.#state, - session: this.session, + id, + direction, + pending, + stats, + state: this.#inner.state, + session, }, opts)}`; } } class QuicSession { - /** @type {QuicEndpoint} */ - #endpoint = undefined; - /** @type {boolean} */ - #isPendingClose = false; - /** @type {boolean} */ - #selfInitiatedClose = false; - /** - * Flag set at the top of `destroy()` to make the method safely - * re-entrant. Distinct from `#handle === undefined` so callbacks - * that fire from C++ during teardown (e.g. `onSessionClose` -> - * `[kFinishClose]`) still see a live `#handle` and can complete - * their work. - * @type {boolean} - */ - #destroying = false; - /** - * Set to `true` once the TLS handshake has completed successfully - * (i.e. `[kHandshake]` has fired). Used to gate operations that only - * make sense for a fully-opened session - notably, attempting to - * send a `CONNECTION_CLOSE` from `endpoint.destroy(error)` cascade. - * The C++ side cannot create a valid `CONNECTION_CLOSE` packet - * before handshake completion and falls back to a path that - * re-enters JS `destroy()` and trips our `#destroying` guard, - * leaving the C++ side asserting an inconsistent state. - * @type {boolean} - */ - #handshakeCompleted = false; /** @type {object|undefined} */ #handle; - /** @type {PromiseWithResolvers} */ - #pendingClose = PromiseWithResolvers(); - /** @type {PromiseWithResolvers} */ - #pendingOpen = PromiseWithResolvers(); - /** @type {QuicSessionState} */ - #state; - /** @type {QuicSessionStats} */ - #stats; - /** @type {Set} */ - #streams = new SafeSet(); - /** @type {Function|undefined} */ - #onerror = undefined; - /** @type {OnStreamCallback} */ - #onstream = undefined; - /** @type {OnDatagramCallback|undefined} */ - #ondatagram = undefined; - /** @type {OnDatagramStatusCallback|undefined} */ - #ondatagramstatus = undefined; - /** @type {Function|undefined} */ - #onpathvalidation = undefined; - /** @type {Function|undefined} */ - #onsessionticket = undefined; - /** @type {Function|undefined} */ - #onversionnegotiation = undefined; - /** @type {Function|undefined} */ - #onhandshake = undefined; - /** @type {Function|undefined} */ - #onnewtoken = undefined; - /** @type {Function|undefined} */ - #onearlyrejected = undefined; - /** @type {Function|undefined} */ - #onorigin = undefined; - /** @type {Function|undefined} */ - #ongoaway = undefined; - /** @type {Function|undefined} */ - #onkeylog = undefined; - /** @type {Function|undefined} */ - #onqlog = undefined; - #pendingQlog = undefined; - #handshakeInfo = undefined; - /** @type {{ local: SocketAddress, remote: SocketAddress }|undefined} */ - #path = undefined; - #certificate = undefined; - #peerCertificate = undefined; - #ephemeralKeyInfo = undefined; + + #inner = { + __proto__: null, + /** @type {QuicEndpoint} */ + endpoint: undefined, + isPendingClose: false, + selfInitiatedClose: false, + destroying: false, + handshakeCompleted: false, + pendingClose: PromiseWithResolvers(), + pendingOpen: PromiseWithResolvers(), + /** @type {QuicSessionState} */ + state: undefined, + /** @type {QuicSessionStats} */ + stats: undefined, + streams: new SafeSet(), + onerror: undefined, + onstream: undefined, + ondatagram: undefined, + ondatagramstatus: undefined, + onpathvalidation: undefined, + onsessionticket: undefined, + onversionnegotiation: undefined, + onhandshake: undefined, + onnewtoken: undefined, + onearlyrejected: undefined, + onorigin: undefined, + ongoaway: undefined, + onkeylog: undefined, + onqlog: undefined, + pendingQlog: undefined, + // Default to 'manual' (no auto-rejection). Client sessions override + // this via kVerifyPeer in kConnect. Server sessions keep 'manual' + // because server-side cert validation is handled by rejectUnauthorized + // at the C++ level. + verifyPeer: 'manual', + handshakeInfo: undefined, + /** @type {QuicSessionPath|undefined} */ + path: undefined, + certificate: undefined, + peerCertificate: undefined, + ephemeralKeyInfo: undefined, + localTransportParams: undefined, + remoteTransportParams: undefined, + }; static { - getQuicSessionState = function(session) { - QuicSession.#assertIsQuicSession(session); - return session.#state; + isQuicSession = function(val) { + return val != null && typeof val === 'object' && #handle in val; }; - } - static #assertIsQuicSession(val) { - if (val == null || !(#handle in val)) { - throw new ERR_INVALID_THIS('QuicSession'); - } + assertIsQuicSession = function(val) { + if (!isQuicSession(val)) { + throw new ERR_INVALID_THIS('QuicSession'); + } + }; + + getQuicSessionState = function(session) { + assertIsQuicSession(session); + return session.#inner.state; + }; } /** @@ -2644,20 +2721,22 @@ class QuicSession { */ constructor(privateSymbol, handle, endpoint) { // Instances of QuicSession can only be created internally. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } + assertPrivateSymbol(privateSymbol); - this.#endpoint = endpoint; this.#handle = handle; this.#handle[kOwner] = this; + + const inner = this.#inner; + inner.endpoint = endpoint; // Move any qlog entries that arrived before the wrapper existed. if (handle._pendingQlog !== undefined) { - this.#pendingQlog = handle._pendingQlog; + inner.pendingQlog = handle._pendingQlog; handle._pendingQlog = undefined; } - this.#stats = new QuicSessionStats(kPrivateConstructor, handle.stats); - this.#state = new QuicSessionState(kPrivateConstructor, handle.state); + inner.stats = new QuicSessionStats( + kPrivateConstructor, handle.stats, handle.statsByteOffset); + inner.state = new QuicSessionState( + kPrivateConstructor, handle.state, handle.stateByteOffset); if (hasObserver('quic')) { startPerf(this, kPerfEntry, { type: 'quic', name: 'QuicSession' }); @@ -2666,34 +2745,82 @@ class QuicSession { debug('session created'); } + get applicationOptions() { + // We don't cache application options because they may be updated by the + // C++ layer after session creation depending on the behavior of the + // application. + if (this.destroyed) return null; + return this.#handle.applicationOptions(); + } + + get localTransportParams() { + if (this.#inner.localTransportParams !== undefined) { + return this.#inner.localTransportParams; + } + // If the handle is already gone, we cannot retrieve the transport params. + if (this.destroyed) return null; + const params = this.#handle.localTransportParams(); + if (params.preferredAddressIpv4 !== undefined) { + params.preferredAddressIpv4 = new InternalSocketAddress(params.preferredAddressIpv4); + } + if (params.preferredAddressIpv6 !== undefined) { + params.preferredAddressIpv6 = new InternalSocketAddress(params.preferredAddressIpv6); + } + return this.#inner.localTransportParams = params; + } + + get remoteTransportParams() { + if (this.#inner.remoteTransportParams !== undefined) { + return this.#inner.remoteTransportParams; + } + // If the handle is already gone, we cannot retrieve the transport params. + if (this.destroyed) return null; + const params = this.#handle.remoteTransportParams(); + // If params is undefined, the transport parameters have not yet been received. + // Note the distinction between this and the case where the handle is gone. + // If the handle is gone, we return null because we know the transport + // parameters will be unavailable. If the transport parameters have not yet + // been received, we return undefined to indicate that they may still become + // available in the future. + if (params === undefined) return undefined; + if (params.preferredAddressIpv4 !== undefined) { + params.preferredAddressIpv4 = new InternalSocketAddress(params.preferredAddressIpv4); + } + if (params.preferredAddressIpv6 !== undefined) { + params.preferredAddressIpv6 = new InternalSocketAddress(params.preferredAddressIpv6); + } + return this.#inner.remoteTransportParams = params; + } + /** @type {boolean} */ get #isClosedOrClosing() { - return this.#handle === undefined || this.#isPendingClose; + return this.#handle === undefined || this.#inner.isPendingClose; } /** @type {Function|undefined} */ get onerror() { - QuicSession.#assertIsQuicSession(this); - return this.#onerror; + assertIsQuicSession(this); + return this.#inner.onerror; } set onerror(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onerror = undefined; + inner.onerror = undefined; } else { validateFunction(fn, 'onerror'); - this.#onerror = FunctionPrototypeBind(fn, this); + inner.onerror = FunctionPrototypeBind(fn, this); // When an onerror handler is provided, mark the pending promises // as handled so that rejections from destroy(error) don't surface // as unhandled rejections. The onerror callback is the // application's error handler for this session. - markPromiseAsHandled(this.#pendingClose.promise); - markPromiseAsHandled(this.#pendingOpen.promise); + markPromiseAsHandled(inner.pendingClose.promise); + markPromiseAsHandled(inner.pendingOpen.promise); // Also mark existing streams' closed promises. Stream rejections // during session destruction are expected collateral when the // session has an error handler. - for (const stream of this.#streams) { + for (const stream of inner.streams) { markPromiseAsHandled(stream.closed); } } @@ -2701,35 +2828,37 @@ class QuicSession { /** @type {OnStreamCallback} */ get onstream() { - QuicSession.#assertIsQuicSession(this); - return this.#onstream; + assertIsQuicSession(this); + return this.#inner.onstream; } set onstream(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onstream = undefined; + inner.onstream = undefined; } else { validateFunction(fn, 'onstream'); - this.#onstream = FunctionPrototypeBind(fn, this); + inner.onstream = FunctionPrototypeBind(fn, this); } } /** @type {OnDatagramCallback} */ get ondatagram() { - QuicSession.#assertIsQuicSession(this); - return this.#ondatagram; + assertIsQuicSession(this); + return this.#inner.ondatagram; } set ondatagram(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#ondatagram = undefined; - this.#state.hasDatagramListener = false; + inner.ondatagram = undefined; + inner.state.hasDatagramListener = false; } else { validateFunction(fn, 'ondatagram'); - this.#ondatagram = FunctionPrototypeBind(fn, this); - this.#state.hasDatagramListener = true; + inner.ondatagram = FunctionPrototypeBind(fn, this); + inner.state.hasDatagramListener = true; } } @@ -2739,71 +2868,75 @@ class QuicSession { * @type {OnDatagramStatusCallback} */ get ondatagramstatus() { - QuicSession.#assertIsQuicSession(this); - return this.#ondatagramstatus; + assertIsQuicSession(this); + return this.#inner.ondatagramstatus; } set ondatagramstatus(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#ondatagramstatus = undefined; - this.#state.hasDatagramStatusListener = false; + inner.ondatagramstatus = undefined; + inner.state.hasDatagramStatusListener = false; } else { validateFunction(fn, 'ondatagramstatus'); - this.#ondatagramstatus = FunctionPrototypeBind(fn, this); - this.#state.hasDatagramStatusListener = true; + inner.ondatagramstatus = FunctionPrototypeBind(fn, this); + inner.state.hasDatagramStatusListener = true; } } /** @type {Function|undefined} */ get onpathvalidation() { - QuicSession.#assertIsQuicSession(this); - return this.#onpathvalidation; + assertIsQuicSession(this); + return this.#inner.onpathvalidation; } set onpathvalidation(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onpathvalidation = undefined; - this.#state.hasPathValidationListener = false; + inner.onpathvalidation = undefined; + inner.state.hasPathValidationListener = false; } else { validateFunction(fn, 'onpathvalidation'); - this.#onpathvalidation = FunctionPrototypeBind(fn, this); - this.#state.hasPathValidationListener = true; + inner.onpathvalidation = FunctionPrototypeBind(fn, this); + inner.state.hasPathValidationListener = true; } } get onkeylog() { - QuicSession.#assertIsQuicSession(this); - return this.#onkeylog; + assertIsQuicSession(this); + return this.#inner.onkeylog; } set onkeylog(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onkeylog = undefined; + inner.onkeylog = undefined; } else { validateFunction(fn, 'onkeylog'); - this.#onkeylog = FunctionPrototypeBind(fn, this); + inner.onkeylog = FunctionPrototypeBind(fn, this); } } get onqlog() { - QuicSession.#assertIsQuicSession(this); - return this.#onqlog; + assertIsQuicSession(this); + return this.#inner.onqlog; } set onqlog(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onqlog = undefined; + inner.onqlog = undefined; } else { validateFunction(fn, 'onqlog'); - this.#onqlog = FunctionPrototypeBind(fn, this); + inner.onqlog = FunctionPrototypeBind(fn, this); // Flush any qlog entries that were cached before the callback was set. - if (this.#pendingQlog !== undefined) { - const pending = this.#pendingQlog; - this.#pendingQlog = undefined; + if (inner.pendingQlog !== undefined) { + const pending = inner.pendingQlog; + inner.pendingQlog = undefined; for (let i = 0; i < pending.length; i += 2) { this[kQlog](pending[i], pending[i + 1]); } @@ -2813,119 +2946,126 @@ class QuicSession { /** @type {Function|undefined} */ get onsessionticket() { - QuicSession.#assertIsQuicSession(this); - return this.#onsessionticket; + assertIsQuicSession(this); + return this.#inner.onsessionticket; } set onsessionticket(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onsessionticket = undefined; - this.#state.hasSessionTicketListener = false; + inner.onsessionticket = undefined; + inner.state.hasSessionTicketListener = false; } else { validateFunction(fn, 'onsessionticket'); - this.#onsessionticket = FunctionPrototypeBind(fn, this); - this.#state.hasSessionTicketListener = true; + inner.onsessionticket = FunctionPrototypeBind(fn, this); + inner.state.hasSessionTicketListener = true; } } /** @type {Function|undefined} */ get onversionnegotiation() { - QuicSession.#assertIsQuicSession(this); - return this.#onversionnegotiation; + assertIsQuicSession(this); + return this.#inner.onversionnegotiation; } set onversionnegotiation(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onversionnegotiation = undefined; + inner.onversionnegotiation = undefined; } else { validateFunction(fn, 'onversionnegotiation'); - this.#onversionnegotiation = FunctionPrototypeBind(fn, this); + inner.onversionnegotiation = FunctionPrototypeBind(fn, this); } } /** @type {Function|undefined} */ get onhandshake() { - QuicSession.#assertIsQuicSession(this); - return this.#onhandshake; + assertIsQuicSession(this); + return this.#inner.onhandshake; } set onhandshake(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onhandshake = undefined; + inner.onhandshake = undefined; } else { validateFunction(fn, 'onhandshake'); - this.#onhandshake = FunctionPrototypeBind(fn, this); + inner.onhandshake = FunctionPrototypeBind(fn, this); } } /** @type {Function|undefined} */ get onnewtoken() { - QuicSession.#assertIsQuicSession(this); - return this.#onnewtoken; + assertIsQuicSession(this); + return this.#inner.onnewtoken; } set onnewtoken(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onnewtoken = undefined; - this.#state.hasNewTokenListener = false; + inner.onnewtoken = undefined; + inner.state.hasNewTokenListener = false; } else { validateFunction(fn, 'onnewtoken'); - this.#onnewtoken = FunctionPrototypeBind(fn, this); - this.#state.hasNewTokenListener = true; + inner.onnewtoken = FunctionPrototypeBind(fn, this); + inner.state.hasNewTokenListener = true; } } /** @type {Function|undefined} */ get onearlyrejected() { - QuicSession.#assertIsQuicSession(this); - return this.#onearlyrejected; + assertIsQuicSession(this); + return this.#inner.onearlyrejected; } set onearlyrejected(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onearlyrejected = undefined; + inner.onearlyrejected = undefined; } else { validateFunction(fn, 'onearlyrejected'); - this.#onearlyrejected = FunctionPrototypeBind(fn, this); + inner.onearlyrejected = FunctionPrototypeBind(fn, this); } } /** @type {Function|undefined} */ get onorigin() { - QuicSession.#assertIsQuicSession(this); - return this.#onorigin; + assertIsQuicSession(this); + return this.#inner.onorigin; } set onorigin(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#onorigin = undefined; - this.#state.hasOriginListener = false; + inner.onorigin = undefined; + inner.state.hasOriginListener = false; } else { validateFunction(fn, 'onorigin'); - this.#onorigin = FunctionPrototypeBind(fn, this); - this.#state.hasOriginListener = true; + inner.onorigin = FunctionPrototypeBind(fn, this); + inner.state.hasOriginListener = true; } } /** @type {Function|undefined} */ get ongoaway() { - QuicSession.#assertIsQuicSession(this); - return this.#ongoaway; + assertIsQuicSession(this); + return this.#inner.ongoaway; } set ongoaway(fn) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; if (fn === undefined) { - this.#ongoaway = undefined; + inner.ongoaway = undefined; } else { validateFunction(fn, 'ongoaway'); - this.#ongoaway = FunctionPrototypeBind(fn, this); + inner.ongoaway = FunctionPrototypeBind(fn, this); } } @@ -2935,8 +3075,8 @@ class QuicSession { * @type {bigint} */ get maxDatagramSize() { - QuicSession.#assertIsQuicSession(this); - return this.#state.maxDatagramSize; + assertIsQuicSession(this); + return this.#inner.state.maxDatagramSize; } /** @@ -2946,14 +3086,14 @@ class QuicSession { * @type {number} */ get maxPendingDatagrams() { - QuicSession.#assertIsQuicSession(this); - return this.#state.maxPendingDatagrams; + assertIsQuicSession(this); + return this.#inner.state.maxPendingDatagrams; } set maxPendingDatagrams(val) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); validateInteger(val, 'maxPendingDatagrams', 0, 0xFFFF); - this.#state.maxPendingDatagrams = val; + this.#inner.state.maxPendingDatagrams = val; } /** @@ -2961,8 +3101,8 @@ class QuicSession { * @type {QuicSessionStats} */ get stats() { - QuicSession.#assertIsQuicSession(this); - return this.#stats; + assertIsQuicSession(this); + return this.#inner.stats; } /** @@ -2971,19 +3111,19 @@ class QuicSession { * @type {QuicEndpoint|null} */ get endpoint() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.destroyed) return null; - return this.#endpoint; + return this.#inner.endpoint; } /** * The local and remote socket addresses associated with the session. - * @type {{ local: SocketAddress, remote: SocketAddress } | undefined} + * @type {QuicSessionPath | undefined} */ get path() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#path ??= { + return this.#inner.path ??= { __proto__: null, local: new InternalSocketAddress(this.#handle.getLocalAddress()), remote: new InternalSocketAddress(this.#handle.getRemoteAddress()), @@ -2995,9 +3135,9 @@ class QuicSession { * @type {object|undefined} */ get certificate() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#certificate ??= this.#handle.getCertificate(); + return this.#inner.certificate ??= this.#handle.getCertificate(); } /** @@ -3006,9 +3146,9 @@ class QuicSession { * @type {object|undefined} */ get peerCertificate() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#peerCertificate ??= this.#handle.getPeerCertificate(); + return this.#inner.peerCertificate ??= this.#handle.getPeerCertificate(); } /** @@ -3018,9 +3158,9 @@ class QuicSession { * @type {object|undefined} */ get ephemeralKeyInfo() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#ephemeralKeyInfo ??= this.#handle.getEphemeralKey(); + return this.#inner.ephemeralKeyInfo ??= this.#handle.getEphemeralKey(); } /** @@ -3029,11 +3169,12 @@ class QuicSession { * @returns {QuicStream} */ async #createStream(direction, options = kEmptyObject) { + const inner = this.#inner; if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed. New streams cannot be opened.'); } const dir = direction === kStreamDirectionBidirectional ? 'bidi' : 'uni'; - if (this.#state.isStreamOpenAllowed) { + if (inner.state.isStreamOpenAllowed) { debug(`opening new pending ${dir} stream`); } else { debug(`opening new ${dir} stream`); @@ -3062,20 +3203,21 @@ class QuicSession { throw new ERR_QUIC_OPEN_STREAM_FAILED(); } - if (this.#state.isPrioritySupported) { + if (inner.state.isPrioritySupported) { const urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3; handle.setPriority((urgency << 1) | (incremental ? 1 : 0)); } - const stream = new QuicStream(kPrivateConstructor, handle, this, direction); - this.#streams.add(stream); - if (typeof this.#onerror === 'function') { + const stream = new QuicStream( + kPrivateConstructor, handle, this, direction, true /* isLocal */); + inner.streams.add(stream); + if (typeof this.#inner.onerror === 'function') { markPromiseAsHandled(stream.closed); } // If the body was a FileHandle, store it on the stream so it is // closed automatically when the stream finishes. - if (body instanceof FileHandle) { + if (FileHandle.isFileHandle(body)) { stream[kAttachFileHandle](body); } @@ -3110,7 +3252,7 @@ class QuicSession { * @returns {Promise} */ async createBidirectionalStream(options = kEmptyObject) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); return await this.#createStream(kStreamDirectionBidirectional, options); } @@ -3121,7 +3263,7 @@ class QuicSession { * @returns {Promise} */ async createUnidirectionalStream(options = kEmptyObject) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); return await this.#createStream(kStreamDirectionUnidirectional, options); } @@ -3146,12 +3288,12 @@ class QuicSession { * @returns {Promise} The datagram ID */ async sendDatagram(datagram, encoding = 'utf8') { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed'); } - const maxDatagramSize = this.#state.maxDatagramSize; + const maxDatagramSize = this.#inner.state.maxDatagramSize; // The peer max datagram size is either unknown or they have explicitly // indicated that they do not support datagrams by setting it to 0. In @@ -3204,7 +3346,7 @@ class QuicSession { * Initiate a key update. */ updateKey() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed'); } @@ -3238,12 +3380,13 @@ class QuicSession { * @returns {Promise} */ close(options = kEmptyObject) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); options = validateCloseOptions(options); + const inner = this.#inner; if (!this.#isClosedOrClosing) { - this.#isPendingClose = true; + inner.isPendingClose = true; if (options?.code !== undefined) { - this.#selfInitiatedClose = true; + inner.selfInitiatedClose = true; } debug('gracefully closing the session'); @@ -3261,13 +3404,13 @@ class QuicSession { /** @type {boolean} */ get closing() { - return this.#isPendingClose; + return this.#inner.isPendingClose; } /** @type {Promise} */ get opened() { - QuicSession.#assertIsQuicSession(this); - return this.#pendingOpen.promise; + assertIsQuicSession(this); + return this.#inner.pendingOpen.promise; } /** @@ -3276,13 +3419,13 @@ class QuicSession { * @type {Promise} */ get closed() { - QuicSession.#assertIsQuicSession(this); - return this.#pendingClose.promise; + assertIsQuicSession(this); + return this.#inner.pendingClose.promise; } /** @type {boolean} */ get destroyed() { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); return this.#handle === undefined; } @@ -3302,7 +3445,8 @@ class QuicSession { * string included in the CONNECTION_CLOSE frame (diagnostic only). */ destroy(error, options) { - QuicSession.#assertIsQuicSession(this); + assertIsQuicSession(this); + const inner = this.#inner; // Two distinct guards (see also `QuicStream.destroy`): // * `#destroying` flips synchronously here so any re-entrant call // (e.g. from a user `onerror` callback or from a cascading @@ -3313,10 +3457,10 @@ class QuicSession { // "fully torn down". Defense-in-depth for paths that may have // finished teardown without setting `#destroying` and for // repeat invocations after this method has fully run. - if (this.#destroying || this.destroyed) return; + if (inner.destroying || this.destroyed) return; if (options !== undefined) options = validateCloseOptions(options); - this.#destroying = true; + inner.destroying = true; debug('destroying the session'); @@ -3328,31 +3472,31 @@ class QuicSession { error, }); } - if (typeof this.#onerror === 'function') { - invokeOnerror(this.#onerror, error); + if (typeof inner.onerror === 'function') { + invokeOnerror(inner.onerror, error); } } // First, forcefully and immediately destroy all open streams, if any. - for (const stream of this.#streams) { + for (const stream of inner.streams) { stream.destroy(error); } // The streams should remove themselves when they are destroyed but let's // be doubly sure. - if (this.#streams.size) { + if (inner.streams.size) { process.emitWarning( - `The session is destroyed with ${this.#streams.size} active streams. ` + + `The session is destroyed with ${inner.streams.size} active streams. ` + 'This should not happen and indicates a bug in Node.js. Please open an ' + 'issue in the Node.js GitHub repository at https://github.com/nodejs/node ' + 'to report the problem.', ); } - this.#streams.clear(); + inner.streams.clear(); // Remove this session immediately from the endpoint - this.#endpoint[kRemoveSession](this); - this.#endpoint = undefined; - this.#isPendingClose = false; + inner.endpoint[kRemoveSession](this); + inner.endpoint = undefined; + inner.isPendingClose = false; // If the handshake never completed, reject the opened promise. The // session is being destroyed, so the handshake will never complete @@ -3368,54 +3512,54 @@ class QuicSession { // rejection warning - common for server-side sessions delivered via // `onsession`, which often do not await opened. The rejection is // still observable via `await session.opened`. - if (this.#pendingOpen.reject) { - markPromiseAsHandled(this.#pendingOpen.promise); - this.#pendingOpen.reject(error ?? new ERR_INVALID_STATE( + if (inner.pendingOpen.reject) { + markPromiseAsHandled(inner.pendingOpen.promise); + inner.pendingOpen.reject(error ?? new ERR_INVALID_STATE( 'Session was destroyed before it opened')); } if (error) { // If the session is still waiting to be closed, and error // is specified, reject the closed promise. - this.#pendingClose.reject?.(error); + inner.pendingClose.reject?.(error); } else { - this.#pendingClose.resolve?.(); + inner.pendingClose.resolve?.(); } - this.#pendingClose.reject = undefined; - this.#pendingClose.resolve = undefined; - this.#pendingOpen.reject = undefined; - this.#pendingOpen.resolve = undefined; + inner.pendingClose.reject = undefined; + inner.pendingClose.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.pendingOpen.resolve = undefined; - this.#state[kFinishClose](); - this.#stats[kFinishClose](); + inner.state[kFinishClose](); + inner.stats[kFinishClose](); if (this[kPerfEntry] && hasObserver('quic')) { stopPerf(this, kPerfEntry, { detail: { - stats: this.stats, - handshake: this.#handshakeInfo, - path: this.#path, + stats: inner.stats, + handshake: inner.handshakeInfo, + path: inner.path, }, }); } - this.#onerror = undefined; - this.#onstream = undefined; - this.#ondatagram = undefined; - this.#ondatagramstatus = undefined; - this.#onpathvalidation = undefined; - this.#onsessionticket = undefined; - this.#onkeylog = undefined; - this.#onversionnegotiation = undefined; - this.#onhandshake = undefined; - this.#onnewtoken = undefined; - this.#onorigin = undefined; - this.#ongoaway = undefined; - this.#path = undefined; - this.#certificate = undefined; - this.#peerCertificate = undefined; - this.#ephemeralKeyInfo = undefined; + inner.onerror = undefined; + inner.onstream = undefined; + inner.ondatagram = undefined; + inner.ondatagramstatus = undefined; + inner.onpathvalidation = undefined; + inner.onsessionticket = undefined; + inner.onkeylog = undefined; + inner.onversionnegotiation = undefined; + inner.onhandshake = undefined; + inner.onnewtoken = undefined; + inner.onorigin = undefined; + inner.ongoaway = undefined; + inner.path = undefined; + inner.certificate = undefined; + inner.peerCertificate = undefined; + inner.ephemeralKeyInfo = undefined; // Destroy the underlying C++ handle. Pass close error options if // provided so the CONNECTION_CLOSE frame carries the correct code. @@ -3431,7 +3575,7 @@ class QuicSession { __proto__: null, session: this, error, - stats: this.stats, + stats: inner.stats, }); } } @@ -3443,7 +3587,8 @@ class QuicSession { * @param {bigint} lastStreamId */ [kGoaway](lastStreamId) { - this.#isPendingClose = true; + const inner = this.#inner; + inner.isPendingClose = true; if (onSessionClosingChannel.hasSubscribers) { onSessionClosingChannel.publish({ __proto__: null, session: this }); } @@ -3454,8 +3599,8 @@ class QuicSession { lastStreamId, }); } - if (this.#ongoaway) { - safeCallbackInvoke(this.#ongoaway, this, lastStreamId); + if (typeof inner.ongoaway === 'function') { + safeCallbackInvoke(inner.ongoaway, this, lastStreamId); } } @@ -3464,7 +3609,7 @@ class QuicSession { * @param {number} code * @param {string} [reason] */ - [kFinishClose](errorType, code, reason) { + [kFinishClose](errorType, code, reason, errorName) { // If code is zero, then we closed without an error. Yay! We can destroy // safely without specifying an error. if (code === 0n) { @@ -3473,12 +3618,13 @@ class QuicSession { return; } - debug('finishing closing the session with an error', errorType, code, reason); + debug('finishing closing the session with an error', + errorType, code, reason, errorName); // If the local side initiated this close with an error code (via // close({ code })), this is an intentional shutdown; not an error. // The closed promise should resolve, not reject. - if (this.#selfInitiatedClose) { + if (this.#inner.selfInitiatedClose) { this.destroy(); return; } @@ -3500,10 +3646,14 @@ class QuicSession { // session would leak with `closed` hanging forever. switch (errorType) { case 0: /* Transport Error */ - this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason)); + this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName)); break; case 1: /* Application Error */ - this.destroy(new ERR_QUIC_APPLICATION_ERROR(code, reason)); + this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', code, reason, errorName)); break; case 2: /* Version Negotiation Error */ this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR()); @@ -3512,19 +3662,23 @@ class QuicSession { this.destroy(); break; default: - this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason)); + this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName)); break; } } [kKeylog](line) { - if (this.destroyed || this.onkeylog === undefined) return; - safeCallbackInvoke(this.#onkeylog, this, line); + const inner = this.#inner; + if (this.destroyed || inner.onkeylog === undefined) return; + safeCallbackInvoke(inner.onkeylog, this, line); } [kQlog](data, fin) { - if (this.onqlog === undefined) return; - safeCallbackInvoke(this.#onqlog, this, data, fin); + const inner = this.#inner; + if (inner.onqlog === undefined) return; + safeCallbackInvoke(inner.onqlog, this, data, fin); } /** @@ -3534,7 +3688,8 @@ class QuicSession { [kDatagram](u8, early) { // The datagram event should only be called if the session has // an ondatagram callback. The callback should always exist here. - assert(typeof this.#ondatagram === 'function', 'Unexpected datagram event'); + const inner = this.#inner; + assert(typeof inner.ondatagram === 'function', 'Unexpected datagram event'); if (this.destroyed) return; const length = TypedArrayPrototypeGetByteLength(u8); if (onSessionReceiveDatagramChannel.hasSubscribers) { @@ -3545,7 +3700,7 @@ class QuicSession { session: this, }); } - safeCallbackInvoke(this.#ondatagram, this, u8, early); + safeCallbackInvoke(inner.ondatagram, this, u8, early); } /** @@ -3553,9 +3708,10 @@ class QuicSession { * @param {'lost'|'acknowledged'} status */ [kDatagramStatus](id, status) { + const inner = this.#inner; // The datagram status event should only be called if the session has // an ondatagramstatus callback. The callback should always exist here. - assert(typeof this.#ondatagramstatus === 'function', 'Unexpected datagram status event'); + assert(typeof inner.ondatagramstatus === 'function', 'Unexpected datagram status event'); if (this.destroyed) return; if (onSessionReceiveDatagramStatusChannel.hasSubscribers) { onSessionReceiveDatagramStatusChannel.publish({ @@ -3565,7 +3721,7 @@ class QuicSession { session: this, }); } - safeCallbackInvoke(this.#ondatagramstatus, this, id, status); + safeCallbackInvoke(inner.ondatagramstatus, this, id, status); } /** @@ -3578,7 +3734,8 @@ class QuicSession { */ [kPathValidation](result, newLocalAddress, newRemoteAddress, oldLocalAddress, oldRemoteAddress, preferredAddress) { - assert(typeof this.#onpathvalidation === 'function', + const inner = this.#inner; + assert(typeof inner.onpathvalidation === 'function', 'Unexpected path validation event'); if (this.destroyed) return; const newLocal = new InternalSocketAddress(newLocalAddress); @@ -3599,7 +3756,7 @@ class QuicSession { session: this, }); } - safeCallbackInvoke(this.#onpathvalidation, this, result, newLocal, newRemote, + safeCallbackInvoke(inner.onpathvalidation, this, result, newLocal, newRemote, oldLocal, oldRemote, preferredAddress); } @@ -3607,7 +3764,8 @@ class QuicSession { * @param {object} ticket */ [kSessionTicket](ticket) { - assert(typeof this.#onsessionticket === 'function', + const inner = this.#inner; + assert(typeof inner.onsessionticket === 'function', 'Unexpected session ticket event'); if (this.destroyed) return; if (onSessionTicketChannel.hasSubscribers) { @@ -3617,7 +3775,7 @@ class QuicSession { session: this, }); } - safeCallbackInvoke(this.#onsessionticket, this, ticket); + safeCallbackInvoke(inner.onsessionticket, this, ticket); } /** @@ -3625,7 +3783,8 @@ class QuicSession { * @param {SocketAddress} address */ [kNewToken](token, address) { - assert(typeof this.#onnewtoken === 'function', + const inner = this.#inner; + assert(typeof inner.onnewtoken === 'function', 'Unexpected new token event'); if (this.destroyed) return; const addr = new InternalSocketAddress(address); @@ -3637,7 +3796,7 @@ class QuicSession { session: this, }); } - safeCallbackInvoke(this.#onnewtoken, this, token, addr); + safeCallbackInvoke(inner.onnewtoken, this, token, addr); } [kEarlyDataRejected]() { @@ -3648,8 +3807,9 @@ class QuicSession { session: this, }); } - if (typeof this.#onearlyrejected === 'function') { - safeCallbackInvoke(this.#onearlyrejected, this); + const inner = this.#inner; + if (typeof inner.onearlyrejected === 'function') { + safeCallbackInvoke(inner.onearlyrejected, this); } } @@ -3669,8 +3829,9 @@ class QuicSession { session: this, }); } - if (this.#onversionnegotiation) { - safeCallbackInvoke(this.#onversionnegotiation, this, + const inner = this.#inner; + if (typeof inner.onversionnegotiation === 'function') { + safeCallbackInvoke(inner.onversionnegotiation, this, version, requestedVersions, supportedVersions); } // Version negotiation is always a fatal event - the session must be @@ -3683,9 +3844,9 @@ class QuicSession { * @param {string[]} origins */ [kOrigin](origins) { - assert(typeof this.#onorigin === 'function', - 'Unexpected origin event'); if (this.destroyed) return; + const inner = this.#inner; + assert(typeof inner.onorigin === 'function', 'Unexpected origin event'); if (onSessionOriginChannel.hasSubscribers) { onSessionOriginChannel.publish({ __proto__: null, @@ -3693,7 +3854,7 @@ class QuicSession { session: this, }); } - safeCallbackInvoke(this.#onorigin, this, origins); + safeCallbackInvoke(inner.onorigin, this, origins); } /** @@ -3706,13 +3867,15 @@ class QuicSession { */ [kHandshake](servername, protocol, cipher, cipherVersion, validationErrorReason, validationErrorCode, earlyDataAttempted, earlyDataAccepted) { - if (this.destroyed || !this.#pendingOpen.resolve) return; + const inner = this.#inner; + if (this.destroyed || !inner.pendingOpen.resolve) return; + const addr = this.#handle.getRemoteAddress(); const info = { __proto__: null, - local: this.#endpoint.address, + local: inner.endpoint.address, remote: addr !== undefined ? new InternalSocketAddress(addr) : undefined, @@ -3727,7 +3890,7 @@ class QuicSession { }; // Stash timing-relevant handshake info for the perf entry detail. - this.#handshakeInfo = { + inner.handshakeInfo = { __proto__: null, servername, protocol, @@ -3743,19 +3906,47 @@ class QuicSession { }); } - if (this.#onhandshake) { - safeCallbackInvoke(this.#onhandshake, this, info); + if (typeof inner.onhandshake === 'function') { + safeCallbackInvoke(inner.onhandshake, this, info); + } + + // In 'auto' mode, reject the connection if peer certificate validation + // failed. In 'manual' mode, resolve regardless and let the application + // decide. In 'strict' mode, the handshake already failed at the C++ + // level (SSL_VERIFY_PEER) so we won't reach here. + if (inner.verifyPeer === 'auto' && validationErrorReason !== undefined) { + const err = makeQuicError( + 'ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', + 0n, + `Peer certificate validation failed: ${validationErrorReason}` + + ` [${validationErrorCode}]`); + inner.pendingOpen.reject?.(err); + inner.pendingOpen.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.handshakeCompleted = true; + this.destroy(); + return; } - this.#pendingOpen.resolve?.(info); - this.#pendingOpen.resolve = undefined; - this.#pendingOpen.reject = undefined; - this.#handshakeCompleted = true; + inner.pendingOpen.resolve?.(info); + inner.pendingOpen.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.handshakeCompleted = true; } /** @type {boolean} */ get [kHandshakeCompleted]() { - return this.#handshakeCompleted; + return this.#inner.handshakeCompleted; + } + + get [kVerifyPeer]() { + return this.#inner.verifyPeer; + } + + set [kVerifyPeer](value) { + this.#inner.verifyPeer = value; } /** @@ -3763,22 +3954,25 @@ class QuicSession { * @param {number} direction */ [kNewStream](handle, direction) { - const stream = new QuicStream(kPrivateConstructor, handle, this, direction); + const inner = this.#inner; + const stream = new QuicStream(kPrivateConstructor, handle, this, direction, + false /* isLocal */); // Set the default high water mark for received streams. stream.highWaterMark = kDefaultHighWaterMark; // A new stream was received. If we don't have an onstream callback, then // there's nothing we can do about it. Destroy the stream in this case. - if (typeof this.#onstream !== 'function') { + if (typeof inner.onstream !== 'function') { process.emitWarning('A new stream was received but no onstream callback was provided'); stream.destroy(); return; } - this.#streams.add(stream); + + inner.streams.add(stream); // If the session has an onerror handler, mark the stream's closed // promise as handled. See the onerror setter for explanation. - if (typeof this.#onerror === 'function') { + if (typeof inner.onerror === 'function') { markPromiseAsHandled(stream.closed); } @@ -3801,16 +3995,17 @@ class QuicSession { }); } - safeCallbackInvoke(this.#onstream, this, stream); + safeCallbackInvoke(inner.onstream, this, stream); } [kRemoveStream](stream) { - this.#streams.delete(stream); + this.#inner.streams.delete(stream); } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicSession { }'; + } const opts = { __proto__: null, @@ -3818,100 +4013,54 @@ class QuicSession { depth: options.depth == null ? null : options.depth - 1, }; + const { + isPendingClose: closing, + endpoint, + path, + state, + stats, + streams, + } = this.#inner; + return `QuicSession ${inspect({ closed: this.closed, - closing: this.#isPendingClose, + closing, destroyed: this.destroyed, - endpoint: this.endpoint, - path: this.path, - state: this.#state, - stats: this.stats, - streams: this.#streams, + endpoint, + path, + state, + stats, + streams, }, opts)}`; } async [SymbolAsyncDispose]() { await this.close(); } } -let isQuicEndpoint; - // The QuicEndpoint represents a local UDP port binding. It can act as both a // server for receiving peer sessions, or a client for initiating them. The // local UDP port will be lazily bound only when connect() or listen() are // called. class QuicEndpoint { - /** - * The local socket address on which the endpoint is listening (lazily created) - * @type {SocketAddress|undefined} - */ - #address = undefined; - /** - * When true, the endpoint has been marked busy and is temporarily not accepting - * new sessions (only used when the Endpoint is acting as a server) - * @type {boolean} - */ - #busy = false; - /** - * The underlying C++ handle for the endpoint. When undefined the endpoint is - * considered to be closed. - * @type {object} - */ #handle; - /** - * True if endpoint.close() has been called and the [kFinishClose] method has - * not yet been called. - * @type {boolean} - */ - #isPendingClose = false; - /** - * True if the endpoint is acting as a server and actively listening for connections. - * @type {boolean} - */ - #listening = false; - /** - * A promise that is resolved when the endpoint has been closed (or rejected if - * the endpoint closes abruptly due to an error). - * @type {PromiseWithResolvers} - */ - #pendingClose = PromiseWithResolvers(); - /** - * If destroy() is called with an error, the error is stored here and used to reject - * the pendingClose promise when [kFinishClose] is called. - * @type {any} - */ - #pendingError = undefined; - /** - * The collection of active sessions. - * @type {Set} - */ - #sessions = new SafeSet(); - /** - * The internal state of the endpoint. Used to efficiently track and update the - * state of the underlying c++ endpoint handle. - * @type {QuicEndpointState} - */ - #state; - /** - * The collected statistics for the endpoint. - * @type {QuicEndpointStats} - */ - #stats; - /** - * The user provided callback that is invoked when a new session is received. - * (used only when the endpoint is acting as a server) - * @type {OnSessionCallback} - */ - #onsession = undefined; - #sessionCallbacks = undefined; + #inner = { + __proto__: null, + address: undefined, + busy: false, + isPendingClose: false, + listening: false, + pendingClose: PromiseWithResolvers(), + pendingError: undefined, + sessions: new SafeSet(), + stat: undefined, + stats: undefined, + onsession: undefined, + sessionCallbacks: undefined, + }; static { - getQuicEndpointState = function(endpoint) { - assertIsQuicEndpoint(endpoint); - return endpoint.#state; - }; - isQuicEndpoint = function(val) { - return val != null && #handle in val; + return val != null && typeof val === 'object' && #handle in val; }; assertIsQuicEndpoint = function(val) { @@ -3920,6 +4069,11 @@ class QuicEndpoint { } }; + getQuicEndpointState = function(endpoint) { + assertIsQuicEndpoint(endpoint); + return endpoint.#inner.state; + }; + assertEndpointNotClosedOrClosing = function(endpoint) { if (endpoint.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Endpoint is closed'); @@ -3927,7 +4081,7 @@ class QuicEndpoint { }; assertEndpointIsNotBusy = function(endpoint) { - if (endpoint.#state.isBusy) { + if (endpoint.#inner.state.isBusy) { throw new ERR_INVALID_STATE('Endpoint is busy'); } }; @@ -3943,12 +4097,22 @@ class QuicEndpoint { const { retryTokenExpiration, tokenExpiration, - maxConnectionsPerHost = 0, - maxConnectionsTotal = 0, - maxStatelessResetsPerHost, + maxConnectionsPerHost = 100, + maxConnectionsTotal = 10_000, disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, + sessionCreationRate, + sessionCreationBurst, + blockList, + blockListPolicy = 'deny', rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, @@ -3957,11 +4121,22 @@ class QuicEndpoint { idleTimeout, validateAddress, ipv6Only, + reusePort, cc, resetTokenSecret, tokenSecret, } = options; + if (blockList !== undefined) { + if (!BlockList.isBlockList(blockList)) { + throw new ERR_INVALID_ARG_TYPE('options.blockList', + 'net.BlockList', blockList); + } + } + + validateOneOf(blockListPolicy, 'options.blockListPolicy', + ['deny', 'allow']); + // All of the other options will be validated internally by the C++ code if (address !== undefined && !SocketAddress.isSocketAddress(address)) { if (typeof address === 'string') { @@ -3981,10 +4156,21 @@ class QuicEndpoint { // Connection limits are set on the state buffer, not passed to C++. maxConnectionsPerHost, maxConnectionsTotal, - maxStatelessResetsPerHost, disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, + sessionCreationRate, + sessionCreationBurst, + // Pass the C++ handle, not the JS BlockList wrapper. + blockList: blockList?.[kBlockListHandle], + blockListPolicy, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, @@ -3993,6 +4179,7 @@ class QuicEndpoint { idleTimeout, validateAddress, ipv6Only, + reusePort, cc, resetTokenSecret, tokenSecret, @@ -4001,7 +4188,7 @@ class QuicEndpoint { #newSession(handle) { const session = new QuicSession(kPrivateConstructor, handle, this); - this.#sessions.add(session); + this.#inner.sessions.add(session); // Set default pending datagram queue size. session.maxPendingDatagrams = kDefaultMaxPendingDatagrams; return session; @@ -4014,8 +4201,9 @@ class QuicEndpoint { const options = this.#processEndpointOptions(config); this.#handle = new Endpoint_(options); this.#handle[kOwner] = this; - this.#stats = new QuicEndpointStats(kPrivateConstructor, this.#handle.stats); - this.#state = new QuicEndpointState(kPrivateConstructor, this.#handle.state); + const inner = this.#inner; + inner.stats = new QuicEndpointStats(kPrivateConstructor, this.#handle.stats); + inner.state = new QuicEndpointState(kPrivateConstructor, this.#handle.state); // Connection limits are stored in the shared state buffer so they // can be read by C++ and mutated from JS after construction. @@ -4050,11 +4238,11 @@ class QuicEndpoint { */ get stats() { assertIsQuicEndpoint(this); - return this.#stats; + return this.#inner.stats; } get #isClosedOrClosing() { - return this.destroyed || this.#isPendingClose; + return this.destroyed || this.#inner.isPendingClose; } /** @@ -4064,7 +4252,7 @@ class QuicEndpoint { */ get busy() { assertIsQuicEndpoint(this); - return this.#busy; + return this.#inner.busy; } /** @@ -4075,15 +4263,16 @@ class QuicEndpoint { assertEndpointNotClosedOrClosing(this); // The val is allowed to be any truthy value // Non-op if there is no change - if (!!val !== this.#busy) { - debug('toggling endpoint busy status to ', !this.#busy); - this.#busy = !this.#busy; - this.#handle.markBusy(this.#busy); + const inner = this.#inner; + if (!!val !== inner.busy) { + debug('toggling endpoint busy status to ', !inner.busy); + inner.busy = !inner.busy; + this.#handle.markBusy(inner.busy); if (onEndpointBusyChangeChannel.hasSubscribers) { onEndpointBusyChangeChannel.publish({ __proto__: null, endpoint: this, - busy: this.#busy, + busy: inner.busy, }); } } @@ -4096,13 +4285,13 @@ class QuicEndpoint { */ get maxConnectionsPerHost() { assertIsQuicEndpoint(this); - return this.#state.maxConnectionsPerHost; + return this.#inner.state.maxConnectionsPerHost; } set maxConnectionsPerHost(val) { assertIsQuicEndpoint(this); validateInteger(val, 'maxConnectionsPerHost', 0, 0xFFFF); - this.#state.maxConnectionsPerHost = val; + this.#inner.state.maxConnectionsPerHost = val; } /** @@ -4112,13 +4301,13 @@ class QuicEndpoint { */ get maxConnectionsTotal() { assertIsQuicEndpoint(this); - return this.#state.maxConnectionsTotal; + return this.#inner.state.maxConnectionsTotal; } set maxConnectionsTotal(val) { assertIsQuicEndpoint(this); validateInteger(val, 'maxConnectionsTotal', 0, 0xFFFF); - this.#state.maxConnectionsTotal = val; + this.#inner.state.maxConnectionsTotal = val; } /** @@ -4128,11 +4317,11 @@ class QuicEndpoint { get address() { assertIsQuicEndpoint(this); if (this.#isClosedOrClosing) return undefined; - if (this.#address === undefined) { + if (this.#inner.address === undefined) { const addr = this.#handle.address(); - if (addr !== undefined) this.#address = new InternalSocketAddress(addr); + if (addr !== undefined) this.#inner.address = new InternalSocketAddress(addr); } - return this.#address; + return this.#inner.address; } /** @@ -4143,11 +4332,13 @@ class QuicEndpoint { [kListen](onsession, options) { assertEndpointNotClosedOrClosing(this); assertEndpointIsNotBusy(this); - if (this.#listening) { + const inner = this.#inner; + if (inner.listening) { throw new ERR_INVALID_STATE('Endpoint is already listening'); } validateObject(options, 'options'); - this.#onsession = FunctionPrototypeBind(onsession, this); + validateFunction(onsession, 'onsession'); + this.#inner.onsession = FunctionPrototypeBind(onsession, this); const { onerror, @@ -4173,7 +4364,7 @@ class QuicEndpoint { } = options; // Store session and stream callbacks to apply to each new incoming session. - this.#sessionCallbacks = { + inner.sessionCallbacks = { __proto__: null, onerror, onstream, @@ -4195,9 +4386,9 @@ class QuicEndpoint { onwanttrailers, }; - debug('endpoint listening as a server'); this.#handle.listen(rest); - this.#listening = true; + inner.listening = true; + debug('endpoint listening as a server'); } /** @@ -4224,6 +4415,10 @@ class QuicEndpoint { // Set callbacks before any async work to avoid missing events // that fire during or immediately after the handshake. applyCallbacks(session, options); + // Store the verifyPeer policy for use in the handshake handler. + if (options.verifyPeer !== undefined) { + session[kVerifyPeer] = options.verifyPeer; + } return session; } @@ -4239,15 +4434,16 @@ class QuicEndpoint { assertIsQuicEndpoint(this); if (!this.#isClosedOrClosing) { debug('gracefully closing the endpoint'); + const inner = this.#inner; + inner.isPendingClose = true; + this.#handle.closeGracefully(); if (onEndpointClosingChannel.hasSubscribers) { onEndpointClosingChannel.publish({ __proto__: null, endpoint: this, - hasPendingError: this.#pendingError !== undefined, + hasPendingError: inner.pendingError !== undefined, }); } - this.#isPendingClose = true; - this.#handle.closeGracefully(); } return this.closed; } @@ -4260,7 +4456,7 @@ class QuicEndpoint { */ get closed() { assertIsQuicEndpoint(this); - return this.#pendingClose.promise; + return this.#inner.pendingClose.promise; } /** @@ -4269,13 +4465,13 @@ class QuicEndpoint { */ get closing() { assertIsQuicEndpoint(this); - return this.#isPendingClose; + return this.#inner.isPendingClose; } /** @type {boolean} */ get listening() { assertIsQuicEndpoint(this); - return this.#listening; + return this.#inner.listening; } /** @type {boolean} */ @@ -4295,6 +4491,7 @@ class QuicEndpoint { destroy(error) { assertIsQuicEndpoint(this); debug('destroying the endpoint'); + const inner = this.#inner; // Record the error before deciding whether to initiate a close. If // `close()` was already called (e.g. the user kicked off a graceful // shutdown and then a fatal error was reported afterwards via @@ -4303,7 +4500,7 @@ class QuicEndpoint { // last in-flight session finishes draining. Only the *first* error // is recorded, matching how other Node subsystems handle a // double-error race. - if (error !== undefined) this.#pendingError ??= error; + if (error !== undefined) inner.pendingError ??= error; // Force all sessions to be abruptly closed *before* signalling the // endpoint to close gracefully. The order matters: each session's // `destroy(error, options)` asks the C++ side to emit a @@ -4320,7 +4517,7 @@ class QuicEndpoint { // `destroy()`, which trips the `#destroying` guard and leaves the // C++ side asserting an inconsistent destroyed state. const closeOptions = errorToCloseOptions(error); - for (const session of this.#sessions) { + for (const session of inner.sessions) { // Mark each cascaded session's `closed` as handled before // destroying it. This prevents unhandled-rejection warnings when // the session is collateral damage from an endpoint-level destroy @@ -4348,7 +4545,7 @@ class QuicEndpoint { * replace is true, the entire SNI map is replaced. Otherwise, the * provided entries are merged into the existing map. * @param {object} entries - * @param {{replace?: boolean}} [options] + * @param {SNIContextOptions} [options] */ setSNIContexts(entries, options = kEmptyObject) { assertIsQuicEndpoint(this); @@ -4383,38 +4580,39 @@ class QuicEndpoint { debug('endpoint is finishing close', context, status); endpointRegistry.delete(this); this.#handle = undefined; - this.#stats[kFinishClose](); - this.#state[kFinishClose](); + const inner = this.#inner; + inner.stats[kFinishClose](); + inner.state[kFinishClose](); if (this[kPerfEntry] && hasObserver('quic')) { stopPerf(this, kPerfEntry, { detail: { stats: this.stats }, }); } - this.#address = undefined; - this.#busy = false; - this.#listening = false; - this.#isPendingClose = false; + inner.address = undefined; + inner.busy = false; + inner.listening = false; + inner.isPendingClose = false; // As QuicSessions are closed they are expected to remove themselves // from the sessions collection. Just in case they don't, let's force // it by resetting the set so we don't leak memory. Let's emit a warning, // tho, if the set is not empty at this point as that would indicate a // bug in Node.js that should be fixed. - if (this.#sessions.size > 0) { + if (inner.sessions.size > 0) { process.emitWarning( - `The endpoint is closed with ${this.#sessions.size} active sessions. ` + + `The endpoint is closed with ${inner.sessions.size} active sessions. ` + 'This should not happen and indicates a bug in Node.js. Please open an ' + 'issue in the Node.js GitHub repository at https://github.com/nodejs/node ' + 'to report the problem.', ); } - this.#sessions.clear(); + inner.sessions.clear(); // If destroy was called with an error, then the this.#pendingError will be // set. Or, if context indicates an error condition that caused the endpoint // to be closed, the status will indicate the error code. In either case, // we will reject the pending close promise at this point. - const maybeCloseError = maybeGetCloseError(context, status, this.#pendingError); + const maybeCloseError = maybeGetCloseError(context, status, inner.pendingError); if (maybeCloseError !== undefined) { if (onEndpointErrorChannel.hasSubscribers) { onEndpointErrorChannel.publish({ @@ -4423,33 +4621,36 @@ class QuicEndpoint { error: maybeCloseError, }); } - this.#pendingClose.reject(maybeCloseError); + inner.pendingClose.reject(maybeCloseError); } else { // Otherwise we are good to resolve the pending close promise! - this.#pendingClose.resolve(); + inner.pendingClose.resolve(); } if (onEndpointClosedChannel.hasSubscribers) { onEndpointClosedChannel.publish({ __proto__: null, endpoint: this, - stats: this.stats, + stats: inner.stats, }); } // Note that we are intentionally not clearing the // this.#pendingClose.promise here. - this.#pendingClose.resolve = undefined; - this.#pendingClose.reject = undefined; - this.#pendingError = undefined; + inner.pendingClose.resolve = undefined; + inner.pendingClose.reject = undefined; + inner.pendingError = undefined; } [kNewSession](handle) { + const inner = this.#inner; + assert(typeof inner.onsession === 'function', + 'onsession callback not specified'); const session = this.#newSession(handle); // Apply session callbacks stored at listen time before notifying // the onsession callback, to avoid missing events that fire // during or immediately after the handshake. - if (this.#sessionCallbacks) { - applyCallbacks(session, this.#sessionCallbacks); + if (inner.sessionCallbacks) { + applyCallbacks(session, inner.sessionCallbacks); } if (onEndpointServerSessionChannel.hasSubscribers) { onEndpointServerSessionChannel.publish({ @@ -4459,25 +4660,24 @@ class QuicEndpoint { address: session.path?.remote, }); } - assert(typeof this.#onsession === 'function', - 'onsession callback not specified'); // Route through safeCallbackInvoke so that a synchronous throw or a // rejected promise from the user's onsession callback destroys this // endpoint with the error rather than surfacing as an unhandled // exception or unhandled rejection coming out of the C++ -> JS // boundary. - safeCallbackInvoke(this.#onsession, this, session); + safeCallbackInvoke(inner.onsession, this, session); } // Called by the QuicSession when it closes to remove itself from // the active sessions tracked by the QuicEndpoint. [kRemoveSession](session) { - this.#sessions.delete(session); + this.#inner.sessions.delete(session); } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicEndpoint { }'; + } const opts = { __proto__: null, @@ -4485,16 +4685,26 @@ class QuicEndpoint { depth: options.depth == null ? null : options.depth - 1, }; + const { + address, + busy, + isPendingClose: closing, + listening, + sessions, + stats, + state, + } = this.#inner; + return `QuicEndpoint ${inspect({ - address: this.address, - busy: this.busy, + address, + busy, closed: this.closed, - closing: this.#isPendingClose, + closing, destroyed: this.destroyed, - listening: this.#listening, - sessions: this.#sessions, - stats: this.stats, - state: this.#state, + listening, + sessions, + stats, + state, }, opts)}`; } @@ -4836,17 +5046,17 @@ function getPreferredAddressPolicy(policy = 'default') { /** * @param {SessionOptions} options - * @param {{forServer: boolean, addressFamily: string}} [config] + * @param {ProcessSessionOptions} [config] * @returns {SessionOptions} */ -function processSessionOptions(options, config = { __proto__: null }) { +function processSessionOptions(options, config = kEmptyObject) { validateObject(options, 'options'); const { endpoint, reuseEndpoint = true, version, minVersion, - preferredAddressPolicy = 'default', + preferredAddressPolicy = 'ignore', transportParams = kEmptyObject, qlog = false, sessionTicket, @@ -4854,6 +5064,7 @@ function processSessionOptions(options, config = { __proto__: null }) { maxPayloadSize, unacknowledgedPacketThreshold = 0, handshakeTimeout, + initialRtt, keepAlive, maxStreamWindow, maxWindow, @@ -4861,6 +5072,8 @@ function processSessionOptions(options, config = { __proto__: null }) { datagramDropPolicy = 'drop-oldest', drainingPeriodMultiplier = 3, maxDatagramSendAttempts = 5, + streamIdleTimeout, + verifyPeer = 'auto', // HTTP/3 application-specific options. Nested under `application` // to separate protocol-specific settings from transport-level ones. application = kEmptyObject, @@ -4907,6 +5120,9 @@ function processSessionOptions(options, config = { __proto__: null }) { validateOneOf(datagramDropPolicy, 'options.datagramDropPolicy', ['drop-oldest', 'drop-newest']); + validateOneOf(verifyPeer, 'options.verifyPeer', + ['strict', 'auto', 'manual']); + validateInteger(drainingPeriodMultiplier, 'options.drainingPeriodMultiplier', 3, 255); @@ -4956,11 +5172,24 @@ function processSessionOptions(options, config = { __proto__: null }) { preferredAddressIpv4: preferredAddressIpv4?.[kSocketAddressHandle], preferredAddressIpv6: preferredAddressIpv6?.[kSocketAddressHandle], }, - tls: processTlsOptions(options, forServer), + tls: { + ...processTlsOptions(options, forServer), + // Forward strict mode to C++ so SSL_VERIFY_PEER is set on the + // client SSL_CTX. For 'auto' and 'manual' modes, the handshake + // completes regardless and the result is handled in JS. + verifyPeerStrict: verifyPeer === 'strict', + // Enable hostname verification for 'strict' and 'auto' modes. + // SSL_set1_host tells OpenSSL to verify the server certificate's + // SAN/CN matches the servername. Without this, a valid cert for + // any domain would be accepted. + verifyHostname: verifyPeer !== 'manual', + }, + verifyPeer, qlog, maxPayloadSize, unacknowledgedPacketThreshold, handshakeTimeout, + initialRtt, keepAlive, maxStreamWindow, maxWindow, @@ -4970,6 +5199,7 @@ function processSessionOptions(options, config = { __proto__: null }) { datagramDropPolicy, drainingPeriodMultiplier, maxDatagramSendAttempts, + streamIdleTimeout, application, onerror, onstream, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index efaccb4aa00527..82277b115d6e9a 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -5,7 +5,6 @@ /* c8 ignore start */ const { - ArrayBuffer, DataView, DataViewPrototypeGetBigInt64, DataViewPrototypeGetBigUint64, @@ -16,21 +15,15 @@ const { DataViewPrototypeSetUint16, DataViewPrototypeSetUint32, DataViewPrototypeSetUint8, - Float32Array, JSONStringify, - Uint8Array, + Number, } = primordials; -// Determine native byte order. The shared state buffer is written by -// C++ in native byte order, so DataView reads must match. -const kIsLittleEndian = (() => { - // -1 as float32 is 0xBF800000. On little-endian, the bytes are - // [0x00, 0x00, 0x80, 0xBF], so byte[3] is 0xBF (non-zero). - // On big-endian, the bytes are [0xBF, 0x80, 0x00, 0x00], so byte[3] is 0. - const buf = new Float32Array(1); - buf[0] = -1; - return new Uint8Array(buf.buffer)[3] !== 0; -})(); +const { + isBigEndian, +} = internalBinding('os'); + +const kIsLittleEndian = !isBigEndian; const { getOptionValue, @@ -58,8 +51,6 @@ const { kFinishClose, kInspect, kPrivateConstructor, - kWantsHeaders, - kWantsTrailers, } = require('internal/quic/symbols'); // This file defines the helper objects for accessing state for @@ -155,6 +146,11 @@ assert(IDX_STATE_STREAM_WANTS_TRAILERS !== undefined); assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE !== undefined); assert(IDX_STATE_STREAM_RESET_CODE !== undefined); +const kEmptyObject = { __proto__: null }; + +// The internal state objects for endpoints, sessions, and streams. +// These are not exposed directly to users. + class QuicEndpointState { /** @type {DataView} */ #handle; @@ -175,58 +171,67 @@ class QuicEndpointState { /** @type {boolean} */ get isBound() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BOUND); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_BOUND) !== 0; } /** @type {boolean} */ get isReceiving() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_RECEIVING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_RECEIVING) !== 0; } /** @type {boolean} */ get isListening() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_LISTENING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_LISTENING) !== 0; } /** @type {boolean} */ get isClosing() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_CLOSING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_CLOSING) !== 0; } /** @type {boolean} */ get isBusy() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BUSY); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, IDX_STATE_ENDPOINT_BUSY) !== 0; } /** @type {number} */ get maxConnectionsPerHost() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetUint16( - this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, kIsLittleEndian); + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, kIsLittleEndian); } set maxConnectionsPerHost(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; + const handle = this.#handle; + if (handle === undefined) return; DataViewPrototypeSetUint16( - this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, val, kIsLittleEndian); + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, val, kIsLittleEndian); } /** @type {number} */ get maxConnectionsTotal() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetUint16( - this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, kIsLittleEndian); + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, kIsLittleEndian); } set maxConnectionsTotal(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; + const handle = this.#handle; + if (handle === undefined) return; DataViewPrototypeSetUint16( - this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, val, kIsLittleEndian); + handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, val, kIsLittleEndian); } /** @@ -236,8 +241,9 @@ class QuicEndpointState { * @type {bigint} */ get pendingCallbacks() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS, kIsLittleEndian); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64(handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS, kIsLittleEndian); } toString() { @@ -245,68 +251,94 @@ class QuicEndpointState { } toJSON() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return {}; + if (this.#handle === undefined) return kEmptyObject; + const { + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks, + } = this; return { __proto__: null, - isBound: this.isBound, - isReceiving: this.isReceiving, - isListening: this.isListening, - isClosing: this.isClosing, - isBusy: this.isBusy, - maxConnectionsPerHost: this.maxConnectionsPerHost, - maxConnectionsTotal: this.maxConnectionsTotal, - pendingCallbacks: `${this.pendingCallbacks}`, + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks: Number(pendingCallbacks), }; } [kInspect](depth, options) { - if (depth < 0) - return this; - - if (DataViewPrototypeGetByteLength(this.#handle) === 0) { + if (this.#handle === undefined) { return 'QuicEndpointState { }'; } + if (depth < 0) { + return 'QuicEndpointState { }'; + } + const opts = { __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks, + } = this; + return `QuicEndpointState ${inspect({ - isBound: this.isBound, - isReceiving: this.isReceiving, - isListening: this.isListening, - isClosing: this.isClosing, - isBusy: this.isBusy, - pendingCallbacks: this.pendingCallbacks, + isBound, + isReceiving, + isListening, + isClosing, + isBusy, + maxConnectionsPerHost, + maxConnectionsTotal, + pendingCallbacks, }, opts)}`; } [kFinishClose]() { - // Snapshot the state into a new DataView since the underlying - // buffer will be destroyed. - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - this.#handle = new DataView(new ArrayBuffer(0)); + this.#handle = undefined; } } class QuicSessionState { /** @type {DataView} */ #handle; + /** @type {number} */ + #offset = 0; /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {DataView|ArrayBuffer} view + * @param {number} [byteOffset] */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { if (privateSymbol !== kPrivateConstructor) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + if (isArrayBuffer(view)) { + this.#handle = new DataView(view); + } else { + this.#handle = view; } - this.#handle = new DataView(buffer); + this.#offset = byteOffset; } // Listener flags are packed into a single uint32_t bitfield. The bit @@ -319,17 +351,19 @@ class QuicSessionState { static #LISTENER_ORIGIN = 1 << 5; #getListenerFlag(flag) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!(DataViewPrototypeGetUint32( - this.#handle, IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian) & flag); + const handle = this.#handle; + if (handle === undefined) return undefined; + return (DataViewPrototypeGetUint32( + handle, this.#offset + IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian) & flag) !== 0; } #setListenerFlag(flag, val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; + const handle = this.#handle; + if (handle === undefined) return; const current = DataViewPrototypeGetUint32( - this.#handle, IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian); + handle, this.#offset + IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian); DataViewPrototypeSetUint32( - this.#handle, IDX_STATE_SESSION_LISTENER_FLAGS, + handle, this.#offset + IDX_STATE_SESSION_LISTENER_FLAGS, val ? (current | flag) : (current & ~flag), kIsLittleEndian); } @@ -383,50 +417,58 @@ class QuicSessionState { /** @type {boolean} */ get isClosing() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_CLOSING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_CLOSING) !== 0; } /** @type {boolean} */ get isGracefulClose() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_GRACEFUL_CLOSE); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_GRACEFUL_CLOSE) !== 0; } /** @type {boolean} */ get isSilentClose() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SILENT_CLOSE); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_SILENT_CLOSE) !== 0; } /** @type {boolean} */ get isStatelessReset() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STATELESS_RESET); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_STATELESS_RESET) !== 0; } /** @type {boolean} */ get isHandshakeCompleted() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_COMPLETED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_HANDSHAKE_COMPLETED) !== 0; } /** @type {boolean} */ get isHandshakeConfirmed() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_CONFIRMED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_HANDSHAKE_CONFIRMED) !== 0; } /** @type {boolean} */ get isStreamOpenAllowed() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STREAM_OPEN_ALLOWED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_STREAM_OPEN_ALLOWED) !== 0; } /** @type {boolean} */ get isPrioritySupported() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PRIORITY_SUPPORTED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_PRIORITY_SUPPORTED) !== 0; } /** @@ -435,20 +477,23 @@ class QuicSessionState { * @type {number} */ get headersSupported() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HEADERS_SUPPORTED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_HEADERS_SUPPORTED); } /** @type {boolean} */ get isWrapped() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_WRAPPED) !== 0; } /** @type {number} */ get applicationType() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_APPLICATION_TYPE); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_SESSION_APPLICATION_TYPE); } /** @@ -459,9 +504,10 @@ class QuicSessionState { * @type {bigint} */ get noErrorCode() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetBigUint64( - this.#handle, IDX_STATE_SESSION_NO_ERROR_CODE, kIsLittleEndian); + handle, this.#offset + IDX_STATE_SESSION_NO_ERROR_CODE, kIsLittleEndian); } /** @@ -474,34 +520,43 @@ class QuicSessionState { * @type {bigint} */ get internalErrorCode() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetBigUint64( - this.#handle, IDX_STATE_SESSION_INTERNAL_ERROR_CODE, kIsLittleEndian); + handle, this.#offset + IDX_STATE_SESSION_INTERNAL_ERROR_CODE, kIsLittleEndian); } /** @type {number} */ get maxDatagramSize() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return DataViewPrototypeGetUint16(this.#handle, IDX_STATE_SESSION_MAX_DATAGRAM_SIZE, kIsLittleEndian); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint16( + handle, this.#offset + IDX_STATE_SESSION_MAX_DATAGRAM_SIZE, + kIsLittleEndian); } /** @type {bigint} */ get lastDatagramId() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_SESSION_LAST_DATAGRAM_ID, kIsLittleEndian); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64( + handle, this.#offset + IDX_STATE_SESSION_LAST_DATAGRAM_ID, + kIsLittleEndian); } /** @type {number} */ get maxPendingDatagrams() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetUint16( - this.#handle, IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, kIsLittleEndian); + handle, this.#offset + IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, kIsLittleEndian); } set maxPendingDatagrams(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; + const handle = this.#handle; + if (handle === undefined) return; DataViewPrototypeSetUint16( - this.#handle, IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, val, kIsLittleEndian); + handle, this.#offset + IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, val, kIsLittleEndian); } toString() { @@ -509,239 +564,316 @@ class QuicSessionState { } toJSON() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return {}; + if (this.#handle === undefined) return kEmptyObject; + const { + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode, + internalErrorCode, + maxDatagramSize, + lastDatagramId, + maxPendingDatagrams, + } = this; return { __proto__: null, - hasPathValidationListener: this.hasPathValidationListener, - hasDatagramListener: this.hasDatagramListener, - hasDatagramStatusListener: this.hasDatagramStatusListener, - hasSessionTicketListener: this.hasSessionTicketListener, - hasNewTokenListener: this.hasNewTokenListener, - hasOriginListener: this.hasOriginListener, - isClosing: this.isClosing, - isGracefulClose: this.isGracefulClose, - isSilentClose: this.isSilentClose, - isStatelessReset: this.isStatelessReset, - isHandshakeCompleted: this.isHandshakeCompleted, - isHandshakeConfirmed: this.isHandshakeConfirmed, - isStreamOpenAllowed: this.isStreamOpenAllowed, - isPrioritySupported: this.isPrioritySupported, - headersSupported: this.headersSupported, - isWrapped: this.isWrapped, - applicationType: this.applicationType, - noErrorCode: `${this.noErrorCode}`, - internalErrorCode: `${this.internalErrorCode}`, - maxDatagramSize: `${this.maxDatagramSize}`, - lastDatagramId: `${this.lastDatagramId}`, - maxPendingDatagrams: this.maxPendingDatagrams, + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode: `${noErrorCode}`, + internalErrorCode: `${internalErrorCode}`, + maxDatagramSize: `${maxDatagramSize}`, + lastDatagramId: `${lastDatagramId}`, + maxPendingDatagrams, }; } [kInspect](depth, options) { - if (depth < 0) - return this; - - if (DataViewPrototypeGetByteLength(this.#handle) === 0) { + if (this.#handle === undefined) { return 'QuicSessionState { }'; } + if (depth < 0) { + return 'QuicSessionState { }'; + } + const opts = { __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode, + internalErrorCode, + maxDatagramSize, + lastDatagramId, + maxPendingDatagrams, + } = this; + return `QuicSessionState ${inspect({ - hasPathValidationListener: this.hasPathValidationListener, - hasDatagramListener: this.hasDatagramListener, - hasDatagramStatusListener: this.hasDatagramStatusListener, - hasSessionTicketListener: this.hasSessionTicketListener, - hasNewTokenListener: this.hasNewTokenListener, - hasOriginListener: this.hasOriginListener, - isClosing: this.isClosing, - isGracefulClose: this.isGracefulClose, - isSilentClose: this.isSilentClose, - isStatelessReset: this.isStatelessReset, - isHandshakeCompleted: this.isHandshakeCompleted, - isHandshakeConfirmed: this.isHandshakeConfirmed, - isStreamOpenAllowed: this.isStreamOpenAllowed, - isPrioritySupported: this.isPrioritySupported, - headersSupported: this.headersSupported, - isWrapped: this.isWrapped, - applicationType: this.applicationType, - noErrorCode: this.noErrorCode, - internalErrorCode: this.internalErrorCode, - maxDatagramSize: this.maxDatagramSize, - lastDatagramId: this.lastDatagramId, + hasPathValidationListener, + hasDatagramListener, + hasDatagramStatusListener, + hasSessionTicketListener, + hasNewTokenListener, + hasOriginListener, + isClosing, + isGracefulClose, + isSilentClose, + isStatelessReset, + isHandshakeCompleted, + isHandshakeConfirmed, + isStreamOpenAllowed, + isPrioritySupported, + headersSupported, + isWrapped, + applicationType, + noErrorCode, + internalErrorCode, + maxDatagramSize, + lastDatagramId, + maxPendingDatagrams, }, opts)}`; } [kFinishClose]() { - // Snapshot the state into a new DataView since the underlying - // buffer will be destroyed. - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - this.#handle = new DataView(new ArrayBuffer(0)); + this.#handle = undefined; } } class QuicStreamState { /** @type {DataView} */ #handle; + /** @type {number} */ + #offset = 0; + /** @type {bigint|undefined} */ + #id = undefined; /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {DataView|ArrayBuffer} view + * @param {number} [byteOffset] */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { if (privateSymbol !== kPrivateConstructor) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + if (isArrayBuffer(view)) { + this.#handle = new DataView(view); + } else { + this.#handle = view; } - this.#handle = new DataView(buffer); + this.#offset = byteOffset; } /** @type {bigint} */ get id() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return DataViewPrototypeGetBigInt64(this.#handle, IDX_STATE_STREAM_ID, kIsLittleEndian); + const handle = this.#handle; + if (handle === undefined) return this.#id; + return DataViewPrototypeGetBigInt64(handle, this.#offset + IDX_STATE_STREAM_ID, kIsLittleEndian); } /** @type {boolean} */ get pending() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_PENDING); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_PENDING) !== 0; } /** @type {boolean} */ get finSent() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_SENT); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_FIN_SENT) !== 0; } /** @type {boolean} */ get finReceived() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_RECEIVED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_FIN_RECEIVED) !== 0; } /** @type {boolean} */ get readEnded() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_READ_ENDED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_READ_ENDED) !== 0; } /** @type {boolean} */ get writeEnded() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WRITE_ENDED); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WRITE_ENDED) !== 0; } - /** @type {boolean} */ get reset() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RESET); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_RESET) !== 0; } /** @type {boolean} */ get hasOutbound() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_OUTBOUND); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_HAS_OUTBOUND) !== 0; } /** @type {boolean} */ get hasReader() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_READER); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_HAS_READER) !== 0; } /** @type {boolean} */ get wantsBlock() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_BLOCK) !== 0; } /** @type {boolean} */ set wantsBlock(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); } /** @type {boolean} */ - get [kWantsHeaders]() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS); + get wantsHeaders() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_HEADERS) !== 0; } /** @type {boolean} */ - set [kWantsHeaders](val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); + set wantsHeaders(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); } /** @type {boolean} */ get wantsReset() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_RESET) !== 0; } /** @type {boolean} */ set wantsReset(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0); + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0); } /** @type {boolean} */ - get [kWantsTrailers]() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS); + get wantsTrailers() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_TRAILERS) !== 0; } /** @type {boolean} */ - set [kWantsTrailers](val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0); + set wantsTrailers(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0); } /** @type {boolean} */ get early() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RECEIVED_EARLY_DATA); + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_RECEIVED_EARLY_DATA) !== 0; } /** @type {bigint} */ get resetCode() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetBigUint64( - this.#handle, IDX_STATE_STREAM_RESET_CODE, kIsLittleEndian); + handle, this.#offset + IDX_STATE_STREAM_RESET_CODE, kIsLittleEndian); } /** @type {bigint} */ get writeDesiredSize() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetUint32( - this.#handle, IDX_STATE_STREAM_WRITE_DESIRED_SIZE, kIsLittleEndian); + handle, this.#offset + IDX_STATE_STREAM_WRITE_DESIRED_SIZE, kIsLittleEndian); } set writeDesiredSize(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; + const handle = this.#handle; + if (handle === undefined) return; DataViewPrototypeSetUint32( - this.#handle, IDX_STATE_STREAM_WRITE_DESIRED_SIZE, val, kIsLittleEndian); + handle, this.#offset + IDX_STATE_STREAM_WRITE_DESIRED_SIZE, val, kIsLittleEndian); } /** @type {number} */ get highWaterMark() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined; + const handle = this.#handle; + if (handle === undefined) return undefined; return DataViewPrototypeGetUint32( - this.#handle, IDX_STATE_STREAM_HIGH_WATER_MARK, kIsLittleEndian); + handle, this.#offset + IDX_STATE_STREAM_HIGH_WATER_MARK, kIsLittleEndian); } set highWaterMark(val) { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; + const handle = this.#handle; + if (handle === undefined) return; DataViewPrototypeSetUint32( - this.#handle, IDX_STATE_STREAM_HIGH_WATER_MARK, val, kIsLittleEndian); + handle, this.#offset + IDX_STATE_STREAM_HIGH_WATER_MARK, val, kIsLittleEndian); } toString() { @@ -749,59 +881,108 @@ class QuicStreamState { } toJSON() { - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return {}; + if (this.#handle === undefined) return kEmptyObject; + const { + id, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, + } = this; return { __proto__: null, - id: `${this.id}`, - pending: this.pending, - finSent: this.finSent, - finReceived: this.finReceived, - readEnded: this.readEnded, - writeEnded: this.writeEnded, - reset: this.reset, - hasOutbound: this.hasOutbound, - hasReader: this.hasReader, - wantsBlock: this.wantsBlock, - wantsReset: this.wantsReset, - early: this.early, + id: `${id}`, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, }; } [kInspect](depth, options) { - if (depth < 0) - return this; - if (DataViewPrototypeGetByteLength(this.#handle) === 0) { return 'QuicStreamState { }'; } + if (depth < 0) { + return 'QuicStreamState { }'; + } + const opts = { __proto__: null, ...options, depth: options.depth == null ? null : options.depth - 1, }; + const { + id, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, + } = this; + return `QuicStreamState ${inspect({ - id: this.id, - pending: this.pending, - finSent: this.finSent, - finReceived: this.finReceived, - readEnded: this.readEnded, - writeEnded: this.writeEnded, - reset: this.reset, - hasOutbound: this.hasOutbound, - hasReader: this.hasReader, - wantsBlock: this.wantsBlock, - wantsReset: this.wantsReset, - early: this.early, + id, + pending, + finSent, + finReceived, + readEnded, + writeEnded, + reset, + hasOutbound, + hasReader, + wantsBlock, + wantsReset, + wantsHeaders, + wantsTrailers, + early, + resetCode, + writeDesiredSize, + highWaterMark, }, opts)}`; } [kFinishClose]() { - // Snapshot the state into a new DataView since the underlying - // buffer will be destroyed. - if (DataViewPrototypeGetByteLength(this.#handle) === 0) return; - this.#handle = new DataView(new ArrayBuffer(0)); + // Cache the stream ID since the buffer will be zeroed out and the ID will be lost. + this.#id = this.id; + this.#handle = undefined; } } diff --git a/lib/internal/quic/stats.js b/lib/internal/quic/stats.js index 280cf5a26f419b..cd986827c12c47 100644 --- a/lib/internal/quic/stats.js +++ b/lib/internal/quic/stats.js @@ -7,6 +7,8 @@ const { BigUint64Array, JSONStringify, + Symbol, + TypedArrayPrototypeSubarray, } = primordials; const { @@ -25,6 +27,7 @@ const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, + ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -57,9 +60,15 @@ const { IDX_STATS_ENDPOINT_CLIENT_SESSIONS, IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, IDX_STATS_ENDPOINT_RETRY_COUNT, + IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED, IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, + IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED, IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, + IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED, IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, + IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED, + IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED, + IDX_STATS_ENDPOINT_PACKETS_BLOCKED, IDX_STATS_SESSION_CREATED_AT, IDX_STATS_SESSION_DESTROYED_AT, @@ -88,11 +97,12 @@ const { IDX_STATS_SESSION_BYTES_LOST, IDX_STATS_SESSION_PING_RECV, IDX_STATS_SESSION_PKT_DISCARDED, - IDX_STATS_SESSION_DATAGRAMS_RECEIVED, IDX_STATS_SESSION_DATAGRAMS_SENT, IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED, IDX_STATS_SESSION_DATAGRAMS_LOST, + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT, + IDX_STATS_SESSION_COUNT, IDX_STATS_STREAM_CREATED_AT, IDX_STATS_STREAM_OPENED_AT, @@ -105,6 +115,9 @@ const { IDX_STATS_STREAM_MAX_OFFSET_ACK, IDX_STATS_STREAM_MAX_OFFSET_RECV, IDX_STATS_STREAM_FINAL_SIZE, + IDX_STATS_STREAM_BYTES_ACCUMULATED, + IDX_STATS_STREAM_MAX_BYTES_ACCUMULATED, + IDX_STATS_STREAM_COUNT, } = internalBinding('quic'); assert(IDX_STATS_ENDPOINT_CREATED_AT !== undefined); @@ -117,9 +130,15 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined); assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS !== undefined); assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT !== undefined); assert(IDX_STATS_ENDPOINT_RETRY_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED !== undefined); +assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED !== undefined); +assert(IDX_STATS_ENDPOINT_PACKETS_BLOCKED !== undefined); assert(IDX_STATS_SESSION_CREATED_AT !== undefined); assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined); assert(IDX_STATS_SESSION_CLOSING_AT !== undefined); @@ -151,6 +170,7 @@ assert(IDX_STATS_SESSION_DATAGRAMS_RECEIVED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_LOST !== undefined); +assert(IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT !== undefined); assert(IDX_STATS_STREAM_CREATED_AT !== undefined); assert(IDX_STATS_STREAM_OPENED_AT !== undefined); assert(IDX_STATS_STREAM_RECEIVED_AT !== undefined); @@ -162,6 +182,25 @@ assert(IDX_STATS_STREAM_MAX_OFFSET !== undefined); assert(IDX_STATS_STREAM_MAX_OFFSET_ACK !== undefined); assert(IDX_STATS_STREAM_MAX_OFFSET_RECV !== undefined); assert(IDX_STATS_STREAM_FINAL_SIZE !== undefined); +assert(IDX_STATS_STREAM_BYTES_ACCUMULATED !== undefined); +assert(IDX_STATS_STREAM_MAX_BYTES_ACCUMULATED !== undefined); +assert(IDX_STATS_STREAM_COUNT !== undefined); +assert(IDX_STATS_SESSION_COUNT !== undefined); + +const kCreateDisconnected = Symbol('kCreateDisconnected'); + +let assertIsQuicEndpointStats; +let assertIsQuicSessionStats; +let assertIsQuicStreamStats; +let isQuicEndpointStats; +let isQuicSessionStats; +let isQuicStreamStats; + +function assertIsPrivateConstructor(privateSymbol) { + if (privateSymbol !== kPrivateConstructor) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } +} class QuicEndpointStats { /** @type {BigUint64Array} */ @@ -169,6 +208,18 @@ class QuicEndpointStats { /** @type {boolean} */ #disconnected = false; + static { + isQuicEndpointStats = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicEndpointStats = function(val) { + if (!isQuicEndpointStats(val)) { + throw new ERR_INVALID_THIS('QuicEndpointStats'); + } + }; + } + /** * @param {symbol} privateSymbol * @param {ArrayBuffer} buffer @@ -176,9 +227,7 @@ class QuicEndpointStats { constructor(privateSymbol, buffer) { // We use the kPrivateConstructor symbol to restrict the ability to // create new instances of QuicEndpointStats to internal code. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } + assertIsPrivateConstructor(privateSymbol); if (!isArrayBuffer(buffer)) { throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); } @@ -187,98 +236,177 @@ class QuicEndpointStats { /** @type {bigint} */ get createdAt() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_CREATED_AT]; } /** @type {bigint} */ get destroyedAt() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_DESTROYED_AT]; } /** @type {bigint} */ get bytesReceived() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_BYTES_RECEIVED]; } /** @type {bigint} */ get bytesSent() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_BYTES_SENT]; } /** @type {bigint} */ get packetsReceived() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_PACKETS_RECEIVED]; } /** @type {bigint} */ get packetsSent() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_PACKETS_SENT]; } /** @type {bigint} */ get serverSessions() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_SERVER_SESSIONS]; } /** @type {bigint} */ get clientSessions() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_CLIENT_SESSIONS]; } /** @type {bigint} */ get serverBusyCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT]; } /** @type {bigint} */ get retryCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT]; } + /** @type {bigint} */ + get retryRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED]; + } + /** @type {bigint} */ get versionNegotiationCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT]; } + /** @type {bigint} */ + get versionNegotiationRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED]; + } + /** @type {bigint} */ get statelessResetCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT]; } + /** @type {bigint} */ + get statelessResetRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED]; + } + /** @type {bigint} */ get immediateCloseCount() { + assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT]; } + /** @type {bigint} */ + get immediateCloseRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED]; + } + + /** @type {bigint} */ + get sessionCreationRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED]; + } + + /** @type {bigint} */ + get packetsBlocked() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_PACKETS_BLOCKED]; + } + toString() { return JSONStringify(this.toJSON()); } toJSON() { + assertIsQuicEndpointStats(this); + const { + createdAt, + destroyedAt, + bytesReceived, + bytesSent, + packetsReceived, + packetsSent, + serverSessions, + clientSessions, + serverBusyCount, + retryCount, + retryRateLimited, + versionNegotiationCount, + versionNegotiationRateLimited, + statelessResetCount, + statelessResetRateLimited, + immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, + } = this; return { __proto__: null, connected: this.isConnected, // We need to convert the values to strings because JSON does not // support BigInts. - createdAt: `${this.createdAt}`, - destroyedAt: `${this.destroyedAt}`, - bytesReceived: `${this.bytesReceived}`, - bytesSent: `${this.bytesSent}`, - packetsReceived: `${this.packetsReceived}`, - packetsSent: `${this.packetsSent}`, - serverSessions: `${this.serverSessions}`, - clientSessions: `${this.clientSessions}`, - serverBusyCount: `${this.serverBusyCount}`, - retryCount: `${this.retryCount}`, - versionNegotiationCount: `${this.versionNegotiationCount}`, - statelessResetCount: `${this.statelessResetCount}`, - immediateCloseCount: `${this.immediateCloseCount}`, + createdAt: `${createdAt}`, + destroyedAt: `${destroyedAt}`, + bytesReceived: `${bytesReceived}`, + bytesSent: `${bytesSent}`, + packetsReceived: `${packetsReceived}`, + packetsSent: `${packetsSent}`, + serverSessions: `${serverSessions}`, + clientSessions: `${clientSessions}`, + serverBusyCount: `${serverBusyCount}`, + retryCount: `${retryCount}`, + retryRateLimited: `${retryRateLimited}`, + versionNegotiationCount: `${versionNegotiationCount}`, + versionNegotiationRateLimited: `${versionNegotiationRateLimited}`, + statelessResetCount: `${statelessResetCount}`, + statelessResetRateLimited: `${statelessResetRateLimited}`, + immediateCloseCount: `${immediateCloseCount}`, + immediateCloseRateLimited: `${immediateCloseRateLimited}`, + sessionCreationRateLimited: `${sessionCreationRateLimited}`, + packetsBlocked: `${packetsBlocked}`, }; } [kInspect](depth, options) { - if (depth < 0) - return this; + assertIsQuicEndpointStats(this); + if (depth < 0) { + return 'QuicEndpointStats { }'; + } const opts = { __proto__: null, @@ -286,21 +414,49 @@ class QuicEndpointStats { depth: options.depth == null ? null : options.depth - 1, }; + const { + createdAt, + destroyedAt, + bytesReceived, + bytesSent, + packetsReceived, + packetsSent, + serverSessions, + clientSessions, + serverBusyCount, + retryCount, + retryRateLimited, + versionNegotiationCount, + versionNegotiationRateLimited, + statelessResetCount, + statelessResetRateLimited, + immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, + } = this; + return `QuicEndpointStats ${inspect({ connected: this.isConnected, - createdAt: this.createdAt, - destroyedAt: this.destroyedAt, - bytesReceived: this.bytesReceived, - bytesSent: this.bytesSent, - packetsReceived: this.packetsReceived, - packetsSent: this.packetsSent, - serverSessions: this.serverSessions, - clientSessions: this.clientSessions, - serverBusyCount: this.serverBusyCount, - retryCount: this.retryCount, - versionNegotiationCount: this.versionNegotiationCount, - statelessResetCount: this.statelessResetCount, - immediateCloseCount: this.immediateCloseCount, + createdAt, + destroyedAt, + bytesReceived, + bytesSent, + packetsReceived, + packetsSent, + serverSessions, + clientSessions, + serverBusyCount, + retryCount, + retryRateLimited, + versionNegotiationCount, + versionNegotiationRateLimited, + statelessResetCount, + statelessResetRateLimited, + immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, }, opts)}`; } @@ -311,12 +467,11 @@ class QuicEndpointStats { * @type {boolean} */ get isConnected() { + assertIsQuicEndpointStats(this); return !this.#disconnected; } [kFinishClose]() { - // Snapshot the stats into a new BigUint64Array since the underlying - // buffer will be destroyed. this.#handle = new BigUint64Array(this.#handle); this.#disconnected = true; } @@ -325,170 +480,222 @@ class QuicEndpointStats { class QuicSessionStats { /** @type {BigUint64Array} */ #handle; - /** @type {boolean} */ #disconnected = false; + #offset = 0; + + static { + isQuicSessionStats = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicSessionStats = function(val) { + if (!isQuicSessionStats(val)) { + throw new ERR_INVALID_THIS('QuicSessionStats'); + } + }; + } + /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {ArrayBuffer} view + * @param {number} [byteOffset] */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { // We use the kPrivateConstructor symbol to restrict the ability to // create new instances of QuicSessionStats to internal code. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + assertIsPrivateConstructor(privateSymbol); + if (isArrayBuffer(view)) { + this.#handle = new BigUint64Array(view); + } else { + this.#handle = view; } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); - } - this.#handle = new BigUint64Array(buffer); + this.#offset = byteOffset / 8; } /** @type {bigint} */ get createdAt() { - return this.#handle[IDX_STATS_SESSION_CREATED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_CREATED_AT]; } /** @type {bigint} */ get destroyedAt() { - return this.#handle[IDX_STATS_SESSION_DESTROYED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DESTROYED_AT]; } /** @type {bigint} */ get closingAt() { - return this.#handle[IDX_STATS_SESSION_CLOSING_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_CLOSING_AT]; } /** @type {bigint} */ get handshakeCompletedAt() { - return this.#handle[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT]; } /** @type {bigint} */ get handshakeConfirmedAt() { - return this.#handle[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT]; } /** @type {bigint} */ get bytesReceived() { - return this.#handle[IDX_STATS_SESSION_BYTES_RECEIVED]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_RECEIVED]; } /** @type {bigint} */ get bidiInStreamCount() { - return this.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT]; } /** @type {bigint} */ get bidiOutStreamCount() { - return this.#handle[IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT]; } /** @type {bigint} */ get uniInStreamCount() { - return this.#handle[IDX_STATS_SESSION_UNI_IN_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_UNI_IN_STREAM_COUNT]; } /** @type {bigint} */ get uniOutStreamCount() { - return this.#handle[IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT]; } /** @type {bigint} */ get maxBytesInFlight() { - return this.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT]; } /** @type {bigint} */ get bytesInFlight() { - return this.#handle[IDX_STATS_SESSION_BYTES_IN_FLIGHT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_IN_FLIGHT]; } /** @type {bigint} */ get blockCount() { - return this.#handle[IDX_STATS_SESSION_BLOCK_COUNT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BLOCK_COUNT]; } /** @type {bigint} */ get cwnd() { - return this.#handle[IDX_STATS_SESSION_CWND]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_CWND]; } /** @type {bigint} */ get latestRtt() { - return this.#handle[IDX_STATS_SESSION_LATEST_RTT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_LATEST_RTT]; } /** @type {bigint} */ get minRtt() { - return this.#handle[IDX_STATS_SESSION_MIN_RTT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_MIN_RTT]; } /** @type {bigint} */ get rttVar() { - return this.#handle[IDX_STATS_SESSION_RTTVAR]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_RTTVAR]; } /** @type {bigint} */ get smoothedRtt() { - return this.#handle[IDX_STATS_SESSION_SMOOTHED_RTT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_SMOOTHED_RTT]; } /** @type {bigint} */ get ssthresh() { - return this.#handle[IDX_STATS_SESSION_SSTHRESH]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_SSTHRESH]; } get pktSent() { - return this.#handle[IDX_STATS_SESSION_PKT_SENT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_SENT]; } get bytesSent() { - return this.#handle[IDX_STATS_SESSION_BYTES_SENT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_SENT]; } get pktRecv() { - return this.#handle[IDX_STATS_SESSION_PKT_RECV]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_RECV]; } get bytesRecv() { - return this.#handle[IDX_STATS_SESSION_BYTES_RECV]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_RECV]; } get pktLost() { - return this.#handle[IDX_STATS_SESSION_PKT_LOST]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_LOST]; } get bytesLost() { - return this.#handle[IDX_STATS_SESSION_BYTES_LOST]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_BYTES_LOST]; } get pingRecv() { - return this.#handle[IDX_STATS_SESSION_PING_RECV]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PING_RECV]; } get pktDiscarded() { - return this.#handle[IDX_STATS_SESSION_PKT_DISCARDED]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_PKT_DISCARDED]; } /** @type {bigint} */ get datagramsReceived() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_RECEIVED]; } /** @type {bigint} */ get datagramsSent() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_SENT]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_SENT]; } /** @type {bigint} */ get datagramsAcknowledged() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED]; } /** @type {bigint} */ get datagramsLost() { - return this.#handle[IDX_STATS_SESSION_DATAGRAMS_LOST]; + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_LOST]; + } + + /** @type {bigint} */ + get streamsIdleTimedOut() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT]; } toString() { @@ -496,47 +703,83 @@ class QuicSessionStats { } toJSON() { + assertIsQuicSessionStats(this); + const { + createdAt, + closingAt, + handshakeCompletedAt, + handshakeConfirmedAt, + bytesReceived, + bidiInStreamCount, + bidiOutStreamCount, + uniInStreamCount, + uniOutStreamCount, + maxBytesInFlight, + bytesInFlight, + blockCount, + cwnd, + latestRtt, + minRtt, + rttVar, + smoothedRtt, + ssthresh, + pktSent, + bytesSent, + pktRecv, + bytesRecv, + pktLost, + bytesLost, + pingRecv, + pktDiscarded, + datagramsReceived, + datagramsSent, + datagramsAcknowledged, + datagramsLost, + streamsIdleTimedOut, + } = this; return { __proto__: null, connected: this.isConnected, // We need to convert the values to strings because JSON does not // support BigInts. - createdAt: `${this.createdAt}`, - closingAt: `${this.closingAt}`, - handshakeCompletedAt: `${this.handshakeCompletedAt}`, - handshakeConfirmedAt: `${this.handshakeConfirmedAt}`, - bytesReceived: `${this.bytesReceived}`, - bidiInStreamCount: `${this.bidiInStreamCount}`, - bidiOutStreamCount: `${this.bidiOutStreamCount}`, - uniInStreamCount: `${this.uniInStreamCount}`, - uniOutStreamCount: `${this.uniOutStreamCount}`, - maxBytesInFlight: `${this.maxBytesInFlight}`, - bytesInFlight: `${this.bytesInFlight}`, - blockCount: `${this.blockCount}`, - cwnd: `${this.cwnd}`, - latestRtt: `${this.latestRtt}`, - minRtt: `${this.minRtt}`, - rttVar: `${this.rttVar}`, - smoothedRtt: `${this.smoothedRtt}`, - ssthresh: `${this.ssthresh}`, - pktSent: `${this.pktSent}`, - bytesSent: `${this.bytesSent}`, - pktRecv: `${this.pktRecv}`, - bytesRecv: `${this.bytesRecv}`, - pktLost: `${this.pktLost}`, - bytesLost: `${this.bytesLost}`, - pingRecv: `${this.pingRecv}`, - pktDiscarded: `${this.pktDiscarded}`, - datagramsReceived: `${this.datagramsReceived}`, - datagramsSent: `${this.datagramsSent}`, - datagramsAcknowledged: `${this.datagramsAcknowledged}`, - datagramsLost: `${this.datagramsLost}`, + createdAt: `${createdAt}`, + closingAt: `${closingAt}`, + handshakeCompletedAt: `${handshakeCompletedAt}`, + handshakeConfirmedAt: `${handshakeConfirmedAt}`, + bytesReceived: `${bytesReceived}`, + bidiInStreamCount: `${bidiInStreamCount}`, + bidiOutStreamCount: `${bidiOutStreamCount}`, + uniInStreamCount: `${uniInStreamCount}`, + uniOutStreamCount: `${uniOutStreamCount}`, + maxBytesInFlight: `${maxBytesInFlight}`, + bytesInFlight: `${bytesInFlight}`, + blockCount: `${blockCount}`, + cwnd: `${cwnd}`, + latestRtt: `${latestRtt}`, + minRtt: `${minRtt}`, + rttVar: `${rttVar}`, + smoothedRtt: `${smoothedRtt}`, + ssthresh: `${ssthresh}`, + pktSent: `${pktSent}`, + bytesSent: `${bytesSent}`, + pktRecv: `${pktRecv}`, + bytesRecv: `${bytesRecv}`, + pktLost: `${pktLost}`, + bytesLost: `${bytesLost}`, + pingRecv: `${pingRecv}`, + pktDiscarded: `${pktDiscarded}`, + datagramsReceived: `${datagramsReceived}`, + datagramsSent: `${datagramsSent}`, + datagramsAcknowledged: `${datagramsAcknowledged}`, + datagramsLost: `${datagramsLost}`, + streamsIdleTimedOut: `${streamsIdleTimedOut}`, }; } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicSessionStats { }'; + } const opts = { __proto__: null, @@ -544,38 +787,73 @@ class QuicSessionStats { depth: options.depth == null ? null : options.depth - 1, }; + const { + createdAt, + closingAt, + handshakeCompletedAt, + handshakeConfirmedAt, + bytesReceived, + bidiInStreamCount, + bidiOutStreamCount, + uniInStreamCount, + uniOutStreamCount, + maxBytesInFlight, + bytesInFlight, + blockCount, + cwnd, + latestRtt, + minRtt, + rttVar, + smoothedRtt, + ssthresh, + pktSent, + bytesSent, + pktRecv, + bytesRecv, + pktLost, + bytesLost, + pingRecv, + pktDiscarded, + datagramsReceived, + datagramsSent, + datagramsAcknowledged, + datagramsLost, + streamsIdleTimedOut, + } = this; + return `QuicSessionStats ${inspect({ connected: this.isConnected, - createdAt: this.createdAt, - closingAt: this.closingAt, - handshakeCompletedAt: this.handshakeCompletedAt, - handshakeConfirmedAt: this.handshakeConfirmedAt, - bytesReceived: this.bytesReceived, - bidiInStreamCount: this.bidiInStreamCount, - bidiOutStreamCount: this.bidiOutStreamCount, - uniInStreamCount: this.uniInStreamCount, - uniOutStreamCount: this.uniOutStreamCount, - maxBytesInFlight: this.maxBytesInFlight, - bytesInFlight: this.bytesInFlight, - blockCount: this.blockCount, - cwnd: this.cwnd, - latestRtt: this.latestRtt, - minRtt: this.minRtt, - rttVar: this.rttVar, - smoothedRtt: this.smoothedRtt, - ssthresh: this.ssthresh, - pktSent: this.pktSent, - bytesSent: this.bytesSent, - pktRecv: this.pktRecv, - bytesRecv: this.bytesRecv, - pktLost: this.pktLost, - bytesLost: this.bytesLost, - pingRecv: this.pingRecv, - pktDiscarded: this.pktDiscarded, - datagramsReceived: this.datagramsReceived, - datagramsSent: this.datagramsSent, - datagramsAcknowledged: this.datagramsAcknowledged, - datagramsLost: this.datagramsLost, + createdAt, + closingAt, + handshakeCompletedAt, + handshakeConfirmedAt, + bytesReceived, + bidiInStreamCount, + bidiOutStreamCount, + uniInStreamCount, + uniOutStreamCount, + maxBytesInFlight, + bytesInFlight, + blockCount, + cwnd, + latestRtt, + minRtt, + rttVar, + smoothedRtt, + ssthresh, + pktSent, + bytesSent, + pktRecv, + bytesRecv, + pktLost, + bytesLost, + pingRecv, + pktDiscarded, + datagramsReceived, + datagramsSent, + datagramsAcknowledged, + datagramsLost, + streamsIdleTimedOut, }, opts)}`; } @@ -590,9 +868,11 @@ class QuicSessionStats { } [kFinishClose]() { - // Snapshot the stats into a new BigUint64Array since the underlying - // buffer will be destroyed. - this.#handle = new BigUint64Array(this.#handle); + const view = TypedArrayPrototypeSubarray(this.#handle, + this.#offset, + this.#offset + IDX_STATS_STREAM_COUNT); + this.#handle = new BigUint64Array(view); + this.#offset = 0; this.#disconnected = true; } } @@ -600,78 +880,114 @@ class QuicSessionStats { class QuicStreamStats { /** @type {BigUint64Array} */ #handle; - /** type {boolean} */ + #offset = 0; #disconnected = false; + static { + isQuicStreamStats = function(val) { + return val != null && typeof val === 'object' && #handle in val; + }; + + assertIsQuicStreamStats = function(val) { + if (!isQuicStreamStats(val)) { + throw new ERR_INVALID_THIS('QuicStreamStats'); + } + }; + } + /** * @param {symbol} privateSymbol - * @param {ArrayBuffer} buffer + * @param {BigUint64Array|ArrayBuffer} view + * @param {number} [byteOffset] - byte offset into the shared page view */ - constructor(privateSymbol, buffer) { + constructor(privateSymbol, view, byteOffset = 0) { // We use the kPrivateConstructor symbol to restrict the ability to // create new instances of QuicStreamStats to internal code. - if (privateSymbol !== kPrivateConstructor) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + assertIsPrivateConstructor(privateSymbol); + if (isArrayBuffer(view)) { + this.#handle = new BigUint64Array(view); + } else { + this.#handle = view; } - if (!isArrayBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); - } - this.#handle = new BigUint64Array(buffer); + this.#offset = byteOffset / 8; } /** @type {bigint} */ get createdAt() { - return this.#handle[IDX_STATS_STREAM_CREATED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_CREATED_AT]; } /** @type {bigint} */ get openedAt() { - return this.#handle[IDX_STATS_STREAM_OPENED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_OPENED_AT]; } /** @type {bigint} */ get receivedAt() { - return this.#handle[IDX_STATS_STREAM_RECEIVED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_RECEIVED_AT]; } /** @type {bigint} */ get ackedAt() { - return this.#handle[IDX_STATS_STREAM_ACKED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_ACKED_AT]; } /** @type {bigint} */ get destroyedAt() { - return this.#handle[IDX_STATS_STREAM_DESTROYED_AT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_DESTROYED_AT]; } /** @type {bigint} */ get bytesReceived() { - return this.#handle[IDX_STATS_STREAM_BYTES_RECEIVED]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_BYTES_RECEIVED]; } /** @type {bigint} */ get bytesSent() { - return this.#handle[IDX_STATS_STREAM_BYTES_SENT]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_BYTES_SENT]; } /** @type {bigint} */ get maxOffset() { - return this.#handle[IDX_STATS_STREAM_MAX_OFFSET]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_OFFSET]; } /** @type {bigint} */ get maxOffsetAcknowledged() { - return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_ACK]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_OFFSET_ACK]; } /** @type {bigint} */ get maxOffsetReceived() { - return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_RECV]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_OFFSET_RECV]; } /** @type {bigint} */ get finalSize() { - return this.#handle[IDX_STATS_STREAM_FINAL_SIZE]; + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_FINAL_SIZE]; + } + + /** @type {bigint} Current bytes in the receive accumulation buffer. */ + get bytesAccumulated() { + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_BYTES_ACCUMULATED]; + } + + /** @type {bigint} Peak bytes accumulated over the stream's lifetime. */ + get maxBytesAccumulated() { + assertIsQuicStreamStats(this); + return this.#handle[this.#offset + IDX_STATS_STREAM_MAX_BYTES_ACCUMULATED]; } toString() { @@ -679,28 +995,47 @@ class QuicStreamStats { } toJSON() { + assertIsQuicStreamStats(this); + const { + createdAt, + openedAt, + receivedAt, + ackedAt, + destroyedAt, + bytesReceived, + bytesSent, + maxOffset, + maxOffsetAcknowledged, + maxOffsetReceived, + finalSize, + bytesAccumulated, + maxBytesAccumulated, + } = this; return { __proto__: null, connected: this.isConnected, // We need to convert the values to strings because JSON does not // support BigInts. - createdAt: `${this.createdAt}`, - openedAt: `${this.openedAt}`, - receivedAt: `${this.receivedAt}`, - ackedAt: `${this.ackedAt}`, - destroyedAt: `${this.destroyedAt}`, - bytesReceived: `${this.bytesReceived}`, - bytesSent: `${this.bytesSent}`, - maxOffset: `${this.maxOffset}`, - maxOffsetAcknowledged: `${this.maxOffsetAcknowledged}`, - maxOffsetReceived: `${this.maxOffsetReceived}`, - finalSize: `${this.finalSize}`, + createdAt: `${createdAt}`, + openedAt: `${openedAt}`, + receivedAt: `${receivedAt}`, + ackedAt: `${ackedAt}`, + destroyedAt: `${destroyedAt}`, + bytesReceived: `${bytesReceived}`, + bytesSent: `${bytesSent}`, + maxOffset: `${maxOffset}`, + maxOffsetAcknowledged: `${maxOffsetAcknowledged}`, + maxOffsetReceived: `${maxOffsetReceived}`, + finalSize: `${finalSize}`, + bytesAccumulated: `${bytesAccumulated}`, + maxBytesAccumulated: `${maxBytesAccumulated}`, }; } [kInspect](depth, options) { - if (depth < 0) - return this; + if (depth < 0) { + return 'QuicStreamStats { }'; + } const opts = { __proto__: null, @@ -708,19 +1043,37 @@ class QuicStreamStats { depth: options.depth == null ? null : options.depth - 1, }; + const { + createdAt, + openedAt, + receivedAt, + ackedAt, + destroyedAt, + bytesReceived, + bytesSent, + maxOffset, + maxOffsetAcknowledged, + maxOffsetReceived, + finalSize, + bytesAccumulated, + maxBytesAccumulated, + } = this; + return `QuicStreamStats ${inspect({ connected: this.isConnected, - createdAt: this.createdAt, - openedAt: this.openedAt, - receivedAt: this.receivedAt, - ackedAt: this.ackedAt, - destroyedAt: this.destroyedAt, - bytesReceived: this.bytesReceived, - bytesSent: this.bytesSent, - maxOffset: this.maxOffset, - maxOffsetAcknowledged: this.maxOffsetAcknowledged, - maxOffsetReceived: this.maxOffsetReceived, - finalSize: this.finalSize, + createdAt, + openedAt, + receivedAt, + ackedAt, + destroyedAt, + bytesReceived, + bytesSent, + maxOffset, + maxOffsetAcknowledged, + maxOffsetReceived, + finalSize, + bytesAccumulated, + maxBytesAccumulated, }, opts)}`; } @@ -735,17 +1088,29 @@ class QuicStreamStats { } [kFinishClose]() { - // Snapshot the stats into a new BigUint64Array since the underlying - // buffer will be destroyed. - this.#handle = new BigUint64Array(this.#handle); + const view = TypedArrayPrototypeSubarray(this.#handle, + this.#offset, + this.#offset + IDX_STATS_STREAM_COUNT); + this.#handle = new BigUint64Array(view); + this.#offset = 0; this.#disconnected = true; } + + // Creates an immediately disconnected QuicStreamStats object. Used when + // lazily creating stats for a stream that has already been destroyed. + static [kCreateDisconnected]() { + const count = IDX_STATS_STREAM_COUNT; + const stats = new QuicStreamStats(kPrivateConstructor, new BigUint64Array(count), 0); + stats.#disconnected = true; + return stats; + } } module.exports = { QuicEndpointStats, QuicSessionStats, QuicStreamStats, + kCreateDisconnected, }; /* c8 ignore stop */ diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 9a8e9155f0b636..288d3ebae2a8e8 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -38,6 +38,7 @@ const kFinishClose = Symbol('kFinishClose'); const kGoaway = Symbol('kGoaway'); const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); +const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); @@ -57,8 +58,6 @@ const kSendHeaders = Symbol('kSendHeaders'); const kSessionTicket = Symbol('kSessionTicket'); const kTrailers = Symbol('kTrailers'); const kVersionNegotiation = Symbol('kVersionNegotiation'); -const kWantsHeaders = Symbol('kWantsHeaders'); -const kWantsTrailers = Symbol('kWantsTrailers'); module.exports = { kAttachFileHandle, @@ -72,6 +71,7 @@ module.exports = { kGoaway, kHandshake, kHandshakeCompleted, + kVerifyPeer, kHeaders, kInspect, kKeylog, @@ -93,8 +93,6 @@ module.exports = { kSessionTicket, kTrailers, kVersionNegotiation, - kWantsHeaders, - kWantsTrailers, }; /* c8 ignore stop */ diff --git a/lib/internal/streams/compose.js b/lib/internal/streams/compose.js index d664a430b8d75a..7baa7974ffdf05 100644 --- a/lib/internal/streams/compose.js +++ b/lib/internal/streams/compose.js @@ -82,7 +82,6 @@ module.exports = function compose(...streams) { let ondrain; let onfinish; - let onreadable; let onclose; let d; @@ -184,31 +183,19 @@ module.exports = function compose(...streams) { if (readable) { if (isNodeStream(tail)) { - tail.on('readable', function() { - if (onreadable) { - const cb = onreadable; - onreadable = null; - cb(); + d._read = function() { + tail.resume(); + }; + + tail.on('data', function(chunk) { + if (!d.push(chunk)) { + tail.pause(); } }); tail.on('end', function() { d.push(null); }); - - d._read = function() { - while (true) { - const buf = tail.read(); - if (buf === null) { - onreadable = d._read; - return; - } - - if (!d.push(buf)) { - return; - } - } - }; } else if (isWebStream(tail)) { const readable = isTransformStream(tail) ? tail.readable : tail; const reader = readable.getReader(); @@ -238,7 +225,6 @@ module.exports = function compose(...streams) { err = new AbortError(); } - onreadable = null; ondrain = null; onfinish = null; diff --git a/lib/internal/streams/end-of-stream.js b/lib/internal/streams/end-of-stream.js index e995755079f8c1..f310cd6936fbe7 100644 --- a/lib/internal/streams/end-of-stream.js +++ b/lib/internal/streams/end-of-stream.js @@ -7,6 +7,7 @@ const { Promise, PromisePrototypeThen, ReflectApply, + Symbol, SymbolDispose, } = primordials; @@ -66,6 +67,50 @@ function bindAsyncResource(fn, type) { }; } +/** + * Returns the current stream error tracked by eos(), if any. + * @param {import('stream').Stream} stream + * @returns {Error | null} + */ +function getEosErrored(stream) { + const errored = isWritableErrored(stream) || isReadableErrored(stream); + return typeof errored !== 'boolean' && errored || null; +} + +/** + * Returns the error eos() would report from an immediate close, including + * premature close detection for unfinished readable or writable sides. + * @param {import('stream').Stream} stream + * @param {boolean} readable + * @param {boolean | null} readableFinished + * @param {boolean} writable + * @param {boolean | null} writableFinished + * @returns {Error | null} + */ +function getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished) { + const errored = getEosErrored(stream); + if (errored) { + return errored; + } + + if (readable && !readableFinished && isReadableNodeStream(stream, true)) { + if (!isReadableFinished(stream, false)) { + return new ERR_STREAM_PREMATURE_CLOSE(); + } + } + if (writable && !writableFinished) { + if (!isWritableFinished(stream, false)) { + return new ERR_STREAM_PREMATURE_CLOSE(); + } + } + + return null; +} + +// Internal only: if eos() can settle immediately, invoke the callback before +// returning cleanup. Callers must tolerate cleanup yet to be assigned. +const kEosNodeSynchronousCallback = Symbol('kEosNodeSynchronousCallback'); + function eos(stream, options, callback) { if (arguments.length === 2) { callback = options; @@ -78,14 +123,6 @@ function eos(stream, options, callback) { validateFunction(callback, 'callback'); validateAbortSignal(options.signal, 'options.signal'); - if (AsyncContextFrame.current() || enabledHooksExist()) { - // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which - // is a bottleneck here. - callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM')); - } else { - callback = once(callback); - } - if (isReadableStream(stream) || isWritableStream(stream)) { return eosWeb(stream, options, callback); } @@ -97,15 +134,6 @@ function eos(stream, options, callback) { const readable = options.readable ?? isReadableNodeStream(stream); const writable = options.writable ?? isWritableNodeStream(stream); - const wState = stream._writableState; - const rState = stream._readableState; - - const onlegacyfinish = () => { - if (!stream.writable) { - onfinish(); - } - }; - // TODO (ronag): Improve soft detection to include core modules and // common ecosystem modules that do properly emit 'close' but fail // this generic check. @@ -114,8 +142,85 @@ function eos(stream, options, callback) { isReadableNodeStream(stream) === readable && isWritableNodeStream(stream) === writable ); - let writableFinished = isWritableFinished(stream, false); + let readableFinished = isReadableFinished(stream, false); + + const wState = stream._writableState; + const rState = stream._readableState; + + /** + * @type {Error | null | undefined} + * undefined: to be determined + * null: no error + * Error: an error occurred + */ + let immediateResult; + if (isClosed(stream)) { + immediateResult = getEosOnCloseError( + stream, + readable, + readableFinished, + writable, + writableFinished, + ); + } else if (wState?.errorEmitted || rState?.errorEmitted) { + if (!willEmitClose) { + immediateResult = getEosErrored(stream); + } + } else if ( + !readable && + (!willEmitClose || isReadable(stream)) && + (writableFinished || isWritable(stream) === false) && + (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) + ) { + immediateResult = getEosErrored(stream); + } else if ( + !writable && + (!willEmitClose || isWritable(stream)) && + (readableFinished || isReadable(stream) === false) + ) { + immediateResult = getEosErrored(stream); + } else if ((rState && stream.req && stream.aborted)) { + immediateResult = getEosErrored(stream); + } + let cleanup = () => { + callback = nop; + }; + if (immediateResult !== undefined) { + if (options.error !== false) { + stream.on('error', nop); + cleanup = () => { + callback = nop; + stream.removeListener('error', nop); + }; + } + } else if (options.signal?.aborted) { + immediateResult = new AbortError(undefined, { cause: options.signal.reason }); + } + if (immediateResult !== undefined && options[kEosNodeSynchronousCallback]) { + ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult]); + return cleanup; + } + + if (AsyncContextFrame.current() || enabledHooksExist()) { + // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which + // is a bottleneck here. + callback = bindAsyncResource(callback, 'STREAM_END_OF_STREAM'); + } + + if (immediateResult !== undefined) { + process.nextTick(() => ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult])); + return cleanup; + } + + callback = once(callback); + + const onlegacyfinish = () => { + if (!stream.writable) { + onfinish(); + } + }; + const onfinish = () => { writableFinished = true; // Stream should not be destroyed here. If it is that @@ -134,7 +239,6 @@ function eos(stream, options, callback) { } }; - let readableFinished = isReadableFinished(stream, false); const onend = () => { readableFinished = true; // Stream should not be destroyed here. If it is that @@ -157,41 +261,13 @@ function eos(stream, options, callback) { callback.call(stream, err); }; - let closed = isClosed(stream); - const onclose = () => { - closed = true; - - const errored = isWritableErrored(stream) || isReadableErrored(stream); - - if (errored && typeof errored !== 'boolean') { - return callback.call(stream, errored); - } - - if (readable && !readableFinished && isReadableNodeStream(stream, true)) { - if (!isReadableFinished(stream, false)) - return callback.call(stream, - new ERR_STREAM_PREMATURE_CLOSE()); - } - if (writable && !writableFinished) { - if (!isWritableFinished(stream, false)) - return callback.call(stream, - new ERR_STREAM_PREMATURE_CLOSE()); - } - - callback.call(stream); - }; - - const onclosed = () => { - closed = true; - - const errored = isWritableErrored(stream) || isReadableErrored(stream); - - if (errored && typeof errored !== 'boolean') { - return callback.call(stream, errored); + const error = getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished); + if (error === null) { + callback.call(stream); + } else { + callback.call(stream, error); } - - callback.call(stream); }; const onrequest = () => { @@ -225,30 +301,7 @@ function eos(stream, options, callback) { } stream.on('close', onclose); - if (closed) { - process.nextTick(onclose); - } else if (wState?.errorEmitted || rState?.errorEmitted) { - if (!willEmitClose) { - process.nextTick(onclosed); - } - } else if ( - !readable && - (!willEmitClose || isReadable(stream)) && - (writableFinished || isWritable(stream) === false) && - (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) - ) { - process.nextTick(onclosed); - } else if ( - !writable && - (!willEmitClose || isWritable(stream)) && - (readableFinished || isReadable(stream) === false) - ) { - process.nextTick(onclosed); - } else if ((rState && stream.req && stream.aborted)) { - process.nextTick(onclosed); - } - - const cleanup = () => { + cleanup = () => { callback = nop; stream.removeListener('aborted', onclose); stream.removeListener('complete', onfinish); @@ -263,7 +316,7 @@ function eos(stream, options, callback) { stream.removeListener('close', onclose); }; - if (options.signal && !closed) { + if (options.signal) { const abort = () => { // Keep it because cleanup removes it. const endCallback = callback; @@ -272,23 +325,27 @@ function eos(stream, options, callback) { stream, new AbortError(undefined, { cause: options.signal.reason })); }; - if (options.signal.aborted) { - process.nextTick(abort); - } else { - addAbortListener ??= require('internal/events/abort_listener').addAbortListener; - const disposable = addAbortListener(options.signal, abort); - const originalCallback = callback; - callback = once((...args) => { - disposable[SymbolDispose](); - ReflectApply(originalCallback, stream, args); - }); - } + addAbortListener ??= require('internal/events/abort_listener').addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + ReflectApply(originalCallback, stream, args); + }); } return cleanup; } function eosWeb(stream, options, callback) { + if (AsyncContextFrame.current() || enabledHooksExist()) { + // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which + // is a bottleneck here. + callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM')); + } else { + callback = once(callback); + } + let isAborted = false; let abort = nop; if (options.signal) { @@ -347,4 +404,5 @@ function finished(stream, opts) { module.exports = { eos, finished, + kEosNodeSynchronousCallback, }; diff --git a/lib/internal/streams/fast-utf8-stream.js b/lib/internal/streams/fast-utf8-stream.js index cb86f245302620..68601cf5c388c7 100644 --- a/lib/internal/streams/fast-utf8-stream.js +++ b/lib/internal/streams/fast-utf8-stream.js @@ -6,15 +6,12 @@ const { ArrayPrototypePush, - AtomicsWait, - Int32Array, MathMax, - Number, SymbolDispose, } = primordials; const { - constructSharedArrayBuffer, + sleep, } = require('internal/util'); const { @@ -50,22 +47,6 @@ const { const BUSY_WRITE_TIMEOUT = 100; const kEmptyBuffer = Buffer.allocUnsafe(0); -const kNil = new Int32Array(constructSharedArrayBuffer(4)); - -function sleep(ms) { - // Also filters out NaN, non-number types, including empty strings, but allows bigints - const valid = ms > 0 && ms < Infinity; - if (valid === false) { - if (typeof ms !== 'number' && typeof ms !== 'bigint') { - throw new ERR_INVALID_ARG_TYPE('ms', ['number', 'bigint'], ms); - } - throw new ERR_INVALID_ARG_VALUE.RangeError('ms', ms, - 'must be a number greater than 0 and less than Infinity'); - } - - AtomicsWait(kNil, 0, 0, Number(ms)); -} - // 16 KB. Don't write more than docker buffer size. // https://github.com/moby/moby/blob/513ec73831269947d38a644c278ce3cac36783b2/daemon/logger/copier.go#L13 const kMaxWrite = 16 * 1024; diff --git a/lib/internal/streams/iter/broadcast.js b/lib/internal/streams/iter/broadcast.js index 7b6fc3525d122f..e6a404729d6e0b 100644 --- a/lib/internal/streams/iter/broadcast.js +++ b/lib/internal/streams/iter/broadcast.js @@ -686,6 +686,10 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) { signal.addEventListener('abort', onAbort, { __proto__: null, once: true }); } +function onBroadcastCancel(broadcastImpl, signal) { + onSignalAbort(signal, () => broadcastImpl.cancel(signal.reason)); +} + // ============================================================================= // Public API // ============================================================================= @@ -720,7 +724,7 @@ function broadcast(options = { __proto__: null }) { broadcastImpl.setWriter(writer); if (signal) { - onSignalAbort(signal, () => broadcastImpl.cancel()); + onBroadcastCancel(broadcastImpl, signal); } return { __proto__: null, writer, broadcast: broadcastImpl }; diff --git a/lib/internal/streams/iter/classic.js b/lib/internal/streams/iter/classic.js index fd5f811ea52d97..73c6bc48b954c8 100644 --- a/lib/internal/streams/iter/classic.js +++ b/lib/internal/streams/iter/classic.js @@ -57,6 +57,7 @@ const { const { toAsyncStreamable: kToAsyncStreamable, kValidatedSource, + kSyncWriteAccepted, drainableProtocol, } = require('internal/streams/iter/types'); @@ -764,13 +765,41 @@ function toWritable(writer) { const hasEndSync = hasEnd && typeof writer.endSync === 'function'; const hasFail = typeof writer.fail === 'function'; + const hasSyncWriteAccepted = + typeof writer[kSyncWriteAccepted] === 'function'; - // Try-sync-first pattern: attempt the synchronous method and - // fall back to the async method if it returns false (indicating - // the sync path was not accepted) or throws. When the sync path - // succeeds, the callback is deferred via queueMicrotask to - // preserve the async resolution contract that Writable internals - // expect from _write/_writev/_final callbacks. + function syncWriteAccepted() { + return hasSyncWriteAccepted && writer[kSyncWriteAccepted](); + } + + function finishAfterSyncBackpressure(cb) { + let ondrain; + try { + if (typeof writer[drainableProtocol] === 'function') { + ondrain = writer[drainableProtocol](); + } + } catch (err) { + cb(err); + return; + } + if (ondrain !== null && ondrain !== undefined) { + PromisePrototypeThen(ondrain, (drained) => { + if (drained === false) { + cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer')); + return; + } + cb(); + }, cb); + return; + } + queueMicrotask(cb); + } + + // Try-sync-first pattern: attempt the synchronous method and fall back to the + // async method if it returns false without accepting the data, or if it + // throws. When the sync path succeeds, the callback is deferred via + // queueMicrotask to preserve the async resolution contract that Writable + // internals expect from _write/_writev/_final callbacks. function _write(chunk, encoding, cb) { const bytes = typeof chunk === 'string' ? @@ -781,6 +810,11 @@ function toWritable(writer) { queueMicrotask(cb); return; } + if (syncWriteAccepted()) { + // The chunk was accepted; false only signaled backpressure. + finishAfterSyncBackpressure(cb); + return; + } } catch { // Sync path threw -- fall through to async. } @@ -805,6 +839,11 @@ function toWritable(writer) { queueMicrotask(cb); return; } + if (syncWriteAccepted()) { + // The chunks were accepted; false only signaled backpressure. + finishAfterSyncBackpressure(cb); + return; + } } catch { // Sync path threw -- fall through to async. } diff --git a/lib/internal/streams/iter/duplex.js b/lib/internal/streams/iter/duplex.js index 591837f70eb4cb..bd06f37303cfc6 100644 --- a/lib/internal/streams/iter/duplex.js +++ b/lib/internal/streams/iter/duplex.js @@ -74,9 +74,7 @@ function duplex(options = { __proto__: null }) { if (aClosed) return; aClosed = true; // End the writer (signals end-of-stream to B's readable) - if (aWriter.endSync() < 0) { - await aWriter.end(); - } + aWriter.endSync(); // Stop iteration of this channel's readable if (aReadableIterator?.return) { await aReadableIterator.return(); @@ -104,9 +102,7 @@ function duplex(options = { __proto__: null }) { async close() { if (bClosed) return; bClosed = true; - if (bWriter.endSync() < 0) { - await bWriter.end(); - } + bWriter.endSync(); if (bReadableIterator?.return) { await bReadableIterator.return(); bReadableIterator = null; diff --git a/lib/internal/streams/iter/pull.js b/lib/internal/streams/iter/pull.js index 3ff88b251d182a..95871e99037d58 100644 --- a/lib/internal/streams/iter/pull.js +++ b/lib/internal/streams/iter/pull.js @@ -273,6 +273,17 @@ function* processTransformResultSync(result) { result); } +/** + * Append normalized transform result batches to an array (sync). + * @param {Array} target + * @param {*} result + */ +function appendTransformResultSync(target, result) { + for (const batch of processTransformResultSync(result)) { + ArrayPrototypePush(target, batch); + } +} + /** * Process transform result (async). * @yields {Uint8Array[]} @@ -356,6 +367,18 @@ async function* processTransformResultAsync(result) { result); } +/** + * Append normalized transform result batches to an array (async). + * @param {Array} target + * @param {*} result + * @returns {Promise} + */ +async function appendTransformResultAsync(target, result) { + for await (const batch of processTransformResultAsync(result)) { + ArrayPrototypePush(target, batch); + } +} + // ============================================================================= // Sync Pipeline Implementation // ============================================================================= @@ -398,18 +421,19 @@ function* applyFusedStatelessSyncTransforms(source, run) { yield* processTransformResultSync(current); } } - // Flush - let current = null; + // Flush each transform after all upstream data, including data emitted by + // earlier flushes, has been processed by that transform. + let pending = []; for (let i = 0; i < run.length; i++) { - const result = run[i](current); - if (result === null) { - current = null; - continue; + const next = []; + for (let j = 0; j < pending.length; j++) { + appendTransformResultSync(next, run[i](pending[j])); } - current = result; + appendTransformResultSync(next, run[i](null)); + pending = next; } - if (current != null) { - yield* processTransformResultSync(current); + for (let i = 0; i < pending.length; i++) { + yield pending[i]; } } @@ -522,30 +546,23 @@ async function* applyFusedStatelessAsyncTransforms(source, run, signal) { yield* processTransformResultAsync(current); } } - // Flush: send null through each transform in order - let current = null; + // Flush each transform after all upstream data, including data emitted by + // earlier flushes, has been processed by that transform. + let pending = []; for (let i = 0; i < run.length; i++) { - const result = run[i](current, { __proto__: null, signal }); - if (result === null) { - current = null; - continue; - } - if (isPromise(result)) { - current = await result; - } else { - current = result; + const next = []; + for (let j = 0; j < pending.length; j++) { + await appendTransformResultAsync( + next, + run[i](pending[j], { __proto__: null, signal })); } + await appendTransformResultAsync( + next, + run[i](null, { __proto__: null, signal })); + pending = next; } - if (current !== null) { - if (isUint8ArrayBatch(current)) { - if (current.length > 0) yield current; - } else if (isUint8Array(current)) { - yield [current]; - } else if (typeof current === 'string') { - yield [toUint8Array(current)]; - } else { - yield* processTransformResultAsync(current); - } + for (let i = 0; i < pending.length; i++) { + yield pending[i]; } } diff --git a/lib/internal/streams/iter/push.js b/lib/internal/streams/iter/push.js index c5b12663f83c24..c0dca9e03f6e6a 100644 --- a/lib/internal/streams/iter/push.js +++ b/lib/internal/streams/iter/push.js @@ -32,6 +32,7 @@ const { const { drainableProtocol, + kSyncWriteAccepted, kSyncWriteAcceptedOnFalse, } = require('internal/streams/iter/types'); @@ -273,7 +274,10 @@ class PushQueue { if (this.#writerState === 'errored') { return -2; // Signal to reject with stored error } - if (this.#writerState === 'closing' || this.#writerState === 'closed') { + if (this.#writerState === 'closing') { + return -3; // Signal to PushWriter: wait for drain to complete + } + if (this.#writerState === 'closed') { return this.#bytesWritten; // Idempotent } @@ -422,6 +426,7 @@ class PushQueue { if (this.#consumerState !== 'active') return; this.#consumerState = 'returned'; this.#cleanup(); + this.#resolvePendingReads(); this.#rejectPendingWrites( new ERR_INVALID_STATE.TypeError('Stream closed by consumer')); // If closing, reject the pending end promise @@ -439,7 +444,12 @@ class PushQueue { this.#consumerState = 'thrown'; this.#error = error; this.#cleanup(); + this.#rejectPendingReads(error); this.#rejectPendingWrites(error); + if (this.#writerState === 'closing' && this.#pendingEnd) { + this.#pendingEnd.reject(error); + this.#pendingEnd = null; + } // Reject pending drains - the consumer errored this.#rejectPendingDrains(error); } @@ -481,6 +491,9 @@ class PushQueue { } else if (this.#writerState === 'errored' && this.#error) { const pending = this.#pendingReads.shift(); pending.reject(this.#error); + } else if (this.#consumerState === 'returned') { + const pending = this.#pendingReads.shift(); + pending.resolve({ __proto__: null, done: true, value: undefined }); } else { break; } @@ -545,11 +558,16 @@ class PushQueue { class PushWriter { #queue; + #syncWriteAccepted = false; constructor(queue) { this.#queue = queue; } + [kSyncWriteAccepted]() { + return this.#syncWriteAccepted; + } + [drainableProtocol]() { const desired = this.desiredSize; if (desired === null) return null; @@ -589,6 +607,7 @@ class PushWriter { } writeSync(chunk) { + this.#syncWriteAccepted = false; const bytes = toUint8Array(chunk); const result = this.#queue.writeSync([bytes]); if (!result && this.#queue.backpressurePolicy === 'block' && @@ -596,12 +615,15 @@ class PushWriter { // Block policy: force-enqueue and return false as backpressure signal. // Data IS accepted; false tells caller to slow down. this.#queue.forceEnqueue([bytes]); + this.#syncWriteAccepted = true; return false; } + this.#syncWriteAccepted = result; return result; } writevSync(chunks) { + this.#syncWriteAccepted = false; if (!ArrayIsArray(chunks)) { throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks); } @@ -610,8 +632,10 @@ class PushWriter { if (!result && this.#queue.backpressurePolicy === 'block' && this.#queue.desiredSize === 0) { this.#queue.forceEnqueue(bytes); + this.#syncWriteAccepted = true; return false; } + this.#syncWriteAccepted = result; return result; } @@ -624,6 +648,10 @@ class PushWriter { if (result === -3) { // Closing: buffer has data, create deferred promise that resolves // when consumer drains past the end sentinel + const pendingEndPromise = this.#queue.pendingEndPromise; + if (pendingEndPromise !== null) { + return pendingEndPromise; + } const { promise, resolve, reject } = PromiseWithResolvers(); this.#queue.setPendingEnd({ __proto__: null, promise, resolve, reject }); return promise; diff --git a/lib/internal/streams/iter/share.js b/lib/internal/streams/iter/share.js index 0160bc7eace009..f755550712efa7 100644 --- a/lib/internal/streams/iter/share.js +++ b/lib/internal/streams/iter/share.js @@ -113,6 +113,7 @@ class ShareImpl { resolve: null, reject: null, detached: false, + pendingNext: PromiseResolve(), }; this.#consumers.add(state); @@ -129,61 +130,72 @@ class ShareImpl { return { __proto__: null, [SymbolAsyncIterator]() { - return { - __proto__: null, - async next() { - if (self.#sourceError) { - state.detached = true; - self.#consumers.delete(state); - throw self.#sourceError; + const getNext = async () => { + if (self.#sourceError) { + state.detached = true; + self.#consumers.delete(state); + throw self.#sourceError; + } + + // Loop until we get data, source is exhausted, or + // consumer is detached. Multiple consumers may be woken + // after a single pull - those that find no data at their + // cursor must re-pull rather than terminating prematurely. + for (;;) { + if (state.detached) { + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; } - // Loop until we get data, source is exhausted, or - // consumer is detached. Multiple consumers may be woken - // after a single pull - those that find no data at their - // cursor must re-pull rather than terminating prematurely. - for (;;) { - if (state.detached) { - return { __proto__: null, done: true, value: undefined }; - } + if (self.#cancelled) { + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } - if (self.#cancelled) { - state.detached = true; - self.#deleteConsumer(state); - return { __proto__: null, done: true, value: undefined }; + // Check if data is available in buffer + const bufferIndex = state.cursor - self.#bufferStart; + if (bufferIndex < self.#buffer.length) { + const chunk = self.#buffer.get(bufferIndex); + const cursor = state.cursor; + state.cursor++; + if (cursor === self.#cachedMinCursor && + --self.#cachedMinCursorConsumers === 0) { + self.#tryTrimBuffer(); } + return { __proto__: null, done: false, value: chunk }; + } - // Check if data is available in buffer - const bufferIndex = state.cursor - self.#bufferStart; - if (bufferIndex < self.#buffer.length) { - const chunk = self.#buffer.get(bufferIndex); - const cursor = state.cursor; - state.cursor++; - if (cursor === self.#cachedMinCursor && - --self.#cachedMinCursorConsumers === 0) { - self.#tryTrimBuffer(); - } - return { __proto__: null, done: false, value: chunk }; - } + if (self.#sourceExhausted) { + state.detached = true; + self.#deleteConsumer(state); + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } - if (self.#sourceExhausted) { - state.detached = true; - self.#deleteConsumer(state); - if (self.#sourceError) throw self.#sourceError; - return { __proto__: null, done: true, value: undefined }; - } + // Need to pull from source - check buffer limit + const canPull = await self.#waitForBufferSpace(); + if (!canPull) { + state.detached = true; + self.#deleteConsumer(state); + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } - // Need to pull from source - check buffer limit - const canPull = await self.#waitForBufferSpace(); - if (!canPull) { - state.detached = true; - self.#deleteConsumer(state); - if (self.#sourceError) throw self.#sourceError; - return { __proto__: null, done: true, value: undefined }; - } + await self.#pullFromSource(); + } + }; - await self.#pullFromSource(); - } + return { + __proto__: null, + next() { + const next = PromisePrototypeThen( + state.pendingNext, + getNext, + getNext); + state.pendingNext = + PromisePrototypeThen(next, undefined, () => {}); + return next; }, async return() { @@ -628,6 +640,10 @@ class SyncShareImpl { } } +function onShareCancel(shareImpl, signal) { + onSignalAbort(signal, () => shareImpl.cancel(signal.reason)); +} + // ============================================================================= // Public API // ============================================================================= @@ -657,7 +673,7 @@ function share(source, options = { __proto__: null }) { const shareImpl = new ShareImpl(normalized, opts); if (signal) { - onSignalAbort(signal, () => shareImpl.cancel()); + onShareCancel(shareImpl, signal); } return shareImpl; diff --git a/lib/internal/streams/iter/types.js b/lib/internal/streams/iter/types.js index 71112b1515c081..e72972d91ad243 100644 --- a/lib/internal/streams/iter/types.js +++ b/lib/internal/streams/iter/types.js @@ -64,11 +64,24 @@ const kValidatedTransform = Symbol('kValidatedTransform'); */ const kValidatedSource = Symbol('kValidatedSource'); +/** + * Internal sentinel for writers whose sync write methods can return false + * after accepting data as a backpressure signal. + */ +const kSyncWriteAccepted = Symbol('kSyncWriteAccepted'); + +/** + * Internal sentinel for writers whose sync write methods may return false + * after accepting data when backpressure is applied. Such writers must expose + * desiredSize so callers can distinguish accepted backpressure from a sync + * write that was not performed. + */ const kSyncWriteAcceptedOnFalse = Symbol('kSyncWriteAcceptedOnFalse'); module.exports = { broadcastProtocol, drainableProtocol, + kSyncWriteAccepted, kSyncWriteAcceptedOnFalse, kValidatedSource, kValidatedTransform, diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index 89d384e9f2fb8f..94913461cb6044 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -466,6 +466,9 @@ function _write(stream, chunk, encoding, cb) { } if (typeof chunk === 'string') { + if (encoding === 'buffer') { + throw new ERR_UNKNOWN_ENCODING(encoding); + } if ((state[kState] & kDecodeStrings) !== 0) { chunk = Buffer.from(chunk, encoding); encoding = 'buffer'; diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..37ff2473b68011 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -16,6 +16,7 @@ const { StringPrototypeIncludes, StringPrototypeLocaleCompare, StringPrototypeStartsWith, + StringPrototypeTrim, } = primordials; const { copyFileSync, @@ -44,6 +45,20 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +const kTypeOnlyImportRegex = /^\s*import\s+type\b/u; +const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u; + +let stripTypeScriptTypesForCoverage; + +function getStripTypeScriptTypesForCoverage() { + if (!process.config.variables.node_use_amaro) { + return; + } + + stripTypeScriptTypesForCoverage ??= + require('internal/modules/typescript').stripTypeScriptTypesForCoverage; + return stripTypeScriptTypesForCoverage; +} class CoverageLine { constructor(line, startOffset, src, length = src?.length) { @@ -69,6 +84,7 @@ class TestCoverage { } #sourceLines = new SafeMap(); + #typeScriptLines = new SafeSet(); getLines(fileUrl, source) { // Split the file source into lines. Make sure the lines maintain their @@ -133,6 +149,57 @@ class TestCoverage { return lines; } + markTypeScriptOnlyLines(fileUrl, source) { + if (this.#typeScriptLines.has(fileUrl)) { + return; + } + this.#typeScriptLines.add(fileUrl); + + if (RegExpPrototypeExec(kTypeScriptSourceRegex, fileUrl) === null) { + return; + } + + const lines = this.getLines(fileUrl, source); + if (!lines) { + return; + } + + let strippedLines; + const stripSource = getStripTypeScriptTypesForCoverage(); + + if (stripSource) { + source ??= readFileSync(fileURLToPath(fileUrl), 'utf8'); + + try { + strippedLines = RegExpPrototypeSymbolSplit( + kLineSplitRegex, + stripSource(source), + ); + } catch { + strippedLines = undefined; + } + } + + for (let i = 0; i < lines.length; ++i) { + const originalLine = lines[i].src; + + if (StringPrototypeTrim(originalLine).length === 0) { + continue; + } + + if (strippedLines?.[i] !== undefined) { + if (StringPrototypeTrim(strippedLines[i]).length === 0) { + lines[i].ignore = true; + } + continue; + } + + if (RegExpPrototypeExec(kTypeOnlyImportRegex, originalLine) !== null) { + lines[i].ignore = true; + } + } + } + summary() { internalBinding('profiler').takeCoverage(); const coverage = this.getCoverageFromDirectory(); @@ -368,10 +435,12 @@ class TestCoverage { offset += length + 1; return coverageLine; }); - if (data.sourcesContent != null) { - for (let j = 0; j < data.sources.length; ++j) { - this.getLines(data.sources[j], data.sourcesContent[j]); + for (let j = 0; j < data.sources.length; ++j) { + const source = data.sourcesContent?.[j]; + if (source != null) { + this.getLines(data.sources[j], source); } + this.markTypeScriptOnlyLines(data.sources[j], source); } const sourceMap = new SourceMap(data, { __proto__: null, lineLengths }); @@ -533,6 +602,8 @@ function setupCoverage(options) { return null; } + internalBinding('profiler').startCoverage(); + // Ensure that NODE_V8_COVERAGE is set so that coverage can propagate to // child processes. process.env.NODE_V8_COVERAGE = coverageDirectory; diff --git a/lib/internal/test_runner/reporter/rerun.js b/lib/internal/test_runner/reporter/rerun.js index 9658a7fb70ff0b..3f9ae102fea33e 100644 --- a/lib/internal/test_runner/reporter/rerun.js +++ b/lib/internal/test_runner/reporter/rerun.js @@ -48,21 +48,25 @@ function reportReruns(previousRuns, globalOptions) { } - if (type === 'test:pass') { - let identifier = getTestId(data); - if (disambiguator[identifier] !== undefined) { - identifier += `:(${disambiguator[identifier]})`; - disambiguator[identifier] += 1; + if (type === 'test:pass' || type === 'test:fail') { + const baseIdentifier = getTestId(data); + let identifier = baseIdentifier; + if (disambiguator[baseIdentifier] !== undefined) { + identifier += `:(${disambiguator[baseIdentifier]})`; + disambiguator[baseIdentifier] += 1; } else { - disambiguator[identifier] = 1; + disambiguator[baseIdentifier] = 1; + } + if (type === 'test:pass') { + const children = ArrayPrototypeMap(currentTest.children, (child) => child.data); + obj[identifier] = { + __proto__: null, + name: data.name, + children, + passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt, + duration_ms: data.details.duration_ms, + }; } - const children = ArrayPrototypeMap(currentTest.children, (child) => child.data); - obj[identifier] = { - __proto__: null, - name: data.name, - children, - passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt, - }; } } diff --git a/lib/internal/test_runner/reporter/utils.js b/lib/internal/test_runner/reporter/utils.js index d90040b9727aa2..67030680019838 100644 --- a/lib/internal/test_runner/reporter/utils.js +++ b/lib/internal/test_runner/reporter/utils.js @@ -73,7 +73,10 @@ function formatTestReport(type, data, showErrorDetails = true, prefix = '', inde let symbol = reporterUnicodeSymbolMap[type] ?? ' '; const { skip, todo, expectFailure } = data; const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : ''; - let title = `${data.name}${duration_ms}`; + const replayed = data.details?.passed_on_attempt !== undefined ? + ` ${colors.gray}(passed on attempt ${data.details.passed_on_attempt})${colors.white}` : + ''; + let title = `${data.name}${duration_ms}${replayed}`; if (skip !== undefined) { title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`; diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index d6cb6438d2b52a..d150943783e975 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -128,6 +128,11 @@ const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', const kCanceledTests = new SafeSet() .add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure); +// Execution-ordered events are forwarded immediately, bypassing the +// per-file declaration-order buffer. +const kExecutionOrderedEvents = new SafeSet() + .add('test:enqueue').add('test:dequeue').add('test:complete'); + let kResistStopPropagation; // Worker ID pool management for concurrent test execution @@ -331,6 +336,10 @@ class FileTest extends Test { } } addToReport(item) { + if (kExecutionOrderedEvents.has(item.type)) { + this.#handleReportItem(item); + return; + } this.#accumulateReportItem(item); if (!this.isClearToSend()) { ArrayPrototypePush(this.#reportBuffer, item); @@ -871,6 +880,8 @@ function run(options = kEmptyObject) { coverageExcludeGlobs = [coverageExcludeGlobs]; } validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs'); + } else if (coverage) { + coverageExcludeGlobs = [kDefaultPattern]; } if (coverageIncludeGlobs != null) { if (!ArrayIsArray(coverageIncludeGlobs)) { diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index eb888905798bcd..d19d6349f104bf 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -10,10 +10,12 @@ const { ArrayPrototypeSplice, ArrayPrototypeUnshift, ArrayPrototypeUnshiftApply, + BigInt, Error, FunctionPrototype, MathFloor, MathMax, + MathRound, Number, NumberPrototypeToFixed, ObjectFreeze, @@ -791,26 +793,38 @@ class Test extends AsyncResource { } if (this.loc != null && this.root.harness.previousRuns != null) { - let testIdentifier = `${relative(this.config.cwd, this.loc.file)}:${this.loc.line}:${this.loc.column}`; - const disambiguator = this.root.testDisambiguator.get(testIdentifier); + const baseIdentifier = `${relative(this.config.cwd, this.loc.file)}:${this.loc.line}:${this.loc.column}`; + let testIdentifier = baseIdentifier; + const disambiguator = this.root.testDisambiguator.get(baseIdentifier); if (disambiguator !== undefined) { testIdentifier += `:(${disambiguator})`; - this.root.testDisambiguator.set(testIdentifier, disambiguator + 1); + this.root.testDisambiguator.set(baseIdentifier, disambiguator + 1); } else { - this.root.testDisambiguator.set(testIdentifier, 1); + this.root.testDisambiguator.set(baseIdentifier, 1); } this.attempt = this.root.harness.previousRuns.length; const previousAttempt = this.root.harness.previousRuns[this.attempt - 1]?.[testIdentifier]; if (previousAttempt != null) { this.passedAttempt = previousAttempt.passed_on_attempt; + if (previousAttempt.duration_ms !== undefined) { + this.replayedDurationNs = BigInt(MathRound(previousAttempt.duration_ms * 1_000_000)); + } this.fn = () => { + // Restore the original duration on the synthetic replay. Suites are + // skipped here because Suite.run() unconditionally reassigns + // startTime later; only non-suite tests benefit from setting it. + if (this.reportedType !== 'suite') { + this.startTime = hrtime(); + this.endTime = this.startTime + (this.replayedDurationNs ?? 0n); + } for (let i = 0; i < (previousAttempt.children?.length ?? 0); i++) { const child = previousAttempt.children[i]; const t = this.createSubtest(Test, child.name, { __proto__: null }, noop, { __proto__: null, loc: [child.line, child.column, child.file], }, noop); - t.endTime = t.startTime = hrtime(); + t.startTime = hrtime(); + t.endTime = t.startTime + (t.replayedDurationNs ?? 0n); // For suites, Suite.run() starts the subtests via SafePromiseAll. // Starting them here as well would run them twice, re-invoking the // synthetic children-creator against a now-incremented disambiguator @@ -942,7 +956,7 @@ class Test extends AsyncResource { const deferred = this.dequeuePendingSubtest(); const test = deferred.test; this.assignReportOrder(test); - test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType, test.testId, test.tags); + test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType, test.testId, this.testId, test.tags); await test.run(); deferred.resolve(); } @@ -1199,7 +1213,8 @@ class Test extends AsyncResource { // it. Otherwise, return a Promise to the caller and mark the test as // pending for later execution. this.parent.unfinishedSubtests.add(this); - this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType, this.testId, this.tags); + this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType, + this.testId, this.parent?.testId, this.tags); if (this.root.harness.buildPromise || !this.parent.hasConcurrency()) { const deferred = PromiseWithResolvers(); @@ -1222,7 +1237,8 @@ class Test extends AsyncResource { } this.parent.assignReportOrder(this); - this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType, this.testId, this.tags); + this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType, + this.testId, this.parent?.testId, this.tags); return this.run(); } @@ -1496,7 +1512,7 @@ class Test extends AsyncResource { this.testNumber ||= ++this.parent.outputSubtestCount; this.reporter.complete( this.nesting, this.loc, this.testNumber, this.name, - report.details, report.directive, this.testId, this.tags, + report.details, report.directive, this.testId, this.parent?.testId, this.tags, ); this.parent.activeSubtests--; } @@ -1652,12 +1668,12 @@ class Test extends AsyncResource { if (this.passed) { this.reporter.ok( this.nesting, this.loc, this.testNumber, this.name, - report.details, report.directive, this.testId, this.tags, + report.details, report.directive, this.testId, this.parent?.testId, this.tags, ); } else { this.reporter.fail( this.nesting, this.loc, this.testNumber, this.name, - report.details, report.directive, this.testId, this.tags, + report.details, report.directive, this.testId, this.parent?.testId, this.tags, ); } @@ -1672,7 +1688,7 @@ class Test extends AsyncResource { } this.#reportedSubtest = true; this.parent.reportStarted(); - this.reporter.start(this.nesting, this.loc, this.name, this.testId, this.tags); + this.reporter.start(this.nesting, this.loc, this.name, this.testId, this.parent?.testId, this.tags); } clearExecutionTime() { @@ -1734,7 +1750,7 @@ class TestHook extends Test { __proto__: null, duration_ms: this.duration(), error, - }, undefined, undefined, parent.tags); + }, undefined, undefined, undefined, parent.tags); } } } @@ -1796,15 +1812,22 @@ class Suite extends Test { publishError(err); } this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); - } finally { - if (testChannel.end.hasSubscribers) { - publishEnd(); - } } + this.#publishEnd = publishEnd; this.buildPhaseFinished = true; } + #publishEnd = null; + + #publishSuiteEnd() { + const publishEnd = this.#publishEnd; + this.#publishEnd = null; + if (publishEnd !== null && testChannel.end.hasSubscribers) { + publishEnd(); + } + } + #ctx; getCtx() { this.#ctx ??= new TestContext(this); @@ -1856,6 +1879,7 @@ class Suite extends Test { } } finally { stopPromise?.[SymbolDispose](); + this.#publishSuiteEnd(); } this.postRun(); diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index 622e6372fa1d13..8be33f90dfa0f2 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -35,13 +35,14 @@ class TestsStream extends Readable { } } - fail(nesting, loc, testNumber, name, details, directive, testId, tags) { + fail(nesting, loc, testNumber, name, details, directive, testId, parentId, tags) { this[kEmitMessage]('test:fail', { __proto__: null, name, nesting, testNumber, testId, + parentId, details, tags: ArrayPrototypeSlice(tags), ...loc, @@ -49,13 +50,14 @@ class TestsStream extends Readable { }); } - ok(nesting, loc, testNumber, name, details, directive, testId, tags) { + ok(nesting, loc, testNumber, name, details, directive, testId, parentId, tags) { this[kEmitMessage]('test:pass', { __proto__: null, name, nesting, testNumber, testId, + parentId, details, tags: ArrayPrototypeSlice(tags), ...loc, @@ -63,13 +65,14 @@ class TestsStream extends Readable { }); } - complete(nesting, loc, testNumber, name, details, directive, testId, tags) { + complete(nesting, loc, testNumber, name, details, directive, testId, parentId, tags) { this[kEmitMessage]('test:complete', { __proto__: null, name, nesting, testNumber, testId, + parentId, details, tags: ArrayPrototypeSlice(tags), ...loc, @@ -98,36 +101,39 @@ class TestsStream extends Readable { return { __proto__: null, expectFailure: expectation ?? true }; } - enqueue(nesting, loc, name, type, testId, tags) { + enqueue(nesting, loc, name, type, testId, parentId, tags) { this[kEmitMessage]('test:enqueue', { __proto__: null, nesting, name, type, testId, + parentId, tags: ArrayPrototypeSlice(tags), ...loc, }); } - dequeue(nesting, loc, name, type, testId, tags) { + dequeue(nesting, loc, name, type, testId, parentId, tags) { this[kEmitMessage]('test:dequeue', { __proto__: null, nesting, name, type, testId, + parentId, tags: ArrayPrototypeSlice(tags), ...loc, }); } - start(nesting, loc, name, testId, tags) { + start(nesting, loc, name, testId, parentId, tags) { this[kEmitMessage]('test:start', { __proto__: null, nesting, name, testId, + parentId, tags: ArrayPrototypeSlice(tags), ...loc, }); diff --git a/lib/internal/util.js b/lib/internal/util.js index 34af9ca6f61a6f..2f72e636ab90dc 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -17,7 +17,6 @@ const { ObjectGetOwnPropertyDescriptors, ObjectGetPrototypeOf, ObjectKeys, - ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, ObjectValues, Promise, @@ -354,26 +353,6 @@ function cachedResult(fn) { }; } -// Useful for Wrapping an ES6 Class with a constructor Function that -// does not require the new keyword. For instance: -// class A { constructor(x) {this.x = x;}} -// const B = createClassWrapper(A); -// B() instanceof A // true -// B() instanceof B // true -function createClassWrapper(type) { - function fn(...args) { - return ReflectConstruct(type, args, new.target || type); - } - // Mask the wrapper function name and length values - ObjectDefineProperties(fn, { - name: { __proto__: null, value: type.name }, - length: { __proto__: null, value: type.length }, - }); - ObjectSetPrototypeOf(fn, type); - fn.prototype = type.prototype; - return fn; -} - let signalsToNamesMapping; function getSignalsToNamesMapping() { if (signalsToNamesMapping !== undefined) @@ -649,16 +628,6 @@ function exposeNamespace(target, name, namespaceObject) { }); } -function exposeGetterAndSetter(target, name, getter, setter = undefined) { - ObjectDefineProperty(target, name, { - __proto__: null, - enumerable: false, - configurable: true, - get: getter, - set: setter, - }); -} - function defineReplaceableLazyAttribute(target, id, keys, writable = true, check) { let mod; for (let i = 0; i < keys.length; i++) { @@ -726,24 +695,13 @@ const lazyDOMException = (message, name) => { }; -const kEnumerableProperty = { __proto__: null }; -kEnumerableProperty.enumerable = true; -ObjectFreeze(kEnumerableProperty); +const kEnumerableProperty = ObjectFreeze({ + __proto__: null, + enumerable: true, +}); const kEmptyObject = ObjectFreeze({ __proto__: null }); -function filterOwnProperties(source, keys) { - const filtered = { __proto__: null }; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (ObjectPrototypeHasOwnProperty(source, key)) { - filtered[key] = source[key]; - } - } - - return filtered; -} - /** * Mimics `obj[key] = value` but ignoring potential prototype inheritance. * @param {any} obj @@ -976,7 +934,6 @@ module.exports = { constructSharedArrayBuffer, convertProcessSignalToExitCode, convertToValidSignal, - createClassWrapper, decorateErrorStack, defineOperation, defineLazyProperties, @@ -989,13 +946,10 @@ module.exports = { exposeInterface, exposeLazyInterfaces, exposeNamespace, - exposeGetterAndSetter, filterDuplicateStrings, - filterOwnProperties, getConstructorOf, getCIDR, getCWDURL, - getInternalGlobal, getStructuredStack, getSystemErrorMap, getSystemErrorName, diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index a57c1241bf82be..38e327fad2081a 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -90,7 +90,10 @@ const { streamBaseState, } = internalBinding('stream_wrap'); -const { eos } = require('internal/streams/end-of-stream'); +const { + eos, + kEosNodeSynchronousCallback, +} = require('internal/streams/end-of-stream'); const { zlib } = internalBinding('constants'); const { UV_EOF } = internalBinding('uv'); @@ -136,6 +139,8 @@ function handleKnownInternalErrors(cause) { } } +const noop = () => {}; + /** * @typedef {import('../../stream').Writable} Writable * @typedef {import('../../stream').Readable} Readable @@ -233,6 +238,9 @@ function newWritableStreamFromStreamWritable(streamWritable, options = kEmptyObj } if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) { backpressurePromise = PromiseWithResolvers(); + if (!streamWritable.writableNeedDrain) { + backpressurePromise.resolve(); + } return SafePromisePrototypeFinally( backpressurePromise.promise, () => { backpressurePromise = undefined; @@ -443,6 +451,8 @@ function newStreamWritableFromWritableStream(writableStream, options = kEmptyObj return writable; } +const kErrorSentinelAttached = Symbol('kErrorSentinelAttached'); + /** * @typedef {import('./queuingstrategies').QueuingStrategy} QueuingStrategy * @param {Readable} streamReadable @@ -469,89 +479,81 @@ function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObj } const isBYOB = options.type === 'bytes'; - - if (isDestroyed(streamReadable) || !isReadable(streamReadable)) { - const readable = new ReadableStream(); - readable.cancel(); - return readable; - } - - const objectMode = streamReadable.readableObjectMode; - const highWaterMark = streamReadable.readableHighWaterMark; - - const evaluateStrategyOrFallback = (strategy) => { - // If the stream is BYOB, we only use highWaterMark - if (isBYOB) - return { highWaterMark }; - // If there is a strategy available, use it - if (strategy) - return strategy; - - if (objectMode) { - // When running in objectMode explicitly but no strategy, we just fall - // back to CountQueuingStrategy - return new CountQueuingStrategy({ highWaterMark }); - } - - return new ByteLengthQueuingStrategy({ highWaterMark }); - }; - - const strategy = evaluateStrategyOrFallback(options?.strategy); - let controller; let wasCanceled = false; + let strategy; - function onData(chunk) { - // Copy the Buffer to detach it from the pool. - if (Buffer.isBuffer(chunk) && !objectMode) - chunk = new Uint8Array(chunk); - controller.enqueue(chunk); - if (controller.desiredSize <= 0) - streamReadable.pause(); - } + /** @type {UnderlyingSource} */ + const underlyingSource = { + __proto__: null, + type: isBYOB ? 'bytes' : undefined, + start(c) { controller = c; }, + cancel(reason) { + wasCanceled = true; + destroy(streamReadable, reason); + }, + }; - streamReadable.pause(); + const readable = isReadable(streamReadable); + const objectMode = streamReadable.readableObjectMode; + if (readable) { + underlyingSource.pull = function pull() { + streamReadable.resume(); + }; + + const highWaterMark = streamReadable.readableHighWaterMark; + strategy = isBYOB ? { highWaterMark } : + options.strategy ?? new (objectMode ? CountQueuingStrategy : ByteLengthQueuingStrategy)({ highWaterMark }); + } + const readableStream = new ReadableStream(underlyingSource, strategy); - const cleanup = eos(streamReadable, (error) => { + // When adapting a Duplex as a ReadableStream, readable completion should not + // wait for a half-open writable side to finish as well. + let cleanup = noop; + cleanup = eos(streamReadable, { + __proto__: null, + writable: false, + [kEosNodeSynchronousCallback]: true, + }, (error) => { error = handleKnownInternalErrors(error); + // If eos calls the callback synchronously, cleanup is still a no-op here. cleanup(); - // This is a protection against non-standard, legacy streams - // that happen to emit an error event again after finished is called. - streamReadable.on('error', () => {}); - if (error) - return controller.error(error); - // Was already canceled + + if (!(kErrorSentinelAttached in streamReadable)) { + // This is a protection against non-standard, legacy streams + // that happen to emit an error event again after finished is called. + streamReadable.on('error', noop); + streamReadable[kErrorSentinelAttached] = true; + } if (wasCanceled) { return; } + wasCanceled = true; + if (error) + return controller.error(error); controller.close(); + if (isBYOB) + controller.byobRequest?.respond(0); }); - streamReadable.on('data', onData); - - return new ReadableStream({ - type: isBYOB ? 'bytes' : undefined, - start(c) { - controller = c; - if (isBYOB) { - streamReadable.once('end', () => { - // close the controller - controller.close(); - // And unlock the last BYOB read request - controller.byobRequest?.respond(0); - wasCanceled = true; - }); - } - }, - - pull() { streamReadable.resume(); }, + if (wasCanceled) { + // `eos` called the callback synchronously + cleanup(); + } else if (readable) { + streamReadable.pause(); + + streamReadable.on('data', function onData(chunk) { + // Copy the Buffer to detach it from the pool. + if (Buffer.isBuffer(chunk) && !objectMode) + chunk = new Uint8Array(chunk); + controller.enqueue(chunk); + if (controller.desiredSize <= 0) + streamReadable.pause(); + }); + } - cancel(reason) { - wasCanceled = true; - destroy(streamReadable, reason); - }, - }, strategy); + return readableStream; } /** diff --git a/lib/util.js b/lib/util.js index 9601593eaf404a..adebd890adcd71 100644 --- a/lib/util.js +++ b/lib/util.js @@ -38,6 +38,7 @@ const { ObjectValues, ReflectApply, RegExpPrototypeExec, + SafeMap, StringPrototypeSlice, StringPrototypeToWellFormed, } = primordials; @@ -114,8 +115,20 @@ const kEscapeEnd = 'm'; const kDimCode = 2; const kBoldCode = 1; +// Close sequence for 24-bit foreground colors (reset to default foreground) +const kHexCloseSeq = kEscape + '39' + kEscapeEnd; + let styleCache; +const kHexStyleCacheMax = 256; + +let hexStyleCache; + +function getHexStyleCache() { + hexStyleCache ??= new SafeMap(); + return hexStyleCache; +} + function getStyleCache() { if (styleCache === undefined) { styleCache = { __proto__: null }; @@ -137,6 +150,28 @@ function getStyleCache() { return styleCache; } +/** + * Returns the cached ANSI escape sequences for a hex color. + * Computes and caches on first use to avoid repeated Buffer allocations. + * @param {string} hex A valid hex color string (#RGB or #RRGGBB) + * @returns {{openSeq: string, closeSeq: string}} + */ +function getHexStyle(hex) { + const cache = getHexStyleCache(); + const cached = cache.get(hex); + if (cached !== undefined) return cached; + const { 0: r, 1: g, 2: b } = hexToRgb(hex); + const style = { + __proto__: null, + openSeq: kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd, + closeSeq: kHexCloseSeq, + }; + if (cache.size >= kHexStyleCacheMax) + cache.delete(cache.keys().next().value); + cache.set(hex, style); + return style; +} + function replaceCloseCode(str, closeSeq, openSeq, keepClose) { const closeLen = closeSeq.length; let index = str.indexOf(closeSeq); @@ -163,15 +198,6 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) { // Matches #RGB or #RRGGBB const hexColorRegExp = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; -/** - * Validates whether a string is a valid hex color code. - * @param {string} hex The hex string to validate (e.g., '#fff' or '#ffffff') - * @returns {boolean} True if valid hex color, false otherwise - */ -function isValidHexColor(hex) { - return typeof hex === 'string' && RegExpPrototypeExec(hexColorRegExp, hex) !== null; -} - /** * Parses a hex color string into RGB components. * Supports both 3-digit (#RGB) and 6-digit (#RRGGBB) formats. @@ -225,6 +251,17 @@ function styleText(format, text, options) { const processed = replaceCloseCode(text, style.closeSeq, style.openSeq, style.keepClose); return style.openSeq + processed + style.closeSeq; } + + if (format[0] === '#') { + let hexStyle = getHexStyleCache().get(format); + if (hexStyle === undefined && RegExpPrototypeExec(hexColorRegExp, format) !== null) { + hexStyle = getHexStyle(format); + } + if (hexStyle !== undefined) { + const processed = replaceCloseCode(text, hexStyle.closeSeq, hexStyle.openSeq, false); + return hexStyle.openSeq + processed + hexStyle.closeSeq; + } + } } validateString(text, 'text'); @@ -255,24 +292,26 @@ function styleText(format, text, options) { for (const key of formatArray) { if (key === 'none') continue; - if (isValidHexColor(key)) { - if (skipColorize) continue; - const { 0: r, 1: g, 2: b } = hexToRgb(key); - const openSeq = kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd; - const closeSeq = kEscape + '39' + kEscapeEnd; - openCodes += openSeq; - closeCodes = closeSeq + closeCodes; - processedText = replaceCloseCode(processedText, closeSeq, openSeq, false); + if (typeof key === 'string' && key[0] === '#') { + let hexStyle = getHexStyleCache().get(key); + if (hexStyle === undefined) { + if (RegExpPrototypeExec(hexColorRegExp, key) === null) { + throw new ERR_INVALID_ARG_VALUE('format', key, + 'must be a valid hex color (#RGB or #RRGGBB)'); + } + if (skipColorize) continue; + hexStyle = getHexStyle(key); + } else if (skipColorize) { + continue; + } + openCodes += hexStyle.openSeq; + closeCodes = hexStyle.closeSeq + closeCodes; + processedText = replaceCloseCode(processedText, hexStyle.closeSeq, hexStyle.openSeq, false); continue; } const style = cache[key]; if (style === undefined) { - // Check if it looks like an invalid hex color (starts with #) - if (typeof key === 'string' && key[0] === '#') { - throw new ERR_INVALID_ARG_VALUE('format', key, - 'must be a valid hex color (#RGB or #RRGGBB)'); - } validateOneOf(key, 'format', ObjectGetOwnPropertyNames(inspect.colors)); } openCodes += style.openSeq; diff --git a/node.gyp b/node.gyp index c06e95a98e5ce9..82c5bdaecfec3d 100644 --- a/node.gyp +++ b/node.gyp @@ -452,6 +452,7 @@ 'test/cctest/test_quic_cid.cc', 'test/cctest/test_quic_error.cc', 'test/cctest/test_quic_preferredaddress.cc', + 'test/cctest/test_quic_tokenbucket.cc', 'test/cctest/test_quic_tokens.cc', ], 'node_cctest_inspector_sources': [ @@ -1021,11 +1022,6 @@ '@rpath/lib<(node_core_target_name).<(shlib_suffix)' }, }], - [ 'node_use_node_code_cache=="true"', { - 'defines': [ - 'NODE_USE_NODE_CODE_CACHE=1', - ], - }], ['node_shared=="true" and OS in "aix os400"', { 'product_name': 'node_base', }], diff --git a/src/aliased_struct-inl.h b/src/aliased_struct-inl.h index 17d5ff58097e22..ff70f423eb1bd1 100644 --- a/src/aliased_struct-inl.h +++ b/src/aliased_struct-inl.h @@ -47,6 +47,95 @@ AliasedStruct::~AliasedStruct() { if (ptr_ != nullptr) ptr_->~T(); } +// --------------------------------------------------------------------------- +// AliasedStructArena implementation +// --------------------------------------------------------------------------- + +template +typename AliasedStructArena::Page* +AliasedStructArena::FindOrCreatePage(v8::Isolate* isolate) { + for (auto& p : pages_) { + if (p->HasFreeSlots()) return p.get(); + } + auto p = std::make_unique(); + p->Init(isolate); + Page* raw = p.get(); + pages_.push_back(std::move(p)); + return raw; +} + +template +template +typename AliasedStructArena::Slot +AliasedStructArena::Allocate(v8::Isolate* isolate, + Args&&... args) { + Page* page = FindOrCreatePage(isolate); + DCHECK(page->HasFreeSlots()); + + uint32_t idx = page->free_head; + T* raw = &page->base[idx]; + + // Advance freelist before placement new overwrites the linkage. + page->free_head = *reinterpret_cast(raw); + page->used_count++; + + // Placement-construct T in the slot. + T* ptr = new (raw) T(std::forward(args)...); + + Slot slot; + slot.page = static_cast(page); + slot.ptr = static_cast(ptr); + slot.index = idx; + slot.byte_offset = reinterpret_cast(ptr) - + static_cast(page->store->Data()); + return slot; +} + +template +void AliasedStructArena::Release( + typename AliasedStructArena::Slot&& slot) { + if (!slot) return; + auto* page = static_cast(slot.page); + auto* ptr = static_cast(slot.ptr); + uint32_t idx = slot.index; + + // Destruct and zero so JS views see clean data. + ptr->~T(); + memset(ptr, 0, sizeof(T)); + + // Push onto page freelist. + *reinterpret_cast(ptr) = page->free_head; + page->free_head = idx; + page->used_count--; + + slot.page = nullptr; + slot.ptr = nullptr; + + // Drop empty pages. The shared_ptr ensures the + // underlying memory stays alive until V8 GCs any remaining JS + // references to the page's ArrayBuffer/views. + if (page->used_count == 0) { + for (auto it = pages_.begin(); it != pages_.end(); ++it) { + if (it->get() == page) { + pages_.erase(it); + break; + } + } + } +} + +template +void AliasedStructArena::ReleaseSlot(ArenaSlotBase& base) { + Slot slot; + slot.page = base.page; + slot.ptr = base.ptr; + slot.index = base.index; + slot.byte_offset = base.byte_offset; + Release(std::move(slot)); + base.page = nullptr; + base.ptr = nullptr; +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/aliased_struct.h b/src/aliased_struct.h index e4df393f4985a3..97753192723feb 100644 --- a/src/aliased_struct.h +++ b/src/aliased_struct.h @@ -3,9 +3,10 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include +#include #include "node_internals.h" #include "v8.h" -#include namespace node { @@ -56,6 +57,190 @@ class AliasedStruct final { v8::Global buffer_; }; +// --------------------------------------------------------------------------- +// ArenaSlot — type-erased handle to a slot in an AliasedStructArena page. +// This can be stored in headers where T is incomplete. The typed accessors +// are provided via a thin typed wrapper (AliasedStructArena::Slot). +struct ArenaSlotBase { + // Opaque page pointer — only the arena knows the concrete type. + void* page = nullptr; + void* ptr = nullptr; + uint32_t index = 0; + size_t byte_offset = 0; + + explicit operator bool() const { return ptr != nullptr; } + + // Returns the page's ArrayBuffer. Implemented below after ArenaPageHeader. + v8::Local GetArrayBuffer(v8::Isolate* isolate) const; + + size_t GetByteOffset() const { return byte_offset; } + + // Returns the page's cached DataView over the full page. + // Callers use byte_offset to index into the correct slot region. + v8::Local GetPageDataView(v8::Isolate* isolate) const; + + // Returns the page's cached BigUint64Array over the full page. + // Callers use byte_offset / sizeof(uint64_t) to index into the + // correct slot region. + v8::Local GetPageBigUint64Array( + v8::Isolate* isolate) const; +}; + +// --------------------------------------------------------------------------- +// AliasedStructArena — pool allocator for AliasedStruct-style shared +// memory. Instead of creating a separate ArrayBuffer + BackingStore per +// instance, the arena pre-allocates pages of N slots backed by a single +// ArrayBuffer each. Callers receive a Slot handle that provides the same +// T*/operator-> interface as AliasedStruct, plus the ability to create a +// JS typed-array view over just that slot's region of the page buffer. +// +// Pages target kPageBytes (default 16 KB) for L1 cache residency during +// sequential access patterns. Slots are recycled via an intrusive +// freelist, and empty pages are dropped when their last slot is released. +// +// Usage: +// AliasedStructArena arena; +// auto slot = arena.Allocate(isolate); +// slot->some_field = 42; +// auto view = slot.GetArrayBuffer(isolate); // JS-visible view +// ... +// arena.Release(std::move(slot)); // return to freelist +// +template +class AliasedStructArena final { + public: + static constexpr size_t kSlotsPerPage = kPageBytes / sizeof(T); + static_assert(kSlotsPerPage >= 4, "Page too small for type T"); + static_assert(sizeof(T) >= sizeof(uint32_t), + "T must be at least 4 bytes for freelist linkage"); + + AliasedStructArena() = default; + ~AliasedStructArena() = default; + + AliasedStructArena(const AliasedStructArena&) = delete; + AliasedStructArena& operator=(const AliasedStructArena&) = delete; + + struct Page { + std::shared_ptr store; + v8::Global buffer; + // Lazily created full-page views shared by all slots in + // this page. Typically only one is used per arena. + v8::Global data_view; + v8::Global big_uint64_array; + size_t page_byte_length = 0; + T* base = nullptr; + uint32_t free_head = 0; + uint32_t used_count = 0; + static constexpr uint32_t kNoFreeSlot = UINT32_MAX; + + void Init(v8::Isolate* isolate) { + const v8::HandleScope handle_scope(isolate); + const size_t total_bytes = kSlotsPerPage * sizeof(T); + store = v8::ArrayBuffer::NewBackingStore(isolate, total_bytes); + memset(store->Data(), 0, total_bytes); + base = static_cast(store->Data()); + page_byte_length = total_bytes; + v8::Local ab = v8::ArrayBuffer::New(isolate, store); + buffer = v8::Global(isolate, ab); + + // Build freelist: each slot points to the next. + for (uint32_t i = 0; i < kSlotsPerPage - 1; i++) { + *reinterpret_cast(&base[i]) = i + 1; + } + *reinterpret_cast(&base[kSlotsPerPage - 1]) = kNoFreeSlot; + free_head = 0; + used_count = 0; + } + + bool HasFreeSlots() const { return free_head != kNoFreeSlot; } + }; + + // Typed slot handle — wraps ArenaSlotBase with T* accessors. + class Slot : public ArenaSlotBase { + public: + Slot() = default; + + const T& operator*() const { return *static_cast(ptr); } + T& operator*() { return *static_cast(ptr); } + const T* operator->() const { return static_cast(ptr); } + T* operator->() { return static_cast(ptr); } + T* Data() { return static_cast(ptr); } + const T* Data() const { return static_cast(ptr); } + }; + + // Allocate a slot, placement-constructing T with the given args. + // Creates a new page if all existing pages are full. + template + Slot Allocate(v8::Isolate* isolate, Args&&... args); + + // Release a slot back to the arena freelist. Calls ~T() and zeros + // the memory so that any JS views see clean data. + void Release(Slot&& slot); + + // Release a slot given a type-erased ArenaSlotBase reference. + // Convenience for callers that store ArenaSlotBase in headers where + // T is incomplete. + void ReleaseSlot(ArenaSlotBase& base); + + private: + Page* FindOrCreatePage(v8::Isolate* isolate); + + std::vector> pages_; +}; + +// ArenaSlotBase accessors need to reach the v8::Globals inside a Page. +// All AliasedStructArena::Page types share the same leading layout. +// The page_byte_length field allows lazy view creation without knowing T. +namespace detail { +struct ArenaPageHeader { + std::shared_ptr store; + v8::Global buffer; + v8::Global data_view; + v8::Global big_uint64_array; + size_t page_byte_length = 0; + + v8::Local GetDataView(v8::Isolate* isolate) { + if (data_view.IsEmpty()) { + const v8::HandleScope handle_scope(isolate); + auto dv = v8::DataView::New(buffer.Get(isolate), 0, page_byte_length); + data_view = v8::Global(isolate, dv); + } + return data_view.Get(isolate); + } + + v8::Local GetBigUint64Array(v8::Isolate* isolate) { + if (big_uint64_array.IsEmpty()) { + const v8::HandleScope handle_scope(isolate); + auto bu = v8::BigUint64Array::New( + buffer.Get(isolate), 0, page_byte_length / sizeof(uint64_t)); + big_uint64_array = v8::Global(isolate, bu); + } + return big_uint64_array.Get(isolate); + } +}; +} // namespace detail + +inline v8::Local ArenaSlotBase::GetArrayBuffer( + v8::Isolate* isolate) const { + DCHECK_NOT_NULL(page); + auto* header = static_cast(page); + return header->buffer.Get(isolate); +} + +inline v8::Local ArenaSlotBase::GetPageDataView( + v8::Isolate* isolate) const { + DCHECK_NOT_NULL(page); + auto* header = static_cast(page); + return header->GetDataView(isolate); +} + +inline v8::Local ArenaSlotBase::GetPageBigUint64Array( + v8::Isolate* isolate) const { + DCHECK_NOT_NULL(page); + auto* header = static_cast(page); + return header->GetBigUint64Array(isolate); +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/api/environment.cc b/src/api/environment.cc index 25657d99bddaa3..c3fd1e58373516 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -2,6 +2,7 @@ #if HAVE_OPENSSL #include "crypto/crypto_util.h" #endif // HAVE_OPENSSL +#include "env.h" #include "env_properties.h" #include "node.h" #include "node_builtins.h" @@ -1053,6 +1054,18 @@ Maybe InitializeContext(Local context) { return Just(true); } +void RegisterContext(Environment* env, + v8::Local context, + std::string_view name, + std::string_view origin) { + ContextInfo info{std::string(name), std::string(origin)}; + env->AssignToContext(context, nullptr, info); +} + +void UnregisterContext(Environment* env, v8::Local context) { + env->UnassignFromContext(context); +} + uv_loop_t* GetCurrentEventLoop(Isolate* isolate) { HandleScope handle_scope(isolate); Local context = isolate->GetCurrentContext(); diff --git a/src/crypto/README.md b/src/crypto/README.md index 263a512cdefc9b..4059ae23711b84 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -186,7 +186,8 @@ All operations that are not either Stream-based or single-use functions are built around the `CryptoJob` class. A `CryptoJob` encapsulates a single crypto operation that can be -invoked synchronously or asynchronously. +invoked synchronously, asynchronously, or as a Web Crypto API +Promise-based job. The `CryptoJob` class itself is a C++ template that takes a single `CryptoJobTraits` struct as a parameter. The `CryptoJobTraits` @@ -228,14 +229,15 @@ specializations and will either be called synchronously within the current thread or from within the libuv threadpool. Every `CryptoJob` instance exposes a `run()` function to the -JavaScript layer. When called, `run()` with either dispatch the -job to the libuv threadpool or invoke the Implementation -function synchronously. If invoked synchronously, run() will -return a JavaScript array. The first value in the array is -either an `Error` or `undefined`. If the operation was successful, -the second value in the array will contain the result of the -operation. Typically, the result is an `ArrayBuffer`, but -certain `CryptoJob` types can alter the output. +JavaScript layer. When called, `run()` will either dispatch the +job to the libuv threadpool, invoke the Implementation function +synchronously, or return a `Promise` for Web Crypto API jobs. If +invoked synchronously, `run()` will return a JavaScript array. +The first value in the array is either an `Error` or `undefined`. +If the operation was successful, the second value in the array +will contain the result of the operation. Typically, the result +is an `ArrayBuffer`, but certain `CryptoJob` types can alter the +output. If the `CryptoJob` is processed asynchronously, then the job must have an `ondone` property whose value is a function that @@ -244,11 +246,19 @@ be called with two arguments. The first is either an `Error` or `undefined`, and the second is the result of the operation if successful. +If the `CryptoJob` is processed as a Web Crypto API job, then +`run()` returns a Promise. Operation-specific failures are +rejected with an `OperationError`, and successful jobs resolve +with the Web Crypto API result shape expected by the JavaScript +implementation. + For `CipherJob` types, the output is always an `ArrayBuffer`. For `KeyGenJob` types, the output is either a single KeyObject, or an array containing a Public/Private key pair represented -either as a `KeyObjectHandle` object or a `Buffer`. +either as a `KeyObjectHandle` object or a `Buffer`. Web Crypto +API key generation jobs return a `CryptoKey` or a `CryptoKeyPair` +object. For `DeriveBitsJob` type output is typically an `ArrayBuffer` but can be other values (`RandomBytesJob` for instance, fills an @@ -273,11 +283,12 @@ should be used to throw JavaScript errors when necessary. ### Operation mode -All crypto functions in Node.js operate in one of three +All crypto functions in Node.js operate in one of these modes: * Synchronous single-call * Asynchronous single-call +* Web Crypto API Promise-based * Stream-oriented It is often possible to perform various operations across diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index 815c972837049a..9172def7d4ebee 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -418,9 +418,7 @@ bool ValidateIV( THROW_ERR_OUT_OF_RANGE(env, "iv is too big"); return false; } - params->iv = (mode == kCryptoJobAsync) - ? iv.ToCopy() - : iv.ToByteSource(); + params->iv = (IsCryptoJobAsync(mode)) ? iv.ToCopy() : iv.ToByteSource(); return true; } @@ -466,9 +464,9 @@ bool ValidateAdditionalData( THROW_ERR_OUT_OF_RANGE(env, "additionalData is too big"); return false; } - params->additional_data = mode == kCryptoJobAsync - ? additional.ToCopy() - : additional.ToByteSource(); + params->additional_data = IsCryptoJobAsync(mode) + ? additional.ToCopy() + : additional.ToByteSource(); } return true; } @@ -495,7 +493,7 @@ AESCipherConfig& AESCipherConfig::operator=(AESCipherConfig&& other) noexcept { void AESCipherConfig::MemoryInfo(MemoryTracker* tracker) const { // If mode is sync, then the data in each of these properties // is not owned by the AESCipherConfig, so we ignore it. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("iv", iv.size()); tracker->TrackFieldWithSize("additional_data", additional_data.size()); } diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index d5207f4be57bb2..42c5179f2b2340 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -1,5 +1,8 @@ #include "crypto/crypto_argon2.h" #include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" +#include "memory_tracker-inl.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_ARGON2 @@ -19,6 +22,7 @@ using v8::Value; Argon2Config::Argon2Config(Argon2Config&& other) noexcept : mode{other.mode}, + key{std::move(other.key)}, pass{std::move(other.pass)}, salt{std::move(other.salt)}, secret{std::move(other.secret)}, @@ -36,8 +40,9 @@ Argon2Config& Argon2Config::operator=(Argon2Config&& other) noexcept { } void Argon2Config::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) { - tracker->TrackFieldWithSize("pass", pass.size()); + if (key) tracker->TrackField("key", key); + if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("secret", secret.size()); tracker->TrackFieldWithSize("ad", ad.size()); @@ -59,14 +64,23 @@ Maybe Argon2Traits::AdditionalConfig( config->mode = mode; - ArrayBufferOrViewContents pass(args[offset]); + CHECK(KeyObjectHandle::HasInstance(env, args[offset]) || + IsAnyBufferSource(args[offset])); // pass ArrayBufferOrViewContents salt(args[offset + 1]); ArrayBufferOrViewContents secret(args[offset + 6]); ArrayBufferOrViewContents ad(args[offset + 7]); - if (!pass.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); - return Nothing(); + if (KeyObjectHandle::HasInstance(env, args[offset])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset], Nothing()); + config->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents pass(args[offset]); + if (!pass.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + config->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); } if (!salt.CheckSizeInt32()) [[unlikely]] { @@ -84,8 +98,7 @@ Maybe Argon2Traits::AdditionalConfig( return Nothing(); } - const bool isAsync = mode == kCryptoJobAsync; - config->pass = isAsync ? pass.ToCopy() : pass.ToByteSource(); + const bool isAsync = IsCryptoJobAsync(mode); config->salt = isAsync ? salt.ToCopy() : salt.ToByteSource(); config->secret = isAsync ? secret.ToCopy() : secret.ToByteSource(); config->ad = isAsync ? ad.ToCopy() : ad.ToByteSource(); @@ -119,7 +132,13 @@ bool Argon2Traits::DeriveBits(Environment* env, } // Both the pass and salt may be zero-length at this point - auto dp = ncrypto::argon2(config.pass, + const ncrypto::Buffer pass{ + .data = config.key ? config.key.GetSymmetricKey() + : config.pass.data(), + .len = config.key ? config.key.GetSymmetricKeySize() : config.pass.size(), + }; + + auto dp = ncrypto::argon2(pass, config.salt, config.lanes, config.keylen, diff --git a/src/crypto/crypto_argon2.h b/src/crypto/crypto_argon2.h index 354d0a4be6f392..058293805c073a 100644 --- a/src/crypto/crypto_argon2.h +++ b/src/crypto/crypto_argon2.h @@ -3,6 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include "crypto/crypto_keys.h" #include "crypto/crypto_util.h" namespace node::crypto { @@ -22,6 +23,7 @@ namespace node::crypto { struct Argon2Config final : public MemoryRetainer { CryptoJobMode mode; + KeyObjectData key; ByteSource pass; ByteSource salt; ByteSource secret; diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc index 43d63fa8c5e409..cfe43122d5aa55 100644 --- a/src/crypto/crypto_chacha20_poly1305.cc +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -48,7 +48,7 @@ bool ValidateIV(Environment* env, return false; } - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { params->iv = iv.ToCopy(); } else { params->iv = iv.ToByteSource(); @@ -68,7 +68,7 @@ bool ValidateAdditionalData(Environment* env, return false; } - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { params->additional_data = additional_data.ToCopy(); } else { params->additional_data = additional_data.ToByteSource(); @@ -96,7 +96,7 @@ ChaCha20Poly1305CipherConfig& ChaCha20Poly1305CipherConfig::operator=( void ChaCha20Poly1305CipherConfig::MemoryInfo(MemoryTracker* tracker) const { // If mode is sync, then the data in each of these properties // is not owned by the ChaCha20Poly1305CipherConfig, so we ignore it. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("iv", iv.size()); tracker->TrackFieldWithSize("additional_data", additional_data.size()); } diff --git a/src/crypto/crypto_cipher.h b/src/crypto/crypto_cipher.h index 006d18a7118761..a00afa6a0f9f81 100644 --- a/src/crypto/crypto_cipher.h +++ b/src/crypto/crypto_cipher.h @@ -164,13 +164,7 @@ class CipherJob final : public CryptoJob { } new CipherJob( - env, - args.This(), - mode, - key, - cipher_mode, - data, - std::move(params)); + env, args.This(), mode, key, cipher_mode, data, std::move(params)); } static void Initialize( @@ -197,7 +191,7 @@ class CipherJob final : public CryptoJob { std::move(params)), key_(key->Data().addRef()), cipher_mode_(cipher_mode), - in_(mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource()) {} + in_(IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource()) {} const KeyObjectData& key() const { return key_; } @@ -261,7 +255,7 @@ class CipherJob final : public CryptoJob { SET_SELF_SIZE(CipherJob) void MemoryInfo(MemoryTracker* tracker) const override { - if (CryptoJob::mode() == kCryptoJobAsync) + if (IsCryptoJobAsync(CryptoJob::mode())) tracker->TrackFieldWithSize("in", in_.size()); tracker->TrackFieldWithSize("out", out_.size()); CryptoJob::MemoryInfo(tracker); diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 9355b7f7a6ca64..a89e3391dbf896 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -488,10 +488,10 @@ bool ExportJWKEcKey(Environment* env, return false; } - if (target->Set( - env->context(), - env->jwk_kty_string(), - env->jwk_ec_string()).IsNothing()) { + if (!target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_ec_string()) + .FromMaybe(false)) { return false; } @@ -531,10 +531,9 @@ bool ExportJWKEcKey(Environment* env, return false; } } - if (target->Set( - env->context(), - env->jwk_crv_string(), - crv_name).IsNothing()) { + if (!target + ->DefineOwnProperty(env->context(), env->jwk_crv_string(), crv_name) + .FromMaybe(false)) { return false; } @@ -577,20 +576,23 @@ bool ExportJWKEdKey(Environment* env, const ncrypto::Buffer out = data; return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) .ToLocal(&encoded) && - target->Set(env->context(), key, encoded).IsJust(); + target->DefineOwnProperty(env->context(), key, encoded) + .FromMaybe(false); }; return !( - target - ->Set(env->context(), - env->jwk_crv_string(), - OneByteString(env->isolate(), curve)) - .IsNothing() || + !target + ->DefineOwnProperty(env->context(), + env->jwk_crv_string(), + OneByteString(env->isolate(), curve)) + .FromMaybe(false) || (key.GetKeyType() == kKeyTypePrivate && !trySetKey(env, pkey.rawPrivateKey(), target, env->jwk_d_string())) || !trySetKey(env, pkey.rawPublicKey(), target, env->jwk_x_string()) || - target->Set(env->context(), env->jwk_kty_string(), env->jwk_okp_string()) - .IsNothing()); + !target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_okp_string()) + .FromMaybe(false)); } KeyObjectData ImportJWKEdKey(Environment* env, Local jwk) { Local crv_value; diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index c42926bb4ce61f..44181cd045b429 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -510,8 +510,7 @@ HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { void HashConfig::MemoryInfo(MemoryTracker* tracker) const { // If the Job is sync, then the HashConfig does not own the data. - if (mode == kCryptoJobAsync) - tracker->TrackFieldWithSize("in", in.size()); + if (IsCryptoJobAsync(mode)) tracker->TrackFieldWithSize("in", in.size()); } MaybeLocal HashTraits::EncodeOutput(Environment* env, @@ -542,9 +541,7 @@ Maybe HashTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->in = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); unsigned int expected = EVP_MD_size(params->digest); params->length = expected; diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc index 53b8f75c39bd97..eb40ddad41c6e3 100644 --- a/src/crypto/crypto_hkdf.cc +++ b/src/crypto/crypto_hkdf.cc @@ -24,6 +24,7 @@ HKDFConfig::HKDFConfig(HKDFConfig&& other) noexcept length(other.length), digest(other.digest), key(std::move(other.key)), + key_data(std::move(other.key_data)), salt(std::move(other.salt)), info(std::move(other.info)) {} @@ -49,7 +50,8 @@ Maybe HKDFTraits::AdditionalConfig( params->mode = mode; CHECK(args[offset]->IsString()); // Hash - CHECK(args[offset + 1]->IsObject()); // Key + CHECK(KeyObjectHandle::HasInstance(env, args[offset + 1]) || + IsAnyBufferSource(args[offset + 1])); // Key CHECK(IsAnyBufferSource(args[offset + 2])); // Salt CHECK(IsAnyBufferSource(args[offset + 3])); // Info CHECK(args[offset + 4]->IsUint32()); // Length @@ -61,9 +63,19 @@ Maybe HKDFTraits::AdditionalConfig( return Nothing(); } - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); - params->key = key->Data().addRef(); + if (KeyObjectHandle::HasInstance(env, args[offset + 1])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); + params->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents key_data(args[offset + 1]); + if (!key_data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "key is too big"); + return Nothing(); + } + params->key_data = + IsCryptoJobAsync(mode) ? key_data.ToCopy() : key_data.ToByteSource(); + } ArrayBufferOrViewContents salt(args[offset + 2]); ArrayBufferOrViewContents info(args[offset + 3]); @@ -77,13 +89,9 @@ Maybe HKDFTraits::AdditionalConfig( return Nothing(); } - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); - params->info = mode == kCryptoJobAsync - ? info.ToCopy() - : info.ToByteSource(); + params->info = IsCryptoJobAsync(mode) ? info.ToCopy() : info.ToByteSource(); params->length = args[offset + 4].As()->Value(); // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as the @@ -102,12 +110,16 @@ bool HKDFTraits::DeriveBits(Environment* env, ByteSource* out, CryptoJobMode mode, CryptoErrorStore* errors) { + const ncrypto::Buffer key_data{ + .data = params.key ? reinterpret_cast( + params.key.GetSymmetricKey()) + : params.key_data.data(), + .len = params.key ? params.key.GetSymmetricKeySize() + : params.key_data.size(), + }; + auto dp = ncrypto::hkdf(params.digest, - ncrypto::Buffer{ - .data = reinterpret_cast( - params.key.GetSymmetricKey()), - .len = params.key.GetSymmetricKeySize(), - }, + key_data, ncrypto::Buffer{ .data = params.info.data(), .len = params.info.size(), @@ -128,9 +140,10 @@ bool HKDFTraits::DeriveBits(Environment* env, } void HKDFConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("key", key); + if (key) tracker->TrackField("key", key); // If the job is sync, then the HKDFConfig does not own the data - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("key", key_data.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("info", info.size()); } diff --git a/src/crypto/crypto_hkdf.h b/src/crypto/crypto_hkdf.h index 9f624d73dc1936..bda6df6341219a 100644 --- a/src/crypto/crypto_hkdf.h +++ b/src/crypto/crypto_hkdf.h @@ -16,6 +16,7 @@ struct HKDFConfig final : public MemoryRetainer { size_t length; ncrypto::Digest digest; KeyObjectData key; + ByteSource key_data; ByteSource salt; ByteSource info; diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc index 80d8608b434c31..acd4b819de38fc 100644 --- a/src/crypto/crypto_hmac.cc +++ b/src/crypto/crypto_hmac.cc @@ -172,7 +172,7 @@ HmacConfig& HmacConfig::operator=(HmacConfig&& other) noexcept { void HmacConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the HmacConfig does not own the data - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); } @@ -210,9 +210,7 @@ Maybe HmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (!args[offset + 4]->IsUndefined()) { ArrayBufferOrViewContents signature(args[offset + 4]); @@ -220,9 +218,8 @@ Maybe HmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); } - params->signature = mode == kCryptoJobAsync - ? signature.ToCopy() - : signature.ToByteSource(); + params->signature = + IsCryptoJobAsync(mode) ? signature.ToCopy() : signature.ToByteSource(); } return JustVoid(); diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index d30c6aaef6253f..09fbf0844f48f2 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -16,6 +16,7 @@ namespace node { using ncrypto::EVPKeyPointer; using v8::Array; +using v8::ArrayBufferView; using v8::FunctionCallbackInfo; using v8::Local; using v8::Maybe; @@ -41,7 +42,7 @@ KEMConfiguration& KEMConfiguration::operator=( void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("ciphertext", ciphertext.size()); } } @@ -173,6 +174,23 @@ MaybeLocal KEMEncapsulateTraits::EncodeOutput( return MaybeLocal(); } + if (params.job_mode == kCryptoJobWebCrypto) { + Local result = Object::New(env->isolate()); + if (!result + ->DefineOwnProperty(env->context(), + OneByteString(env->isolate(), "sharedKey"), + shared_key_obj.As()->Buffer()) + .FromMaybe(false) || + !result + ->DefineOwnProperty(env->context(), + OneByteString(env->isolate(), "ciphertext"), + ciphertext_obj.As()->Buffer()) + .FromMaybe(false)) { + return MaybeLocal(); + } + return result; + } + // Return an array [sharedKey, ciphertext]. Local result = Array::New(env->isolate(), 2); if (result->Set(env->context(), 0, shared_key_obj).IsNothing() || @@ -209,7 +227,7 @@ Maybe KEMDecapsulateTraits::AdditionalConfig( } params->ciphertext = - mode == kCryptoJobAsync ? ciphertext.ToCopy() : ciphertext.ToByteSource(); + IsCryptoJobAsync(mode) ? ciphertext.ToCopy() : ciphertext.ToByteSource(); return v8::JustVoid(); } diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index e43d8cb0475ff2..1702dfabb4af2a 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -22,6 +22,20 @@ enum class KeyGenJobStatus { FAILED }; +struct WebCryptoKeyGenConfig final { + v8::Global algorithm; + uint32_t usages_mask = 0; + uint32_t public_usages_mask = 0; + uint32_t private_usages_mask = 0; + bool extractable = false; + + WebCryptoKeyGenConfig() = default; + WebCryptoKeyGenConfig(WebCryptoKeyGenConfig&&) = default; + WebCryptoKeyGenConfig& operator=(WebCryptoKeyGenConfig&&) = default; + WebCryptoKeyGenConfig(const WebCryptoKeyGenConfig&) = delete; + WebCryptoKeyGenConfig& operator=(const WebCryptoKeyGenConfig&) = delete; +}; + // A Base CryptoJob for generating secret keys or key pairs. // The KeyGenTraits is largely responsible for the details of // the implementation, while KeyGenJob handles the common @@ -48,7 +62,29 @@ class KeyGenJob final : public CryptoJob { return; } - new KeyGenJob(env, args.This(), mode, std::move(params)); + WebCryptoKeyGenConfig config; + if (mode == kCryptoJobWebCrypto) { + if constexpr (KeyGenTraits::kWebCryptoKeyPair) { + CHECK(args[offset]->IsObject()); + CHECK(args[offset + 1]->IsUint32()); + CHECK(args[offset + 2]->IsUint32()); + CHECK(args[offset + 3]->IsBoolean()); + config.algorithm.Reset(env->isolate(), args[offset]); + config.public_usages_mask = args[offset + 1].As()->Value(); + config.private_usages_mask = args[offset + 2].As()->Value(); + config.extractable = args[offset + 3]->IsTrue(); + } else { + CHECK(args[offset]->IsObject()); + CHECK(args[offset + 1]->IsUint32()); + CHECK(args[offset + 2]->IsBoolean()); + config.algorithm.Reset(env->isolate(), args[offset]); + config.usages_mask = args[offset + 1].As()->Value(); + config.extractable = args[offset + 2]->IsTrue(); + } + } + + new KeyGenJob( + env, args.This(), mode, std::move(params), std::move(config)); } static void Initialize( @@ -61,17 +97,14 @@ class KeyGenJob final : public CryptoJob { CryptoJob::RegisterExternalReferences(New, registry); } - KeyGenJob( - Environment* env, - v8::Local object, - CryptoJobMode mode, - AdditionalParams&& params) + KeyGenJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params, + WebCryptoKeyGenConfig&& config) : CryptoJob( - env, - object, - KeyGenTraits::Provider, - mode, - std::move(params)) {} + env, object, KeyGenTraits::Provider, mode, std::move(params)), + webcrypto_config_(std::move(config)) {} void DoThreadPoolWork() override { AdditionalParams* params = CryptoJob::params(); @@ -98,7 +131,11 @@ class KeyGenJob final : public CryptoJob { if (status_ == KeyGenJobStatus::OK) { v8::TryCatch try_catch(env->isolate()); - if (KeyGenTraits::EncodeKey(env, params).ToLocal(result)) { + v8::MaybeLocal encoded = + CryptoJob::mode() == kCryptoJobWebCrypto + ? EncodeWebCryptoKey(env, params) + : KeyGenTraits::EncodeKey(env, params); + if (encoded.ToLocal(result)) { *err = Undefined(env->isolate()); } else { CHECK(try_catch.HasCaught()); @@ -122,6 +159,53 @@ class KeyGenJob final : public CryptoJob { SET_SELF_SIZE(KeyGenJob) private: + v8::MaybeLocal EncodeWebCryptoKey(Environment* env, + AdditionalParams* params) { + v8::Isolate* isolate = env->isolate(); + v8::Local algorithm = + v8::Local::New(isolate, webcrypto_config_.algorithm); + + if constexpr (KeyGenTraits::kWebCryptoKeyPair) { + v8::Local public_key; + v8::Local private_key; + if (!NativeCryptoKey::Create(env, + params->key.addRefWithType(kKeyTypePublic), + algorithm, + webcrypto_config_.public_usages_mask, + true) + .ToLocal(&public_key) || + !NativeCryptoKey::Create(env, + params->key.addRefWithType(kKeyTypePrivate), + algorithm, + webcrypto_config_.private_usages_mask, + webcrypto_config_.extractable) + .ToLocal(&private_key)) { + return {}; + } + + v8::Local ret = v8::Object::New(isolate); + if (!ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "publicKey"), + public_key) + .FromMaybe(false) || + !ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "privateKey"), + private_key) + .FromMaybe(false)) { + return {}; + } + return ret; + } else { + auto data = KeyObjectData::CreateSecret(std::move(params->out)); + return NativeCryptoKey::Create(env, + data, + algorithm, + webcrypto_config_.usages_mask, + webcrypto_config_.extractable); + } + } + + WebCryptoKeyGenConfig webcrypto_config_; KeyGenJobStatus status_ = KeyGenJobStatus::FAILED; }; @@ -130,6 +214,7 @@ template struct KeyPairGenTraits final { using AdditionalParameters = typename KeyPairAlgorithmTraits::AdditionalParameters; + static constexpr bool kWebCryptoKeyPair = true; static const AsyncWrap::ProviderType Provider = AsyncWrap::PROVIDER_KEYPAIRGENREQUEST; @@ -146,8 +231,13 @@ struct KeyPairGenTraits final { // process input parameters. This allows each job to have a variable // number of input parameters specific to each job type. if (KeyPairAlgorithmTraits::AdditionalConfig(mode, args, offset, params) - .IsNothing() || - !KeyObjectData::GetPublicKeyEncodingFromJs( + .IsNothing()) { + return v8::Nothing(); + } + + if (mode == kCryptoJobWebCrypto) return v8::JustVoid(); + + if (!KeyObjectData::GetPublicKeyEncodingFromJs( args, offset, kKeyContextGenerate) .To(¶ms->public_key_encoding) || !KeyObjectData::GetPrivateKeyEncodingFromJs( @@ -204,6 +294,7 @@ struct SecretKeyGenConfig final : public MemoryRetainer { struct SecretKeyGenTraits final { using AdditionalParameters = SecretKeyGenConfig; + static constexpr bool kWebCryptoKeyPair = false; static const AsyncWrap::ProviderType Provider = AsyncWrap::PROVIDER_KEYGENREQUEST; static constexpr const char* JobName = "SecretKeyGenJob"; @@ -287,4 +378,3 @@ using SecretKeyGenJob = KeyGenJob; #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #endif // SRC_CRYPTO_CRYPTO_KEYGEN_H_ - diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index aac059696596e4..c1b7aee576519e 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -27,6 +27,7 @@ using ncrypto::EVPKeyCtxPointer; using ncrypto::EVPKeyPointer; using ncrypto::MarkPopErrorOnReturn; using v8::Array; +using v8::Boolean; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -155,9 +156,11 @@ bool ExportJWKSecretKey(Environment* env, BASE64URL) .ToLocal(&raw) && target - ->Set(env->context(), env->jwk_kty_string(), env->jwk_oct_string()) - .IsJust() && - target->Set(env->context(), env->jwk_k_string(), raw).IsJust(); + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_oct_string()) + .FromMaybe(false) && + target->DefineOwnProperty(env->context(), env->jwk_k_string(), raw) + .FromMaybe(false); } KeyObjectData ImportJWKSecretKey(Environment* env, Local jwk) { @@ -1716,6 +1719,38 @@ bool NativeCryptoKey::HasInstance(Environment* env, Local value) { return IsNativeCryptoKey(env, value); } +MaybeLocal NativeCryptoKey::Create(Environment* env, + const KeyObjectData& data, + Local algorithm, + uint32_t usages_mask, + bool extractable) { + Local context = env->context(); + Isolate* isolate = env->isolate(); + CHECK(algorithm->IsObject()); + + Local handle; + if (!KeyObjectHandle::Create(env, data).ToLocal(&handle)) return {}; + + if (env->crypto_internal_cryptokey_constructor().IsEmpty()) { + Local arg = FIXED_ONE_BYTE_STRING(isolate, "internal/crypto/keys"); + if (env->builtin_module_require() + ->Call(context, Null(isolate), 1, &arg) + .IsEmpty()) { + return {}; + } + } + + Local cryptokey_ctor = env->crypto_internal_cryptokey_constructor(); + CHECK(!cryptokey_ctor.IsEmpty()); + Local ctor_args[] = { + handle, + algorithm, + Uint32::NewFromUnsigned(isolate, usages_mask), + Boolean::New(isolate, extractable), + }; + return cryptokey_ctor->NewInstance(context, arraysize(ctor_args), ctor_args); +} + void NativeCryptoKey::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 4); diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 8bba206a08239e..6adedc89fafffe 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -271,6 +271,12 @@ class NativeCryptoKey : public BaseObject { static void CreateCryptoKeyClass( const v8::FunctionCallbackInfo& args); + static v8::MaybeLocal Create(Environment* env, + const KeyObjectData& data, + v8::Local algorithm, + uint32_t usages_mask, + bool extractable); + // True if `value` is a real NativeCryptoKey instance. Uses the // FunctionTemplate stored on the Environment as a brand check. // Used by `GetSlots` to validate its receiver. diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index ed4a8e9d526983..1b685bb5f6983c 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -45,7 +45,7 @@ KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { void KmacConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the KmacConfig does not own the data. - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); tracker->TrackFieldWithSize("customization", customization.size()); @@ -90,7 +90,7 @@ Maybe KmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); return Nothing(); } - params->customization = mode == kCryptoJobAsync + params->customization = IsCryptoJobAsync(mode) ? customization.ToCopy() : customization.ToByteSource(); } @@ -104,7 +104,7 @@ Maybe KmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (!args[offset + 6]->IsUndefined()) { ArrayBufferOrViewContents signature(args[offset + 6]); @@ -113,7 +113,7 @@ Maybe KmacTraits::AdditionalConfig( return Nothing(); } params->signature = - mode == kCryptoJobAsync ? signature.ToCopy() : signature.ToByteSource(); + IsCryptoJobAsync(mode) ? signature.ToCopy() : signature.ToByteSource(); } return JustVoid(); diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc index 8bdb44d3bdca31..5c3fc438774334 100644 --- a/src/crypto/crypto_pbkdf2.cc +++ b/src/crypto/crypto_pbkdf2.cc @@ -1,5 +1,7 @@ #include "crypto/crypto_pbkdf2.h" #include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -21,6 +23,7 @@ using v8::Value; namespace crypto { PBKDF2Config::PBKDF2Config(PBKDF2Config&& other) noexcept : mode(other.mode), + key(std::move(other.key)), pass(std::move(other.pass)), salt(std::move(other.salt)), iterations(other.iterations), @@ -34,9 +37,10 @@ PBKDF2Config& PBKDF2Config::operator=(PBKDF2Config&& other) noexcept { } void PBKDF2Config::MemoryInfo(MemoryTracker* tracker) const { - // The job is sync, the PBKDF2Config does not own the data. - if (mode == kCryptoJobAsync) { - tracker->TrackFieldWithSize("pass", pass.size()); + // If the job is sync, PBKDF2Config does not own the data. + if (key) tracker->TrackField("key", key); + if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } } @@ -63,12 +67,21 @@ Maybe PBKDF2Traits::AdditionalConfig( params->mode = mode; - ArrayBufferOrViewContents pass(args[offset]); + CHECK(KeyObjectHandle::HasInstance(env, args[offset]) || + IsAnyBufferSource(args[offset])); // pass ArrayBufferOrViewContents salt(args[offset + 1]); - if (!pass.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); - return Nothing(); + if (KeyObjectHandle::HasInstance(env, args[offset])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset], Nothing()); + params->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents pass(args[offset]); + if (!pass.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); } if (!salt.CheckSizeInt32()) [[unlikely]] { @@ -76,13 +89,7 @@ Maybe PBKDF2Traits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? pass.ToCopy() - : pass.ToByteSource(); - - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsInt32()); // iteration_count CHECK(args[offset + 3]->IsInt32()); // length @@ -116,11 +123,14 @@ bool PBKDF2Traits::DeriveBits(Environment* env, CryptoJobMode mode, CryptoErrorStore* errors) { // Both pass and salt may be zero length here. + const ncrypto::Buffer pass{ + .data = params.key ? params.key.GetSymmetricKey() + : params.pass.data(), + .len = params.key ? params.key.GetSymmetricKeySize() : params.pass.size(), + }; + auto dp = ncrypto::pbkdf2(params.digest, - ncrypto::Buffer{ - .data = params.pass.data(), - .len = params.pass.size(), - }, + pass, ncrypto::Buffer{ .data = params.salt.data(), .len = params.salt.size(), diff --git a/src/crypto/crypto_pbkdf2.h b/src/crypto/crypto_pbkdf2.h index 5ce3077e1aff8c..639fb4293ee1b4 100644 --- a/src/crypto/crypto_pbkdf2.h +++ b/src/crypto/crypto_pbkdf2.h @@ -3,8 +3,9 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#include "crypto/crypto_util.h" #include "async_wrap.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" #include "env.h" #include "memory_tracker.h" #include "v8.h" @@ -26,6 +27,7 @@ namespace crypto { struct PBKDF2Config final : public MemoryRetainer { CryptoJobMode mode; + KeyObjectData key; ByteSource pass; ByteSource salt; int32_t iterations; diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc index 8d4af1e7801180..e12894bc596317 100644 --- a/src/crypto/crypto_pqc.cc +++ b/src/crypto/crypto_pqc.cc @@ -145,7 +145,8 @@ bool TrySetEncodedKey(Environment* env, const ncrypto::Buffer out = data; return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) .ToLocal(&encoded) && - target->Set(env->context(), key, encoded).IsJust(); + target->DefineOwnProperty(env->context(), key, encoded) + .FromMaybe(false); } } // namespace @@ -172,16 +173,18 @@ bool ExportJwkPqcKey(Environment* env, } } - return !( - target->Set(env->context(), env->jwk_kty_string(), env->jwk_akp_string()) - .IsNothing() || - target - ->Set(env->context(), - env->jwk_alg_string(), - OneByteString(env->isolate(), alg->name)) - .IsNothing() || - !TrySetEncodedKey( - env, pkey.rawPublicKey(), target, env->jwk_pub_string())); + return !(!target + ->DefineOwnProperty(env->context(), + env->jwk_kty_string(), + env->jwk_akp_string()) + .FromMaybe(false) || + !target + ->DefineOwnProperty(env->context(), + env->jwk_alg_string(), + OneByteString(env->isolate(), alg->name)) + .FromMaybe(false) || + !TrySetEncodedKey( + env, pkey.rawPublicKey(), target, env->jwk_pub_string())); } KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index e722f87b23fcbe..acec4b993613cd 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -223,7 +223,7 @@ RSACipherConfig::RSACipherConfig(RSACipherConfig&& other) noexcept digest(other.digest) {} void RSACipherConfig::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) + if (IsCryptoJobAsync(mode)) tracker->TrackFieldWithSize("label", label.size()); } @@ -295,8 +295,10 @@ bool ExportJWKRsaKey(Environment* env, const ncrypto::Rsa rsa = m_pkey; if (!rsa || - target->Set(env->context(), env->jwk_kty_string(), env->jwk_rsa_string()) - .IsNothing()) { + !target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_rsa_string()) + .FromMaybe(false)) { return false; } diff --git a/src/crypto/crypto_scrypt.cc b/src/crypto/crypto_scrypt.cc index eba141f372f536..91ed9fee71f052 100644 --- a/src/crypto/crypto_scrypt.cc +++ b/src/crypto/crypto_scrypt.cc @@ -38,7 +38,7 @@ ScryptConfig& ScryptConfig::operator=(ScryptConfig&& other) noexcept { } void ScryptConfig::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } @@ -72,13 +72,9 @@ Maybe ScryptTraits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? pass.ToCopy() - : pass.ToByteSource(); + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsUint32()); // N CHECK(args[offset + 3]->IsUint32()); // r diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index d8a4fe395a5f47..153ac843677970 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -564,7 +564,7 @@ SignConfiguration& SignConfiguration::operator=( void SignConfiguration::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); tracker->TrackFieldWithSize("context_string", context_string.size()); @@ -603,9 +603,7 @@ Maybe SignTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (args[offset + 7]->IsString()) { Utf8Value digest(env->isolate(), args[offset + 7]); @@ -642,7 +640,7 @@ Maybe SignTraits::AdditionalConfig( return Nothing(); } params->flags |= SignConfiguration::kHasContextString; - params->context_string = mode == kCryptoJobAsync + params->context_string = IsCryptoJobAsync(mode) ? context_string.ToCopy() : context_string.ToByteSource(); } @@ -660,9 +658,8 @@ Maybe SignTraits::AdditionalConfig( if (UseP1363Encoding(akey, params->dsa_encoding)) { params->signature = ConvertSignatureToDER(akey, signature.ToByteSource()); } else { - params->signature = mode == kCryptoJobAsync - ? signature.ToCopy() - : signature.ToByteSource(); + params->signature = IsCryptoJobAsync(mode) ? signature.ToCopy() + : signature.ToByteSource(); } } diff --git a/src/crypto/crypto_turboshake.cc b/src/crypto/crypto_turboshake.cc index 26107f82aebbd3..06b2ef9d6f5aea 100644 --- a/src/crypto/crypto_turboshake.cc +++ b/src/crypto/crypto_turboshake.cc @@ -419,7 +419,7 @@ TurboShakeConfig& TurboShakeConfig::operator=( } void TurboShakeConfig::MemoryInfo(MemoryTracker* tracker) const { - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource tracker->TrackFieldWithSize("data", data.size()); } @@ -464,7 +464,7 @@ Maybe TurboShakeTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); return JustVoid(); } @@ -527,7 +527,7 @@ KangarooTwelveConfig& KangarooTwelveConfig::operator=( } void KangarooTwelveConfig::MemoryInfo(MemoryTracker* tracker) const { - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("customization", customization.size()); @@ -563,7 +563,7 @@ Maybe KangarooTwelveTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); return Nothing(); } - params->customization = mode == kCryptoJobAsync + params->customization = IsCryptoJobAsync(mode) ? customization.ToCopy() : customization.ToByteSource(); } @@ -578,7 +578,7 @@ Maybe KangarooTwelveTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); return JustVoid(); } diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index b9d037fb72352b..42b248d84b43e5 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -32,7 +32,9 @@ using ncrypto::DataPointer; using ncrypto::EnginePointer; #endif // !OPENSSL_NO_ENGINE using ncrypto::SSLPointer; +using v8::Array; using v8::ArrayBuffer; +using v8::ArrayBufferView; using v8::BackingStore; using v8::BackingStoreInitializationMode; using v8::BackingStoreOnFailureMode; @@ -40,6 +42,7 @@ using v8::BigInt; using v8::Context; using v8::EscapableHandleScope; using v8::Exception; +using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; @@ -702,17 +705,72 @@ Maybe SetEncodedValue(Environment* env, if (!EncodeBignum(env, bn, size).ToLocal(&value)) { return Nothing(); } - return target->Set(env->context(), name, value).IsJust() ? JustVoid() - : Nothing(); + return target->DefineOwnProperty(env->context(), name, value).FromMaybe(false) + ? JustVoid() + : Nothing(); } CryptoJobMode GetCryptoJobMode(v8::Local args) { CHECK(args->IsUint32()); uint32_t mode = args.As()->Value(); - CHECK_LE(mode, kCryptoJobSync); + CHECK_LE(mode, kCryptoJobWebCrypto); return static_cast(mode); } +bool IsCryptoJobAsync(CryptoJobMode mode) { + return mode == kCryptoJobAsync || mode == kCryptoJobWebCrypto; +} + +MaybeLocal CreateWebCryptoJobError(Environment* env, + Local cause) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Local per_context_bindings; + Local domexception_ctor; + if (!GetPerContextExports(context).ToLocal(&per_context_bindings) || + !per_context_bindings + ->Get(context, FIXED_ONE_BYTE_STRING(isolate, "DOMException")) + .ToLocal(&domexception_ctor)) { + return {}; + } + CHECK(domexception_ctor->IsFunction()); + + Local options = Object::New(isolate); + if (options + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "name"), + FIXED_ONE_BYTE_STRING(isolate, "OperationError")) + .IsNothing() || + options->Set(context, FIXED_ONE_BYTE_STRING(isolate, "cause"), cause) + .IsNothing()) { + return {}; + } + + Local argv[] = { + FIXED_ONE_BYTE_STRING(isolate, + "The operation failed for an operation-specific " + "reason"), + options, + }; + + return domexception_ctor.As()->NewInstance( + context, arraysize(argv), argv); +} + +MaybeLocal ToWebCryptoJobResult(Environment* env, Local value) { + if (value->IsArrayBuffer()) { + return value; + } + + if (Buffer::HasInstance(value)) { + return value.As()->Buffer(); + } + + CHECK(value->IsBoolean() || (value->IsObject() && !value->IsArray() && + !value->IsArrayBufferView())); + return value; +} + namespace { // SecureBuffer uses OpenSSL's secure heap feature to allocate a // Uint8Array. Without --secure-heap, OpenSSL's secure heap is disabled, @@ -780,6 +838,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kCryptoJobAsync); NODE_DEFINE_CONSTANT(target, kCryptoJobSync); + NODE_DEFINE_CONSTANT(target, kCryptoJobWebCrypto); SetMethod(context, target, "secureBuffer", SecureBuffer); SetMethodNoSideEffect(context, target, "secureHeapUsed", SecureHeapUsed); diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index a5b4829bc23cf2..742f23b0f5e789 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -249,12 +249,16 @@ class ByteSource final { : data_(data), allocated_data_(allocated_data), size_(size) {} }; -enum CryptoJobMode { - kCryptoJobAsync, - kCryptoJobSync -}; +enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync, kCryptoJobWebCrypto }; CryptoJobMode GetCryptoJobMode(v8::Local args); +bool IsCryptoJobAsync(CryptoJobMode mode); + +v8::MaybeLocal CreateWebCryptoJobError(Environment* env, + v8::Local cause); + +v8::MaybeLocal ToWebCryptoJobResult(Environment* env, + v8::Local value); template class CryptoJob : public AsyncWrap, public ThreadPoolWork { @@ -283,9 +287,53 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { void AfterThreadPoolWork(int status) override { Environment* env = AsyncWrap::env(); - CHECK_EQ(mode_, kCryptoJobAsync); + CHECK(IsCryptoJobAsync(mode_)); CHECK(status == 0 || status == UV_ECANCELED); std::unique_ptr ptr(this); + if (mode_ == kCryptoJobWebCrypto) { + v8::HandleScope handle_scope(env->isolate()); + v8::Context::Scope context_scope(env->context()); + InternalCallbackScope callback_scope(this); + + if (status == UV_ECANCELED) { + v8::Local exception = v8::Exception::Error( + OneByteString(env->isolate(), "The operation was canceled")); + ptr->RejectWebCrypto(exception); + return; + } + + v8::Local err; + v8::Local result; + { + node::errors::TryCatchScope try_catch(env); + if (ptr->ToResult(&err, &result).IsNothing()) { + CHECK(try_catch.HasCaught()); + CHECK(try_catch.CanContinue()); + err = try_catch.Exception(); + } + } + + if (!err.IsEmpty() && !err->IsUndefined()) { + ptr->RejectWebCrypto(err); + return; + } + + CHECK(!result.IsEmpty()); + v8::Local webcrypto_result; + { + node::errors::TryCatchScope try_catch(env); + if (!ToWebCryptoJobResult(env, result).ToLocal(&webcrypto_result)) { + CHECK(try_catch.HasCaught()); + CHECK(try_catch.CanContinue()); + ptr->RejectWebCrypto(try_catch.Exception()); + return; + } + } + + ptr->ResolveWebCrypto(webcrypto_result); + return; + } + // If the job was canceled do not execute the callback. // TODO(@jasnell): We should likely revisit skipping the // callback on cancel as that could leave the JS in a pending @@ -340,6 +388,19 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { CryptoJob* job; ASSIGN_OR_RETURN_UNWRAP(&job, args.This()); + if (job->mode() == kCryptoJobWebCrypto) { + v8::Local resolver; + if (!v8::Promise::Resolver::New(env->context()).ToLocal(&resolver)) { + return; + } + + CHECK(job->resolver_.IsEmpty()); + job->resolver_.Reset(env->isolate(), resolver); + args.GetReturnValue().Set(resolver->GetPromise()); + + return job->ScheduleWork(); + } + if (job->mode() == kCryptoJobAsync) return job->ScheduleWork(); @@ -376,9 +437,80 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { } private: + void ResolveWebCrypto(v8::Local value) { + Environment* env = AsyncWrap::env(); + v8::Local context = env->context(); + v8::Local resolver = + v8::Local::New(env->isolate(), resolver_); + + bool should_delete_then = false; + v8::Local then_key; + v8::Local exception; + { + node::errors::TryCatchScope try_catch(env); + if (value->IsObject()) { + then_key = FIXED_ONE_BYTE_STRING(env->isolate(), "then"); + v8::Local object = value.As(); + v8::Maybe has_own_then = + object->HasOwnProperty(context, then_key); + if (has_own_then.IsNothing()) { + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } else if (!has_own_then.FromJust()) { + if (object + ->DefineOwnProperty(context, + then_key, + v8::Undefined(env->isolate()), + v8::DontEnum) + .FromMaybe(false)) { + should_delete_then = true; + } else if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } else { + exception = v8::Exception::Error(OneByteString( + env->isolate(), "Failed to prepare WebCrypto job result")); + } + } + } + + if (exception.IsEmpty() && resolver->Resolve(context, value).IsJust()) { + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } + resolver_.Reset(); + return; + } + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } + + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } + if (!exception.IsEmpty()) { + USE(resolver->Reject(context, exception)); + } + resolver_.Reset(); + } + + void RejectWebCrypto(v8::Local cause) { + Environment* env = AsyncWrap::env(); + v8::Local exception; + if (!CreateWebCryptoJobError(env, cause).ToLocal(&exception)) { + exception = cause; + } + v8::Local resolver = + v8::Local::New(env->isolate(), resolver_); + USE(resolver->Reject(env->context(), exception)); + resolver_.Reset(); + } + const CryptoJobMode mode_; CryptoErrorStore errors_; AdditionalParams params_; + v8::Global resolver_; }; template @@ -413,17 +545,12 @@ class DeriveBitsJob final : public CryptoJob { CryptoJob::RegisterExternalReferences(New, registry); } - DeriveBitsJob( - Environment* env, - v8::Local object, - CryptoJobMode mode, - AdditionalParams&& params) + DeriveBitsJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params) : CryptoJob( - env, - object, - DeriveBitsTraits::Provider, - mode, - std::move(params)) {} + env, object, DeriveBitsTraits::Provider, mode, std::move(params)) {} void DoThreadPoolWork() override { ncrypto::ClearErrorOnReturn clear_error_on_return; diff --git a/src/dataqueue/queue.cc b/src/dataqueue/queue.cc index 283d441e9e6336..8516362b7a8788 100644 --- a/src/dataqueue/queue.cc +++ b/src/dataqueue/queue.cc @@ -179,6 +179,11 @@ class DataQueueImpl final : public DataQueue, for (auto& listener : backpressure_listeners_) listener->EntryRead(amount); } + void NotifyBeforePull() { + if (idempotent_) return; + for (auto& listener : backpressure_listeners_) listener->BeforePull(); + } + bool HasBackpressureListeners() const noexcept { return !backpressure_listeners_.empty(); } @@ -381,6 +386,11 @@ class NonIdempotentDataQueueReader final size_t max_count_hint = bob::kMaxCountHint) override { std::shared_ptr self = shared_from_this(); + // Let listeners flush pending data before we check the entries list. + // This allows, for example, the QUIC Stream to flush its receive + // accumulation buffer into the queue before the pull proceeds. + data_queue_->NotifyBeforePull(); + // If ended is true, this reader has already reached the end and cannot // provide any more data. if (ended_) { diff --git a/src/dataqueue/queue.h b/src/dataqueue/queue.h index a37bd27549986e..190c54111b3e01 100644 --- a/src/dataqueue/queue.h +++ b/src/dataqueue/queue.h @@ -147,6 +147,11 @@ class DataQueue : public MemoryRetainer { class BackpressureListener { public: virtual void EntryRead(size_t amount) = 0; + + // Called before the reader pulls from the DataQueue. Gives + // the listener a chance to flush pending data into the queue + // before the pull checks the entries list. + virtual void BeforePull() {} }; // A DataQueue::Entry represents a logical chunk of data in the queue. diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index c569375383e8d9..f7bd93a98d05b5 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -418,15 +418,20 @@ void BindingData::DecodeUTF8(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); // list, flags CHECK_GE(args.Length(), 1); + auto isShared = args[0]->IsSharedArrayBuffer(); - if (!(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer() || - args[0]->IsArrayBufferView())) { + if (!(args[0]->IsArrayBuffer() || isShared || args[0]->IsArrayBufferView())) { return node::THROW_ERR_INVALID_ARG_TYPE( env->isolate(), "The \"list\" argument must be an instance of SharedArrayBuffer, " "ArrayBuffer or ArrayBufferView."); } + if (args[0]->IsArrayBufferView()) { + Local view = args[0].As(); + isShared = view->Buffer()->IsSharedArrayBuffer(); + } + ArrayBufferViewContents buffer(args[0]); bool ignore_bom = args[1]->IsTrue(); @@ -435,6 +440,13 @@ void BindingData::DecodeUTF8(const FunctionCallbackInfo& args) { const char* data = buffer.data(); size_t length = buffer.length(); + std::unique_ptr data_copy; + if (isShared && length != 0) { + data_copy = std::make_unique_for_overwrite(length); + memcpy(data_copy.get(), data, length); + data = data_copy.get(); + } + if (!ignore_bom && length >= 3) { if (memcmp(data, "\xEF\xBB\xBF", 3) == 0) { data += 3; @@ -459,14 +471,15 @@ void BindingData::DecodeUTF8(const FunctionCallbackInfo& args) { return node::THROW_ERR_ENCODING_INVALID_ENCODED_DATA( env->isolate(), "The encoded data was not valid for encoding utf-8"); } - - // TODO(chalker): save on utf8 validity recheck in StringBytes::Encode() } if (length == 0) return args.GetReturnValue().SetEmptyString(); Local ret; - if (StringBytes::Encode(env->isolate(), data, length, UTF8).ToLocal(&ret)) { + v8::MaybeLocal encoded = + has_fatal ? StringBytes::EncodeValidUtf8(env->isolate(), data, length) + : StringBytes::Encode(env->isolate(), data, length, UTF8); + if (encoded.ToLocal(&ret)) { args.GetReturnValue().Set(ret); } } diff --git a/src/env.h b/src/env.h index 616f6e7d04109a..61512d9494ff61 100644 --- a/src/env.h +++ b/src/env.h @@ -257,6 +257,8 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { struct ContextInfo { explicit ContextInfo(const std::string& name) : name(name) {} + ContextInfo(const std::string& name, const std::string& origin) + : name(name), origin(origin) {} const std::string name; std::string origin; bool is_default = false; diff --git a/src/env_properties.h b/src/env_properties.h index 6530f89ec918ac..804db1963e0a1d 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -48,8 +48,8 @@ V(async_id_symbol, "async_id_symbol") \ V(ffi_sb_shared_buffer_symbol, "ffi_sb_shared_buffer_symbol") \ V(ffi_sb_invoke_slow_symbol, "ffi_sb_invoke_slow_symbol") \ - V(ffi_sb_params_symbol, "ffi_sb_params_symbol") \ - V(ffi_sb_result_symbol, "ffi_sb_result_symbol") \ + V(ffi_sb_arguments_symbol, "ffi_sb_arguments_symbol") \ + V(ffi_sb_return_symbol, "ffi_sb_return_symbol") \ V(constructor_key_symbol, "constructor_key_symbol") \ V(handle_onclose_symbol, "handle_onclose") \ V(no_message_symbol, "no_message_symbol") \ @@ -293,7 +293,6 @@ V(password_string, "password") \ V(path_string, "path") \ V(pathname_string, "pathname") \ - V(parameters_string, "parameters") \ V(pending_handle_string, "pendingHandle") \ V(permission_string, "permission") \ V(phase_string, "phase") \ @@ -329,9 +328,8 @@ V(require_string, "require") \ V(resource_string, "resource") \ V(result_string, "result") \ - V(return_string, "return") \ - V(returns_string, "returns") \ V(return_arrays_string, "returnArrays") \ + V(return_string, "return") \ V(salt_length_string, "saltLength") \ V(search_string, "search") \ V(servername_string, "servername") \ diff --git a/src/ffi/types.cc b/src/ffi/types.cc index e8469ebc0bbcc6..cd9fde06dcbfca 100644 --- a/src/ffi/types.cc +++ b/src/ffi/types.cc @@ -83,63 +83,26 @@ Maybe ParseFunctionSignature(Environment* env, std::string_view name, Local signature) { Local context = env->context(); - Local returns_key = env->returns_string(); Local return_key = env->return_string(); - Local result_key = env->result_string(); - Local parameters_key = env->parameters_string(); Local arguments_key = env->arguments_string(); - bool has_returns; bool has_return; - bool has_result; - bool has_parameters; bool has_arguments; - if (!signature->Has(context, returns_key).To(&has_returns) || - !signature->Has(context, return_key).To(&has_return) || - !signature->Has(context, result_key).To(&has_result) || - !signature->Has(context, parameters_key).To(&has_parameters) || + if (!signature->Has(context, return_key).To(&has_return) || !signature->Has(context, arguments_key).To(&has_arguments)) { return {}; } - if (has_returns + has_return + has_result > 1) { - THROW_ERR_INVALID_ARG_VALUE( - env, - "Function signature of %s" - " must have either 'returns', 'return' or 'result' " - "property", - name); - return {}; - } - - if (has_arguments && has_parameters) { - THROW_ERR_INVALID_ARG_VALUE(env, - "Function signature of %s" - " must have either 'parameters' or 'arguments' " - "property", - name); - return {}; - } - ffi_type* return_type = &ffi_type_void; std::vector args; std::string return_type_name = "void"; std::vector arg_type_names; Isolate* isolate = env->isolate(); - if (has_returns || has_return || has_result) { - Local return_type_key; - if (has_returns) { - return_type_key = returns_key; - } else if (has_return) { - return_type_key = return_key; - } else { - return_type_key = result_key; - } - + if (has_return) { Local return_type_val; - if (!signature->Get(context, return_type_key).ToLocal(&return_type_val)) { + if (!signature->Get(context, return_key).ToLocal(&return_type_val)) { return {}; } @@ -162,10 +125,9 @@ Maybe ParseFunctionSignature(Environment* env, return_type_name = return_type_str.ToString(); } - if (has_arguments || has_parameters) { + if (has_arguments) { Local arguments_val; - if (!signature->Get(context, has_arguments ? arguments_key : parameters_key) - .ToLocal(&arguments_val)) { + if (!signature->Get(context, arguments_key).ToLocal(&arguments_val)) { return {}; } @@ -202,6 +164,15 @@ Maybe ParseFunctionSignature(Environment* env, if (!ToFFIType(env, arg_str.ToStringView()).To(&arg_type)) { return {}; } + if (arg_type == &ffi_type_void) { + THROW_ERR_INVALID_ARG_VALUE( + env, + "Argument %u of function %s must not be 'void'; " + "use an empty array for no-argument functions", + i, + name); + return {}; + } args.push_back(arg_type); arg_type_names.emplace_back(arg_str.ToString()); diff --git a/src/inspector_profiler.cc b/src/inspector_profiler.cc index 28653a3939daef..559b4fd27d56ab 100644 --- a/src/inspector_profiler.cc +++ b/src/inspector_profiler.cc @@ -548,6 +548,30 @@ static void SetSourceMapCacheGetter(const FunctionCallbackInfo& args) { env->set_source_map_cache_getter(args[0].As()); } +static void StartCoverage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Debug(env, + DebugCategory::INSPECTOR_PROFILER, + "StartCoverage, connection %s nullptr\n", + env->coverage_connection() == nullptr ? "==" : "!="); + + if (env->coverage_connection() != nullptr) { + return; + } + + // The parent of `--test --test-isolation=process` intentionally has no + // inspector (see Environment::should_create_inspector); workers handle + // coverage themselves. Without an inspector, V8CoverageConnection would + // get a null session and crash on the first DispatchMessage. + if (!env->should_create_inspector()) { + return; + } + + env->set_coverage_connection(std::make_unique(env)); + env->coverage_connection()->Start(); +} + static void TakeCoverage(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); V8CoverageConnection* connection = env->coverage_connection(); @@ -601,6 +625,7 @@ static void Initialize(Local target, SetMethod(context, target, "setCoverageDirectory", SetCoverageDirectory); SetMethod( context, target, "setSourceMapCacheGetter", SetSourceMapCacheGetter); + SetMethod(context, target, "startCoverage", StartCoverage); SetMethod(context, target, "takeCoverage", TakeCoverage); SetMethod(context, target, "stopCoverage", StopCoverage); SetMethod(context, target, "endCoverage", EndCoverage); @@ -609,6 +634,7 @@ static void Initialize(Local target, void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(SetCoverageDirectory); registry->Register(SetSourceMapCacheGetter); + registry->Register(StartCoverage); registry->Register(TakeCoverage); registry->Register(StopCoverage); registry->Register(EndCoverage); diff --git a/src/node.h b/src/node.h index eba7b8698187b1..e25fe19c9ab30c 100644 --- a/src/node.h +++ b/src/node.h @@ -563,6 +563,8 @@ NODE_EXTERN v8::Isolate* NewIsolate( const IsolateSettings& settings = {}); // Creates a new context with Node.js-specific tweaks. +// Call `RegisterContext` after the context been created to register +// the context with Node.js specific setups like the inspector. NODE_EXTERN v8::Local NewContext( v8::Isolate* isolate, v8::Local object_template = @@ -572,6 +574,18 @@ NODE_EXTERN v8::Local NewContext( // Return value indicates success of operation NODE_EXTERN v8::Maybe InitializeContext(v8::Local context); +// Associate the context with the given Environment. This registers the context +// as known to Node.js, makes it available to the inspector. This also registers +// Node.js promise hooks on the context. +NODE_EXTERN void RegisterContext(Environment* env, + v8::Local context, + std::string_view name = "", + std::string_view origin = ""); +// Unregister the context. Call this when the embedder finished all work with +// this context. +NODE_EXTERN void UnregisterContext(Environment* env, + v8::Local context); + // If `platform` is passed, it will be used to register new Worker instances. // It can be `nullptr`, in which case creating new Workers inside of // Environments that use this `IsolateData` will not work. diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 2778422ea4e7b7..b3b12e85988ad6 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -567,19 +567,31 @@ void StringSlice(const FunctionCallbackInfo& args) { THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); ArrayBufferViewContents buffer(args[0]); - if (buffer.length() == 0) - return args.GetReturnValue().SetEmptyString(); + auto buffer_length = buffer.length(); + const char* data_ptr = buffer.data(); + + Local view = args[0].As(); + + if (buffer_length == 0) return args.GetReturnValue().SetEmptyString(); size_t start = 0; size_t end = 0; THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[1], 0, &start)); - THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[2], buffer.length(), &end)); - if (end < start) end = start; - THROW_AND_RETURN_IF_OOB(Just(end <= buffer.length())); + THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[2], buffer_length, &end)); + if (end <= start) return args.GetReturnValue().SetEmptyString(); + THROW_AND_RETURN_IF_OOB(Just(end <= buffer_length)); size_t length = end - start; + std::unique_ptr data_copy; + if (view->Buffer()->IsSharedArrayBuffer()) { + data_copy = std::make_unique_for_overwrite(length); + memcpy(data_copy.get(), data_ptr + start, length); + data_ptr = data_copy.get(); + start = 0; + } + Local ret; - if (StringBytes::Encode(isolate, buffer.data() + start, length, encoding) + if (StringBytes::Encode(isolate, data_ptr + start, length, encoding) .ToLocal(&ret)) { args.GetReturnValue().Set(ret); } diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 14534ab89650b4..f319420ae02f35 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -700,8 +700,7 @@ Intercepted ContextifyContext::PropertyDefinerCallback( if (desc.has_configurable()) { desc_for_sandbox->set_configurable(desc.configurable()); } - // Set the property on the sandbox. - USE(sandbox->DefineProperty(context, property, *desc_for_sandbox)); + return sandbox->DefineProperty(context, property, *desc_for_sandbox); }; if (desc.has_get() || desc.has_set()) { @@ -709,23 +708,23 @@ Intercepted ContextifyContext::PropertyDefinerCallback( desc.has_get() ? desc.get() : Undefined(isolate).As(), desc.has_set() ? desc.set() : Undefined(isolate).As()); - define_prop_on_sandbox(&desc_for_sandbox); - // TODO(https://github.com/nodejs/node/issues/52634): this should return - // kYes to behave according to the expected semantics. + if (define_prop_on_sandbox(&desc_for_sandbox).FromMaybe(false)) + return Intercepted::kYes; return Intercepted::kNo; } else { Local value = desc.has_value() ? desc.value() : Undefined(isolate).As(); + Maybe result; if (desc.has_writable()) { PropertyDescriptor desc_for_sandbox(value, desc.writable()); - define_prop_on_sandbox(&desc_for_sandbox); + result = define_prop_on_sandbox(&desc_for_sandbox); } else { PropertyDescriptor desc_for_sandbox(value); - define_prop_on_sandbox(&desc_for_sandbox); + result = define_prop_on_sandbox(&desc_for_sandbox); } - // TODO(https://github.com/nodejs/node/issues/52634): this should return - // kYes to behave according to the expected semantics. + + if (result.FromMaybe(false)) return Intercepted::kYes; return Intercepted::kNo; } } diff --git a/src/node_errors.cc b/src/node_errors.cc index 74326496132773..63db97f6a56db0 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -1064,7 +1064,12 @@ void PerIsolateMessageListener(Local message, Local error) { filename, message->GetLineNumber(env->context()).FromMaybe(-1), msg); - USE(ProcessEmitWarningGeneric(env, warning, "V8")); + // Defer the warning to the next event loop iteration. This prevents + // crashes when V8 emits warnings during code evaluation with + // throwOnSideEffect. + env->SetImmediate([warning](Environment* env) { + ProcessEmitWarningGeneric(env, warning, "V8"); + }); break; } case Isolate::MessageErrorLevel::kMessageError: diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 34f999b015123b..b8e6df7d29eb6c 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -312,28 +312,28 @@ MaybeLocal DynamicLibrary::CreateFunction( // Attach the original signature type names so the JS wrapper can // rebuild the signature from a raw function when the caller did not - // pass parameters and result explicitly. The `lib.functions` accessor + // pass arguments and return explicitly. The `lib.functions` accessor // path relies on this. - Local params_arr; - if (!ToV8Value(context, fn->arg_type_names, isolate).ToLocal(¶ms_arr)) { + Local args_arr; + if (!ToV8Value(context, fn->arg_type_names, isolate).ToLocal(&args_arr)) { return MaybeLocal(); } if (!ret->DefineOwnProperty(context, - env->ffi_sb_params_symbol(), - params_arr, + env->ffi_sb_arguments_symbol(), + args_arr, internal_attrs) .FromMaybe(false)) { return MaybeLocal(); } - Local result_name; + Local return_name; if (!ToV8Value(context, fn->return_type_name, isolate) - .ToLocal(&result_name)) { + .ToLocal(&return_name)) { return MaybeLocal(); } if (!ret->DefineOwnProperty(context, - env->ffi_sb_result_symbol(), - result_name, + env->ffi_sb_return_symbol(), + return_name, internal_attrs) .FromMaybe(false)) { return MaybeLocal(); @@ -1102,6 +1102,7 @@ Local DynamicLibrary::GetConstructorTemplate( static_cast(ReadOnly)); SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close); + SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions); SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol); @@ -1196,13 +1197,13 @@ static void Initialize(Local target, .Check(); target ->Set(context, - FIXED_ONE_BYTE_STRING(isolate, "kSbParams"), - env->ffi_sb_params_symbol()) + FIXED_ONE_BYTE_STRING(isolate, "kSbArguments"), + env->ffi_sb_arguments_symbol()) .Check(); target ->Set(context, - FIXED_ONE_BYTE_STRING(isolate, "kSbResult"), - env->ffi_sb_result_symbol()) + FIXED_ONE_BYTE_STRING(isolate, "kSbReturn"), + env->ffi_sb_return_symbol()) .Check(); } diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index 50d7f9e6916096..46e61b30bd1ae7 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -96,6 +96,7 @@ const uint32_t kLenientOptionalLFAfterCR = 1 << 6; const uint32_t kLenientOptionalCRLFAfterChunk = 1 << 7; const uint32_t kLenientOptionalCRBeforeLF = 1 << 8; const uint32_t kLenientSpacesAfterChunkSize = 1 << 9; +const uint32_t kLenientHeaderValueRelaxed = 1 << 10; const uint32_t kLenientAll = kLenientHeaders | kLenientChunkedLength | kLenientKeepAlive | kLenientTransferEncoding | kLenientVersion | kLenientDataAfterClose | @@ -1006,6 +1007,11 @@ class Parser : public AsyncWrap, public StreamListener { if (lenient_flags & kLenientSpacesAfterChunkSize) { llhttp_set_lenient_spaces_after_chunk_size(&parser_, 1); } +#if LLHTTP_VERSION_MAJOR * 1000 + LLHTTP_VERSION_MINOR >= 9004 + if (lenient_flags & kLenientHeaderValueRelaxed) { + llhttp_set_lenient_header_value_relaxed(&parser_, 1); + } +#endif header_nread_ = 0; url_.Reset(); @@ -1332,6 +1338,16 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, Integer::NewFromUnsigned(isolate, kLenientOptionalCRBeforeLF)); t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientSpacesAfterChunkSize"), Integer::NewFromUnsigned(isolate, kLenientSpacesAfterChunkSize)); + // kLenientHeaderValueRelaxed requires llhttp >= 9.4.0 for the + // llhttp_set_lenient_header_value_relaxed() API. Export 0 on older + // shared-library builds so JS can detect feature availability. +#if LLHTTP_VERSION_MAJOR * 1000 + LLHTTP_VERSION_MINOR >= 9004 + t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientHeaderValueRelaxed"), + Integer::NewFromUnsigned(isolate, kLenientHeaderValueRelaxed)); +#else + t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientHeaderValueRelaxed"), + Integer::NewFromUnsigned(isolate, 0)); +#endif t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientAll"), Integer::NewFromUnsigned(isolate, kLenientAll)); diff --git a/src/node_root_certs.h b/src/node_root_certs.h index e3c77a175f9ccf..53ac2034c8a810 100644 --- a/src/node_root_certs.h +++ b/src/node_root_certs.h @@ -26,240 +26,6 @@ "j2A781q0tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8\n" "-----END CERTIFICATE-----", -/* QuoVadis Root CA 2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNV\n" -"BAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0w\n" -"NjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBR\n" -"dW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqG\n" -"SIb3DQEBAQUAA4ICDwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4Gt\n" -"Mh6QRr+jhiYaHv5+HBg6XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp\n" -"3MJGF/hd/aTa/55JWpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsR\n" -"E8Scd3bBrrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\n" -"+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI\n" -"0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2\n" -"BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIizPtGo/KPaHbDRsSNU30R2be1B\n" -"2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOhD7osFRXql7PSorW+8oyWHhqPHWyk\n" -"YTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyP\n" -"ZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQAB\n" -"o4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwz\n" -"JQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\n" -"MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1Zh\n" -"ZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUvZ+YT\n" -"RYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3\n" -"UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgt\n" -"JodmVjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q8\n" -"0m/DShcK+JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W\n" -"6ZM/57Es3zrWIozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQj\n" -"rLhVoQPRTUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\n" -"mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6y\n" -"hhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO\n" -"1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAF\n" -"ZdWCEOrCMc0u\n" -"-----END CERTIFICATE-----", - -/* QuoVadis Root CA 3 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNV\n" -"BAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0w\n" -"NjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBR\n" -"dW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqG\n" -"SIb3DQEBAQUAA4ICDwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTP\n" -"krgEQK0CSzGrvI2RaNggDhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZ\n" -"z3HmDyl2/7FWeUUrH556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2Objyj\n" -"Ptr7guXd8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv\n" -"vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mta\n" -"a7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJ\n" -"k8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1\n" -"ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEXMJPpGovgc2PZapKUSU60rUqFxKMi\n" -"MPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArl\n" -"zW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQAB\n" -"o4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMw\n" -"gcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0\n" -"aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0aWZpY2F0\n" -"ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYBBQUH\n" -"AgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD\n" -"VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1\n" -"XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEb\n" -"MBkGA1UEAxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62g\n" -"LEz6wPJv92ZVqyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon\n" -"24QRiSemd1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd\n" -"+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hR\n" -"OJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j5\n" -"6hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6l\n" -"i92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8S\n" -"h17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7\n" -"j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEo\n" -"kt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7\n" -"zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto=\n" -"-----END CERTIFICATE-----", - -/* DigiCert Assured ID Root CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYD\n" -"VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n" -"Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAw\n" -"MDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg\n" -"SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1\n" -"cmVkIElEIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOA\n" -"XLGH87dg+XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT\n" -"XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\n" -"wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/l\n" -"bQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcX\n" -"xH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQE\n" -"AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAf\n" -"BgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog68\n" -"3+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqo\n" -"R+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+\n" -"fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\n" -"H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe+o0bJW1s\n" -"j6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n" -"-----END CERTIFICATE-----", - -/* DigiCert Global Root CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYD\n" -"VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n" -"Y29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa\n" -"Fw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx\n" -"GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBS\n" -"b290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKP\n" -"C3eQyaKl7hLOllsBCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscF\n" -"s3YnFo97nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" -"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6g\n" -"SzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSii\n" -"cNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYD\n" -"VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgw\n" -"FoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1E\n" -"nE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDi\n" -"qw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBA\n" -"I+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" -"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQkCAUw7C29\n" -"C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" -"-----END CERTIFICATE-----", - -/* DigiCert High Assurance EV Root CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYD\n" -"VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n" -"Y29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2\n" -"MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp\n" -"Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNl\n" -"cnQgSGlnaCBBc3N1cmFuY2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" -"AQoCggEBAMbM5XPm+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlB\n" -"WTrT3JTWPNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n" -"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeB\n" -"QVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5\n" -"OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsgEsxBu24LUTi4S8sCAwEAAaNj\n" -"MGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9H\n" -"AdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3\n" -"DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1\n" -"ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VH\n" -"MWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n" -"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCevEsXCS+0\n" -"yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K\n" -"-----END CERTIFICATE-----", - -/* SwissSign Gold CA - G2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNI\n" -"MRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0g\n" -"RzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMG\n" -"A1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIIC\n" -"IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJC\n" -"Eyq8ZVeCQD5XJM1QiyUqt2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcf\n" -"DmJlD909Vopz2q5+bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpi\n" -"kJKVyh+c6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE\n" -"emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT\n" -"28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdV\n" -"xVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02yMszYF9rNt85mndT9Xv+9lz4p\n" -"ded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkOpeUDDniOJihC8AcLYiAQZzlG+qkD\n" -"zAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR7ySArqpWl2/5rX3aYT+Ydzyl\n" -"kbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+Zr\n" -"zsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E\n" -"FgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn\n" -"8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDovL3JlcG9z\n" -"aXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm5djV\n" -"9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr\n" -"44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8\n" -"AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0V\n" -"qbe/vd6mGu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9Qkvfsywe\n" -"xcZdylU6oJxpmo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/Eb\n" -"MFYOkrCChdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3\n" -"92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG\n" -"2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/Y\n" -"YPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkk\n" -"W8mw0FfB+j564ZfJ\n" -"-----END CERTIFICATE-----", - -/* SecureTrust CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYD\n" -"VQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNl\n" -"Y3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UE\n" -"BhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1\n" -"cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7C\n" -"T8rU4niVWJxB4Q2ZQCQXOZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29\n" -"vo6pQT64lO0pGtSO0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZ\n" -"bf2IzIaowW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n" -"7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xH\n" -"CzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIE\n" -"Bh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE\n" -"/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2NybC5zZWN1cmV0cnVz\n" -"dC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDt\n" -"T0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQ\n" -"f2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cp\n" -"rp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\n" -"CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR3ItHuuG5\n" -"1WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n" -"-----END CERTIFICATE-----", - -/* Secure Global CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYD\n" -"VQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNl\n" -"Y3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYD\n" -"VQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNl\n" -"Y3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxV\n" -"aQZx5RNoJLNP2MwhR/jxYDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6Mpjh\n" -"HZevj8fcyTiW89sa/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ\n" -"/kG5VacJjnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\n" -"HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPi\n" -"XB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGC\n" -"NxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9E\n" -"BMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJl\n" -"dHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IB\n" -"AQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQV\n" -"DpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895\n" -"P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\n" -"iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xcf8LDmBxr\n" -"ThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n" -"-----END CERTIFICATE-----", - -/* COMODO Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkG\n" -"A1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9y\n" -"ZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZp\n" -"Y2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQsw\n" -"CQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm\n" -"b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRp\n" -"ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECL\n" -"i3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\n" -"2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7eu\n" -"NJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC\n" -"8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQF\n" -"ZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4GOMIGLMB0GA1Ud\n" -"DgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw\n" -"AwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9D\n" -"ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5\n" -"t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv\n" -"IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/RxdMosIG\n" -"lgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmcIGfE\n" -"7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN\n" -"+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ==\n" -"-----END CERTIFICATE-----", - /* COMODO ECC Certification Authority */ "-----BEGIN CERTIFICATE-----\n" "MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UE\n" @@ -277,28 +43,6 @@ "V9mSOdY=\n" "-----END CERTIFICATE-----", -/* Certigna */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZS\n" -"MRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMw\n" -"NVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczER\n" -"MA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ\n" -"1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lI\n" -"zw7sebYs5zRLcAglozyHGxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxr\n" -"yIRWijOp5yIVUxbwzBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJb\n" -"zg4ij02Q130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2\n" -"JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0T\n" -"AQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AU\n" -"Gu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlt\n" -"eW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEG\n" -"CWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl\n" -"1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxA\n" -"GYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9q\n" -"cEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w\n" -"t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/QwWyH8EZE0\n" -"vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==\n" -"-----END CERTIFICATE-----", - /* ePKI Root Certification Authority */ "-----BEGIN CERTIFICATE-----\n" "MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYD\n" @@ -331,26 +75,6 @@ "EZw=\n" "-----END CERTIFICATE-----", -/* certSIGN ROOT CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREw\n" -"DwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQx\n" -"NzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lH\n" -"TjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" -"AQoCggEBALczuX7IJUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oq\n" -"rl0Hj0rDKH/v+yv6efHHrfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsA\n" -"fsT8AzNXDe3i+s5dRdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUo\n" -"Se1b16kQOA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv\n" -"JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNC\n" -"MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPx\n" -"fIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJLjX8+HXd5n9liPRyTMks1zJO\n" -"890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6\n" -"IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KT\n" -"afcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI\n" -"0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5V\n" -"aZVDADlN9u6wWk5JRFRYX0KD\n" -"-----END CERTIFICATE-----", - /* NetLock Arany (Class Gold) Főtanúsítvány */ "-----BEGIN CERTIFICATE-----\n" "MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTER\n" @@ -420,39 +144,6 @@ "WD9f\n" "-----END CERTIFICATE-----", -/* Izenpe.com */ -"-----BEGIN CERTIFICATE-----\n" -"MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYD\n" -"VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcN\n" -"MDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwL\n" -"SVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC\n" -"DwAwggIKAoICAQDJ03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5Tz\n" -"cqQsRNiekpsUOqHnJJAKClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpz\n" -"bm3benhB6QiIEn6HLmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJ\n" -"GjMxCrFXuaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD\n" -"yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8\n" -"hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG7\n" -"0t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyNBjNaooXlkDWgYlwWTvDjovoD\n" -"GrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+0rnq49qlw0dpEuDb8PYZi+17cNcC\n" -"1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQD\n" -"fo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNV\n" -"HREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4g\n" -"LSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB\n" -"BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAxMCBWaXRv\n" -"cmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\n" -"FB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l\n" -"Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9\n" -"fbgakEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJO\n" -"ubv5vr8qhT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m\n" -"5hzkQiCeR7Csg1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Py\n" -"e6kfLqCTVyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk\n" -"LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqt\n" -"ujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZ\n" -"pR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6i\n" -"SNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE4\n" -"1V4tC5h9Pmzb/CaIxw==\n" -"-----END CERTIFICATE-----", - /* Go Daddy Root Certificate Authority - G2 */ "-----BEGIN CERTIFICATE-----\n" "MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNV\n" @@ -521,90 +212,6 @@ "/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6\n" "-----END CERTIFICATE-----", -/* AffirmTrust Commercial */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMx\n" -"FDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFs\n" -"MB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNV\n" -"BAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjAN\n" -"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTW\n" -"zsO3qyxPxkEylFf6EqdbDuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U\n" -"6Mje+SJIZMblq8Yrba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNA\n" -"FxHUdPALMeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1\n" -"yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1J\n" -"dX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8w\n" -"DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAFis\n" -"9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M\n" -"06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1Ua\n" -"ADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjip\n" -"M1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclN\n" -"msxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=\n" -"-----END CERTIFICATE-----", - -/* AffirmTrust Networking */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMx\n" -"FDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5n\n" -"MB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNV\n" -"BAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjAN\n" -"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWRE\n" -"ZY9nZOIG41w3SfYvm4SEHi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ\n" -"/Ls6rnla1fTWcbuakCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXL\n" -"viRmVSRLQESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp\n" -"6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKB\n" -"Nv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0w\n" -"DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAIlX\n" -"shZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t\n" -"3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA\n" -"3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzek\n" -"ujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfx\n" -"ojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=\n" -"-----END CERTIFICATE-----", - -/* AffirmTrust Premium */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMx\n" -"FDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4X\n" -"DTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoM\n" -"C0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG\n" -"9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64t\n" -"b+eT2TZwamjPjlGjhVtnBKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/\n" -"0qRY7iZNyaqoe5rZ+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/\n" -"K+k8rNrSs8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5\n" -"HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua\n" -"2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/\n" -"9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+SqHZGnEJlPqQewQcDWkYtuJfz\n" -"t9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m\n" -"6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKP\n" -"KrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNC\n" -"MEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYD\n" -"VR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2\n" -"KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMgNt58D2kT\n" -"iKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC6C1Y\n" -"91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S\n" -"L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQ\n" -"wUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFo\n" -"oC8k4gmVBtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5Yw\n" -"H2AG7hsj/oFgIxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/\n" -"qzWaVYa8GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO\n" -"RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAlo\n" -"GRwYQw==\n" -"-----END CERTIFICATE-----", - -/* AffirmTrust Premium ECC */ -"-----BEGIN CERTIFICATE-----\n" -"MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDAS\n" -"BgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAe\n" -"Fw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQK\n" -"DAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcq\n" -"hkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQU\n" -"X+iOGasvLkjmrBhDeKzQN8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR\n" -"4ptlKymjQjBAMB0GA1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTAD\n" -"AQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs\n" -"aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9C\n" -"a/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==\n" -"-----END CERTIFICATE-----", - /* Certum Trusted Network CA */ "-----BEGIN CERTIFICATE-----\n" "MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYD\n" @@ -933,35 +540,6 @@ "aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0=\n" "-----END CERTIFICATE-----", -/* TeliaSonera Root CA v1 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIG\n" -"A1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcN\n" -"MDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEf\n" -"MB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIP\n" -"ADCCAgoCggIBAMK+6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3\n" -"t+XmfHnqjLWCi65ItqwA3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq\n" -"/t75rH2D+1665I+XZ75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1\n" -"jF3oI7x+/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs\n" -"81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAg\n" -"HNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzT\n" -"jU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMusDor8zagrC/kb2HCUQk5PotT\n" -"ubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7Rc\n" -"We/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUB\n" -"iJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB\n" -"/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjAN\n" -"BgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl\n" -"dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx0GtnLLCo\n" -"4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfWpb/I\n" -"mWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV\n" -"G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KF\n" -"dSpcc41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrE\n" -"gUy7onOTJsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQ\n" -"mz1wHiRszYd2qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfuj\n" -"uLpwQMcnHL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx\n" -"SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=\n" -"-----END CERTIFICATE-----", - /* T-TeleSec GlobalRoot Class 2 */ "-----BEGIN CERTIFICATE-----\n" "MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNV\n" @@ -1355,50 +933,6 @@ "BOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c\n" "-----END CERTIFICATE-----", -/* Entrust Root Certification Authority - G2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAU\n" -"BgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn\n" -"YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9y\n" -"aXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0\n" -"aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UE\n" -"BhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVz\n" -"dC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBm\n" -"b3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj\n" -"YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6\n" -"hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3\n" -"gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWNcCG0szLni6LVhjkCsbjSR87k\n" -"yUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKUs/Ja5CeanyTXxuzQmyWC48zCxEXF\n" -"jJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+\n" -"tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1Ud\n" -"DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2f\n" -"kBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/\n" -"jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZRkfz6/dj\n" -"wUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDginWyT\n" -"msQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+\n" -"vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ\n" -"19xOe4pIb4tF9g==\n" -"-----END CERTIFICATE-----", - -/* Entrust Root Certification Authority - EC1 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMC\n" -"VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5u\n" -"ZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3Ig\n" -"YXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRp\n" -"b24gQXV0aG9yaXR5IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8x\n" -"CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3\n" -"LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJ\n" -"bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD\n" -"ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQT\n" -"ydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9\n" -"ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/\n" -"BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLdj5xrdjekIplWDpOBqUEFlEUJJ\n" -"MAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHv\n" -"AvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZB\n" -"WyVgrtBIGu4G\n" -"-----END CERTIFICATE-----", - /* CFCA EV ROOT */ "-----BEGIN CERTIFICATE-----\n" "MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4G\n" @@ -2187,71 +1721,6 @@ "hVdJIgc=\n" "-----END CERTIFICATE-----", -/* Trustwave Global Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQG\n" -"EwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRy\n" -"dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0\n" -"aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGI\n" -"MQswCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAf\n" -"BgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEds\n" -"b2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC\n" -"AgoCggIBALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn\n" -"swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogD\n" -"nXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXo\n" -"LG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ\n" -"9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+\n" -"VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqI\n" -"yE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m\n" -"4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm9\n" -"43xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n\n" -"twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1UdEwEB/wQF\n" -"MAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIBBjAN\n" -"BgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H\n" -"PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1K\n" -"aA0HZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgim\n" -"QlRXtpla4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0W\n" -"BpanI5ojSP5RvbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92Y\n" -"HJtZuSPTMaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe\n" -"qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVy\n" -"QYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8\n" -"AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzL\n" -"J8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTKyeC2nOnOcXHebD8WpHk=\n" -"-----END CERTIFICATE-----", - -/* Trustwave Global ECC P256 Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJV\n" -"UzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\n" -"d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1\n" -"NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1\n" -"MTBaMIGRMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNh\n" -"Z28xITAfBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\n" -"YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49\n" -"AgEGCCqGSM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN\n" -"FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0P\n" -"AQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwID\n" -"RwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyG\n" -"bbOcNEhjhAnFjXca4syc4XR7\n" -"-----END CERTIFICATE-----", - -/* Trustwave Global ECC P384 Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJV\n" -"UzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\n" -"d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4\n" -"NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2\n" -"NDNaMIGRMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNh\n" -"Z28xITAfBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\n" -"YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49\n" -"AgEGBSuBBAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ\n" -"j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhl\n" -"oKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ\n" -"0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMD\n" -"Er5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3\n" -"g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw==\n" -"-----END CERTIFICATE-----", - /* NAVER Global Root Certification Authority */ "-----BEGIN CERTIFICATE-----\n" "MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTEL\n" @@ -2343,37 +1812,6 @@ "wWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=\n" "-----END CERTIFICATE-----", -/* GLOBALTRUST 2020 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMC\n" -"QVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9C\n" -"QUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UE\n" -"BhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBH\n" -"TE9CQUxUUlVTVCAyMDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc\n" -"7/aVj6B3GyvTY4+ETUWiD59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4\n" -"UeDLgztzOG53ig9ZYybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7M\n" -"potQsjj3QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw\n" -"yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQf\n" -"Es4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiK\n" -"weR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkHr96i5OTUawuzXnzUJIBHKWk7\n" -"buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlGDfV0OoIu0G4skaMxXDtG6nsEEFZe\n" -"gB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfK\n" -"N0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQ\n" -"jJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B\n" -"Af8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu\n" -"H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jAVC/f7GLD\n" -"w56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw4Lx0\n" -"SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9\n" -"iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ\n" -"0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPP\n" -"m2eggAe2HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQ\n" -"Sa9+pTeAsRxSvTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCE\n" -"uGwyEn6CMUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn\n" -"4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlx\n" -"fv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8\n" -"vTmR9W0Nv3vXkg==\n" -"-----END CERTIFICATE-----", - /* ANF Secure Server Root CA */ "-----BEGIN CERTIFICATE-----\n" "MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2\n" @@ -2699,36 +2137,6 @@ "NQzcmRk13NfIRmPVNnGuV/u3gm3c\n" "-----END CERTIFICATE-----", -/* GTS Root R2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG\n" -"EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RT\n" -"IFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJV\n" -"UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv\n" -"b3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3\n" -"GTXd98GdVarTzTukk3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfg\n" -"LFuv5AS/T3KgGjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/B\n" -"W9BuXvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k\n" -"RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FL\n" -"PD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66H\n" -"jucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8YzodDqs5xoic4DSMPclQsciOzsS\n" -"rZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvG\n" -"eJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9Om\n" -"TN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGF\n" -"PP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAd\n" -"BgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H\n" -"vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM80mJhwQTt\n" -"zuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyCB19m3H0Q/gxhswWV\n" -"7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2uNmSRXbBoGOqKYcl3qJfEycel\n" -"/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMgyALOWr7Z6v2yTcQvG99fevX4i8buMTol\n" -"UVVnjWQye+mew4K6Ki3pHrTgSAai/GevHyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFe\n" -"nTgCR2y59PYjJbigapordwj6xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGo\n" -"o7z7GJa7Um8M7YNRTOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCM\n" -"Elv924SgJPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV\n" -"7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl6WLAYv7Y\n" -"TVWW4tAR+kg0Eeye7QUd5MjWHYbL\n" -"-----END CERTIFICATE-----", - /* GTS Root R3 */ "-----BEGIN CERTIFICATE-----\n" "MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV\n" @@ -3202,22 +2610,6 @@ "NSWSs1A=\n" "-----END CERTIFICATE-----", -/* FIRMAPROFESIONAL CA ROOT-A WEB */ -"-----BEGIN CERTIFICATE-----\n" -"MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQswCQYDVQQG\n" -"EwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYy\n" -"NjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwHhcNMjIw\n" -"NDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQswCQYDVQQGEwJFUzEcMBoGA1UECgwTRmly\n" -"bWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5G\n" -"SVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARH\n" -"U+osEaR3xyrq89Zfe9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K\n" -"6k84Si6CcyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB\n" -"/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0OBBYEFJPh\n" -"Q2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjAd\n" -"fKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFwhVmpHqTm6iMxoAACMQD94viz\n" -"rxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dGXSaQpYXFuXqUPoeovQA=\n" -"-----END CERTIFICATE-----", - /* TWCA CYBER Root CA */ "-----BEGIN CERTIFICATE-----\n" "MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQMQswCQYD\n" diff --git a/src/node_sockaddr-inl.h b/src/node_sockaddr-inl.h index bc055da535c2d4..b02999d047800f 100644 --- a/src/node_sockaddr-inl.h +++ b/src/node_sockaddr-inl.h @@ -186,10 +186,10 @@ typename T::Type* SocketAddressLRU::Peek( } template -void SocketAddressLRU::CheckExpired() { +void SocketAddressLRU::CheckExpired(uint64_t now) { auto it = list_.rbegin(); while (it != list_.rend()) { - if (T::CheckExpired(it->first, it->second)) { + if (T::CheckExpired(it->first, it->second, now)) { map_.erase(it->first); list_.pop_back(); it = list_.rbegin(); @@ -211,21 +211,20 @@ void SocketAddressLRU::MemoryInfo(MemoryTracker* tracker) const { // cache and adjust if necessary. Whether the item exists or not, // purge expired items. template -typename T::Type* SocketAddressLRU::Upsert( - const SocketAddress& address) { - - auto on_exit = OnScopeLeave([&]() { CheckExpired(); }); +typename T::Type* SocketAddressLRU::Upsert(const SocketAddress& address, + uint64_t now) { + auto on_exit = OnScopeLeave([&]() { CheckExpired(now); }); auto it = map_.find(address); if (it != std::end(map_)) { list_.splice(list_.begin(), list_, it->second); - T::Touch(it->first, &it->second->second); + T::Touch(it->first, &it->second->second, now); return &it->second->second; } list_.push_front(Pair(address, { })); map_[address] = list_.begin(); - T::Touch(list_.begin()->first, &list_.begin()->second); + T::Touch(list_.begin()->first, &list_.begin()->second, now); // Drop the last item in the list if we are // over the size limit... diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index a9f1a7376bc1fa..9348f0ac8e4dfc 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -434,8 +434,7 @@ void SocketAddressBlockList::AddSocketAddressMask( rules_.emplace_front(std::move(rule)); } -bool SocketAddressBlockList::Apply( - const std::shared_ptr& address) { +bool SocketAddressBlockList::Apply(const SocketAddress& address) { Mutex::ScopedLock lock(mutex_); for (const auto& rule : rules_) { if (rule->Apply(address)) return true; @@ -457,8 +456,8 @@ SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule( : network(network_), prefix(prefix_) {} bool SocketAddressBlockList::SocketAddressRule::Apply( - const std::shared_ptr& address) { - return this->address->is_match(*address.get()); + const SocketAddress& address) { + return this->address->is_match(address); } std::string SocketAddressBlockList::SocketAddressRule::ToString() { @@ -470,8 +469,8 @@ std::string SocketAddressBlockList::SocketAddressRule::ToString() { } bool SocketAddressBlockList::SocketAddressRangeRule::Apply( - const std::shared_ptr& address) { - return *address.get() >= *start.get() && *address.get() <= *end.get(); + const SocketAddress& address) { + return address >= *start.get() && address <= *end.get(); } std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { @@ -485,8 +484,8 @@ std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { } bool SocketAddressBlockList::SocketAddressMaskRule::Apply( - const std::shared_ptr& address) { - return address->is_in_network(*network.get(), prefix); + const SocketAddress& address) { + return address.is_in_network(*network.get(), prefix); } std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { @@ -656,7 +655,7 @@ void SocketAddressBlockListWrap::Check( SocketAddressBase* addr; ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); - args.GetReturnValue().Set(wrap->blocklist_->Apply(addr->address())); + args.GetReturnValue().Set(wrap->blocklist_->Apply(*addr->address())); } void SocketAddressBlockListWrap::GetRules( diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index d67a26e8615cdc..05bb127b012f83 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -213,8 +213,9 @@ class SocketAddressLRU : public MemoryRetainer { // If the item already exists, returns a reference to // the existing item, adjusting items position in the // LRU. If the item does not exist, emplaces the item - // and returns the new item. - Type* Upsert(const SocketAddress& address); + // and returns the new item. The caller provides a + // timestamp to avoid redundant uv_hrtime() calls. + Type* Upsert(const SocketAddress& address, uint64_t now); // Returns a reference to the item if it exists, or // nullptr. The position in the LRU is not modified. @@ -231,7 +232,7 @@ class SocketAddressLRU : public MemoryRetainer { using Pair = std::pair; using Iterator = typename std::list::iterator; - void CheckExpired(); + void CheckExpired(uint64_t now); std::list list_; SocketAddress::Map map_; @@ -257,14 +258,14 @@ class SocketAddressBlockList : public MemoryRetainer { void AddSocketAddressMask(const std::shared_ptr& address, int prefix); - bool Apply(const std::shared_ptr& address); + bool Apply(const SocketAddress& address); size_t size() const { return rules_.size(); } v8::MaybeLocal ListRules(Environment* env); struct Rule : public MemoryRetainer { - virtual bool Apply(const std::shared_ptr& address) = 0; + virtual bool Apply(const SocketAddress& address) = 0; inline v8::MaybeLocal ToV8String(Environment* env); virtual std::string ToString() = 0; }; @@ -274,7 +275,7 @@ class SocketAddressBlockList : public MemoryRetainer { explicit SocketAddressRule(const std::shared_ptr& address); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -289,7 +290,7 @@ class SocketAddressBlockList : public MemoryRetainer { SocketAddressRangeRule(const std::shared_ptr& start, const std::shared_ptr& end); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -304,7 +305,7 @@ class SocketAddressBlockList : public MemoryRetainer { SocketAddressMaskRule(const std::shared_ptr& address, int prefix); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -352,6 +353,10 @@ class SocketAddressBlockListWrap : public BaseObject { std::shared_ptr blocklist = std::make_shared()); + inline const std::shared_ptr& blocklist() const { + return blocklist_; + } + void MemoryInfo(node::MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(SocketAddressBlockListWrap) SET_SELF_SIZE(SocketAddressBlockListWrap) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index f23f25ba0d58fe..522d3c24cfba70 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2237,7 +2237,6 @@ static int xConflict(void* pCtx, int eConflict, sqlite3_changeset_iter* pIter) { static int xFilter(void* pCtx, const char* zTab) { auto ctx = static_cast(pCtx); - if (!ctx->filterCallback) return 1; return ctx->filterCallback(zTab) ? 1 : 0; } @@ -2348,7 +2347,7 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { db->connection_, buf.length(), const_cast(static_cast(buf.data())), - xFilter, + context.filterCallback ? xFilter : nullptr, xConflict, static_cast(&context)); if (r == SQLITE_OK) { diff --git a/src/node_version.h b/src/node_version.h index 44f405454851a4..e560dc1a13e36d 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -23,13 +23,13 @@ #define SRC_NODE_VERSION_H_ #define NODE_MAJOR_VERSION 26 -#define NODE_MINOR_VERSION 2 -#define NODE_PATCH_VERSION 1 +#define NODE_MINOR_VERSION 3 +#define NODE_PATCH_VERSION 0 #define NODE_VERSION_IS_LTS 0 #define NODE_VERSION_LTS_CODENAME "" -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_STRINGIFY #define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n) diff --git a/src/node_webstorage.cc b/src/node_webstorage.cc index 10c3ccd68e49a1..21f846fbeb6225 100644 --- a/src/node_webstorage.cc +++ b/src/node_webstorage.cc @@ -43,6 +43,7 @@ using v8::PropertyAttribute; using v8::PropertyCallbackInfo; using v8::PropertyDescriptor; using v8::PropertyHandlerFlags; +using v8::Signature; using v8::String; using v8::Value; @@ -737,8 +738,9 @@ static void Initialize(Local target, Local(), PropertyHandlerFlags::kHasNoSideEffect)); - Local length_getter = - FunctionTemplate::New(isolate, StorageLengthGetter); + Local length_signature = Signature::New(isolate, ctor_tmpl); + Local length_getter = FunctionTemplate::New( + isolate, StorageLengthGetter, Local(), length_signature); ctor_tmpl->PrototypeTemplate()->SetAccessorProperty(env->length_string(), length_getter, Local(), diff --git a/src/permission/addon_permission.cc b/src/permission/addon_permission.cc index 20123a72e6e06e..66035556102ff3 100644 --- a/src/permission/addon_permission.cc +++ b/src/permission/addon_permission.cc @@ -14,6 +14,12 @@ void AddonPermission::Apply(Environment* env, deny_all_ = true; } +void AddonPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool AddonPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/addon_permission.h b/src/permission/addon_permission.h index 9702862d098703..b3eed910fe9f89 100644 --- a/src/permission/addon_permission.h +++ b/src/permission/addon_permission.h @@ -15,6 +15,9 @@ class AddonPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/child_process_permission.cc b/src/permission/child_process_permission.cc index 1dfbaecf1b11ed..7d31ff24f81398 100644 --- a/src/permission/child_process_permission.cc +++ b/src/permission/child_process_permission.cc @@ -15,6 +15,12 @@ void ChildProcessPermission::Apply(Environment* env, deny_all_ = true; } +void ChildProcessPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool ChildProcessPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/child_process_permission.h b/src/permission/child_process_permission.h index 7c9078c5b30714..33612b1c10a9af 100644 --- a/src/permission/child_process_permission.h +++ b/src/permission/child_process_permission.h @@ -15,6 +15,9 @@ class ChildProcessPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/ffi_permission.cc b/src/permission/ffi_permission.cc index b388c8fcc44a06..4b00d4c07b9c59 100644 --- a/src/permission/ffi_permission.cc +++ b/src/permission/ffi_permission.cc @@ -14,6 +14,12 @@ void FFIPermission::Apply(Environment* env, deny_all_ = true; } +void FFIPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool FFIPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/ffi_permission.h b/src/permission/ffi_permission.h index 70fb7a4b9a2e1d..3acd3c4642a048 100644 --- a/src/permission/ffi_permission.h +++ b/src/permission/ffi_permission.h @@ -15,6 +15,9 @@ class FFIPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/fs_permission.cc b/src/permission/fs_permission.cc index 3b817c5bcdce75..98146cf825ad38 100644 --- a/src/permission/fs_permission.cc +++ b/src/permission/fs_permission.cc @@ -154,15 +154,96 @@ void FSPermission::Apply(Environment* env, } } +void FSPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + if (param.empty()) { + // Drop all access for this scope + if (scope == PermissionScope::kFileSystemRead || + scope == PermissionScope::kFileSystem) { + deny_all_in_ = true; + allow_all_in_ = false; + granted_in_fs_.Clear(); + granted_paths_in_.clear(); + } + if (scope == PermissionScope::kFileSystemWrite || + scope == PermissionScope::kFileSystem) { + deny_all_out_ = true; + allow_all_out_ = false; + granted_out_fs_.Clear(); + granted_paths_out_.clear(); + } + return; + } + + // When allowed with *, you can only drop * (no specific paths) + std::string resolved = PathResolve(env, {param}); + if (scope == PermissionScope::kFileSystemRead || + scope == PermissionScope::kFileSystem) { + if (!allow_all_in_) { + RevokeAccess(PermissionScope::kFileSystemRead, resolved); + } + } + if (scope == PermissionScope::kFileSystemWrite || + scope == PermissionScope::kFileSystem) { + if (!allow_all_out_) { + RevokeAccess(PermissionScope::kFileSystemWrite, resolved); + } + } +} + +void FSPermission::RevokeAccess(PermissionScope perm, const std::string& res) { + const std::string path = WildcardIfDir(res); + if (perm == PermissionScope::kFileSystemRead) { + auto it = + std::find(granted_paths_in_.begin(), granted_paths_in_.end(), path); + if (it != granted_paths_in_.end()) { + granted_paths_in_.erase(it); + RebuildTree(PermissionScope::kFileSystemRead); + } + } else if (perm == PermissionScope::kFileSystemWrite) { + auto it = + std::find(granted_paths_out_.begin(), granted_paths_out_.end(), path); + if (it != granted_paths_out_.end()) { + granted_paths_out_.erase(it); + RebuildTree(PermissionScope::kFileSystemWrite); + } + } +} + +void FSPermission::RebuildTree(PermissionScope scope) { + if (scope == PermissionScope::kFileSystemRead) { + granted_in_fs_.Clear(); + if (granted_paths_in_.empty()) { + deny_all_in_ = true; + } else { + for (const auto& path : granted_paths_in_) { + granted_in_fs_.Insert(path); + } + } + } else if (scope == PermissionScope::kFileSystemWrite) { + granted_out_fs_.Clear(); + if (granted_paths_out_.empty()) { + deny_all_out_ = true; + } else { + for (const auto& path : granted_paths_out_) { + granted_out_fs_.Insert(path); + } + } + } +} + void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) { const std::string path = WildcardIfDir(res); if (perm == PermissionScope::kFileSystemRead && !granted_in_fs_.Lookup(path)) { granted_in_fs_.Insert(path); + granted_paths_in_.push_back(path); deny_all_in_ = false; } else if (perm == PermissionScope::kFileSystemWrite && !granted_out_fs_.Lookup(path)) { granted_out_fs_.Insert(path); + granted_paths_out_.push_back(path); deny_all_out_ = false; } } @@ -196,6 +277,16 @@ FSPermission::RadixTree::~RadixTree() { FreeRecursivelyNode(root_node_); } +void FSPermission::RadixTree::Clear() { + for (auto& c : root_node_->children) { + FreeRecursivelyNode(c.second); + } + root_node_->children.clear(); + delete root_node_->wildcard_child; + root_node_->wildcard_child = nullptr; + root_node_->is_leaf = false; +} + bool FSPermission::RadixTree::Lookup(const std::string_view& s, bool when_empty_return) const { FSPermission::RadixTree::Node* current_node = root_node_; diff --git a/src/permission/fs_permission.h b/src/permission/fs_permission.h index 22b29b017e2061..19d72ef654c9f0 100644 --- a/src/permission/fs_permission.h +++ b/src/permission/fs_permission.h @@ -18,6 +18,9 @@ class FSPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const override; @@ -139,6 +142,7 @@ class FSPermission final : public PermissionBase { RadixTree(); ~RadixTree(); void Insert(const std::string& s); + void Clear(); bool Lookup(const std::string_view& s) const { return Lookup(s, false); } bool Lookup(const std::string_view& s, bool when_empty_return) const; @@ -148,10 +152,15 @@ class FSPermission final : public PermissionBase { private: void GrantAccess(PermissionScope scope, const std::string& param); + void RevokeAccess(PermissionScope scope, const std::string& param); + void RebuildTree(PermissionScope scope); // fs granted on startup RadixTree granted_in_fs_; RadixTree granted_out_fs_; + std::vector granted_paths_in_; + std::vector granted_paths_out_; + bool deny_all_in_ = true; bool deny_all_out_ = true; diff --git a/src/permission/inspector_permission.cc b/src/permission/inspector_permission.cc index 95114e6634ab3f..ee775e778dcf52 100644 --- a/src/permission/inspector_permission.cc +++ b/src/permission/inspector_permission.cc @@ -14,6 +14,12 @@ void InspectorPermission::Apply(Environment* env, deny_all_ = true; } +void InspectorPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool InspectorPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/inspector_permission.h b/src/permission/inspector_permission.h index 9b214099095ee8..d851fb2fa25303 100644 --- a/src/permission/inspector_permission.h +++ b/src/permission/inspector_permission.h @@ -15,6 +15,9 @@ class InspectorPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/net_permission.cc b/src/permission/net_permission.cc index f1380f9b321a89..5f1cc139aa745e 100644 --- a/src/permission/net_permission.cc +++ b/src/permission/net_permission.cc @@ -13,6 +13,12 @@ void NetPermission::Apply(Environment* env, allow_net_ = true; } +void NetPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + allow_net_ = false; +} + bool NetPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/net_permission.h b/src/permission/net_permission.h index 3be4f5bc7c40eb..26b055b255a63d 100644 --- a/src/permission/net_permission.h +++ b/src/permission/net_permission.h @@ -15,6 +15,9 @@ class NetPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const override; diff --git a/src/permission/permission.cc b/src/permission/permission.cc index 0da8fdcd0b9c1e..c593502d94eb49 100644 --- a/src/permission/permission.cc +++ b/src/permission/permission.cc @@ -53,6 +53,29 @@ constexpr std::string_view GetDiagnosticsChannelName(PermissionScope scope) { } } +// permission.drop('fs.read', '/tmp/') +// permission.drop('child') +static void Drop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + + const std::string deny_scope = Utf8Value(env->isolate(), args[0]).ToString(); + PermissionScope scope = Permission::StringToPermission(deny_scope); + if (scope == PermissionScope::kPermissionsRoot) { + return; + } + + if (args.Length() > 1 && !args[1]->IsUndefined()) { + Utf8Value utf8_arg(env->isolate(), args[1]); + if (utf8_arg.length() > 0) { + env->permission()->Drop(env, scope, utf8_arg.ToStringView()); + return; + } + } + + env->permission()->Drop(env, scope); +} + // permission.has('fs.in', '/tmp/') // permission.has('fs.in') static void Has(const FunctionCallbackInfo& args) { @@ -283,17 +306,61 @@ void Permission::Apply(Environment* env, } } +void Permission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + auto permission = nodes_.find(scope); + if (permission != nodes_.end()) { + permission->second->Drop(env, scope, param); + } + + // Publish to diagnostics channel so observers can track drops + auto channel_name = GetDiagnosticsChannelName(scope); + if (!channel_name.empty() && !publishing_) { + auto ch = GetOrCreateChannel(env, scope); + if (ch && ch->HasSubscribers()) { + publishing_ = true; + v8::Isolate* isolate = env->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Local context = env->context(); + v8::Local msg = + v8::Object::New(isolate, v8::Null(isolate), nullptr, nullptr, 0); + const char* perm_str = PermissionToString(scope); + msg->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "permission"), + v8::String::NewFromUtf8(isolate, perm_str).ToLocalChecked()) + .Check(); + msg->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "resource"), + v8::String::NewFromUtf8(isolate, + param.data(), + v8::NewStringType::kNormal, + static_cast(param.size())) + .ToLocalChecked()) + .Check(); + msg->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "drop"), + v8::Boolean::New(isolate, true)) + .Check(); + ch->Publish(env, msg); + publishing_ = false; + } + } +} + void Initialize(Local target, Local unused, Local context, void* priv) { SetMethodNoSideEffect(context, target, "has", Has); + SetMethod(context, target, "drop", Drop); target->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromJust(); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(Has); + registry->Register(Drop); } } // namespace permission diff --git a/src/permission/permission.h b/src/permission/permission.h index b43c9648093b08..84e3ea67ed5e04 100644 --- a/src/permission/permission.h +++ b/src/permission/permission.h @@ -118,6 +118,10 @@ class Permission { void Apply(Environment* env, const std::vector& allow, PermissionScope scope); + // Runtime Call + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = ""); void EnablePermissions(); void EnableWarningOnly(); diff --git a/src/permission/permission_base.h b/src/permission/permission_base.h index 4c796c770dccf8..11f4d6c6bfa65f 100644 --- a/src/permission/permission_base.h +++ b/src/permission/permission_base.h @@ -59,6 +59,9 @@ class PermissionBase { virtual void Apply(Environment* env, const std::vector& allow, PermissionScope scope) = 0; + virtual void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") = 0; virtual bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const = 0; diff --git a/src/permission/wasi_permission.cc b/src/permission/wasi_permission.cc index 625b61cf1c1bbe..00ce927eb6254f 100644 --- a/src/permission/wasi_permission.cc +++ b/src/permission/wasi_permission.cc @@ -15,6 +15,12 @@ void WASIPermission::Apply(Environment* env, deny_all_ = true; } +void WASIPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool WASIPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/wasi_permission.h b/src/permission/wasi_permission.h index 2a12592d142eea..b5cdaca928dde8 100644 --- a/src/permission/wasi_permission.h +++ b/src/permission/wasi_permission.h @@ -15,6 +15,9 @@ class WASIPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/worker_permission.cc b/src/permission/worker_permission.cc index 3a51cf12e4ee85..aa6867eb1e0f17 100644 --- a/src/permission/worker_permission.cc +++ b/src/permission/worker_permission.cc @@ -15,6 +15,12 @@ void WorkerPermission::Apply(Environment* env, deny_all_ = true; } +void WorkerPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool WorkerPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/worker_permission.h b/src/permission/worker_permission.h index 9ec40a3d1c66ca..fc7abe0d50f459 100644 --- a/src/permission/worker_permission.h +++ b/src/permission/worker_permission.h @@ -15,6 +15,9 @@ class WorkerPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/quic/application.cc b/src/quic/application.cc index b5d8c8609fa3dc..ce5d5e12154d8a 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -19,9 +19,13 @@ namespace node { +using v8::BigInt; +using v8::Boolean; +using v8::DictionaryTemplate; using v8::Just; using v8::Local; using v8::Maybe; +using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::Value; @@ -111,9 +115,54 @@ Maybe Session::Application_Options::From( #undef SET + // Ensure the advertised max_field_section_size in SETTINGS is at least + // as large as max_header_length. Otherwise the peer would be told to + // restrict headers to a smaller size than what CanAddHeader accepts. + if (options.max_field_section_size < options.max_header_length) { + options.max_field_section_size = options.max_header_length; + } + return Just(options); } +MaybeLocal Session::Application_Options::ToObject( + Environment* env) const { + auto& binding_data = BindingData::Get(env); + auto tmpl = binding_data.application_options_template(); + static constexpr std::string_view names[] = { + "maxHeaderPairs", + "maxHeaderLength", + "maxFieldSectionSize", + "qpackMaxDtableCapacity", + "qpackEncoderMaxDtableCapacity", + "qpackBlockedStreams", + "enableConnectProtocol", + "enableDatagrams", + }; + if (tmpl.IsEmpty()) { + tmpl = DictionaryTemplate::New(env->isolate(), names); + binding_data.set_application_options_template(tmpl); + } + MaybeLocal values[] = { + BigInt::NewFromUnsigned(env->isolate(), max_header_pairs), + BigInt::NewFromUnsigned(env->isolate(), max_header_length), + BigInt::NewFromUnsigned(env->isolate(), max_field_section_size), + BigInt::NewFromUnsigned(env->isolate(), qpack_max_dtable_capacity), + BigInt::NewFromUnsigned(env->isolate(), + qpack_encoder_max_dtable_capacity), + BigInt::NewFromUnsigned(env->isolate(), qpack_blocked_streams), + Boolean::New(env->isolate(), enable_connect_protocol), + Boolean::New(env->isolate(), enable_datagrams), + }; + static_assert(std::size(values) == std::size(names)); + + auto obj = tmpl->NewInstance(env->context(), values); + if (obj->SetPrototypeV2(env->context(), Null(env->isolate())).IsNothing()) { + return {}; + } + return obj; +} + // ============================================================================ std::string Session::Application::StreamData::ToString() const { @@ -239,7 +288,8 @@ void Session::Application::ReceiveStreamReset(Stream* stream, // < 0 (other): fatal error, session already closed ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path, uint8_t* dest, - size_t destlen) { + size_t destlen, + uint64_t ts) { CHECK(session_->HasPendingDatagrams()); auto max_attempts = session_->config().options.max_datagram_send_attempts; @@ -262,9 +312,12 @@ ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path, int accepted = 0; int dg_flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; + // PacketInfo for the datagram path. When libuv gains per-socket ECN + // marking, the value from ngtcp2 should be forwarded to the send path. + PacketInfo dg_pi; ssize_t dg_nwrite = ngtcp2_conn_writev_datagram(*session_, &path->path, - nullptr, + dg_pi, dest, destlen, &accepted, @@ -272,7 +325,7 @@ ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path, dg.id, &dgvec, 1, - uv_hrtime()); + ts); if (accepted) { // Nice, the datagram was accepted! @@ -329,20 +382,51 @@ void Session::Application::SendPendingData() { if (!session().can_send_packets()) [[unlikely]] { return; } - static constexpr size_t kMaxPackets = 32; + // Upper bound on packets per SendPendingData call. ngtcp2's send quantum + // is typically 64 KB, which at 1200-byte minimum packet size is ~53 + // packets. 64 covers the worst case with headroom. The actual count per + // call is dynamically capped by ngtcp2_conn_get_send_quantum(). + static constexpr size_t kMaxPackets = 64; Debug(session_, "Application sending pending data"); + // Cache the timestamp once for the entire send loop. ngtcp2 does not + // require nanosecond-accurate monotonicity within a single burst — + // a single timestamp per SendPendingData call is what other QUIC + // implementations use (e.g., quiche, msquic). When kernel-level + // packet pacing becomes available via libuv, this timestamp becomes + // the base for computing per-packet transmit timestamps. + const uint64_t ts = uv_hrtime(); PathStorage path; StreamData stream_data; bool closed = false; + + // Batch accumulation: packets are collected here and flushed via + // Session::SendBatch when the loop exits, the batch is full, or + // on early return. This enables synchronous batched delivery via + // uv_udp_try_send2 (sendmmsg) from the deferred flush path. + Packet::Ptr batch[kMaxPackets]; + PathStorage batch_paths[kMaxPackets]; + size_t batch_count = 0; + + auto flush_batch = [&] { + if (batch_count == 0) return; + session_->SendBatch(batch, batch_paths, batch_count); + batch_count = 0; + }; + auto update_stats = OnScopeLeave([&] { if (closed) return; - auto& s = session(); - if (!s.is_destroyed()) [[likely]] { - s.UpdatePacketTxTime(); - s.UpdateTimer(); - s.UpdateDataStats(); - } + // Flush any remaining accumulated packets before updating stats. + flush_batch(); + if (session().is_destroyed()) [[unlikely]] + return; + + // Get a strong pointer to protect against potential destruction during + // updating the time and data stats. + BaseObjectPtr s(session_); + s->UpdatePacketTxTime(); + s->UpdateTimer(); + s->UpdateDataStats(); }); // The maximum size of packet to create. @@ -353,7 +437,7 @@ void Session::Application::SendPendingData() { kMaxPackets, ngtcp2_conn_get_send_quantum(*session_) / max_packet_size); if (max_packet_count == 0) return; - // The number of packets that have been sent in this call to SendPendingData. + // The number of packets that have been prepared in this call. size_t packet_send_count = 0; Packet::Ptr packet; @@ -368,6 +452,16 @@ void Session::Application::SendPendingData() { return true; }; + // Accumulate a completed packet into the batch. + auto enqueue_packet = + [&](Packet::Ptr& pkt, size_t len, const PacketInfo& pi) { + Debug(session_, "Enqueuing packet with %zu bytes into batch", len); + pkt->Truncate(len); + pkt->set_pkt_info(pi); + path.CopyTo(&batch_paths[batch_count]); + batch[batch_count++] = std::move(pkt); + }; + // We're going to enter a loop here to prepare and send no more than // max_packet_count packets. for (;;) { @@ -405,8 +499,14 @@ void Session::Application::SendPendingData() { } // Awesome, let's write our packet! - ssize_t nwrite = WriteVStream( - &path, packet->data(), &ndatalen, packet->length(), stream_data); + PacketInfo pi; + ssize_t nwrite = WriteVStream(&path, + &pi, + packet->data(), + &ndatalen, + packet->length(), + stream_data, + ts); // When ndatalen is > 0, that's our indication that stream data was accepted // in to the packet. Yay! @@ -493,7 +593,7 @@ void Session::Application::SendPendingData() { // if there is one. Otherwise just loop around and keep going. if (session_->HasPendingDatagrams()) { auto result = TryWritePendingDatagram( - &path, packet->data(), packet->length()); + &path, packet->data(), packet->length(), ts); // When result is 0, either the datagram was congestion controlled, // didn't fit in the packet, or was abandoned. Skip and continue. @@ -502,8 +602,7 @@ void Session::Application::SendPendingData() { if (result > 0) { size_t len = result; Debug(session_, "Sending packet with %zu bytes", len); - packet->Truncate(len); - session_->Send(std::move(packet), path); + enqueue_packet(packet, len, pi); if (++packet_send_count == max_packet_count) return; } else if (result < 0) { // Any negative result other than NGTCP2_ERR_WRITE_MORE @@ -540,8 +639,7 @@ void Session::Application::SendPendingData() { // is the size of the packet we are sending. size_t len = nwrite; Debug(session_, "Sending packet with %zu bytes", len); - packet->Truncate(len); - session_->Send(std::move(packet), path); + enqueue_packet(packet, len, pi); if (++packet_send_count == max_packet_count) return; // If there are pending datagrams, try sending them in a fresh packet. @@ -557,11 +655,10 @@ void Session::Application::SendPendingData() { return session_->Close(CloseMethod::SILENT); } auto result = - TryWritePendingDatagram(&path, packet->data(), packet->length()); + TryWritePendingDatagram(&path, packet->data(), packet->length(), ts); if (result > 0) { Debug(session_, "Sending datagram packet with %zd bytes", result); - packet->Truncate(static_cast(result)); - session_->Send(std::move(packet), path); + enqueue_packet(packet, static_cast(result), PacketInfo()); if (++packet_send_count == max_packet_count) return; } else if (result < 0 && result != NGTCP2_ERR_WRITE_MORE) { // Fatal error — session already closed by TryWritePendingDatagram. @@ -574,17 +671,21 @@ void Session::Application::SendPendingData() { } ssize_t Session::Application::WriteVStream(PathStorage* path, + PacketInfo* pi, uint8_t* dest, ssize_t* ndatalen, size_t max_packet_size, - const StreamData& stream_data) { + const StreamData& stream_data, + uint64_t ts) { DCHECK_LE(stream_data.count, kMaxVectorCount); uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + // The PacketInfo out-param is populated by ngtcp2 with the ECN codepoint + // to apply when sending this packet. When libuv gains per-socket ECN + // marking, the value should be forwarded to the send path. return ngtcp2_conn_writev_stream(*session_, &path->path, - // TODO(@jasnell): ECN blocked on libuv - nullptr, + *pi, dest, max_packet_size, ndatalen, @@ -592,7 +693,7 @@ ssize_t Session::Application::WriteVStream(PathStorage* path, stream_data.id, stream_data, stream_data.count, - uv_hrtime()); + ts); } // ============================================================================ @@ -603,7 +704,10 @@ class DefaultApplication final : public Session::Application { // Marked NOLINT because the cpp linter gets confused about this using // statement not being sorted with the using v8 statements at the top // of the namespace. - using Application::Application; // NOLINT + DefaultApplication(Session* session, const Options& options) + : Session::Application(session, options), options_(options) {} + + const Options& options() const override { return options_; } Session::Application::Type type() const override { return Session::Application::Type::DEFAULT; @@ -620,8 +724,11 @@ class DefaultApplication final : public Session::Application { void EarlyDataRejected() override { // Destroy all open streams — ngtcp2 has already discarded their - // internal state when it rejected the early data. - session().DestroyAllStreams(QuicError::ForApplication(0)); + // internal state when it rejected the early data. Use the + // application's internal error code since this is an error + // condition (code 0 would be treated as a clean close). + session().DestroyAllStreams( + QuicError::ForApplication(GetInternalErrorCode())); if (!session().is_destroyed()) { session().EmitEarlyDataRejected(); } @@ -775,6 +882,8 @@ class DefaultApplication final : public Session::Application { } } + Options options_; + Stream::Queue stream_queue_; }; diff --git a/src/quic/application.h b/src/quic/application.h index 673a4000e4ba2d..0df9b9f0a0e68d 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -38,6 +38,10 @@ class Session::Application : public MemoryRetainer { Application(Session* session, const Options& options); DISALLOW_COPY_AND_MOVE(Application) + // Get the active options for this application. These may differ from the + // options passed at construction time since some options can be negotiated. + virtual const Options& options() const = 0; + // The type of Application, exposed via the session state so JS // can observe which Application was selected after ALPN negotiation. // This is used primarily for testing/debugging. @@ -267,14 +271,19 @@ class Session::Application : public MemoryRetainer { // the datagram is either congestion limited or was abandoned ssize_t TryWritePendingDatagram(PathStorage* path, uint8_t* dest, - size_t destlen); + size_t destlen, + uint64_t ts); - // Write the given stream_data into the buffer. + // Write the given stream_data into the buffer. The PacketInfo out-param + // is populated by ngtcp2 with per-packet metadata (e.g., ECN codepoint) + // that should be applied when sending the packet. ssize_t WriteVStream(PathStorage* path, + PacketInfo* pi, uint8_t* buf, ssize_t* ndatalen, size_t max_packet_size, - const StreamData& stream_data); + const StreamData& stream_data, + uint64_t ts); Session* session_ = nullptr; }; diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 4a3b3dba11f196..31467a8477a792 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -20,8 +20,11 @@ namespace node { using mem::kReserveSizeAndAlign; +using v8::DictionaryTemplate; using v8::Function; using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Isolate; using v8::Local; using v8::Object; using v8::String; @@ -148,12 +151,88 @@ void* Nghttp3Realloc(void* ptr, size_t size, void* ud) { } } // namespace +// ============================================================================ +// CheckWrap / CheckWrapHandle + +void CheckWrap::Start() { + if (check_.data == nullptr) return; + uv_check_start(&check_, OnCheck); +} + +void CheckWrap::Stop() { + if (check_.data == nullptr) return; + uv_check_stop(&check_); +} + +void CheckWrap::Close() { + check_.data = nullptr; + env_->CloseHandle(reinterpret_cast(&check_), CheckClosedCb); +} + +void CheckWrap::Ref() { + if (check_.data == nullptr) return; + uv_ref(reinterpret_cast(&check_)); +} + +void CheckWrap::Unref() { + if (check_.data == nullptr) return; + uv_unref(reinterpret_cast(&check_)); +} + +void CheckWrap::OnCheck(uv_check_t* check) { + CheckWrap* wrap = ContainerOf(&CheckWrap::check_, check); + wrap->fn_(); +} + +void CheckWrap::CheckClosedCb(uv_handle_t* handle) { + std::unique_ptr ptr( + ContainerOf(&CheckWrap::check_, reinterpret_cast(handle))); +} + +void CheckWrapHandle::Start() { + if (check_ != nullptr) check_->Start(); +} + +void CheckWrapHandle::Stop() { + if (check_ != nullptr) check_->Stop(); +} + +void CheckWrapHandle::Close() { + if (check_ != nullptr) { + check_->env()->RemoveCleanupHook(CleanupHook, this); + check_->Close(); + } + check_ = nullptr; +} + +void CheckWrapHandle::Ref() { + if (check_ != nullptr) check_->Ref(); +} + +void CheckWrapHandle::Unref() { + if (check_ != nullptr) check_->Unref(); +} + +void CheckWrapHandle::MemoryInfo(MemoryTracker* tracker) const { + if (check_ != nullptr) tracker->TrackField("check", *check_); +} + +void CheckWrapHandle::CleanupHook(void* data) { + static_cast(data)->Close(); +} + +// ============================================================================ + BindingData& BindingData::Get(Environment* env) { return *(env->principal_realm()->GetBindingData()); } BindingData::~BindingData() { quic_alloc_state.binding = nullptr; + // flush_check_ is cleaned up by ~CheckWrapHandle() after the destructor + // body completes. The inner CheckWrap (and its uv_check_t) will be freed + // later by the uv_close callback, after CleanupHandles() runs uv_run(). + pending_flush_sessions_.clear(); } ngtcp2_mem* BindingData::ngtcp2_allocator() { @@ -197,7 +276,7 @@ void BindingData::DecreaseAllocatedSize(size_t size) { // Forwards detailed(verbose) debugging information from nghttp3. Enabled using // the NODE_DEBUG_NATIVE=NGHTTP3 category. void nghttp3_debug_log(const char* fmt, va_list args) { - auto isolate = v8::Isolate::GetCurrent(); + auto isolate = Isolate::GetCurrent(); if (isolate == nullptr) return; auto env = Environment::GetCurrent(isolate); if (env->enabled_debug_list()->enabled(DebugCategory::NGHTTP3)) { @@ -219,8 +298,11 @@ void BindingData::RegisterExternalReferences( } BindingData::BindingData(Realm* realm, Local object) - : BaseObject(realm, object) { + : BaseObject(realm, object), + flush_check_(env(), [this]() { OnFlushCheck(); }) { MakeWeak(); + // Unref so the check handle doesn't keep the event loop alive on its own. + flush_check_.Unref(); } SessionManager& BindingData::session_manager() { @@ -230,6 +312,44 @@ SessionManager& BindingData::session_manager() { return *session_manager_; } +void BindingData::ScheduleSessionFlush(const BaseObjectPtr& session) { + pending_flush_sessions_.push_back(session); + if (!flush_check_started_) { + flush_check_.Start(); + flush_check_started_ = true; + } +} + +void BindingData::OnFlushCheck() { + if (pending_flush_sessions_.empty()) { + flush_check_.Stop(); + flush_check_started_ = false; + return; + } + + HandleScope scope(env()->isolate()); + + // Swap to a local vector before iterating. SendPendingData may trigger + // MakeCallback which runs JS that could cause more packet receives via + // re-entry (e.g., a stream data callback that synchronously writes to + // another session). Any sessions added during the flush remain in + // pending_flush_sessions_ and are picked up on the next check tick. + auto sessions = std::move(pending_flush_sessions_); + for (auto& session : sessions) { + session->flags_.pending_flush = false; + if (!session->is_destroyed()) { + session->FlushPendingData(); + } + } + + // If no new sessions were added during the flush, stop the check + // to avoid per-tick callback overhead when idle. + if (pending_flush_sessions_.empty()) { + flush_check_.Stop(); + flush_check_started_ = false; + } +} + void BindingData::MemoryInfo(MemoryTracker* tracker) const { #define V(name, _) tracker->TrackField(#name, name##_callback()); @@ -258,6 +378,26 @@ QUIC_CONSTRUCTORS(V) #undef V +void BindingData::set_transport_params_template( + Local tmpl) { + transport_params_template_.Reset(env()->isolate(), tmpl); +} + +Local BindingData::transport_params_template() const { + return PersistentToLocal::Default(env()->isolate(), + transport_params_template_); +} + +void BindingData::set_application_options_template( + Local tmpl) { + application_options_template_.Reset(env()->isolate(), tmpl); +} + +Local BindingData::application_options_template() const { + return PersistentToLocal::Default(env()->isolate(), + application_options_template_); +} + #define V(name, _) \ void BindingData::set_##name##_callback(Local fn) { \ name##_callback_.Reset(env()->isolate(), fn); \ @@ -295,6 +435,14 @@ QUIC_JS_CALLBACKS(V) #undef V +Local BindingData::error_name_string(const char* name) { + auto& slot = error_name_strings_[name]; + if (slot.IsEmpty()) { + slot.Set(env()->isolate(), OneByteString(env()->isolate(), name)); + } + return slot.Get(env()->isolate()); +} + JS_METHOD_IMPL(BindingData::SetCallbacks) { auto env = Environment::GetCurrent(args); auto isolate = env->isolate(); @@ -318,28 +466,28 @@ JS_METHOD_IMPL(BindingData::SetCallbacks) { } NgTcp2CallbackScope::NgTcp2CallbackScope(Session* session) : session(session) { - CHECK(!session->in_ngtcp2_callback_scope_); - session->in_ngtcp2_callback_scope_ = true; + CHECK(!session->flags_.in_ngtcp2_callback_scope); + session->flags_.in_ngtcp2_callback_scope = true; } NgTcp2CallbackScope::~NgTcp2CallbackScope() { - session->in_ngtcp2_callback_scope_ = false; - if (session->destroy_deferred_) { - session->destroy_deferred_ = false; + session->flags_.in_ngtcp2_callback_scope = false; + if (session->flags_.destroy_deferred) { + session->flags_.destroy_deferred = false; session->Destroy(); } } NgHttp3CallbackScope::NgHttp3CallbackScope(Session* session) : session(session) { - CHECK(!session->in_nghttp3_callback_scope_); - session->in_nghttp3_callback_scope_ = true; + CHECK(!session->flags_.in_nghttp3_callback_scope); + session->flags_.in_nghttp3_callback_scope = true; } NgHttp3CallbackScope::~NgHttp3CallbackScope() { - session->in_nghttp3_callback_scope_ = false; - if (session->destroy_deferred_) { - session->destroy_deferred_ = false; + session->flags_.in_nghttp3_callback_scope = false; + if (session->flags_.destroy_deferred) { + session->flags_.destroy_deferred = false; session->Destroy(); } } diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index cc3c3a49f5647a..2116a35c158ea6 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -10,9 +10,12 @@ #include #include #include +#include #include +#include #include #include +#include #include "defs.h" namespace node::quic { @@ -67,6 +70,7 @@ class SessionManager; V(ack_delay_exponent, "ackDelayExponent") \ V(active_connection_id_limit, "activeConnectionIDLimit") \ V(address_lru_size, "addressLRUSize") \ + V(allow, "allow") \ V(application, "application") \ V(authoritative, "authoritative") \ V(bbr, "bbr") \ @@ -78,6 +82,7 @@ class SessionManager; V(crl, "crl") \ V(cubic, "cubic") \ V(datagram_drop_policy, "datagramDropPolicy") \ + V(deny, "deny") \ V(disable_stateless_reset, "disableStatelessReset") \ V(draining_period_multiplier, "drainingPeriodMultiplier") \ V(enable_connect_protocol, "enableConnectProtocol") \ @@ -90,6 +95,7 @@ class SessionManager; V(groups, "groups") \ V(handshake_timeout, "handshakeTimeout") \ V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(initial_rtt, "initialRtt") \ V(keep_alive_timeout, "keepAlive") \ V(initial_max_data, "initialMaxData") \ V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ @@ -98,6 +104,7 @@ class SessionManager; V(initial_max_streams_bidi, "initialMaxStreamsBidi") \ V(initial_max_streams_uni, "initialMaxStreamsUni") \ V(ipv6_only, "ipv6Only") \ + V(reuse_port, "reusePort") \ V(keylog, "keylog") \ V(keys, "keys") \ V(lost, "lost") \ @@ -106,14 +113,25 @@ class SessionManager; V(max_connections_total, "maxConnectionsTotal") \ V(max_datagram_frame_size, "maxDatagramFrameSize") \ V(max_datagram_send_attempts, "maxDatagramSendAttempts") \ + V(stream_idle_timeout, "streamIdleTimeout") \ V(max_field_section_size, "maxFieldSectionSize") \ V(max_header_length, "maxHeaderLength") \ V(max_header_pairs, "maxHeaderPairs") \ V(idle_timeout, "idleTimeout") \ V(max_idle_timeout, "maxIdleTimeout") \ V(max_payload_size, "maxPayloadSize") \ - V(max_retries, "maxRetries") \ - V(max_stateless_resets, "maxStatelessResetsPerHost") \ + V(retry_rate, "retryRate") \ + V(retry_burst, "retryBurst") \ + V(stateless_reset_rate, "statelessResetRate") \ + V(stateless_reset_burst, "statelessResetBurst") \ + V(version_negotiation_rate, "versionNegotiationRate") \ + V(version_negotiation_burst, "versionNegotiationBurst") \ + V(immediate_close_rate, "immediateCloseRate") \ + V(immediate_close_burst, "immediateCloseBurst") \ + V(session_creation_rate, "sessionCreationRate") \ + V(session_creation_burst, "sessionCreationBurst") \ + V(block_list, "blockList") \ + V(block_list_policy, "blockListPolicy") \ V(max_stream_window, "maxStreamWindow") \ V(max_window, "maxWindow") \ V(min_version, "minVersion") \ @@ -151,9 +169,86 @@ class SessionManager; V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \ V(validate_address, "validateAddress") \ V(verify_client, "verifyClient") \ + V(verify_hostname, "verifyHostname") \ + V(verify_peer_strict, "verifyPeerStrict") \ V(verify_private_key, "verifyPrivateKey") \ V(version, "version") +// ============================================================================= +// Lightweight wrappers around uv_check_t that ensure safe handle closure. +// The check handle is embedded in a heap-allocated CheckWrap whose destruction +// is deferred until the uv_close callback fires, preventing use-after-free +// when the owning object is destroyed before libuv finishes closing the handle. +// Follows the same two-layer pattern as TimerWrap / TimerWrapHandle +// (see timer_wrap.h). +// TODO(@jasnell): Consider moving it out to a separate file like timer_wrap.h. +class CheckWrap final : public MemoryRetainer { + public: + using CheckCb = std::function; + + template + explicit CheckWrap(Environment* env, Args&&... args) + : env_(env), fn_(std::forward(args)...) { + uv_check_init(env->event_loop(), &check_); + check_.data = this; + } + + DISALLOW_COPY_AND_MOVE(CheckWrap) + + inline Environment* env() const { return env_; } + + void Start(); + void Stop(); + void Close(); + void Ref(); + void Unref(); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(CheckWrap) + SET_SELF_SIZE(CheckWrap) + + private: + static void OnCheck(uv_check_t* check); + static void CheckClosedCb(uv_handle_t* handle); + ~CheckWrap() = default; + + Environment* env_; + CheckCb fn_; + uv_check_t check_; + + friend std::unique_ptr::deleter_type; +}; + +class CheckWrapHandle : public MemoryRetainer { + public: + template + explicit CheckWrapHandle(Environment* env, Args&&... args) + : check_(new CheckWrap(env, std::forward(args)...)) { + env->AddCleanupHook(CleanupHook, this); + } + + DISALLOW_COPY_AND_MOVE(CheckWrapHandle) + + ~CheckWrapHandle() { Close(); } + + inline operator bool() const { return check_ != nullptr; } + + void Start(); + void Stop(); + void Close(); + void Ref(); + void Unref(); + + void MemoryInfo(node::MemoryTracker* tracker) const override; + + SET_MEMORY_INFO_NAME(CheckWrapHandle) + SET_SELF_SIZE(CheckWrapHandle) + + private: + static void CleanupHook(void* data); + CheckWrap* check_; +}; + // ============================================================================= // The BindingState object holds state for the internalBinding('quic') binding // instance. It is mostly used to hold the persistent constructors, strings, and @@ -201,8 +296,17 @@ class BindingData final // routing so that any endpoint can route packets to any session. SessionManager& session_manager(); + // Schedule a session for deferred SendPendingData. Sessions are accumulated + // during the I/O poll phase (via Endpoint::Receive -> Session::ReadPacket) + // and flushed in a uv_check callback immediately after poll completes. + // This batches multiple received packets before generating responses, + // allowing ngtcp2 to make better ACK coalescing decisions. + void ScheduleSessionFlush(const BaseObjectPtr& session); + std::unordered_map> listening_endpoints; + v8::Local error_name_string(const char* name); + size_t current_ngtcp2_memory_ = 0; // The following set up various storage and accessors for common strings, @@ -216,6 +320,12 @@ class BindingData final QUIC_CONSTRUCTORS(V) #undef V + void set_transport_params_template(v8::Local tmpl); + v8::Local transport_params_template() const; + + void set_application_options_template(v8::Local tmpl); + v8::Local application_options_template() const; + #define V(name, _) \ void set_##name##_callback(v8::Local fn); \ v8::Local name##_callback() const; @@ -234,6 +344,9 @@ class BindingData final QUIC_CONSTRUCTORS(V) #undef V + v8::Global transport_params_template_; + v8::Global application_options_template_; + #define V(name, _) v8::Global name##_callback_; QUIC_JS_CALLBACKS(V) #undef V @@ -246,7 +359,33 @@ class BindingData final QUIC_JS_CALLBACKS(V) #undef V + // Lazy cache backing error_name_string() + std::unordered_map> error_name_strings_; + std::unique_ptr session_manager_; + + // Type-erased arena storage. The concrete AliasedStructArena types + // are only complete in the .cc files where Stream::State etc. are defined. + // Each .cc file provides typed accessor methods. The deleters are set + // when the arenas are created so that ~BindingData destroys them correctly. + using ArenaDeleter = void (*)(void*); + using ArenaPtr = std::unique_ptr; + ArenaPtr stream_state_arena_{nullptr, +[](void*) {}}; + ArenaPtr stream_stats_arena_{nullptr, +[](void*) {}}; + ArenaPtr session_state_arena_{nullptr, +[](void*) {}}; + ArenaPtr session_stats_arena_{nullptr, +[](void*) {}}; + ArenaPtr endpoint_state_arena_{nullptr, +[](void*) {}}; + ArenaPtr endpoint_stats_arena_{nullptr, +[](void*) {}}; + + // Deferred send flush state. The CheckWrapHandle fires immediately after + // the I/O poll phase in the same event loop tick, allowing batched + // receive processing: all packets are read during poll, then + // SendPendingData is called once per dirty session in the check callback. + CheckWrapHandle flush_check_; + std::vector> pending_flush_sessions_; + bool flush_check_started_ = false; + + void OnFlushCheck(); }; JS_METHOD_IMPL(IllegalConstructor); diff --git a/src/quic/data.cc b/src/quic/data.cc index be2bf458d28352..9599adec62f805 100644 --- a/src/quic/data.cc +++ b/src/quic/data.cc @@ -1,13 +1,15 @@ #if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC -#include "data.h" #include #include #include #include +#include #include #include +#include "bindingdata.h" +#include "data.h" #include "defs.h" #include "util.h" @@ -17,7 +19,10 @@ using v8::Array; using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::BackingStore; +using v8::BackingStoreInitializationMode; +using v8::BackingStoreOnFailureMode; using v8::BigInt; +using v8::Isolate; using v8::Just; using v8::Local; using v8::Maybe; @@ -89,14 +94,14 @@ Store::Store(std::unique_ptr store, size_t length, size_t offset) } Maybe Store::From(Local buffer) { - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + Isolate* isolate = Isolate::GetCurrent(); Environment* env = Environment::GetCurrent(isolate->GetCurrentContext()); auto length = buffer->ByteLength(); auto dest = ArrayBuffer::NewBackingStore( isolate, length, - v8::BackingStoreInitializationMode::kUninitialized, - v8::BackingStoreOnFailureMode::kReturnNull); + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); if (!dest) { THROW_ERR_MEMORY_ALLOCATION_FAILED(env); return Nothing(); @@ -108,15 +113,15 @@ Maybe Store::From(Local buffer) { } Maybe Store::From(Local view) { - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + Isolate* isolate = Isolate::GetCurrent(); Environment* env = Environment::GetCurrent(isolate->GetCurrentContext()); auto length = view->ByteLength(); auto offset = view->ByteOffset(); auto dest = ArrayBuffer::NewBackingStore( isolate, length, - v8::BackingStoreInitializationMode::kUninitialized, - v8::BackingStoreOnFailureMode::kReturnNull); + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); if (!dest) { THROW_ERR_MEMORY_ALLOCATION_FAILED(env); return Nothing(); @@ -130,24 +135,38 @@ Maybe Store::From(Local view) { } Store Store::CopyFrom(Local buffer) { - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + Isolate* isolate = Isolate::GetCurrent(); auto backing = buffer->GetBackingStore(); auto length = buffer->ByteLength(); auto dest = ArrayBuffer::NewBackingStore( - isolate, length, v8::BackingStoreInitializationMode::kUninitialized); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!dest) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(Environment::GetCurrent(isolate)); + return Store(); + } // copy content memcpy(dest->Data(), backing->Data(), length); return Store(std::move(dest), length, 0); } Store Store::CopyFrom(Local view) { - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + Isolate* isolate = Isolate::GetCurrent(); auto backing = view->Buffer()->GetBackingStore(); auto length = view->ByteLength(); auto offset = view->ByteOffset(); auto dest = ArrayBuffer::NewBackingStore( - isolate, length, v8::BackingStoreInitializationMode::kUninitialized); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); // copy content + if (!dest) { + THROW_ERR_MEMORY_ALLOCATION_FAILED(Environment::GetCurrent(isolate)); + return Store(); + } memcpy(dest->Data(), static_cast(backing->Data()) + offset, length); return Store(std::move(dest), length, 0); } @@ -346,14 +365,70 @@ std::optional QuicError::get_crypto_error() const { return code() & ~NGTCP2_CRYPTO_ERROR; } +const char* QuicError::name() const { + // CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8). + // OpenSSL's SSL_alert_desc_string_long owns a stable string for every + // alert it knows about; we filter out the "unknown" placeholder so the + // JS side can present `errorName` as undefined for unrecognised alerts. + if (auto alert = get_crypto_error()) { + const char* n = SSL_alert_desc_string_long(*alert); + if (n != nullptr && std::string_view(n) != "unknown") return n; + return nullptr; + } + // Named transport-layer error codes from RFC 9000 sec. 20.1 (and the + // RFC 9368 version-negotiation extension). Application error codes are + // opaque to QUIC, so we only decode for transport. + if (type() != Type::TRANSPORT) return nullptr; + switch (code()) { + case NGTCP2_NO_ERROR: + return "NO_ERROR"; + case NGTCP2_INTERNAL_ERROR: + return "INTERNAL_ERROR"; + case NGTCP2_CONNECTION_REFUSED: + return "CONNECTION_REFUSED"; + case NGTCP2_FLOW_CONTROL_ERROR: + return "FLOW_CONTROL_ERROR"; + case NGTCP2_STREAM_LIMIT_ERROR: + return "STREAM_LIMIT_ERROR"; + case NGTCP2_STREAM_STATE_ERROR: + return "STREAM_STATE_ERROR"; + case NGTCP2_FINAL_SIZE_ERROR: + return "FINAL_SIZE_ERROR"; + case NGTCP2_FRAME_ENCODING_ERROR: + return "FRAME_ENCODING_ERROR"; + case NGTCP2_TRANSPORT_PARAMETER_ERROR: + return "TRANSPORT_PARAMETER_ERROR"; + case NGTCP2_CONNECTION_ID_LIMIT_ERROR: + return "CONNECTION_ID_LIMIT_ERROR"; + case NGTCP2_PROTOCOL_VIOLATION: + return "PROTOCOL_VIOLATION"; + case NGTCP2_INVALID_TOKEN: + return "INVALID_TOKEN"; + case NGTCP2_APPLICATION_ERROR: + return "APPLICATION_ERROR"; + case NGTCP2_CRYPTO_BUFFER_EXCEEDED: + return "CRYPTO_BUFFER_EXCEEDED"; + case NGTCP2_KEY_UPDATE_ERROR: + return "KEY_UPDATE_ERROR"; + case NGTCP2_AEAD_LIMIT_REACHED: + return "AEAD_LIMIT_REACHED"; + case NGTCP2_NO_VIABLE_PATH: + return "NO_VIABLE_PATH"; + case NGTCP2_VERSION_NEGOTIATION_ERROR: + return "VERSION_NEGOTIATION_ERROR"; + default: + return nullptr; + } +} + MaybeLocal QuicError::ToV8Value(Environment* env) const { if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) || - (type() == Type::APPLICATION && code() == NGHTTP3_H3_NO_ERROR) || + (type() == Type::APPLICATION && + (code() == 0 || code() == NGHTTP3_H3_NO_ERROR)) || type() == Type::IDLE_CLOSE) { - // Note that we only return undefined for *known* no-error application - // codes. It is possible that other application types use other specific - // no-error codes, but since we don't know which application is being used, - // we'll just return the error code value for those below. + // Application code 0 is the default no-error code for raw QUIC + // applications (DefaultApplication::GetNoErrorCode() returns 0). + // NGHTTP3_H3_NO_ERROR (0x100) is the HTTP/3 no-error code. // Idle close is always clean — the session timed out normally. return Undefined(env->isolate()); } @@ -367,6 +442,7 @@ MaybeLocal QuicError::ToV8Value(Environment* env) const { type_str, BigInt::NewFromUnsigned(env->isolate(), code()), Undefined(env->isolate()), + Undefined(env->isolate()), }; // Note that per the QUIC specification, the reason, if present, is @@ -380,6 +456,13 @@ MaybeLocal QuicError::ToV8Value(Environment* env) const { return {}; } + // Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1 + // names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown + // codes leave the slot as undefined. + if (const char* n = name()) { + argv[3] = BindingData::Get(env).error_name_string(n); + } + return Array::New(env->isolate(), argv, arraysize(argv)).As(); } diff --git a/src/quic/data.h b/src/quic/data.h index 2b6d777caf7b81..9c92a30c1ddf4c 100644 --- a/src/quic/data.h +++ b/src/quic/data.h @@ -19,6 +19,40 @@ namespace node::quic { template concept OneByteType = sizeof(T) == 1; +// Lightweight wrapper around ngtcp2_pkt_info. Insulates the Node.js QUIC +// code from the ngtcp2 struct layout and provides a clean API boundary +// for per-packet metadata (currently ECN codepoint; may grow as ngtcp2 +// and libuv evolve). +// +// Default-constructed PacketInfo is zero-initialized, which ngtcp2 treats +// as ECN Not-ECT — identical to passing nullptr for the pkt_info parameter. +class PacketInfo final { + public: + // ECN codepoints as defined by RFC 3168. + enum class Ecn : uint32_t { + NOT_ECT = 0, // Not ECN-Capable Transport + ECT_1 = 1, // ECN-Capable Transport(1) + ECT_0 = 2, // ECN-Capable Transport(0) + CE = 3, // Congestion Experienced + }; + + PacketInfo() : info_{} {} + explicit PacketInfo(const ngtcp2_pkt_info& info) : info_(info) {} + + // ECN codepoint for this packet. When libuv gains per-packet ECN + // reporting, populate via set_ecn() from the receive metadata + // before passing to ReadPacket(). + Ecn ecn() const { return static_cast(info_.ecn); } + void set_ecn(Ecn ecn) { info_.ecn = static_cast(ecn); } + + // Conversion operators for ngtcp2 API calls. + operator const ngtcp2_pkt_info*() const { return &info_; } + operator ngtcp2_pkt_info*() { return &info_; } + + private: + ngtcp2_pkt_info info_; +}; + struct Path final : public ngtcp2_path { explicit Path(const SocketAddress& local, const SocketAddress& remote); Path(Path&& other) noexcept = default; @@ -231,6 +265,9 @@ class QuicError final : public MemoryRetainer { bool is_crypto_error() const; std::optional get_crypto_error() const; + // Returns a human-readable name for this error if known, or nullptr + const char* name() const; + // Note that since application errors are application-specific and we // don't know which application is being used here, it is possible that // the comparing two different QuicError instances from different applications diff --git a/src/quic/defs.h b/src/quic/defs.h index 6b18c19f4c3c6d..75ae915335be93 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -360,6 +360,33 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS; constexpr size_t kMaxVectorCount = 16; constexpr stream_id kMaxStreamId = std::numeric_limits::max(); +// A token bucket rate limiter using lazy refill. No timer needed — tokens +// are computed on demand from the elapsed time since the last check. +// Used to cap the total rate of stateless responses (retry, reset, +// version negotiation, immediate close) regardless of source address, +// preventing spoofed-source floods from bypassing per-host limits. +struct TokenBucket final { + double rate; // tokens per second (refill rate) + double burst; // maximum tokens (bucket capacity) + double tokens; // current token count + uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime) + + TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {} + TokenBucket(double rate, double burst); + + // Reinitialize the bucket with new rate/burst parameters if it + // hasn't been initialized yet (last_ts == 0). Used for per-host + // buckets in the address LRU where the rate/burst aren't known + // at construction time. + void InitOnce(double r, double b, uint64_t now); + + // Try to consume one token. Refills based on elapsed time, then + // attempts to consume. Returns true if the request is allowed. + // The caller provides the current timestamp to avoid redundant + // uv_hrtime() calls in hot paths. + bool consume(uint64_t now); +}; + class DebugIndentScope final { public: inline DebugIndentScope() { ++indent_; } diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 66413f66cafee2..5a728a0a2a147e 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -29,7 +29,6 @@ namespace node { using v8::Array; using v8::ArrayBufferView; -using v8::BackingStore; using v8::HandleScope; using v8::Integer; using v8::Just; @@ -74,9 +73,15 @@ namespace quic { V(CLIENT_SESSIONS, client_sessions) \ V(SERVER_BUSY_COUNT, server_busy_count) \ V(RETRY_COUNT, retry_count) \ + V(RETRY_RATE_LIMITED, retry_rate_limited) \ V(VERSION_NEGOTIATION_COUNT, version_negotiation_count) \ + V(VERSION_NEGOTIATION_RATE_LIMITED, version_negotiation_rate_limited) \ V(STATELESS_RESET_COUNT, stateless_reset_count) \ - V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) + V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \ + V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \ + V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \ + V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited) \ + V(PACKETS_BLOCKED, packets_blocked) struct Endpoint::State { #define V(_, name, type) type name; @@ -86,6 +91,31 @@ struct Endpoint::State { STAT_STRUCT(Endpoint, ENDPOINT) +TokenBucket::TokenBucket(double rate, double burst) + : rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {} + +void TokenBucket::InitOnce(double r, double b, uint64_t now) { + if (last_ts == 0) { + rate = r; + burst = b; + tokens = b; + last_ts = now; + } +} + +// Try to consume one token. Refills based on elapsed time, then +// attempts to consume. Returns true if the request is allowed. +bool TokenBucket::consume(uint64_t now) { + double elapsed = static_cast(now - last_ts) / 1e9; // seconds + last_ts = now; + tokens = std::min(burst, tokens + elapsed * rate); + if (tokens >= 1.0) { + tokens -= 1.0; + return true; + } + return false; +} + // ============================================================================ // Endpoint::Options namespace { @@ -98,6 +128,7 @@ bool is_diagnostic_packet_loss(double probability) { CHECK(ncrypto::CSPRNG(&c, 1)); return (static_cast(c) / 255) < probability; } +#endif // DEBUG template bool SetOption(Environment* env, @@ -107,18 +138,24 @@ bool SetOption(Environment* env, Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - Local num; - if (!value->ToNumber(env->context()).ToLocal(&num)) { + if (!value->IsNumber()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be a number", nameStr); return false; } - options->*member = num->Value(); + Local num = value.As(); + double dbl = num->Value(); + if (dbl < 0) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be a non-negative number", nameStr); + return false; + } + options->*member = dbl; } return true; } -#endif // DEBUG template bool SetOption(Environment* env, @@ -134,15 +171,15 @@ bool SetOption(Environment* env, env, "The %s option must be an uint8", nameStr); return false; } - Local num; - if (!value->ToUint32(env->context()).ToLocal(&num) || - num->Value() > std::numeric_limits::max()) { + Local num = value.As(); + uint32_t val = num->Value(); + if (val > std::numeric_limits::max()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an uint8", nameStr); return false; } - options->*member = num->Value(); + options->*member = val; } return true; } @@ -196,9 +233,13 @@ Maybe Endpoint::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(retry_token_expiration) || !SET(token_expiration) || - !SET(max_stateless_resets) || !SET(address_lru_size) || - !SET(max_retries) || !SET(validate_address) || - !SET(disable_stateless_reset) || !SET(ipv6_only) || + !SET(address_lru_size) || !SET(validate_address) || + !SET(disable_stateless_reset) || !SET(ipv6_only) || !SET(reuse_port) || + !SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) || + !SET(stateless_reset_burst) || !SET(version_negotiation_rate) || + !SET(version_negotiation_burst) || !SET(immediate_close_rate) || + !SET(immediate_close_burst) || !SET(session_creation_rate) || + !SET(session_creation_burst) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif @@ -228,6 +269,42 @@ Maybe Endpoint::Options::From(Environment* env, } } + // Parse block list option. Expects the C++ SocketAddressBlockListWrap handle + // (the JS side extracts [kHandle] before passing it through). + Local block_list_val; + if (!params->Get(env->context(), state.block_list_string()) + .ToLocal(&block_list_val)) { + return Nothing(); + } + if (!block_list_val->IsUndefined()) { + if (!SocketAddressBlockListWrap::HasInstance(env, block_list_val)) { + THROW_ERR_INVALID_ARG_TYPE( + env, "The blockList option must be a BlockList handle"); + return Nothing(); + } + auto* wrap = + FromJSObject(block_list_val.As()); + options.block_list = wrap->blocklist(); + } + + // Parse block list policy. + Local policy_val; + if (!params->Get(env->context(), state.block_list_policy_string()) + .ToLocal(&policy_val)) { + return Nothing(); + } + if (!policy_val->IsUndefined()) { + if (policy_val->StrictEquals(state.allow_string())) { + options.block_list_policy = Options::BlockListPolicy::ALLOW; + } else if (policy_val->StrictEquals(state.deny_string())) { + options.block_list_policy = Options::BlockListPolicy::DENY; + } else { + THROW_ERR_INVALID_ARG_VALUE( + env, "The blockListPolicy option must be 'deny' or 'allow'"); + return Nothing(); + } + } + return Just(options); #undef SET @@ -252,10 +329,26 @@ std::string Endpoint::Options::ToString() const { " seconds"; res += prefix + "token expiration: " + std::to_string(token_expiration) + " seconds"; - res += - prefix + "max stateless resets: " + std::to_string(max_stateless_resets); res += prefix + "address lru size: " + std::to_string(address_lru_size); - res += prefix + "max retries: " + std::to_string(max_retries); + res += prefix + "retry rate: " + std::to_string(retry_rate) + "/s"; + res += prefix + "retry burst: " + std::to_string(retry_burst); + res += prefix + + "stateless reset rate: " + std::to_string(stateless_reset_rate) + "/s"; + res += prefix + + "stateless reset burst: " + std::to_string(stateless_reset_burst); + res += prefix + "version negotiation rate: " + + std::to_string(version_negotiation_rate) + "/s"; + res += prefix + "version negotiation burst: " + + std::to_string(version_negotiation_burst); + res += prefix + + "immediate close rate: " + std::to_string(immediate_close_rate) + "/s"; + res += prefix + + "immediate close burst: " + std::to_string(immediate_close_burst); + res += prefix + + "session creation rate: " + std::to_string(session_creation_rate) + + "/s"; + res += prefix + + "session creation burst: " + std::to_string(session_creation_burst); res += prefix + "validate address: " + boolToString(validate_address); res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); @@ -266,6 +359,7 @@ std::string Endpoint::Options::ToString() const { res += prefix + "reset token secret: " + reset_token_secret.ToString(); res += prefix + "token secret: " + token_secret.ToString(); res += prefix + "ipv6 only: " + boolToString(ipv6_only); + res += prefix + "reuse port: " + boolToString(reuse_port); res += prefix + "udp receive buffer size: " + std::to_string(udp_receive_buffer_size); res += @@ -303,7 +397,10 @@ class Endpoint::UDP::Impl final : public HandleWrap { reinterpret_cast(&handle_), PROVIDER_QUIC_UDP), endpoint_(endpoint) { - CHECK_EQ(uv_udp_init(endpoint->env()->event_loop(), &handle_), 0); + CHECK_EQ(uv_udp_init_ex(endpoint->env()->event_loop(), + &handle_, + AF_UNSPEC | UV_UDP_RECVMMSG), + 0); handle_.data = this; } @@ -312,10 +409,26 @@ class Endpoint::UDP::Impl final : public HandleWrap { SET_SELF_SIZE(Impl) private: + // Pre-allocated receive buffer sized for recvmmsg batching. libuv's + // recvmmsg path partitions the alloc buffer into 64KB chunks (one per + // datagram). With kRecvBatchSize chunks we can receive up to that many + // packets in a single recvmmsg syscall. ngtcp2_conn_read_pkt is + // synchronous — it copies what it needs — so the buffer is safely + // reused across batches. + // libuv's recvmmsg partitions the buffer into UV__UDP_DGRAM_MAXSIZE (64KB) + // chunks regardless of actual packet size. QUIC packets are ~1200 bytes, + // so most of each 64KB chunk is wasted. We use a modest batch size to + // balance syscall reduction against memory usage. + static constexpr size_t kDgramMaxSize = 65536; // UV__UDP_DGRAM_MAXSIZE + static constexpr size_t kRecvBatchSize = 5; + static constexpr size_t kRecvBufferSize = kDgramMaxSize * kRecvBatchSize; + std::array recv_buf_ = {}; + static void OnAlloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { - *buf = From(handle)->env()->allocate_managed_buffer(suggested_size); + auto* impl = From(handle); + *buf = uv_buf_init(impl->recv_buf_.data(), kRecvBufferSize); } static void OnReceive(uv_udp_t* handle, @@ -327,26 +440,32 @@ class Endpoint::UDP::Impl final : public HandleWrap { DCHECK_NOT_NULL(impl); DCHECK_NOT_NULL(impl->endpoint_); - auto release_buf = [&]() { - if (buf->base != nullptr) impl->env()->release_managed_buffer(*buf); - }; + // UV_UDP_MMSG_FREE signals the end of a recvmmsg batch — the + // buffer can be reused. Since our buffer is pre-allocated and + // persistent, there is nothing to free. + if (flags & UV_UDP_MMSG_FREE) { + return; + } // Nothing to do in these cases. Specifically, if the nread // is zero or we have received a partial packet, we are just - // going to ignore it. + // going to ignore it. No buffer release needed — recv_buf_ + // is pre-allocated and reused. if (nread == 0 || flags & UV_UDP_PARTIAL) { - release_buf(); return; } if (nread < 0) { - release_buf(); impl->endpoint_->Destroy(CloseContext::RECEIVE_FAILURE, static_cast(nread)); return; } - impl->endpoint_->Receive(uv_buf_init(buf->base, static_cast(nread)), + // UV_UDP_MMSG_CHUNK is set for each packet in a recvmmsg batch. + // Processing is the same as for a single-message receive — ngtcp2 + // copies what it needs synchronously from the buf slice. + impl->endpoint_->Receive(reinterpret_cast(buf->base), + static_cast(nread), SocketAddress(addr)); } @@ -375,17 +494,18 @@ Endpoint::UDP::~UDP() { } int Endpoint::UDP::Bind(const Options& options) { - if (is_bound_) return UV_EALREADY; + if (flags_.is_bound) return UV_EALREADY; if (is_closed_or_closing()) return UV_EBADF; int flags = 0; if (options.local_address->family() == AF_INET6 && options.ipv6_only) flags |= UV_UDP_IPV6ONLY; + if (options.reuse_port) flags |= UV_UDP_REUSEPORT; int err = uv_udp_bind(&impl_->handle_, options.local_address->data(), flags); int size; if (!err) { - is_bound_ = true; + flags_.is_bound = true; size = static_cast(options.udp_receive_buffer_size); if (size > 0) { err = uv_recv_buffer_size(reinterpret_cast(&impl_->handle_), @@ -424,34 +544,34 @@ void Endpoint::UDP::Unref() { int Endpoint::UDP::Start() { if (is_closed_or_closing()) return UV_EBADF; - if (is_started_) return 0; + if (flags_.is_started) return 0; int err = uv_udp_recv_start(&impl_->handle_, Impl::OnAlloc, Impl::OnReceive); - is_started_ = (err == 0); + flags_.is_started = (err == 0); return err; } void Endpoint::UDP::Stop() { - if (is_closed_or_closing() || !is_started_) return; + if (is_closed_or_closing() || !flags_.is_started) return; USE(uv_udp_recv_stop(&impl_->handle_)); - is_started_ = false; + flags_.is_started = false; } void Endpoint::UDP::Close() { if (is_closed_or_closing()) return; DCHECK(impl_); Stop(); - is_bound_ = false; - is_closed_ = true; + flags_.is_bound = false; + flags_.is_closed = true; impl_->Close(); impl_.reset(); } bool Endpoint::UDP::is_bound() const { - return is_bound_; + return flags_.is_bound; } bool Endpoint::UDP::is_closed() const { - return is_closed_; + return flags_.is_closed; } bool Endpoint::UDP::is_closed_or_closing() const { @@ -492,6 +612,24 @@ int Endpoint::UDP::Send(Packet::Ptr packet) { return err; } +int Endpoint::UDP::TrySend(const Packet::Ptr& packet) { + DCHECK(packet); + if (is_closed_or_closing()) return UV_EBADF; + uv_buf_t buf = *packet; + return uv_udp_try_send( + &impl_->handle_, &buf, 1, packet->destination().data()); +} + +int Endpoint::UDP::TrySendBatch(uv_buf_t* bufs[], + unsigned int nbufs[], + struct sockaddr* addrs[], + size_t count) { + DCHECK_GT(count, 0); + if (is_closed_or_closing()) return UV_EBADF; + return uv_udp_try_send2( + &impl_->handle_, static_cast(count), bufs, nbufs, addrs, 0); +} + void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const { if (impl_) tracker->TrackField("impl", impl_); } @@ -536,8 +674,6 @@ void Endpoint::InitPerContext(Realm* realm, Local target) { #undef V NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT); static constexpr auto DEFAULT_RETRYTOKEN_EXPIRATION = RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; @@ -599,7 +735,14 @@ Endpoint::Endpoint(Environment* env, HandleScope scope(this->env()->isolate()); Destroy(); }), - addr_validation_lru_(options_.address_lru_size) { + addr_validation_lru_(options_.address_lru_size), + retry_bucket_(options_.retry_rate, options_.retry_burst), + stateless_reset_bucket_(options_.stateless_reset_rate, + options_.stateless_reset_burst), + version_negotiation_bucket_(options_.version_negotiation_rate, + options_.version_negotiation_burst), + immediate_close_bucket_(options_.immediate_close_rate, + options_.immediate_close_burst) { MakeWeak(); udp_.Unref(); idle_timer_.Unref(); @@ -714,7 +857,9 @@ void Endpoint::RemoveSession(const CID& cid, } } if (primary_session_count_ > 0 && --primary_session_count_ == 0) { - udp_.Unref(); + if (!is_listening()) { + udp_.Unref(); + } session_manager().RemoveSession(cid); // The endpoint may be idle (no sessions, not listening). MaybeDestroy // handles both closing (immediate destroy) and idle timeout (start @@ -812,54 +957,146 @@ void Endpoint::Send(Packet::Ptr packet) { STAT_INCREMENT(Stats, packets_sent); } -void Endpoint::SendRetry(const PathDescriptor& options) { - // Generating and sending retry packets does consume some system resources, - // and it is possible for a malicious peer to trigger sending a large number - // of retry packets, resulting in a potential DOS vector. To help ward that - // off, we track how many retry packets we send to a particular host and - // enforce limits. Note that since we are using an LRU cache these limits - // aren't strict. If a retry is sent, we increment the retry_count statistic - // to give application code a means of detecting and responding to abuse on - // its own. What this count does not give is the rate of retry, so it is still - // somewhat limited. - Debug(this, "Sending retry on path %s", options); - auto info = addr_validation_lru_.Upsert(options.remote_address); - if (++(info->retry_count) <= options_.max_retries) { - auto packet = - Packet::CreateRetryPacket(*this, options, options_.token_secret); - if (packet) { - STAT_INCREMENT(Stats, retry_count); - Send(std::move(packet)); +void Endpoint::SendOrTrySend(Packet::Ptr packet) { +#ifdef DEBUG + if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { + return; + } +#endif + + if (is_closed() || is_closing() || !packet || packet->length() == 0) { + return; + } + + Debug(this, "TrySend %s", packet->ToString()); + + // Attempt synchronous send. On success (returns number of bytes sent), + // the packet is delivered immediately — no callback overhead, no + // waiting for the next poll cycle. + int err = udp_.TrySend(packet); + if (err >= 0) { + // Synchronous send succeeded. + STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); + STAT_INCREMENT(Stats, packets_sent); + // Ptr destructor releases back to arena pool. + return; + } + + if (err == UV_EAGAIN) { + // Socket not writable or async sends are queued. Fall back to the + // async path — the packet will be queued and flushed on the next + // POLLOUT cycle. + Debug(this, "TrySend got EAGAIN, falling back to async Send"); + return Send(std::move(packet)); + } + + // Other errors are fatal. + Debug(this, "TrySend failed with error %d", err); + Destroy(CloseContext::SEND_FAILURE, err); +} + +void Endpoint::SendBatch(Packet::Ptr* packets, size_t count) { + if (count == 0) return; + +#ifdef DEBUG + if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { + for (size_t i = 0; i < count; i++) packets[i].reset(); + return; + } +#endif + + if (is_closed() || is_closing()) { + for (size_t i = 0; i < count; i++) packets[i].reset(); + return; + } + + static constexpr size_t kMaxBatch = 64; + DCHECK_LE(count, kMaxBatch); + + // Build libuv argument arrays directly from the Ptr array. + // Packets with zero length are released and skipped. + uv_buf_t bufs[kMaxBatch]; + uv_buf_t* buf_ptrs[kMaxBatch]; + unsigned int nbufs[kMaxBatch]; + struct sockaddr* addrs[kMaxBatch]; + // Map from valid-index back to the original packets[] index. + size_t index_map[kMaxBatch]; + size_t valid_count = 0; + + for (size_t i = 0; i < count; i++) { + if (!packets[i] || packets[i]->length() == 0) { + packets[i].reset(); + continue; + } + bufs[valid_count] = *packets[i]; + buf_ptrs[valid_count] = &bufs[valid_count]; + nbufs[valid_count] = 1; + addrs[valid_count] = + const_cast(packets[i]->destination().data()); + index_map[valid_count] = i; + valid_count++; + } + + if (valid_count == 0) return; + + // Attempt synchronous batched send via sendmmsg. + int sent = udp_.TrySendBatch(buf_ptrs, nbufs, addrs, valid_count); + + if (sent > 0) { + // Packets [0, sent) were delivered synchronously. + // Release them immediately — no async callback needed. + for (size_t i = 0; i < static_cast(sent); i++) { + size_t idx = index_map[i]; + STAT_INCREMENT_N(Stats, bytes_sent, packets[idx]->length()); + STAT_INCREMENT(Stats, packets_sent); + packets[idx].reset(); } + } + + // Any unsent packets (EAGAIN, partial send, or total failure) fall + // back to async uv_udp_send. + size_t start = (sent > 0) ? static_cast(sent) : 0; + for (size_t i = start; i < valid_count; i++) { + size_t idx = index_map[i]; + Send(std::move(packets[idx])); + } +} + +void Endpoint::SendRetry(const PathDescriptor& options, uint64_t now) { + Debug(this, "Sending retry on path %s", options); + if (!retry_bucket_.consume(now)) { + Debug(this, "Retry rate limit exceeded (global)"); + STAT_INCREMENT(Stats, retry_rate_limited); + return; + } - // If creating the retry is unsuccessful, we just drop things on the floor. - // It's not worth committing any further resources to this one packet. We - // might want to log the failure at some point tho. + auto packet = + Packet::CreateRetryPacket(*this, options, options_.token_secret); + if (packet) { + STAT_INCREMENT(Stats, retry_count); + Send(std::move(packet)); } } -void Endpoint::SendVersionNegotiation(const PathDescriptor& options) { +void Endpoint::SendVersionNegotiation(const PathDescriptor& options, + uint64_t now) { Debug(this, "Sending version negotiation on path %s", options); - // While creating and sending a version negotiation packet does consume a - // small amount of system resources, and while it is fairly trivial for a - // malicious peer to force a version negotiation to be sent, these are more - // trivial to create than the cryptographically generated retry and stateless - // reset packets. If the packet is sent, then we'll at least increment the - // version_negotiation_count statistic so that application code can keep an - // eye on it. + if (!version_negotiation_bucket_.consume(now)) { + Debug(this, "Version negotiation rate limit exceeded (global)"); + STAT_INCREMENT(Stats, version_negotiation_rate_limited); + return; + } + auto packet = Packet::CreateVersionNegotiationPacket(*this, options); if (packet) { STAT_INCREMENT(Stats, version_negotiation_count); Send(std::move(packet)); } - - // If creating the packet is unsuccessful, we just drop things on the floor. - // It's not worth committing any further resources to this one packet. We - // might want to log the failure at some point tho. } bool Endpoint::SendStatelessReset(const PathDescriptor& options, - size_t source_len) { + size_t source_len, + uint64_t now) { if (options_.disable_stateless_reset) [[unlikely]] { return false; } @@ -868,17 +1105,9 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, options, source_len); - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addr_validation_lru_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->reset_count : 0; - return count >= options_.max_stateless_resets; - }; - - // Per the QUIC spec, we need to protect against sending too many stateless - // reset tokens to an endpoint to prevent endless looping. - if (exceeds_limits()) { - Debug(this, "Stateless reset rate limit exceeded"); + if (!stateless_reset_bucket_.consume(now)) { + Debug(this, "Stateless reset rate limit exceeded (global)"); + STAT_INCREMENT(Stats, stateless_reset_rate_limited); return false; } @@ -887,7 +1116,6 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, if (packet) { Debug(this, "Sending stateless reset packet (%zu bytes)", packet->length()); - addr_validation_lru_.Upsert(options.remote_address)->reset_count++; STAT_INCREMENT(Stats, stateless_reset_count); Send(std::move(packet)); return true; @@ -897,13 +1125,18 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, } void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, - QuicError reason) { + QuicError reason, + uint64_t now) { Debug(this, "Sending immediate connection close on path %s with reason %s", options, reason); - // While it is possible for a malicious peer to cause us to create a large - // number of these, generating them is fairly trivial. + if (!immediate_close_bucket_.consume(now)) { + Debug(this, "Immediate connection close rate limit exceeded (global)"); + STAT_INCREMENT(Stats, immediate_close_rate_limited); + return; + } + auto packet = Packet::CreateImmediateConnectionClosePacket(*this, options, reason); if (packet) { @@ -1117,28 +1350,77 @@ void Endpoint::CloseGracefully() { MaybeDestroy(); } -void Endpoint::Receive(const uv_buf_t& buf, +void Endpoint::Receive(const uint8_t* data, + size_t len, const SocketAddress& remote_address) { + const uint64_t now = uv_hrtime(); + + // Block list filtering — applied before any packet processing to + // minimize resource expenditure on blocked sources. + if (options_.block_list) { + bool matched = options_.block_list->Apply(remote_address); + bool drop = (options_.block_list_policy == Options::BlockListPolicy::DENY) + ? matched // deny list: drop if address matches + : !matched; // allow list: drop if address doesn't match + if (drop) { + Debug(this, "Packet from %s blocked by block list", remote_address); + STAT_INCREMENT(Stats, packets_blocked); + return; + } + } + const auto receive = [&](Session* session, - Store&& store, + const uint8_t* pkt_data, + size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& dcid, const CID& scid) { DCHECK_NOT_NULL(session); if (session->is_destroyed()) return; - size_t len = store.length(); - if (session->Receive(std::move(store), local_address, remote_address)) { - STAT_INCREMENT_N(Stats, bytes_received, len); + // Use ReadPacket (no SendPendingDataScope) so that multiple packets + // received in the same I/O burst are processed before any responses + // are generated. The deferred flush via BindingData's uv_check + // callback calls SendPendingData once per dirty session after all + // packets in the burst have been read. + if (session->ReadPacket(pkt_data, + pkt_len, + local_address, + remote_address, + PacketInfo(), + now)) { + STAT_INCREMENT_N(Stats, bytes_received, pkt_len); STAT_INCREMENT(Stats, packets_received); } + // Schedule the session for deferred SendPendingData if it hasn't + // been scheduled already in this burst. + if (!session->is_destroyed() && !session->flags_.pending_flush) { + session->flags_.pending_flush = true; + BindingData::Get(env()).ScheduleSessionFlush( + BaseObjectPtr(session)); + } }; - const auto accept = [&](const Session::Config& config, Store&& store) { + const auto accept = [&](const Session::Config& config, + const uint8_t* pkt_data, + size_t pkt_len) { // One final check. If the endpoint is closed, closing, or is not listening // as a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; + // Per-host session creation rate limit. The bucket is initialized + // on first access with the configured rate/burst from options. + auto info = addr_validation_lru_.Upsert(config.remote_address, now); + info->session_creation_bucket.InitOnce( + options_.session_creation_rate, options_.session_creation_burst, now); + if (!info->session_creation_bucket.consume(now)) { + Debug(this, + "Session creation rate limit exceeded for %s", + config.remote_address); + STAT_INCREMENT(Stats, session_creation_rate_limited); + return; + } + Debug(this, "Creating new session for %s", config.dcid); std::optional no_ticket = std::nullopt; @@ -1164,7 +1446,8 @@ void Endpoint::Receive(const uv_buf_t& buf, return; receive(session.get(), - std::move(store), + pkt_data, + pkt_len, config.local_address, config.remote_address, config.dcid, @@ -1174,7 +1457,8 @@ void Endpoint::Receive(const uv_buf_t& buf, const auto acceptInitialPacket = [&](const uint32_t version, const CID& dcid, const CID& scid, - Store&& store, + const uint8_t* pkt_data, + size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address) { // If we're not listening as a server, do not accept an initial packet. @@ -1184,8 +1468,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // This is our first condition check... A minimal check to see if ngtcp2 can // even recognize this packet as a quic packet. - ngtcp2_vec vec = store; - if (ngtcp2_accept(&hd, vec.base, vec.len) != NGTCP2_SUCCESS) { + if (ngtcp2_accept(&hd, pkt_data, pkt_len) != NGTCP2_SUCCESS) { // Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was // successful, or an error code if it was not. Currently there's only one // documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle @@ -1229,7 +1512,8 @@ void Endpoint::Receive(const uv_buf_t& buf, if (state_->busy) STAT_INCREMENT(Stats, server_busy_count); SendImmediateConnectionClose( PathDescriptor{version, dcid, scid, local_address, remote_address}, - QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), + now); // The packet was successfully processed, even if we did refuse the // connection. STAT_INCREMENT(Stats, packets_received); @@ -1303,7 +1587,8 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Retry token from %s is invalid.", remote_address); SendImmediateConnectionClose( PathDescriptor{version, scid, dcid, local_address, remote_address}, - QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1319,7 +1604,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // Mark the address as validated since the retry round-trip proves // reachability. Debug(this, "Remote address %s is validated", remote_address); - addr_validation_lru_.Upsert(remote_address)->validated = true; + addr_validation_lru_.Upsert(remote_address, now)->validated = true; } // Step 2: Address validation — decide whether to send a Retry or @@ -1335,13 +1620,15 @@ void Endpoint::Receive(const uv_buf_t& buf, "Initial packet has no token. Sending retry to %s to start " "validation", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1362,13 +1649,15 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Regular token from %s is invalid.", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1380,20 +1669,22 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Initial packet from %s has unknown token type", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } } Debug(this, "Remote address %s is validated", remote_address); - addr_validation_lru_.Upsert(remote_address)->validated = true; + addr_validation_lru_.Upsert(remote_address, now)->validated = true; } else if (hd.tokenlen > 0) { Debug(this, "Ignoring initial packet from %s with unexpected token", @@ -1405,13 +1696,15 @@ void Endpoint::Receive(const uv_buf_t& buf, if (options_.validate_address) { Debug( this, "Sending retry to %s due to 0RTT packet", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1423,7 +1716,7 @@ void Endpoint::Receive(const uv_buf_t& buf, } } - accept(config, std::move(store)); + accept(config, pkt_data, pkt_len); }; // When a received packet contains a QUIC short header but cannot be matched @@ -1439,14 +1732,15 @@ void Endpoint::Receive(const uv_buf_t& buf, // possible to avoid a DOS vector. const auto maybeStatelessReset = [&](const CID& dcid, const CID& scid, - Store& store, + const uint8_t* pkt_data, + size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address) { // Support for stateless resets can be disabled by the application. If that // case, or if the packet is too short to contain a reset token, then we // skip the remaining checks. if (options_.disable_stateless_reset || - store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) { + pkt_len < NGTCP2_STATELESS_RESET_TOKENLEN) { return false; } @@ -1454,20 +1748,21 @@ void Endpoint::Receive(const uv_buf_t& buf, // NGTCP2_STATELESS_RESET_TOKENLEN bytes in the received packet. If it is a // stateless reset then then rest of the bytes in the packet are garbage // that we'll ignore. - ngtcp2_vec vec = store; - vec.base += (vec.len - NGTCP2_STATELESS_RESET_TOKENLEN); + const uint8_t* token_pos = + pkt_data + (pkt_len - NGTCP2_STATELESS_RESET_TOKENLEN); // If a Session has been associated with the token, then it is a valid // stateless reset token. We need to dispatch it to the session to be // processed. auto* session = session_manager().FindSessionByStatelessResetToken( - StatelessResetToken(vec.base)); + StatelessResetToken(token_pos)); if (session != nullptr) { // If the session happens to have been destroyed already, we'll // just ignore the packet. if (!session->is_destroyed()) [[likely]] { receive(session, - std::move(store), + pkt_data, + pkt_len, local_address, remote_address, dcid, @@ -1489,28 +1784,8 @@ void Endpoint::Receive(const uv_buf_t& buf, } #endif // DEBUG - // TODO(@jasnell): Implement blocklist support - // if (block_list_->Apply(remote_address)) [[unlikely]] { - // Debug(this, "Ignoring blocked remote address: %s", remote_address); - // return; - // } - - Debug(this, "Received %zu-byte packet from %s", buf.len, remote_address); - - // The managed buffer here contains the received packet. We do not yet know - // at this point if it is a valid QUIC packet. We need to do some basic - // checks. It is critical at this point that we do as little work as possible - // to avoid a DOS vector. - std::shared_ptr backing = env()->release_managed_buffer(buf); - if (!backing) [[unlikely]] { - // At this point something bad happened and we need to treat this as a fatal - // case. There's likely no way to test this specific condition reliably. - return Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOMEM); - } - - Store store(std::move(backing), buf.len, 0); + Debug(this, "Received %zu-byte packet from %s", len, remote_address); - ngtcp2_vec vec = store; ngtcp2_version_cid pversion_cid; // This is our first check to see if the received data can be processed as a @@ -1519,7 +1794,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // valid QUIC header but there is still no guarantee that the packet can be // successfully processed. switch (ngtcp2_pkt_decode_version_cid( - &pversion_cid, vec.base, vec.len, NGTCP2_MAX_CIDLEN)) { + &pversion_cid, data, len, NGTCP2_MAX_CIDLEN)) { case 0: break; // Supported version, continue processing. case NGTCP2_ERR_VERSION_NEGOTIATION: { @@ -1532,8 +1807,12 @@ void Endpoint::Receive(const uv_buf_t& buf, pversion_cid.version); CID dcid(pversion_cid.dcid, pversion_cid.dcidlen); CID scid(pversion_cid.scid, pversion_cid.scidlen); - SendVersionNegotiation(PathDescriptor{ - pversion_cid.version, dcid, scid, local_address(), remote_address}); + SendVersionNegotiation(PathDescriptor{pversion_cid.version, + dcid, + scid, + local_address(), + remote_address}, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1597,7 +1876,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // necessary here. We want to return immediately without committing any // further resources. if (pversion_cid.version == 0 && - maybeStatelessReset(dcid, scid, store, addr, remote_address)) { + maybeStatelessReset(dcid, scid, data, len, addr, remote_address)) { Debug(this, "Packet was a stateless reset"); return; // Stateless reset! Don't do any further processing. } @@ -1612,17 +1891,14 @@ void Endpoint::Receive(const uv_buf_t& buf, SendStatelessReset( PathDescriptor{ pversion_cid.version, dcid, scid, addr, remote_address}, - store.length()); + len, + now); return; } // Process the packet as an initial packet... - return acceptInitialPacket(pversion_cid.version, - dcid, - scid, - std::move(store), - addr, - remote_address); + return acceptInitialPacket( + pversion_cid.version, dcid, scid, data, len, addr, remote_address); } if (session->is_destroyed()) [[unlikely]] { @@ -1634,7 +1910,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // If we got here, the dcid matched the scid of a known local session. Yay! // The session will take over any further processing of the packet. Debug(this, "Dispatching packet to known session"); - receive(session.get(), std::move(store), addr, remote_address, dcid, scid); + receive(session.get(), data, len, addr, remote_address, dcid, scid); // It is important to note that the session may have been destroyed during // the call to receive(...). If that's the case, the session object still @@ -1678,13 +1954,14 @@ void Endpoint::MemoryInfo(MemoryTracker* tracker) const { // Endpoint::SocketAddressInfoTraits bool Endpoint::SocketAddressInfoTraits::CheckExpired( - const SocketAddress& address, const Type& type) { - return (uv_hrtime() - type.timestamp) > kSocketAddressInfoTimeout; + const SocketAddress& address, const Type& type, uint64_t now) { + return (now - type.timestamp) > kSocketAddressInfoTimeout; } void Endpoint::SocketAddressInfoTraits::Touch(const SocketAddress& address, - Type* type) { - type->timestamp = uv_hrtime(); + Type* type, + uint64_t now) { + type->timestamp = now; } // ====================================================================================== diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index b9f20f8659dfa6..f54e028ead4554 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -29,23 +29,28 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // The socket address LRU is used for tracking validated remote addresses. static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = 1024; - // The max stateless resets is the maximum number of stateless reset packets - // that the Endpoint will generate for a given remote host within a window of - // time (while tracking that host in the socket address LRU). This is not - // mandated by QUIC, and the limit is arbitrary. We can set it to whatever - // we'd like. The purpose is to prevent a malicious peer from intentionally - // triggering generation of a large number of stateless resets. Once the - // limit is reached, packets that would have otherwise triggered generation - // of a stateless reset will simply be dropped instead. - static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10; - - // Similar to stateless resets, the max retry limit is the maximum number of - // retry packets that the Endpoint will generate for a given remote host - // within a window of time (while tracking that host in the socket address - // LRU). This is not mandated by QUIC, and the limit is arbitrary. We can set - // it to whatever we'd like. The purpose is to prevent a malicious peer from - // intentionally triggering generation of a large number of retries. - static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10; + // Default rate limits for stateless responses. These are global token + // bucket limits that cap the total rate of each response type regardless + // of source address. This prevents spoofed-source floods from bypassing + // per-host limits (which are keyed by source IP and trivially defeated + // by rotating spoofed addresses). The rate is in responses per second + // and the burst is the maximum tokens the bucket can hold. + static constexpr double DEFAULT_RETRY_RATE = 100; + static constexpr double DEFAULT_RETRY_BURST = 200; + static constexpr double DEFAULT_STATELESS_RESET_RATE = 100; + static constexpr double DEFAULT_STATELESS_RESET_BURST = 200; + static constexpr double DEFAULT_VERSION_NEGOTIATION_RATE = 100; + static constexpr double DEFAULT_VERSION_NEGOTIATION_BURST = 200; + static constexpr double DEFAULT_IMMEDIATE_CLOSE_RATE = 100; + static constexpr double DEFAULT_IMMEDIATE_CLOSE_BURST = 200; + + // Per-host session creation rate limit. This is tracked per validated + // remote address in the address LRU, preventing a single source from + // churning through sessions faster than the server can handle. Unlike + // the global stateless response buckets, this only applies after address + // validation (spoofed sources can't reach this path). + static constexpr double DEFAULT_SESSION_CREATION_RATE = 50; + static constexpr double DEFAULT_SESSION_CREATION_BURST = 100; // Endpoint configuration options struct Options final : public MemoryRetainer { @@ -69,30 +74,28 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { uint64_t token_expiration = RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS; - // A stateless reset in QUIC is a discrete mechanism that one endpoint can - // use to communicate to a peer that it has lost whatever state it - // previously held about a session. Because generating a stateless reset - // consumes resources (even very modestly), they can be a DOS vector in - // which a malicious peer intentionally sends a large number of stateless - // reset eliciting packets. To protect against that risk, we limit the - // number of stateless resets that may be generated for a given remote host - // within a window of time. This is not mandated by QUIC, and the limit is - // arbitrary. We can set it to whatever we'd like. - uint64_t max_stateless_resets = DEFAULT_MAX_STATELESS_RESETS; - - // For tracking the number of connections per host, the number of stateless - // resets that have been sent, and tracking the path verification status of - // a remote host, we maintain an LRU cache of the most recently seen hosts. - // The address_lru_size parameter determines the size of that cache. The - // default is set modestly at 10 times the default max connections per host. + // For tracking the path verification status of remote hosts, we maintain + // an LRU cache of the most recently seen hosts. uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE; - // Similar to stateless resets, we enforce a limit on the number of retry - // packets that can be generated and sent for a remote host. Generating - // retry packets consumes a modest amount of resources and it's fairly - // trivial for a malicious peer to trigger generation of a large number of - // retries, so limiting them helps prevent a DOS vector. - uint64_t max_retries = DEFAULT_MAX_RETRY_LIMIT; + // Global token bucket rate limits for stateless responses. These cap + // the total rate of each response type regardless of source address, + // preventing spoofed-source floods. Rate is in responses per second, + // burst is the maximum number of responses that can be sent in a burst. + double retry_rate = DEFAULT_RETRY_RATE; + double retry_burst = DEFAULT_RETRY_BURST; + double stateless_reset_rate = DEFAULT_STATELESS_RESET_RATE; + double stateless_reset_burst = DEFAULT_STATELESS_RESET_BURST; + double version_negotiation_rate = DEFAULT_VERSION_NEGOTIATION_RATE; + double version_negotiation_burst = DEFAULT_VERSION_NEGOTIATION_BURST; + double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE; + double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST; + + // Per-host session creation rate limit. Tracked per validated remote + // address in the address LRU. Set to high values for benchmarking + // where traffic comes from a single source. + double session_creation_rate = DEFAULT_SESSION_CREATION_RATE; + double session_creation_burst = DEFAULT_SESSION_CREATION_BURST; // The validate_address parameter instructs the Endpoint to perform explicit // address validation using retry tokens. This is strongly recommended and @@ -134,6 +137,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // flag on the underlying uv_udp_t. bool ipv6_only = false; + // When true, multiple endpoints (across separate processes) can bind to + // the same address:port and the kernel will load-balance incoming UDP + // datagrams across them. This sets the UV_UDP_REUSEPORT flag on the + // underlying uv_udp_t. Supported on Linux 3.9+ and DragonFlyBSD 3.6+. + bool reuse_port = false; + uint32_t udp_receive_buffer_size = 0; uint32_t udp_send_buffer_size = 0; @@ -150,6 +159,17 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { static constexpr uint64_t DEFAULT_IDLE_TIMEOUT = 0; uint64_t idle_timeout = DEFAULT_IDLE_TIMEOUT; + // Optional block list for filtering incoming packets by source address. + // When block_list_policy is DENY, packets from addresses matching the + // block list are dropped. When ALLOW, only packets from addresses + // matching the block list are accepted (all others dropped). + enum class BlockListPolicy : uint8_t { + DENY, // Drop packets from matching addresses (blocklist) + ALLOW, // Drop packets from non-matching addresses (allowlist) + }; + std::shared_ptr block_list; + BlockListPolicy block_list_policy = BlockListPolicy::DENY; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Endpoint::Config) SET_SELF_SIZE(Options) @@ -208,6 +228,20 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { void Send(Packet::Ptr packet); + // Attempt synchronous send via uv_udp_try_send. If the socket is + // writable, the packet is sent immediately and the Ptr is released. + // If the socket is not writable (UV_EAGAIN), falls back to the + // async Send path. Used by the deferred flush callback to avoid + // the one-tick latency of async uv_udp_send. + void SendOrTrySend(Packet::Ptr packet); + + // Send a batch of packets using uv_udp_try_send2 (sendmmsg) for + // synchronous batched delivery. Packets successfully sent are released + // immediately. On EAGAIN or partial send, remaining packets fall back + // to async uv_udp_send. The Packet::Ptr array is consumed: all entries + // will be empty (released or moved) on return. + void SendBatch(Packet::Ptr* packets, size_t count); + // Acquire a Packet from the pool. length sets the initial working // size (must be <= pool capacity). The slot is always allocated at // full capacity to avoid fragmentation. @@ -229,24 +263,27 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // ellicit retry packets (It can do so by intentionally sending initial // packets that ignore the retry token). To help mitigate that risk, we limit // the number of retries we send to a given remote endpoint. - void SendRetry(const PathDescriptor& options); + void SendRetry(const PathDescriptor& options, uint64_t now); // Sends a version negotiation packet. This is terminal for the connection and // is sent only when a QUIC packet is received for an unsupported QUIC // version. It is possible that a malicious packet triggered this so we need // to be careful not to commit too many resources. - void SendVersionNegotiation(const PathDescriptor& options); + void SendVersionNegotiation(const PathDescriptor& options, uint64_t now); // Possibly generates and sends a stateless reset packet. This is terminal for // the connection. It is possible that a malicious packet triggered this so we // need to be careful not to commit too many resources. - bool SendStatelessReset(const PathDescriptor& options, size_t source_len); + bool SendStatelessReset(const PathDescriptor& options, + size_t source_len, + uint64_t now); // Shutdown a connection prematurely, before a Session is created. This should // only be called at the start of a session before the crypto keys have been // established. void SendImmediateConnectionClose(const PathDescriptor& options, - QuicError error); + QuicError error, + uint64_t now); // Listen for connections (act as a server). void Listen(const Session::Options& options); @@ -281,6 +318,20 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { void Close(); int Send(Packet::Ptr packet); + // Synchronous send using uv_udp_try_send. Returns the number of + // bytes sent on success, UV_EAGAIN if the socket is not writable + // or the send queue is non-empty, or another negative error code. + // The Ptr is not consumed — the caller manages the lifecycle. + int TrySend(const Packet::Ptr& packet); + + // Synchronous batched send using uv_udp_try_send2 (sendmmsg). + // Takes pre-built libuv argument arrays. Returns the number of + // messages successfully sent (>= 0), or a negative error code. + int TrySendBatch(uv_buf_t* bufs[], + unsigned int nbufs[], + struct sockaddr* addrs[], + size_t count); + // Returns the local UDP socket address to which we are bound, // or fail with an assert if we are not bound. SocketAddress local_address() const; @@ -301,9 +352,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { class Impl; BaseObjectWeakPtr impl_; - bool is_bound_ = false; - bool is_started_ = false; - bool is_closed_ = false; + struct Flags { + uint8_t is_bound : 1 = 0; + uint8_t is_started : 1 = 0; + uint8_t is_closed : 1 = 0; + }; + Flags flags_; }; bool is_closed() const; @@ -381,7 +435,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // Ref() causes a listening Endpoint to keep the event loop active. JS_METHOD(Ref); - void Receive(const uv_buf_t& buf, const SocketAddress& from); + void Receive(const uint8_t* data, size_t len, const SocketAddress& from); AliasedStruct stats_; AliasedStruct state_; @@ -424,18 +478,27 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { struct SocketAddressInfoTraits final { struct Type final { - size_t reset_count; - size_t retry_count; uint64_t timestamp; bool validated; + TokenBucket session_creation_bucket; }; - static bool CheckExpired(const SocketAddress& address, const Type& type); - static void Touch(const SocketAddress& address, Type* type); + static bool CheckExpired(const SocketAddress& address, + const Type& type, + uint64_t now); + static void Touch(const SocketAddress& address, Type* type, uint64_t now); }; SocketAddressLRU addr_validation_lru_; + // Global token buckets for stateless response rate limiting. + // These cap the total server-wide rate of each response type, + // regardless of source address. + TokenBucket retry_bucket_; + TokenBucket stateless_reset_bucket_; + TokenBucket version_negotiation_bucket_; + TokenBucket immediate_close_bucket_; + // Per-IP connection counts for maxConnectionsPerHost enforcement. // Only populated when max_connections_per_host > 0. Entries are // added in AddSession and removed when the count reaches 0 in diff --git a/src/quic/http3.cc b/src/quic/http3.cc index ea07c0a5a596fb..bf4bfb42b57611 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -157,6 +157,8 @@ class Http3ApplicationImpl final : public Session::Application { session->set_priority_supported(); } + const Options& options() const override { return options_; } + Session::Application::Type type() const override { return Session::Application::Type::HTTP3; } @@ -175,10 +177,13 @@ class Http3ApplicationImpl final : public Session::Application { // When 0-RTT is rejected, destroy the nghttp3 connection and all // open streams — ngtcp2 has discarded their internal state. // Reset started_ so Start() is called again via on_receive_rx_key - // at 1RTT to recreate the nghttp3 connection. + // at 1RTT to recreate the nghttp3 connection. Use the + // application's internal error code since this is an error + // condition (code 0 would be treated as a clean close). conn_.reset(); started_ = false; - session().DestroyAllStreams(QuicError::ForApplication(0)); + session().DestroyAllStreams( + QuicError::ForApplication(GetInternalErrorCode())); if (!session().is_destroyed()) { session().EmitEarlyDataRejected(); } @@ -262,11 +267,17 @@ class Http3ApplicationImpl final : public Session::Application { } void BeginShutdown() override { - if (conn_) nghttp3_conn_submit_shutdown_notice(*this); + // Only submit a shutdown notice if the H3 connection was fully + // started (control streams bound). If the TLS handshake failed + // before Start() was called, conn_ exists but its control streams + // are unbound, and nghttp3_conn_submit_shutdown_notice would crash. + if (conn_ && started_) nghttp3_conn_submit_shutdown_notice(*this); } void CompleteShutdown() override { - if (conn_) nghttp3_conn_shutdown(*this); + // Same guard as BeginShutdown — nghttp3_conn_shutdown asserts + // that the control stream is bound (conn->tx.ctrl != NULL). + if (conn_ && started_) nghttp3_conn_shutdown(*this); } bool ReceiveStreamData(stream_id id, @@ -327,9 +338,7 @@ class Http3ApplicationImpl final : public Session::Application { // We cannot add the header if we've either reached // * the max number of header pairs or // * the max number of header bytes (name + value combined) - // current_count is the number of entries in the headers vector - // (each pair = name entry + value entry = 2 entries). - return (current_count / 2 < options_.max_header_pairs) && + return (current_count < options_.max_header_pairs) && (current_headers_length + this_header_length) <= options_.max_header_length; } @@ -819,12 +828,12 @@ class Http3ApplicationImpl final : public Session::Application { stream->BeginHeaders(HeadersKind::INITIAL); } - void OnReceiveHeader(stream_id id, Http3Header&& header) { + void OnReceiveHeader(stream_id id, std::unique_ptr header) { auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; - if (header.name() == ":status" && header.value()[0] == '1') { + if (header->name() == ":status" && header->value()[0] == '1') { Debug(&session(), "HTTP/3 application switching to hints headers for stream %" PRIi64, stream->id()); @@ -833,8 +842,8 @@ class Http3ApplicationImpl final : public Session::Application { IF_QUIC_DEBUG(env()) { Debug(&session(), "Received header \"%s: %s\"", - header.name(), - header.value()); + header->name(), + header->value()); } stream->AddHeader(std::move(header)); } @@ -868,15 +877,15 @@ class Http3ApplicationImpl final : public Session::Application { stream->BeginHeaders(HeadersKind::TRAILING); } - void OnReceiveTrailer(stream_id id, Http3Header&& header) { + void OnReceiveTrailer(stream_id id, std::unique_ptr header) { auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; IF_QUIC_DEBUG(env()) { Debug(&session(), "Received header \"%s: %s\"", - header.name(), - header.value()); + header->name(), + header->value()); } stream->AddHeader(std::move(header)); } @@ -1233,7 +1242,9 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS; - app.OnReceiveHeader(id, Http3Header(app.env(), token, name, value, flags)); + app.OnReceiveHeader( + id, + std::make_unique(app.env(), token, name, value, flags)); return NGTCP2_SUCCESS; } @@ -1275,7 +1286,9 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS; - app.OnReceiveTrailer(id, Http3Header(app.env(), token, name, value, flags)); + app.OnReceiveTrailer( + id, + std::make_unique(app.env(), token, name, value, flags)); return NGTCP2_SUCCESS; } diff --git a/src/quic/packet.h b/src/quic/packet.h index ffeb582471333f..a94ee1264c2a6a 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -68,6 +68,8 @@ class Packet final { size_t length() const { return length_; } size_t capacity() const { return capacity_; } const SocketAddress& destination() const { return destination_; } + const PacketInfo& pkt_info() const { return pkt_info_; } + void set_pkt_info(const PacketInfo& pi) { pkt_info_ = pi; } Listener* listener() const { return listener_; } // Redirect the packet to a different endpoint for cross-endpoint sends @@ -148,6 +150,7 @@ class Packet final { Listener* listener_; // Touched at send time. + PacketInfo pkt_info_; SocketAddress destination_; // Only touched by libuv during uv_udp_send and in the send callback. diff --git a/src/quic/preferredaddress.cc b/src/quic/preferredaddress.cc index 3d584be2d74811..7ce27798a63423 100644 --- a/src/quic/preferredaddress.cc +++ b/src/quic/preferredaddress.cc @@ -17,6 +17,7 @@ namespace node { using v8::Just; using v8::Local; using v8::Maybe; +using v8::Object; using v8::Value; namespace quic { @@ -131,7 +132,7 @@ Maybe PreferredAddress::tryGetPolicy( : Just(FromV8Value(value)); } -void PreferredAddress::Initialize(Environment* env, Local target) { +void PreferredAddress::Initialize(Environment* env, Local target) { // The QUIC_* constants are expected to be exported out to be used on // the JavaScript side of the API. static constexpr auto PREFERRED_ADDRESS_USE = @@ -139,7 +140,7 @@ void PreferredAddress::Initialize(Environment* env, Local target) { static constexpr auto PREFERRED_ADDRESS_IGNORE = static_cast(Policy::IGNORE_PREFERRED); static constexpr auto DEFAULT_PREFERRED_ADDRESS_POLICY = - static_cast(Policy::USE_PREFERRED); + static_cast(Policy::IGNORE_PREFERRED); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_IGNORE); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_USE); diff --git a/src/quic/quic.cc b/src/quic/quic.cc index d39215a06827d3..36e781f1d700b7 100644 --- a/src/quic/quic.cc +++ b/src/quic/quic.cc @@ -13,7 +13,7 @@ #include "node_external_reference.h" #include -#include + namespace node { using v8::Context; @@ -25,7 +25,11 @@ using v8::Value; namespace quic { namespace { -std::once_flag crypto_init_flag; +uv_once_t crypto_init_flag = UV_ONCE_INIT; + +void InitNgtcp2CryptoOnce() { + ngtcp2_crypto_ossl_init(); +} } // namespace void CreatePerIsolateProperties(IsolateData* isolate_data, @@ -39,8 +43,8 @@ void CreatePerContextProperties(Local target, Local unused, Local context, void* priv) { + uv_once(&crypto_init_flag, InitNgtcp2CryptoOnce); Realm* realm = Realm::GetCurrent(context); - std::call_once(crypto_init_flag, ngtcp2_crypto_ossl_init); BindingData::InitPerContext(realm, target); Endpoint::InitPerContext(realm, target); Session::InitPerContext(realm, target); diff --git a/src/quic/session.cc b/src/quic/session.cc index 4af903e0c2a0af..bb94b6a7400766 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -40,7 +40,9 @@ using v8::Array; using v8::ArrayBufferView; using v8::BigInt; using v8::Boolean; +using v8::Function; using v8::FunctionCallbackInfo; +using v8::Global; using v8::HandleScope; using v8::Int32; using v8::Integer; @@ -54,6 +56,7 @@ using v8::Number; using v8::Object; using v8::ObjectTemplate; using v8::String; +using v8::Uint32; using v8::Undefined; using v8::Value; @@ -171,7 +174,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) { V(DATAGRAMS_RECEIVED, datagrams_received) \ V(DATAGRAMS_SENT, datagrams_sent) \ V(DATAGRAMS_ACKNOWLEDGED, datagrams_acknowledged) \ - V(DATAGRAMS_LOST, datagrams_lost) + V(DATAGRAMS_LOST, datagrams_lost) \ + V(STREAMS_IDLE_TIMED_OUT, streams_idle_timed_out) #define NO_SIDE_EFFECT true #define SIDE_EFFECT false @@ -187,7 +191,10 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) { V(SilentClose, silentClose, SIDE_EFFECT) \ V(UpdateKey, updateKey, SIDE_EFFECT) \ V(OpenStream, openStream, SIDE_EFFECT) \ - V(SendDatagram, sendDatagram, SIDE_EFFECT) + V(SendDatagram, sendDatagram, SIDE_EFFECT) \ + V(LocalTransportParams, localTransportParams, NO_SIDE_EFFECT) \ + V(RemoteTransportParams, remoteTransportParams, NO_SIDE_EFFECT) \ + V(ApplicationOptions, applicationOptions, NO_SIDE_EFFECT) struct Session::State final { #define V(_, name, type) type name; @@ -197,6 +204,46 @@ struct Session::State final { STAT_STRUCT(Session, SESSION) +using SessionStateArena = AliasedStructArena; +using SessionStatsArena = AliasedStructArena; + +// Session uses arena-allocated stats, not AliasedStruct, so override the +// STAT_* macros to use impl_->stats() instead of stats_.Data(). +#undef STAT_INCREMENT +#undef STAT_INCREMENT_N +#undef STAT_RECORD_TIMESTAMP +#undef STAT_SET +#undef STAT_GET +#define STAT_INCREMENT(Type, name) \ + IncrementStat(impl_->stats()); +#define STAT_INCREMENT_N(Type, name, amt) \ + IncrementStat(impl_->stats(), amt); +#define STAT_RECORD_TIMESTAMP(Type, name) \ + RecordTimestampStat(impl_->stats()); +#define STAT_SET(Type, name, val) \ + SetStat(impl_->stats(), val) +#define STAT_GET(Type, name) GetStat(impl_->stats()) + +namespace { +SessionStateArena& GetSessionStateArena(BindingData& binding) { + if (!binding.session_state_arena_) { + auto* arena = new SessionStateArena(); + binding.session_state_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.session_state_arena_.get()); +} + +SessionStatsArena& GetSessionStatsArena(BindingData& binding) { + if (!binding.session_stats_arena_) { + auto* arena = new SessionStatsArena(); + binding.session_stats_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.session_stats_arena_.get()); +} +} // namespace + // ============================================================================ class Http3Application; @@ -380,9 +427,9 @@ bool SetOption(Environment* env, template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { - v8::Local value; + const Local& object, + const Local& name) { + Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { if (!value->IsUint32()) { @@ -391,7 +438,7 @@ bool SetOption(Environment* env, env, "The %s option must be an uint8", *nameStr); return false; } - uint32_t val = value.As()->Value(); + uint32_t val = value.As()->Value(); if (val > 255) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( @@ -464,7 +511,18 @@ Session::Config::Config(Environment* env, settings.log_printf = ngtcp2_debug_log; } - settings.handshake_timeout = options.handshake_timeout; + // The handshake_timeout option is in milliseconds; ngtcp2 expects + // nanoseconds (ngtcp2_duration). UINT64_MAX means no timeout. + settings.handshake_timeout = + options.handshake_timeout == UINT64_MAX + ? UINT64_MAX + : options.handshake_timeout * NGTCP2_MILLISECONDS; + + // The initial_rtt option is in milliseconds; ngtcp2 expects nanoseconds. + // A value of 0 leaves the ngtcp2 default (333ms) unchanged. + if (options.initial_rtt > 0) + settings.initial_rtt = options.initial_rtt * NGTCP2_MILLISECONDS; + settings.max_stream_window = options.max_stream_window; settings.max_window = options.max_window; settings.ack_thresh = options.unacknowledged_packet_threshold; @@ -556,10 +614,11 @@ Maybe Session::Options::From(Environment* env, if (!SET(version) || !SET(min_version) || !SET(preferred_address_strategy) || !SET(transport_params) || !SET(tls_options) || !SET(qlog) || - !SET(handshake_timeout) || !SET(keep_alive_timeout) || - !SET(max_stream_window) || !SET(max_window) || !SET(max_payload_size) || - !SET(unacknowledged_packet_threshold) || !SET(cc_algorithm) || - !SET(draining_period_multiplier) || !SET(max_datagram_send_attempts)) { + !SET(handshake_timeout) || !SET(initial_rtt) || + !SET(keep_alive_timeout) || !SET(max_stream_window) || !SET(max_window) || + !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || + !SET(cc_algorithm) || !SET(draining_period_multiplier) || + !SET(max_datagram_send_attempts) || !SET(stream_idle_timeout)) { return Nothing(); } @@ -678,6 +737,12 @@ std::string Session::Options::ToString() const { res += prefix + "handshake timeout: " + std::to_string(handshake_timeout) + " nanoseconds"; } + if (initial_rtt > 0) { + res += prefix + "initial rtt: " + std::to_string(initial_rtt) + + " milliseconds"; + } else { + res += prefix + "initial rtt: "; + } res += prefix + "max stream window: " + std::to_string(max_stream_window); res += prefix + "max window: " + std::to_string(max_window); res += prefix + "max payload size: " + std::to_string(max_payload_size); @@ -707,8 +772,8 @@ std::string Session::Options::ToString() const { // Session::Impl maintains most of the internal state of an active Session. struct Session::Impl final : public MemoryRetainer { Session* session_; - AliasedStruct stats_; - AliasedStruct state_; + ArenaSlotBase stats_slot_; + ArenaSlotBase state_slot_; BaseObjectWeakPtr endpoint_; Config config_; SocketAddress local_address_; @@ -736,20 +801,30 @@ struct Session::Impl final : public MemoryRetainer { // and the stream/datagram data is included in the 0-RTT flight. bool handshake_deferred_ = false; + Stats* stats() { return static_cast(stats_slot_.ptr); } + const Stats* stats() const { + return static_cast(stats_slot_.ptr); + } + State* state() { return static_cast(state_slot_.ptr); } + const State* state() const { + return static_cast(state_slot_.ptr); + } + Impl(Session* session, Endpoint* endpoint, const Config& config) : session_(session), - stats_(env()->isolate()), - state_(env()->isolate()), endpoint_(endpoint), config_(config), local_address_(config.local_address), remote_address_(config.remote_address), timer_(session_->env(), [this] { session_->OnTimeout(); }) { + auto& binding = BindingData::Get(env()); + stats_slot_ = GetSessionStatsArena(binding).Allocate(env()->isolate()); + state_slot_ = GetSessionStateArena(binding).Allocate(env()->isolate()); timer_.Unref(); } DISALLOW_COPY_AND_MOVE(Impl) - inline bool is_closing() const { return state_->closing; } + inline bool is_closing() const { return state()->closing; } ~Impl() { // Ensure that Close() was called before dropping @@ -785,6 +860,10 @@ struct Session::Impl final : public MemoryRetainer { } endpoint->RemoveSession(config_.scid, remote_address_); + + auto& binding = BindingData::Get(env()); + if (stats_slot_) GetSessionStatsArena(binding).ReleaseSlot(stats_slot_); + if (state_slot_) GetSessionStateArena(binding).ReleaseSlot(state_slot_); } void MemoryInfo(MemoryTracker* tracker) const override { @@ -1088,6 +1167,54 @@ struct Session::Impl final : public MemoryRetainer { BigInt::New(env->isolate(), session->SendDatagram(std::move(store)))); } + JS_METHOD(LocalTransportParams) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + ngtcp2_conn* conn = *session; + TransportParams params(ngtcp2_conn_get_local_transport_params(conn)); + Local obj; + if (params.ToObject(env).ToLocal(&obj)) { + args.GetReturnValue().Set(obj); + } + } + + JS_METHOD(RemoteTransportParams) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + ngtcp2_conn* conn = *session; + auto params = ngtcp2_conn_get_remote_transport_params(conn); + if (params == nullptr) { + // Remote transport parameters are not yet available. + return args.GetReturnValue().SetUndefined(); + } + TransportParams tp(params); + Local obj; + if (tp.ToObject(env).ToLocal(&obj)) { + args.GetReturnValue().Set(obj); + } + } + + JS_METHOD(ApplicationOptions) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + Local obj; + if (!session->has_application()) { + // The application has not yet been selected (ALPN negotiation is not + // yet complete on the server) or the session has been destroyed. In + // either case, the application options are not available. + return args.GetReturnValue().SetUndefined(); + } + auto& options = session->application().options(); + if (options.ToObject(env).ToLocal(&obj)) { + args.GetReturnValue().Set(obj); + } + } // Internal ngtcp2 callbacks static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn, @@ -1304,7 +1431,7 @@ struct Session::Impl final : public MemoryRetainer { // NGTCP2_ERR_DRAINING. The actual close handling happens in // Session::Receive when it processes that return value and // checks this flag. - session->impl_->state_->stateless_reset = 1; + session->impl_->state()->stateless_reset = 1; return NGTCP2_SUCCESS; } @@ -1629,10 +1756,7 @@ Session::Session(Endpoint* endpoint, connection_(InitConnection()), tls_session_(tls_context->NewSession(this, session_ticket)) { DCHECK(impl_); - { - auto& stats_ = impl_->stats_; - STAT_RECORD_TIMESTAMP(Stats, created_at); - } + STAT_RECORD_TIMESTAMP(Stats, created_at); // For clients, select the Application immediately — the ALPN is // known upfront from the options. For servers, application_ stays @@ -1661,10 +1785,33 @@ Session::Session(Endpoint* endpoint, MakeWeak(); Debug(this, "Session created."); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->stats_string(), impl_->stats_.GetArrayBuffer()); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->state_string(), impl_->state_.GetArrayBuffer()); + { + const HandleScope handle_scope(env()->isolate()); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + env()->state_string(), + impl_->state_slot_.GetPageDataView(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "stateByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(impl_->state_slot_.GetByteOffset()))); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + env()->stats_string(), + impl_->stats_slot_.GetPageBigUint64Array(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "statsByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(impl_->stats_slot_.GetByteOffset()))); + } UpdateDataStats(); } @@ -1728,16 +1875,15 @@ bool Session::is_server() const { } bool Session::is_destroyed() const { - return !impl_ || destroy_deferred_; + return !impl_ || flags_.destroy_deferred; } bool Session::is_destroyed_or_closing() const { - return !impl_ || impl_->state_->closing; + return !impl_ || impl_->state()->closing; } void Session::Close(CloseMethod method) { if (is_destroyed()) return; - auto& stats_ = impl_->stats_; // If the handshake was deferred (0-RTT client that never sent), // no packets were ever transmitted. Close silently since there is @@ -1752,7 +1898,7 @@ void Session::Close(CloseMethod method) { } STAT_RECORD_TIMESTAMP(Stats, closing_at); - impl_->state_->closing = 1; + impl_->state()->closing = 1; // With both the DEFAULT and SILENT options, we will proceed to closing // the session immediately. All open streams will be immediately destroyed @@ -1768,27 +1914,27 @@ void Session::Close(CloseMethod method) { switch (method) { case CloseMethod::DEFAULT: { Debug(this, "Immediately closing session"); - impl_->state_->silent_close = 0; + impl_->state()->silent_close = 0; return FinishClose(); } case CloseMethod::SILENT: { Debug(this, "Immediately closing session silently"); - impl_->state_->silent_close = 1; + impl_->state()->silent_close = 1; return FinishClose(); } case CloseMethod::GRACEFUL: { // If we are already closing gracefully, do nothing. - if (impl_->state_->graceful_close) [[unlikely]] { + if (impl_->state()->graceful_close) [[unlikely]] { return; } - impl_->state_->graceful_close = 1; + impl_->state()->graceful_close = 1; // application_ may be null for server sessions if close() is called // before the TLS handshake selects the ALPN. Without an application // we cannot do a graceful shutdown (GOAWAY, CONNECTION_CLOSE etc.), // so fall through to a silent close. if (!impl_->application_) { - impl_->state_->silent_close = 1; + impl_->state()->silent_close = 1; return FinishClose(); } @@ -1806,7 +1952,7 @@ void Session::Close(CloseMethod method) { // If there are no open streams, then we can close immediately and // not worry about waiting around. if (impl_->streams_.empty()) { - impl_->state_->silent_close = 0; + impl_->state()->silent_close = 0; return FinishClose(); } @@ -1850,11 +1996,11 @@ void Session::FinishClose() { // trigger MakeCallback (stream destruction, pending queue rejection, // SendConnectionClose, EmitClose). if (is_destroyed()) return; - DCHECK(impl_->state_->closing); + DCHECK(impl_->state()->closing); // Clear the graceful_close flag to prevent RemoveStream() from // re-entering FinishClose() when we destroy streams below. - impl_->state_->graceful_close = 0; + impl_->state()->graceful_close = 0; // Destroy all open streams immediately. We copy the map because // streams remove themselves during destruction. Each Destroy() call @@ -1876,7 +2022,7 @@ void Session::FinishClose() { // Send final application-level shutdown and CONNECTION_CLOSE // unless this is a silent close. - if (!impl_->state_->silent_close) { + if (!impl_->state()->silent_close) { if (impl_->application_) { application().CompleteShutdown(); } @@ -1889,7 +2035,7 @@ void Session::FinishClose() { // If the session was passed to JavaScript, we need to round-trip // through JS so it can clean up before we destroy. The JS side // will synchronously call destroy(), which calls Session::Destroy(). - if (impl_->state_->wrapped) { + if (impl_->state()->wrapped) { EmitClose(impl_->last_error_); } else { Destroy(); @@ -1901,23 +2047,20 @@ void Session::Destroy() { // Ensure the closing flag is set for the ~Impl() DCHECK. Normally // this is set by Session::Close(), but JS destroy() can be called // directly without going through Close() first. - impl_->state_->closing = 1; + impl_->state()->closing = 1; // If we're inside a ngtcp2 or nghttp3 callback scope, we cannot // destroy impl_ now because the callback is executing methods on // objects owned by impl_ (e.g., the Application). Defer the // destruction until the scope exits. - if (in_ngtcp2_callback_scope_ || in_nghttp3_callback_scope_) { + if (flags_.in_ngtcp2_callback_scope || flags_.in_nghttp3_callback_scope) { Debug(this, "Session destroy deferred (in callback scope)"); - destroy_deferred_ = true; + flags_.destroy_deferred = true; return; } Debug(this, "Session destroyed"); - { - auto& stats_ = impl_->stats_; - STAT_RECORD_TIMESTAMP(Stats, destroyed_at); - } + STAT_RECORD_TIMESTAMP(Stats, destroyed_at); impl_.reset(); } @@ -1951,6 +2094,10 @@ TLSSession& Session::tls_session() const { return *tls_session_; } +bool Session::has_application() const { + return !is_destroyed() && impl_->application_ != nullptr; +} + Session::Application& Session::application() const { DCHECK(!is_destroyed()); DCHECK(impl_->application_); @@ -1993,16 +2140,16 @@ void Session::SetApplication(std::unique_ptr app) { return; } } - impl_->state_->application_type = static_cast(app->type()); - impl_->state_->headers_supported = static_cast( + impl_->state()->application_type = static_cast(app->type()); + impl_->state()->headers_supported = static_cast( app->SupportsHeaders() ? HeadersSupportState::SUPPORTED : HeadersSupportState::UNSUPPORTED); // Surface the application's "no error" and "internal error" codes via // session state so that JS-side code (e.g. the stream writer's fail() // path) can resolve the right wire code for the negotiated ALPN // without duplicating the per-application table. - impl_->state_->no_error_code = app->GetNoErrorCode(); - impl_->state_->internal_error_code = app->GetInternalErrorCode(); + impl_->state()->no_error_code = app->GetNoErrorCode(); + impl_->state()->internal_error_code = app->GetInternalErrorCode(); impl_->application_ = std::move(app); } @@ -2052,9 +2199,9 @@ void Session::EmitQlog(uint32_t flags, std::string_view data) { // ngtcp2_conn is mid-destruction. Defer the final chunk via SetImmediate. if (is_destroyed()) { auto isolate = env()->isolate(); - v8::Global recv(isolate, object()); - v8::Global cb( - isolate, BindingData::Get(env()).session_qlog_callback()); + Global recv(isolate, object()); + Global cb(isolate, + BindingData::Get(env()).session_qlog_callback()); std::string buf(data); env()->SetImmediate([recv = std::move(recv), cb = std::move(cb), @@ -2101,22 +2248,35 @@ void Session::SetLastError(QuicError&& error) { impl_->last_error_ = std::move(error); } -bool Session::Receive(Store&& store, +bool Session::Receive(const uint8_t* data, + size_t len, const SocketAddress& local_address, - const SocketAddress& remote_address) { + const SocketAddress& remote_address, + const PacketInfo& pkt_info, + uint64_t ts) { + // Convenience wrapper: reads the packet and immediately triggers + // SendPendingData. Used by paths that need an immediate response + // (e.g., Endpoint::Connect for client Initial packets). + // The hot receive path uses ReadPacket() directly with deferred + // flush via BindingData's uv_check callback. + SendPendingDataScope send_scope(this); + return ReadPacket(data, len, local_address, remote_address, pkt_info, ts); +} + +bool Session::ReadPacket(const uint8_t* data, + size_t len, + const SocketAddress& local_address, + const SocketAddress& remote_address, + const PacketInfo& pkt_info, + uint64_t ts) { DCHECK(!is_destroyed()); impl_->remote_address_ = remote_address; - // When we are done processing this packet, we arrange to send any - // pending data for this session. - SendPendingDataScope send_scope(this); - - ngtcp2_vec vec = store; Path path(local_address, remote_address); Debug(this, "Session is receiving %zu-byte packet received along path %s", - vec.len, + len, path); // It is important to understand that reading the packet will cause @@ -2125,29 +2285,29 @@ bool Session::Receive(Store&& store, // ensures that any deferred destroy waits until all callbacks for this // packet have completed. After calling ngtcp2_conn_read_pkt here, we // will need to double check that the session is not destroyed before - // we try doing anything with it (like updating stats, sending pending - // data, etc). + // we try doing anything with it (like updating stats, etc). int err; { NgTcp2CallbackScope callback_scope(this); - err = ngtcp2_conn_read_pkt(*this, - &path, - // TODO(@jasnell): ECN pkt_info blocked on libuv - nullptr, - vec.base, - vec.len, - uv_hrtime()); + // The PacketInfo carries per-packet metadata (currently ECN codepoint). + // When libuv gains per-packet ECN reporting, the caller should + // populate pkt_info from the receive metadata before calling + // ReadPacket(). + // When ts is 0 (the default), call uv_hrtime() here. The batched + // receive path caches a timestamp and passes it to all ReadPacket() + // calls in the same I/O burst. + if (ts == 0) ts = uv_hrtime(); + err = ngtcp2_conn_read_pkt(*this, &path, pkt_info, data, len, ts); } if (is_destroyed()) return false; - Debug(this, "Session receiving %zu-byte packet with result %d", vec.len, err); + Debug(this, "Session receiving %zu-byte packet with result %d", len, err); switch (err) { case 0: { - Debug(this, "Session successfully received %zu-byte packet", vec.len); + Debug(this, "Session successfully received %zu-byte packet", len); if (!is_destroyed()) [[likely]] { - auto& stats_ = impl_->stats_; - STAT_INCREMENT_N(Stats, bytes_received, vec.len); + STAT_INCREMENT_N(Stats, bytes_received, len); // Process deferred operations that couldn't run inside callback // scopes (e.g., HTTP/3 GOAWAY handling that calls into JS). application().PostReceive(); @@ -2178,7 +2338,7 @@ bool Session::Receive(Store&& store, // There is no point in waiting for a draining period — the // peer has no state. Close immediately with an error. if (!is_destroyed()) [[likely]] { - if (impl_->state_->stateless_reset) { + if (impl_->state()->stateless_reset) { Debug(this, "Session received stateless reset, closing"); SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_DRAINING)); Close(CloseMethod::SILENT); @@ -2212,13 +2372,15 @@ bool Session::Receive(Store&& store, DCHECK(is_server()); Debug(this, "Receiving packet failed: Server must send a retry packet"); if (!is_destroyed()) { - endpoint().SendRetry(PathDescriptor{ - version(), - config().dcid, - config().scid, - impl_->local_address_, - impl_->remote_address_, - }); + endpoint().SendRetry( + PathDescriptor{ + version(), + config().dcid, + config().scid, + impl_->local_address_, + impl_->remote_address_, + }, + uv_hrtime()); Close(CloseMethod::SILENT); } return false; @@ -2245,6 +2407,72 @@ bool Session::Receive(Store&& store, return false; } +void Session::SendBatch(Packet::Ptr* packets, + PathStorage* paths, + size_t count) { + DCHECK(!is_destroyed()); + if (count == 0) return; + + // Separate packets into those going to the primary endpoint and those + // redirected to other endpoints (rare: path validation, preferred address). + // Redirected packets are sent individually via the target endpoint. + static constexpr size_t kMaxBatch = 64; + DCHECK_LE(count, kMaxBatch); + Packet::Ptr primary_packets[kMaxBatch]; + size_t primary_count = 0; + + for (size_t i = 0; i < count; i++) { + if (!packets[i] || !can_send_packets()) { + packets[i].reset(); + continue; + } + + UpdatePath(paths[i]); + + // Check for cross-endpoint redirect. + bool redirected = false; + if (paths[i].path.local.addrlen > 0) { + SocketAddress local_addr(paths[i].path.local.addr); + auto& mgr = BindingData::Get(env()).session_manager(); + Endpoint* target = mgr.FindEndpointForAddress(local_addr); + if (target != nullptr && target != &endpoint()) { + SocketAddress remote_addr(paths[i].path.remote.addr); + packets[i]->Redirect(static_cast(target), + remote_addr); + target->Send(std::move(packets[i])); + redirected = true; + } + } + + if (!redirected) { + primary_packets[primary_count++] = std::move(packets[i]); + } + } + + if (primary_count == 0) return; + + // Use batched send for the primary endpoint. + if (flags_.prefer_try_send) { + endpoint().SendBatch(primary_packets, primary_count); + } else { + // Non-flush path: send individually via async uv_udp_send. + for (size_t i = 0; i < primary_count; i++) { + Send(std::move(primary_packets[i])); + } + } +} + +void Session::FlushPendingData() { + DCHECK(!is_destroyed()); + if (impl_->application_) { + // Prefer synchronous sends during the deferred flush to avoid the + // one-tick latency of async uv_udp_send from the uv_check callback. + flags_.prefer_try_send = true; + application().SendPendingData(); + flags_.prefer_try_send = false; + } +} + void Session::Send(Packet::Ptr packet) { // Sending a Packet is generally best effort. If we're not in a state // where we can send a packet, it's ok to drop it on the floor. The @@ -2261,6 +2489,16 @@ void Session::Send(Packet::Ptr packet) { return; } + // When called from the deferred flush path (uv_check callback), + // prefer synchronous send to avoid the one-tick latency of async + // uv_udp_send. SendOrTrySend uses uv_udp_try_send first, falling + // back to uv_udp_send on EAGAIN. + if (flags_.prefer_try_send) { + Debug(this, "Session is sending (try_send) %s", packet->ToString()); + endpoint().SendOrTrySend(std::move(packet)); + return; + } + Debug(this, "Session is sending %s", packet->ToString()); endpoint().Send(std::move(packet)); } @@ -2333,10 +2571,10 @@ datagram_id Session::SendDatagram(Store&& data) { } // Assign the datagram ID. - datagram_id did = ++impl_->state_->last_datagram_id; + datagram_id did = ++impl_->state()->last_datagram_id; // Check queue capacity. Apply the drop policy when full. - auto max_pending = impl_->state_->max_pending_datagrams; + auto max_pending = impl_->state()->max_pending_datagrams; if (max_pending > 0 && impl_->pending_datagrams_.size() >= max_pending) { auto drop_policy = impl_->config_.options.datagram_drop_policy; if (drop_policy == DatagramDropPolicy::DROP_OLDEST) { @@ -2507,7 +2745,6 @@ void Session::AddStream(BaseObjectPtr stream, // Update tracking statistics for the number of streams associated with this // session. - auto& stats_ = impl_->stats_; if (ngtcp2_conn_is_local_stream(*this, id)) { switch (direction) { case Direction::BIDIRECTIONAL: { @@ -2559,7 +2796,7 @@ void Session::RemoveStream(stream_id id) { // then we can proceed to finishing the close now. Note that the // expectation is that the session will be destroyed once FinishClose // returns. - if (impl_->state_->closing && impl_->state_->graceful_close) { + if (impl_->state()->closing && impl_->state()->graceful_close) { FinishClose(); CHECK(is_destroyed()); } @@ -2575,29 +2812,40 @@ void Session::ShutdownStream(stream_id id, QuicError error) { DCHECK(!is_destroyed()); Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream(*this, - 0, - id, - error.type() == QuicError::Type::APPLICATION - ? error.code() - : application().GetNoErrorCode()); + // STOP_SENDING and RESET_STREAM frames carry application-level error + // codes (RFC 9000 §19.4, §19.5). Map the QuicError to an appropriate + // application code: APPLICATION errors pass through directly; transport + // no-error maps to the application's no-error code; any other error + // maps to the application's internal error code. + error_code code; + if (error.type() == QuicError::Type::APPLICATION) { + code = error.code(); + } else if (error.code() == NGTCP2_NO_ERROR) { + code = application().GetNoErrorCode(); + } else { + code = application().GetInternalErrorCode(); + } + ngtcp2_conn_shutdown_stream(*this, 0, id, code); } -void Session::ShutdownStreamWrite(stream_id id, QuicError code) { +void Session::ShutdownStreamWrite(stream_id id, QuicError error) { DCHECK(!is_destroyed()); - Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); + Debug( + this, "Shutting down stream %" PRIi64 " write with error %s", id, error); SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream_write(*this, - 0, - id, - code.type() == QuicError::Type::APPLICATION - ? code.code() - : application().GetNoErrorCode()); + error_code code; + if (error.type() == QuicError::Type::APPLICATION) { + code = error.code(); + } else if (error.code() == NGTCP2_NO_ERROR) { + code = application().GetNoErrorCode(); + } else { + code = application().GetInternalErrorCode(); + } + ngtcp2_conn_shutdown_stream_write(*this, 0, id, code); } void Session::StreamDataBlocked(stream_id id) { DCHECK(!is_destroyed()); - auto& stats_ = impl_->stats_; STAT_INCREMENT(Stats, block_count); application().BlockStream(id); } @@ -2668,20 +2916,20 @@ bool Session::is_in_draining_period() const { bool Session::wants_session_ticket() const { return !is_destroyed() && - HasListenerFlag(impl_->state_->listener_flags, + HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::SESSION_TICKET); } void Session::SetStreamOpenAllowed() { DCHECK(!is_destroyed()); - impl_->state_->stream_open_allowed = 1; + impl_->state()->stream_open_allowed = 1; } void Session::PopulateEarlyTransportParamsState() { DCHECK(!is_destroyed()); const ngtcp2_transport_params* tp = remote_transport_params(); if (tp != nullptr) { - impl_->state_->max_datagram_size = + impl_->state()->max_datagram_size = MaxDatagramPayload(tp->max_datagram_frame_size); } } @@ -2692,7 +2940,7 @@ bool Session::can_send_packets() const { // or closing period. The callback scope check is per-session so that // one session's ngtcp2 callback does not block unrelated sessions // from sending. - return !is_destroyed() && !in_ngtcp2_callback_scope_ && + return !is_destroyed() && !flags_.in_ngtcp2_callback_scope && !is_in_draining_period() && !is_in_closing_period(); } @@ -2702,7 +2950,7 @@ bool Session::can_create_streams() const { } bool Session::can_open_streams() const { - return !is_destroyed() && impl_->state_->stream_open_allowed; + return !is_destroyed() && impl_->state()->stream_open_allowed; } uint64_t Session::max_data_left() const { @@ -2723,12 +2971,12 @@ uint64_t Session::max_local_streams_bidi() const { void Session::set_wrapped() { DCHECK(!is_destroyed()); - impl_->state_->wrapped = 1; + impl_->state()->wrapped = 1; } void Session::set_priority_supported(bool on) { DCHECK(!is_destroyed()); - impl_->state_->priority_supported = on ? 1 : 0; + impl_->state()->priority_supported = on ? 1 : 0; } void Session::ExtendStreamOffset(stream_id id, size_t amount) { @@ -2761,14 +3009,13 @@ size_t Session::PendingDatagramCount() const { void Session::DatagramSent(datagram_id id) { Debug(this, "Datagram %" PRIu64 " sent", id); - auto& stats_ = impl_->stats_; STAT_INCREMENT(Stats, datagrams_sent); } void Session::UpdateDataStats() { if (is_destroyed()) return; Debug(this, "Updating data stats"); - auto& stats_ = impl_->stats_; + ngtcp2_conn_info info; ngtcp2_conn_get_conn_info(*this, &info); STAT_SET(Stats, bytes_in_flight, info.bytes_in_flight); @@ -2793,6 +3040,39 @@ void Session::UpdateDataStats() { std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); } +void Session::CheckStreamIdleTimeout(uint64_t now) { + if (is_destroyed()) return; + uint64_t timeout = options().stream_idle_timeout; + if (timeout == 0) return; + + uint64_t timeout_ns = timeout * NGTCP2_MILLISECONDS; + auto all_streams = streams(); + + for (const auto& [id, stream] : all_streams) { + if (!stream) continue; + + // Only check peer-initiated streams. Locally-initiated streams + // that haven't been written to are the application's concern. + if (ngtcp2_conn_is_local_stream(*this, id)) continue; + + uint64_t last_activity = stream->last_activity_timestamp(); + if (last_activity > 0 && (now - last_activity) > timeout_ns) { + Debug(this, "Stream %" PRId64 " idle timeout exceeded, destroying", id); + // Notify the peer before destroying. ShutdownStream sends both + // STOP_SENDING and RESET_STREAM as appropriate, using the + // application's no-error code for non-APPLICATION errors (since + // these frames carry application-level error codes per RFC 9000). + // Without this, the peer's stream sits orphaned until the + // session closes. + auto error = + QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout"); + ShutdownStream(id, error); + stream->Destroy(error); + STAT_INCREMENT(Stats, streams_idle_timed_out); + } + } +} + void Session::SendConnectionClose() { // Method is a non-op if the session is already destroyed or the // endpoint cannot send. Note: we intentionally do NOT check @@ -2857,6 +3137,15 @@ void Session::SendConnectionClose() { void Session::OnTimeout() { if (is_destroyed()) return; if (!impl_->application_) return; + // Hold a strong reference to prevent the Session from being freed during + // re-entrant calls. SendPendingData's scope guard calls UpdateTimer(), + // which can synchronously re-enter OnTimeout() when the timer has already + // expired. That re-entrant path can reach FinishClose → EmitClose → + // Destroy → impl_.reset() → ~Impl → RemoveSession(), dropping the last + // BaseObjectPtr from the endpoint map and freeing the Session. Without + // this guard, the outer OnTimeout / SendPendingData frames would operate + // on a freed object. + BaseObjectPtr ref(this); HandleScope scope(env()->isolate()); int ret; { @@ -2868,6 +3157,8 @@ void Session::OnTimeout() { if (is_destroyed()) return; if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { application().SendPendingData(); + if (is_destroyed()) return; + CheckStreamIdleTimeout(uv_hrtime()); return; } if (is_destroyed()) return; @@ -2914,6 +3205,15 @@ void Session::UpdateTimer() { auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; Debug(this, "Updating timeout to %zu milliseconds", timeout); + // If a stream idle timeout is configured, ensure the timer fires at + // least that often so CheckStreamIdleTimeout runs. Without this, an + // idle session with idle streams might not fire the timer until the + // connection idle timeout, which could be much longer. + uint64_t stream_idle = options().stream_idle_timeout; + if (stream_idle > 0 && timeout > stream_idle) { + timeout = stream_idle; + } + // If timeout is zero here, it means our timer is less than a millisecond // off from expiry. Let's bump the timer to 1. impl_->timer_.Update(timeout == 0 ? 1 : timeout); @@ -2922,7 +3222,7 @@ void Session::UpdateTimer() { void Session::DatagramStatus(datagram_id datagramId, quic::DatagramStatus status) { DCHECK(!is_destroyed()); - auto& stats_ = impl_->stats_; + switch (status) { case DatagramStatus::ACKNOWLEDGED: { Debug(this, "Datagram %" PRIu64 " was acknowledged", datagramId); @@ -2940,7 +3240,7 @@ void Session::DatagramStatus(datagram_id datagramId, break; } } - if (HasListenerFlag(impl_->state_->listener_flags, + if (HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::DATAGRAM_STATUS)) { EmitDatagramStatus(datagramId, status); } @@ -2952,13 +3252,13 @@ void Session::DatagramReceived(const uint8_t* data, DCHECK(!is_destroyed()); // If there is nothing watching for the datagram on the JavaScript side, // or if the datagram is zero-length, we just drop it on the floor. - if (!HasListenerFlag(impl_->state_->listener_flags, + if (!HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::DATAGRAM) || datalen == 0) return; Debug(this, "Session is receiving datagram of size %zu", datalen); - auto& stats_ = impl_->stats_; + STAT_INCREMENT(Stats, datagrams_received); JS_TRY_ALLOCATE_BACKING(env(), backing, datalen) memcpy(backing->Data(), data, datalen); @@ -2979,18 +3279,18 @@ void Session::GenerateNewConnectionId(ngtcp2_cid* cid, bool Session::HandshakeCompleted() { DCHECK(!is_destroyed()); - DCHECK(!impl_->state_->handshake_completed); + DCHECK(!impl_->state()->handshake_completed); Debug(this, "Session handshake completed"); - impl_->state_->handshake_completed = 1; - auto& stats_ = impl_->stats_; + impl_->state()->handshake_completed = 1; + STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); SetStreamOpenAllowed(); // Capture the peer's max datagram frame size from the remote transport // parameters so JavaScript can check it without a C++ round-trip. const ngtcp2_transport_params* tp = remote_transport_params(); - impl_->state_->max_datagram_size = + impl_->state()->max_datagram_size = MaxDatagramPayload(tp->max_datagram_frame_size); // If early data was attempted but rejected by the server, @@ -3025,10 +3325,10 @@ bool Session::HandshakeCompleted() { void Session::HandshakeConfirmed() { DCHECK(!is_destroyed()); - DCHECK(!impl_->state_->handshake_confirmed); + DCHECK(!impl_->state()->handshake_confirmed); Debug(this, "Session handshake confirmed"); - impl_->state_->handshake_confirmed = 1; - auto& stats_ = impl_->stats_; + impl_->state()->handshake_confirmed = 1; + STAT_RECORD_TIMESTAMP(Stats, handshake_confirmed_at); } @@ -3151,12 +3451,21 @@ void Session::EmitClose(const QuicError& error) { Integer::New(env()->isolate(), static_cast(error.type())), BigInt::NewFromUnsigned(env()->isolate(), error.code()), Undefined(env()->isolate()), + Undefined(env()->isolate()), }; if (error.reason().length() > 0 && !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { return; } + // Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1 + // names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown + // codes leave the slot as undefined. See QuicError::name() for the + // matching path on stream-level errors. + if (const char* n = error.name()) { + argv[3] = BindingData::Get(env()).error_name_string(n); + } + MakeCallback( BindingData::Get(env()).session_close_callback(), arraysize(argv), argv); @@ -3166,7 +3475,7 @@ void Session::EmitClose(const QuicError& error) { void Session::set_max_datagram_size(uint16_t size) { if (!is_destroyed()) { - impl_->state_->max_datagram_size = size; + impl_->state()->max_datagram_size = size; } } @@ -3281,7 +3590,7 @@ void Session::EmitPathValidation(PathValidationResult result, if (!env()->can_call_into_js()) return; - if (!HasListenerFlag(impl_->state_->listener_flags, + if (!HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::PATH_VALIDATION)) [[likely]] { return; } @@ -3327,7 +3636,7 @@ void Session::EmitSessionTicket(Store&& ticket) { // If there is nothing listening for the session ticket, don't bother // emitting. - if (!HasListenerFlag(impl_->state_->listener_flags, + if (!HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::SESSION_TICKET)) [[likely]] { Debug(this, "Session ticket was discarded"); return; @@ -3384,7 +3693,7 @@ void Session::EmitEarlyDataRejected() { void Session::EmitNewToken(const uint8_t* token, size_t len) { DCHECK(!is_destroyed()); - if (!HasListenerFlag(impl_->state_->listener_flags, + if (!HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::NEW_TOKEN)) return; if (!env()->can_call_into_js()) return; @@ -3460,7 +3769,7 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, void Session::EmitOrigins(std::vector&& origins) { DCHECK(!is_destroyed()); - if (!HasListenerFlag(impl_->state_->listener_flags, + if (!HasListenerFlag(impl_->state()->listener_flags, SessionListenerFlags::ORIGIN)) return; if (!env()->can_call_into_js()) return; @@ -3550,6 +3859,10 @@ void Session::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX); NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN); + static constexpr auto DEFAULT_HANDSHAKE_TIMEOUT = + Session::Options::DEFAULT_HANDSHAKE_TIMEOUT; + NODE_DEFINE_CONSTANT(target, DEFAULT_HANDSHAKE_TIMEOUT); + NODE_DEFINE_STRING_CONSTANT( target, "DEFAULT_CIPHERS", TLSContext::DEFAULT_CIPHERS); NODE_DEFINE_STRING_CONSTANT( @@ -3564,6 +3877,8 @@ void Session::InitPerContext(Realm* realm, Local target) { SESSION_STATE(V) #undef V + NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT); + #define V(name, _) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_##name); SESSION_STATS(V) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT); diff --git a/src/quic/session.h b/src/quic/session.h index 650e8f79ba1428..64c9b8a84d7cf0 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -72,7 +72,11 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint64_t max_header_length = DEFAULT_MAX_HEADER_LENGTH; // HTTP/3 specific options. - uint64_t max_field_section_size = 0; + // The maximum header section size advertised to the peer in SETTINGS. + // Defaults to match max_header_length so the SETTINGS frame accurately + // reflects the enforcement limit. A value of 0 would incorrectly tell + // the peer not to send any headers at all. + uint64_t max_field_section_size = DEFAULT_MAX_HEADER_LENGTH; uint64_t qpack_max_dtable_capacity = 4096; uint64_t qpack_encoder_max_dtable_capacity = 4096; uint64_t qpack_blocked_streams = 100; @@ -91,6 +95,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { std::string ToString() const; + v8::MaybeLocal ToObject(Environment* env) const; + static const Application_Options kDefault; }; @@ -153,8 +159,24 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool qlog = false; // The amount of time (in milliseconds) that the endpoint will wait for the - // completion of the tls handshake. - uint64_t handshake_timeout = UINT64_MAX; + // completion of the TLS handshake. If the handshake does not complete + // within this time, the session is closed. This prevents a peer from + // holding a session open indefinitely in the handshake state, consuming + // server resources (ngtcp2 connection, TLS state, JS objects) without + // ever completing the connection. The default of 10 seconds is generous + // enough to accommodate slow networks with retransmissions while still + // bounding resource exposure. Set to UINT64_MAX to disable. + static constexpr uint64_t DEFAULT_HANDSHAKE_TIMEOUT = 10'000; + uint64_t handshake_timeout = DEFAULT_HANDSHAKE_TIMEOUT; + + // The initial round-trip time estimate in milliseconds. ngtcp2 uses this + // for PTO computation, initial pacing, and early loss detection before + // the first RTT sample is collected. The default of 0 uses ngtcp2's + // built-in default of 333ms, which is appropriate for the general + // internet. For low-latency environments (e.g., loopback or same-rack + // deployments), setting a value closer to the actual RTT avoids + // unnecessarily conservative initial behavior. + uint64_t initial_rtt = 0; // The keep-alive timeout in milliseconds. When set to a non-zero value, // ngtcp2 will automatically send PING frames to keep the connection alive @@ -205,6 +227,14 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // 10.2 requires at least 3x PTO. Range: 3-255. Default: 3. uint8_t draining_period_multiplier = 3; + // The amount of time (in milliseconds) that a stream can be idle + // (no data received) before it is automatically destroyed. This + // protects against slowloris-style attacks where a peer opens streams + // but never sends data, holding server resources indefinitely. + // Only applies to peer-initiated streams. Set to 0 to disable. + static constexpr uint64_t DEFAULT_STREAM_IDLE_TIMEOUT = 30'000; + uint64_t stream_idle_timeout = DEFAULT_STREAM_IDLE_TIMEOUT; + // An optional NEW_TOKEN from a previous connection to the same // server. When set, the token is included in the Initial packet // to skip address validation. Client-side only. @@ -308,6 +338,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint32_t version() const; Endpoint& endpoint() const; TLSSession& tls_session() const; + bool has_application() const; Application& application() const; const Config& config() const; const Options& options() const; @@ -353,9 +384,45 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool early = false; }; - bool Receive(Store&& store, + bool Receive(const uint8_t* data, + size_t len, const SocketAddress& local_address, - const SocketAddress& remote_address); + const SocketAddress& remote_address, + const PacketInfo& pkt_info = PacketInfo(), + uint64_t ts = 0); + + // ReadPacket processes a single inbound packet through ngtcp2 without + // triggering SendPendingData. This is the building block for batched + // receive processing: the caller (Endpoint::Receive) accumulates + // dirty sessions and a uv_check callback flushes them after all + // packets in the I/O burst have been read. + // Receive() is kept as a convenience wrapper that calls ReadPacket() + // then triggers SendPendingData (for paths like Connect that need + // immediate response). + // The data pointer is used synchronously — ngtcp2_conn_read_pkt does + // not retain a reference after returning, so the caller's buffer can + // be reused immediately. + // When ts is 0 (the default), uv_hrtime() is called internally. + // The batched receive path caches a timestamp and passes it to all + // ReadPacket() calls in the same I/O burst. + bool ReadPacket(const uint8_t* data, + size_t len, + const SocketAddress& local_address, + const SocketAddress& remote_address, + const PacketInfo& pkt_info = PacketInfo(), + uint64_t ts = 0); + + // Called by BindingData's flush callback to trigger SendPendingData + // on this session. Encapsulates the application() access so that + // bindingdata.cc doesn't need the full Application type definition. + void FlushPendingData(); + + // Send a batch of packets accumulated by SendPendingData. Uses + // Endpoint::SendBatch (uv_udp_try_send2 / sendmmsg) for synchronous + // batched delivery when called from the deferred flush path. + // Handles per-packet path updates and cross-endpoint redirects. + // All Ptr entries are consumed (released or moved) on return. + void SendBatch(Packet::Ptr* packets, PathStorage* paths, size_t count); void Send(Packet::Ptr packet); void Send(Packet::Ptr packet, const PathStorage& path); @@ -510,6 +577,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // Has to be called after certain operations that generate packets. void UpdatePacketTxTime(); void UpdateDataStats(); + void CheckStreamIdleTimeout(uint64_t now); void UpdatePath(const PathStorage& path); void ProcessPendingBidiStreams(); @@ -563,20 +631,36 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { Side side_; const ngtcp2_mem* allocator_; std::unique_ptr impl_; - // These flags live on Session (not Impl) so that the NgTcp2CallbackScope - // and NgHttp3CallbackScope destructors can safely clear them even after - // Impl has been destroyed via MakeCallback re-entrancy during a callback. - // The scope is placed at the ngtcp2/nghttp3 entry point (e.g. Receive, - // OnTimeout) rather than on individual callbacks, so the deferred destroy - // only fires after all callbacks for that entry point have completed. - bool in_ngtcp2_callback_scope_ = false; - bool in_nghttp3_callback_scope_ = false; - bool destroy_deferred_ = false; + + struct Flags { + // These flags live on Session (not Impl) so that the NgTcp2CallbackScope + // and NgHttp3CallbackScope destructors can safely clear them even after + // Impl has been destroyed via MakeCallback re-entrancy during a callback. + // The scope is placed at the ngtcp2/nghttp3 entry point (e.g. Receive, + // OnTimeout) rather than on individual callbacks, so the deferred destroy + // only fires after all callbacks for that entry point have completed. + uint8_t in_ngtcp2_callback_scope : 1 = 0; + uint8_t in_nghttp3_callback_scope : 1 = 0; + uint8_t destroy_deferred : 1 = 0; + // Set when this session is in BindingData's pending_flush_sessions_ vector. + // Cleared by the flush callback before calling SendPendingData. + // Provides O(1) dedup so a session receiving multiple packets in one I/O + // burst is only scheduled for flush once. + uint8_t pending_flush : 1 = 0; + // When true, Session::Send prefers synchronous delivery via + // Endpoint::SendOrTrySend (uv_udp_try_send with async fallback). + // Set during FlushPendingData to avoid the one-tick latency of + // async-only sends from the uv_check callback. + uint8_t prefer_try_send : 1 = 0; + }; + Flags flags_; + QuicConnectionPointer connection_; std::unique_ptr tls_session_; friend struct NgTcp2CallbackScope; friend struct NgHttp3CallbackScope; friend class Application; + friend class BindingData; friend class DefaultApplication; friend class Http3ApplicationImpl; friend class Endpoint; diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 81e619e28d3720..7186aed89a78e9 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -23,17 +23,21 @@ using v8::Array; using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::BackingStore; +using v8::BackingStoreInitializationMode; using v8::BigInt; using v8::FunctionCallbackInfo; using v8::Global; +using v8::HandleScope; using v8::Integer; using v8::Just; using v8::Local; +using v8::LocalVector; using v8::Maybe; using v8::Nothing; using v8::Object; using v8::ObjectTemplate; using v8::SharedArrayBuffer; +using v8::String; using v8::Uint32; using v8::Uint8Array; using v8::Value; @@ -83,7 +87,11 @@ namespace quic { V(MAX_OFFSET, max_offset) \ V(MAX_OFFSET_ACK, max_offset_ack) \ V(MAX_OFFSET_RECV, max_offset_received) \ - V(FINAL_SIZE, final_size) + V(FINAL_SIZE, final_size) \ + /* Bytes in the receive accumulation buffer */ \ + V(BYTES_ACCUMULATED, bytes_accumulated) \ + /* Peak bytes accumulated over stream lifetime */ \ + V(MAX_BYTES_ACCUMULATED, max_bytes_accumulated) #define STREAM_JS_METHODS(V) \ V(AttachSource, attachSource, false) \ @@ -98,6 +106,91 @@ namespace quic { V(Write, write, false) \ V(EndWrite, endWrite, false) +// ============================================================================ +// RecvAccumulator implementation + +RecvAccumulator::RecvAccumulator(size_t max_capacity) + : buf_(kMinCapacity), max_capacity_(std::max(max_capacity, kMinCapacity)) {} + +size_t RecvAccumulator::Write(const uint8_t* data, size_t len) { + if (len == 0) return 0; + + size_t capacity = buf_.size(); + + // If the buffer is full, caller must flush or grow first. + size_t space = remaining(); + if (space == 0) return 0; + + size_t to_write = std::min(len, space); + + // Write into the buffer, handling wrap-around. + size_t physical_write = write_pos_ % capacity; + size_t first_chunk = std::min(to_write, capacity - physical_write); + memcpy(buf_.data() + physical_write, data, first_chunk); + if (first_chunk < to_write) { + memcpy(buf_.data(), data + first_chunk, to_write - first_chunk); + } + + write_pos_ += to_write; + len_ += to_write; + return to_write; +} + +std::unique_ptr RecvAccumulator::Flush(Environment* env) { + if (len_ == 0) return nullptr; + + size_t capacity = buf_.size(); + + auto store = ArrayBuffer::NewBackingStore( + env->isolate(), len_, BackingStoreInitializationMode::kUninitialized); + auto* dest = static_cast(store->Data()); + + // Copy from the ring buffer. Handle the wrap-around case. + size_t physical_read = read_pos_ % capacity; + size_t first_chunk = std::min(len_, capacity - physical_read); + memcpy(dest, buf_.data() + physical_read, first_chunk); + if (first_chunk < len_) { + memcpy(dest + first_chunk, buf_.data(), len_ - first_chunk); + } + + size_t flushed = len_; + + // Reset cursors. + read_pos_ = 0; + write_pos_ = 0; + len_ = 0; + + // Shrink back toward kMinCapacity if we had expanded. + if (capacity > kMinCapacity) { + buf_.resize(kMinCapacity); + buf_.shrink_to_fit(); + } + + return DataQueue::CreateInMemoryEntryFromBackingStore( + std::move(store), 0, flushed); +} + +void RecvAccumulator::Grow() { + size_t capacity = buf_.size(); + size_t new_capacity = std::min(capacity * 2, max_capacity_); + if (new_capacity <= capacity) return; // already at max + + // Linearize the data and grow in one step. + std::vector new_buf(new_capacity); + if (len_ > 0) { + size_t physical_read = read_pos_ % capacity; + size_t first_chunk = std::min(len_, capacity - physical_read); + memcpy(new_buf.data(), buf_.data() + physical_read, first_chunk); + if (first_chunk < len_) { + memcpy(new_buf.data() + first_chunk, buf_.data(), len_ - first_chunk); + } + } + + buf_ = std::move(new_buf); + read_pos_ = 0; + write_pos_ = len_; +} + // ============================================================================ PendingStream::PendingStream(Direction direction, @@ -151,6 +244,44 @@ struct Stream::State { STAT_STRUCT(Stream, STREAM) +// Stream uses arena-allocated stats, not AliasedStruct, so override the +// STAT_* macros to use the stats() accessor instead of stats_.Data(). +#undef STAT_INCREMENT +#undef STAT_INCREMENT_N +#undef STAT_RECORD_TIMESTAMP +#undef STAT_SET +#undef STAT_GET +#define STAT_INCREMENT(Type, name) IncrementStat(stats()); +#define STAT_INCREMENT_N(Type, name, amt) \ + IncrementStat(stats(), amt); +#define STAT_RECORD_TIMESTAMP(Type, name) \ + RecordTimestampStat(stats()); +#define STAT_SET(Type, name, val) SetStat(stats(), val) +#define STAT_GET(Type, name) GetStat(stats()) + +using StreamStateArena = AliasedStructArena; +using StreamStatsArena = AliasedStructArena; + +namespace { +StreamStateArena& GetStreamStateArena(BindingData& binding) { + if (!binding.stream_state_arena_) { + auto* arena = new StreamStateArena(); + binding.stream_state_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.stream_state_arena_.get()); +} + +StreamStatsArena& GetStreamStatsArena(BindingData& binding) { + if (!binding.stream_stats_arena_) { + auto* arena = new StreamStatsArena(); + binding.stream_stats_arena_ = BindingData::ArenaPtr( + arena, +[](void* p) { delete static_cast(p); }); + } + return *static_cast(binding.stream_stats_arena_.get()); +} +} // namespace + // ============================================================================ namespace { @@ -239,7 +370,7 @@ Maybe> Stream::GetDataQueueFromSource( // object's constructor name is "FileHandle". if (value->IsObject()) { auto obj = value.As(); - Local ctor_name; + Local ctor_name; auto maybe_name = obj->GetConstructorName(); if (!maybe_name.IsEmpty()) { ctor_name = maybe_name; @@ -249,8 +380,7 @@ Maybe> Stream::GetDataQueueFromSource( ASSIGN_OR_RETURN_UNWRAP( &file_handle, value, Nothing>()); Local path; - if (!v8::String::NewFromUtf8(env->isolate(), - file_handle->original_name().c_str()) + if (!ToV8Value(env->context(), file_handle->original_name()) .ToLocal(&path)) { return Nothing>(); } @@ -382,14 +512,14 @@ struct Stream::Impl { code = args[0].As()->Uint64Value(&lossless); } - if (stream->state_->reset == 1) return; + if (stream->state()->reset == 1) return; stream->EndWritable(); // We can release our outbound here now. Since the stream is being reset // on the ngtcp2 side, we do not need to keep any of the data around // waiting for acknowledgement that will never come. stream->outbound_.reset(); - stream->state_->reset = 1; + stream->state()->reset = 1; if (!stream->is_pending()) { if (stream->is_remote_unidirectional()) return; @@ -490,8 +620,9 @@ class Stream::Outbound final : public MemoryRetainer { explicit Outbound(Stream* stream) : stream_(stream), queue_(DataQueue::Create()), - reader_(queue_->get_reader()), - streaming_(true) {} + reader_(queue_->get_reader()) { + flags_.streaming = true; + } void Acknowledge(size_t amount) { size_t remaining = std::min(amount, total_ - uncommitted_); @@ -564,7 +695,7 @@ class Stream::Outbound final : public MemoryRetainer { if (queue_) queue_->cap(); } - bool is_streaming() const { return streaming_; } + bool is_streaming() const { return flags_.streaming; } size_t total() const { return total_; } size_t uncommitted() const { return uncommitted_; } @@ -576,7 +707,7 @@ class Stream::Outbound final : public MemoryRetainer { // Appends an entry to the underlying DataQueue. Only valid when // the Outbound was created in streaming mode. bool AppendEntry(std::unique_ptr entry) { - if (!streaming_ || !queue_) return false; + if (!flags_.streaming || !queue_) return false; auto size = entry->size(); auto result = queue_->append(std::move(entry)); if (result.has_value() && result.value()) { @@ -591,7 +722,7 @@ class Stream::Outbound final : public MemoryRetainer { ngtcp2_vec* data, size_t count, size_t max_count_hint) { - if (next_pending_) { + if (flags_.next_pending) { // An async read is in flight, but there may be uncommitted bytes // from a previous read that ngtcp2 didn't accept (nwrite=0 due // to pacing/congestion). Return those bytes so the send loop can @@ -604,14 +735,14 @@ class Stream::Outbound final : public MemoryRetainer { return bob::Status::STATUS_BLOCK; } - if (errored_) { + if (flags_.errored) { std::move(next)(UV_EBADF, nullptr, 0, [](int) {}); return UV_EBADF; } // If eos_ is true and there are no uncommitted bytes we'll return eos, // otherwise, return whatever is in the uncommitted queue. - if (eos_) { + if (flags_.eos) { if (uncommitted_ > 0) { PullUncommitted(std::move(next)); return bob::Status::STATUS_CONTINUE; @@ -644,8 +775,8 @@ class Stream::Outbound final : public MemoryRetainer { // If next_pending_ is true then a pull from the reader ended up // being asynchronous, our stream is blocking waiting for the data, // but we have an error! oh no! We need to error the stream. - if (next_pending_) { - next_pending_ = false; + if (flags_.next_pending) { + flags_.next_pending = false; stream_->Destroy( QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR)); // We do not need to worry about calling MarkErrored in this case @@ -666,8 +797,8 @@ class Stream::Outbound final : public MemoryRetainer { // session will try to read from it again. // We must clear next_pending_ before calling ResumeStream because // ResumeStream can synchronously re-enter Outbound::Pull. - if (next_pending_) { - next_pending_ = false; + if (flags_.next_pending) { + flags_.next_pending = false; stream_->session().ResumeStream(stream_->id()); } return; @@ -693,8 +824,8 @@ class Stream::Outbound final : public MemoryRetainer { // pull from it again. // We must clear next_pending_ before calling ResumeStream because // ResumeStream can synchronously re-enter Outbound::Pull. - if (next_pending_) { - next_pending_ = false; + if (flags_.next_pending) { + flags_.next_pending = false; stream_->session().ResumeStream(stream_->id()); } }, @@ -756,7 +887,7 @@ class Stream::Outbound final : public MemoryRetainer { // rather than blocking — the async callback will resume the stream when // more data arrives. if (ret == bob::Status::STATUS_WAIT) { - next_pending_ = true; + flags_.next_pending = true; if (uncommitted_ > 0) { PullUncommitted(std::move(next)); return bob::Status::STATUS_CONTINUE; @@ -804,7 +935,7 @@ class Stream::Outbound final : public MemoryRetainer { } void MarkErrored() { - errored_ = true; + flags_.errored = true; head_.reset(); tail_ = nullptr; commit_head_ = nullptr; @@ -815,7 +946,7 @@ class Stream::Outbound final : public MemoryRetainer { } void MarkEnded() { - eos_ = true; + flags_.eos = true; queue_.reset(); reader_.reset(); } @@ -855,17 +986,17 @@ class Stream::Outbound final : public MemoryRetainer { std::shared_ptr queue_; std::shared_ptr reader_; - bool errored_ = false; - - // True when in streaming mode (non-idempotent queue, appendable). - bool streaming_ = false; - - // Will be set to true if the reader_ ends up providing a pull result - // asynchronously. - bool next_pending_ = false; - - // Will be set to true once reader_ has returned eos. - bool eos_ = false; + struct Flags { + uint8_t errored : 1 = 0; + // True when in streaming mode (non-idempotent queue, appendable). + uint8_t streaming : 1 = 0; + // Will be set to true if the reader_ ends up providing a pull result + // asynchronously. + uint8_t next_pending : 1 = 0; + // Will be set to true once reader_ has returned eos. + uint8_t eos : 1 = 0; + }; + Flags flags_; // The collection of buffers that we have pulled from reader_ and that we // are holding onto until they are acknowledged. @@ -945,6 +1076,8 @@ void Stream::InitPerContext(Realm* realm, Local target) { STREAM_STATE(V) #undef V + NODE_DEFINE_CONSTANT(target, IDX_STATS_STREAM_COUNT); + constexpr int QUIC_STREAM_HEADERS_KIND_HINTS = static_cast(HeadersKind::HINTS); constexpr int QUIC_STREAM_HEADERS_KIND_INITIAL = @@ -993,30 +1126,55 @@ Stream::Stream(BaseObjectWeakPtr session, stream_id id, std::shared_ptr source) : AsyncWrap(session->env(), object, PROVIDER_QUIC_STREAM), - stats_(env()->isolate()), - state_(env()->isolate()), session_(std::move(session)), - inbound_(DataQueue::Create()), - headers_(env()->isolate()) { + inbound_(DataQueue::Create()) { + auto& binding = BindingData::Get(env()); + stats_slot_ = GetStreamStatsArena(binding).Allocate(env()->isolate()); + state_slot_ = GetStreamStateArena(binding).Allocate(env()->isolate()); MakeWeak(); DCHECK(id < kMaxStreamId); - state_->id = id; - state_->pending = 0; + state()->id = id; + state()->pending = 0; // Allows us to be notified when data is actually read from the // inbound queue so that we can update the stream flow control. inbound_->addBackpressureListener(this); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->state_string(), state_.GetArrayBuffer()); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->stats_string(), stats_.GetArrayBuffer()); + { + const HandleScope handle_scope(env()->isolate()); + // Pass the page's shared views and this slot's byte offset. JS uses + // the offset to index into the shared view — no per-stream V8 object + // creation. + JS_DEFINE_READONLY_PROPERTY(env(), + object, + env()->state_string(), + state_slot_.GetPageDataView(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "stateByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(state_slot_.GetByteOffset()))); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + env()->stats_string(), + stats_slot_.GetPageBigUint64Array(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "statsByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(stats_slot_.GetByteOffset()))); + } set_outbound(std::move(source)); STAT_RECORD_TIMESTAMP(Stats, created_at); auto params = ngtcp2_conn_get_local_transport_params(this->session()); STAT_SET(Stats, max_offset, params->initial_max_data); - STAT_SET(Stats, opened_at, stats_->created_at); + STAT_SET(Stats, opened_at, stats()->created_at); } Stream::Stream(BaseObjectWeakPtr session, @@ -1024,25 +1182,47 @@ Stream::Stream(BaseObjectWeakPtr session, Direction direction, std::shared_ptr source) : AsyncWrap(session->env(), object, PROVIDER_QUIC_STREAM), - stats_(env()->isolate()), - state_(env()->isolate()), session_(std::move(session)), inbound_(DataQueue::Create()), maybe_pending_stream_( - std::make_unique(direction, this, session_)), - headers_(env()->isolate()) { + std::make_unique(direction, this, session_)) { + auto& binding = BindingData::Get(env()); + stats_slot_ = GetStreamStatsArena(binding).Allocate(env()->isolate()); + state_slot_ = GetStreamStateArena(binding).Allocate(env()->isolate()); MakeWeak(); - state_->id = kMaxStreamId; - state_->pending = 1; + state()->id = kMaxStreamId; + state()->pending = 1; // Allows us to be notified when data is actually read from the // inbound queue so that we can update the stream flow control. inbound_->addBackpressureListener(this); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->state_string(), state_.GetArrayBuffer()); - JS_DEFINE_READONLY_PROPERTY( - env(), object, env()->stats_string(), stats_.GetArrayBuffer()); + { + const HandleScope handle_scope(env()->isolate()); + JS_DEFINE_READONLY_PROPERTY(env(), + object, + env()->state_string(), + state_slot_.GetPageDataView(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "stateByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(state_slot_.GetByteOffset()))); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + env()->stats_string(), + stats_slot_.GetPageBigUint64Array(env()->isolate())); + JS_DEFINE_READONLY_PROPERTY( + env(), + object, + FIXED_ONE_BYTE_STRING(env()->isolate(), "statsByteOffset"), + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(stats_slot_.GetByteOffset()))); + } set_outbound(std::move(source)); @@ -1053,15 +1233,24 @@ Stream::Stream(BaseObjectWeakPtr session, Stream::~Stream() { // Make sure that Destroy() was called before Stream is actually destructed. - DCHECK_NE(stats_->destroyed_at, 0); + DCHECK_NE(stats()->destroyed_at, 0); + + // Release arena slots back to the freelist. + auto& binding = BindingData::Get(env()); + if (stats_slot_) { + GetStreamStatsArena(binding).ReleaseSlot(stats_slot_); + } + if (state_slot_) { + GetStreamStateArena(binding).ReleaseSlot(state_slot_); + } } void Stream::NotifyStreamOpened(stream_id id) { CHECK(is_pending()); DCHECK(id < kMaxStreamId); Debug(this, "Pending stream opened with id %" PRIi64, id); - state_->pending = 0; - state_->id = id; + state()->pending = 0; + state()->id = id; STAT_RECORD_TIMESTAMP(Stats, opened_at); // Now that the stream is actually opened, add it to the sessions // list of known open streams. @@ -1081,7 +1270,8 @@ void Stream::NotifyStreamOpened(stream_id id) { // Headers were enqueued while the application was not yet known // (headers_supported == 0), and the negotiated application does // not support headers. This is a fatal mismatch. - Destroy(QuicError::ForApplication(0)); + Destroy(QuicError::ForApplication( + session().application().GetInternalErrorCode())); return; } decltype(pending_headers_queue_) queue; @@ -1132,32 +1322,37 @@ void Stream::EnqueuePendingHeaders(HeadersKind kind, } bool Stream::is_pending() const { - return state_->pending; + return state()->pending; } stream_id Stream::id() const { - return state_->id; + return state()->id; } Side Stream::origin() const { CHECK(!is_pending()); - return (state_->id & 0b01) ? Side::SERVER : Side::CLIENT; + return (state()->id & 0b01) ? Side::SERVER : Side::CLIENT; } Direction Stream::direction() const { - if (state_->pending) { + if (state()->pending) { CHECK(maybe_pending_stream_.has_value()); auto& val = maybe_pending_stream_.value(); return val->direction(); } - return (state_->id & 0b10) ? Direction::UNIDIRECTIONAL - : Direction::BIDIRECTIONAL; + return (state()->id & 0b10) ? Direction::UNIDIRECTIONAL + : Direction::BIDIRECTIONAL; } Session& Stream::session() const { return *session_; } +uint64_t Stream::last_activity_timestamp() const { + uint64_t ts = stats()->received_at; + return ts != 0 ? ts : stats()->created_at; +} + bool Stream::is_local_unidirectional() const { return direction() == Direction::UNIDIRECTIONAL && ngtcp2_conn_is_local_stream(*session_, id()); @@ -1169,15 +1364,15 @@ bool Stream::is_remote_unidirectional() const { } bool Stream::is_eos() const { - return state_->fin_sent; + return state()->fin_sent; } bool Stream::wants_trailers() const { - return state_->wants_trailers; + return state()->wants_trailers; } void Stream::set_early() { - state_->received_early_data = 1; + state()->received_early_data = 1; } bool Stream::is_writable() const { @@ -1187,7 +1382,7 @@ bool Stream::is_writable() const { !ngtcp2_conn_is_local_stream(session(), id())) { return false; } - return state_->write_ended == 0; + return state()->write_ended == 0; } bool Stream::has_outbound() const { @@ -1209,21 +1404,21 @@ bool Stream::is_readable() const { ngtcp2_conn_is_local_stream(session(), id())) { return false; } - return state_->read_ended == 0; + return state()->read_ended == 0; } BaseObjectPtr Stream::get_reader() { - if (!is_readable() || state_->has_reader) return {}; - state_->has_reader = 1; + if (!is_readable() || state()->has_reader) return {}; + state()->has_reader = 1; auto reader = Blob::Reader::Create(env(), Blob::Create(env(), inbound_)); reader_ = reader; return reader; } void Stream::set_final_size(uint64_t final_size) { - DCHECK_IMPLIES(state_->fin_received == 1, + DCHECK_IMPLIES(state()->fin_received == 1, final_size <= STAT_GET(Stats, final_size)); - state_->fin_received = 1; + state()->fin_received = 1; STAT_SET(Stats, final_size, final_size); } @@ -1232,7 +1427,7 @@ void Stream::set_outbound(std::shared_ptr source) { Debug(this, "Setting the outbound data source"); DCHECK_NULL(outbound_); outbound_ = std::make_unique(this, std::move(source)); - state_->has_outbound = 1; + state()->has_outbound = 1; // Note: We intentionally do NOT call ResumeStream here. During // construction, the stream has not yet been added to the session's // streams map, so FindStream would fail. The caller (CreateStream / @@ -1251,7 +1446,7 @@ void Stream::InitStreaming() { } Debug(this, "Initializing streaming outbound source"); outbound_ = std::make_unique(this); - state_->has_outbound = 1; + state()->has_outbound = 1; if (!is_pending()) session_->ResumeStream(id()); } @@ -1321,6 +1516,28 @@ void Stream::EntryRead(size_t amount) { session().ExtendOffset(amount); } +void Stream::BeforePull() { + // Called before the DataQueue reader pulls. Flush any accumulated + // receive data into the DataQueue so the pull finds it immediately. + if (recv_accumulator_ && recv_accumulator_->available() > 0) { + FlushAccumulation(); + } +} + +void Stream::FlushAccumulation() { + if (!recv_accumulator_ || recv_accumulator_->available() == 0) return; + auto entry = recv_accumulator_->Flush(env()); + if (entry) { + inbound_->append(std::move(entry)); + // Notify the reader that data is now available in the DataQueue. + // This is the only place we notify — not on every ReceiveData call — + // so the reader only wakes up when there is a well-sized entry to + // consume. + if (reader_) reader_->NotifyPull(); + } + STAT_SET(Stats, bytes_accumulated, 0); +} + int Stream::DoPull(bob::Next next, int options, ngtcp2_vec* data, @@ -1355,27 +1572,16 @@ void Stream::set_headers_kind(HeadersKind kind) { headers_kind_ = kind; } -bool Stream::AddHeader(const Header& header) { - size_t len = header.length(); +bool Stream::AddHeader(std::unique_ptr
                                            header) { + size_t len = header->length(); if (!session_->application().CanAddHeader( headers_.size(), headers_length_, len)) { return false; } headers_length_ += len; - - auto& state = BindingData::Get(env()); - - const auto push = [&](auto raw) { - Local value; - if (!raw.ToLocal(&value)) [[unlikely]] { - return false; - } - headers_.push_back(value); - return true; - }; - - return push(header.GetName(&state)) && push(header.GetValue(&state)); + headers_.push_back(std::move(header)); + return true; } void Stream::Acknowledge(size_t datalen) { @@ -1397,7 +1603,7 @@ void Stream::Commit(size_t datalen, bool fin) { Debug(this, "Committing %zu bytes", datalen); STAT_INCREMENT_N(Stats, bytes_sent, datalen); if (outbound_) outbound_->Commit(datalen); - if (fin) state_->fin_sent = 1; + if (fin) state()->fin_sent = 1; } void Stream::EndWritable() { @@ -1407,12 +1613,14 @@ void Stream::EndWritable() { // will be a non-op since we're not going to be writing any more data // into it anyway. if (outbound_) outbound_->Cap(); - state_->write_ended = 1; + state()->write_ended = 1; } void Stream::EndReadable(std::optional maybe_final_size) { if (!is_readable()) return; - state_->read_ended = 1; + state()->read_ended = 1; + // Flush any accumulated data before capping so the reader can see it. + FlushAccumulation(); set_final_size(maybe_final_size.value_or(STAT_GET(Stats, bytes_received))); inbound_->cap(STAT_GET(Stats, final_size)); // Notify the JS reader so it can see EOS. Pass fin=true so the @@ -1422,20 +1630,21 @@ void Stream::EndReadable(std::optional maybe_final_size) { } void Stream::Destroy(QuicError error) { - if (stats_->destroyed_at != 0) return; + if (stats()->destroyed_at != 0) return; + // Record the destroyed at timestamp before notifying the JavaScript side // that the stream is being destroyed. STAT_RECORD_TIMESTAMP(Stats, destroyed_at); DCHECK_NOT_NULL(session_.get()); - if (!state_->pending) { + if (!state()->pending) { Debug( this, "Stream %" PRIi64 " being destroyed with error %s", id(), error); } else { Debug(this, "Pending stream being destroyed with error %s", error); } - state_->pending = 0; + state()->pending = 0; maybe_pending_stream_.reset(); @@ -1448,6 +1657,10 @@ void Stream::Destroy(QuicError error) { // We are going to release our reference to the outbound_ queue here. outbound_.reset(); + // EndReadable() above already flushed accumulated data. Just release + // the ring buffer memory. + recv_accumulator_.reset(); + // We reset the inbound here also. However, it's important to note that // the JavaScript side could still have a reader on the inbound DataQueue, // which may keep that data alive a bit longer. @@ -1465,8 +1678,11 @@ void Stream::Destroy(QuicError error) { auto session = session_; session_.reset(); // EmitClose above triggers MakeCallback which can destroy the session - // via JS re-entrancy. The weak pointer may now be null. - if (session) session->RemoveStream(id()); + // via JS re-entrancy. The weak pointer may still be non-null (the + // Session BaseObject can be kept alive by a BaseObjectPtr elsewhere, + // e.g. OnTimeout's ref) even though impl_ has been reset. We must + // check is_destroyed() to avoid dereferencing the null impl_. + if (session && !session->is_destroyed()) session->RemoveStream(id()); // Critically, make sure that the RemoveStream call is the last thing // trying to use this stream object. Once that call is made, the stream @@ -1482,24 +1698,90 @@ void Stream::ReceiveData(const uint8_t* data, // If reading has ended, or there is no data, there's nothing to do but maybe // end the readable side if this is the last bit of data we've received. Debug(this, "Receiving %zu bytes of data", len); - if (state_->read_ended == 1 || len == 0) { + if (state()->read_ended == 1 || len == 0) { if (flags.fin) EndReadable(); return; } - if (flags.early) state_->received_early_data = 1; + if (flags.early) state()->received_early_data = 1; STAT_INCREMENT_N(Stats, bytes_received, len); STAT_SET(Stats, max_offset_received, STAT_GET(Stats, bytes_received)); STAT_RECORD_TIMESTAMP(Stats, received_at); - JS_TRY_ALLOCATE_BACKING(env(), backing, len) - memcpy(backing->Data(), data, len); - inbound_->append(DataQueue::CreateInMemoryEntryFromBackingStore( - std::move(backing), 0, len)); - // Notify the JS reader that data is available. - if (reader_) reader_->NotifyPull(); + // Lazy-allocate the receive accumulation buffer on first data-carrying + // call. Streams that never receive data (write-only, immediately reset) + // pay zero cost. + if (!recv_accumulator_) { + // Use the per-stream flow control window as the max capacity. The + // peer cannot send more than this without the JS side consuming and + // extending the window, so the ring buffer can never overflow. + auto params = ngtcp2_conn_get_local_transport_params(session()); + size_t max_cap; + if (direction() == Direction::BIDIRECTIONAL) { + max_cap = + ngtcp2_conn_is_local_stream(session(), id()) + ? static_cast(params->initial_max_stream_data_bidi_local) + : static_cast( + params->initial_max_stream_data_bidi_remote); + } else { + max_cap = static_cast(params->initial_max_stream_data_uni); + } + recv_accumulator_ = std::make_unique(max_cap); + } - if (flags.fin) EndReadable(); + // Track whether the accumulator was empty before this call. Used to + // notify the reader exactly once per accumulation cycle (empty → non-empty + // transition) rather than on every callback. + bool was_empty = recv_accumulator_->available() == 0; + + // Accumulate into the ring buffer. When the buffer reaches the flush + // threshold (64 KB of accumulated data), flush it into the DataQueue as + // a single entry and continue writing the remainder. If the reader is + // not consuming (no reader, or reader hasn't pulled), allow the buffer + // to grow beyond 64 KB up to the flow control window instead of + // flushing — keeping data in cheap C++ memory rather than creating + // V8 BackingStore entries nobody is reading yet. + while (len > 0) { + size_t written = recv_accumulator_->Write(data, len); + data += written; + len -= written; + + if (len > 0) { + // Buffer is full. Decide whether to flush or grow. + if (recv_accumulator_->should_flush() && reader_) { + // Reader exists and we've hit the flush threshold — produce a + // well-sized DataQueue entry for the reader to consume. + FlushAccumulation(); + } else if (recv_accumulator_->remaining() == 0) { + // No reader yet, or below flush threshold but physically full. + // Try to grow the buffer to absorb more data. + recv_accumulator_->Grow(); + // If Grow() couldn't expand (already at max), flush as fallback. + if (recv_accumulator_->remaining() == 0) { + FlushAccumulation(); + } + } + } + } + + // Update accumulation stats. + STAT_SET(Stats, bytes_accumulated, recv_accumulator_->available()); + if (recv_accumulator_->available() > STAT_GET(Stats, max_bytes_accumulated)) { + STAT_SET(Stats, max_bytes_accumulated, recv_accumulator_->available()); + } + + if (flags.fin) { + FlushAccumulation(); + EndReadable(); + } else if (reader_ && was_empty) { + // Notify the reader once when the accumulator transitions from empty + // to non-empty. This wakes the reader exactly once per accumulation + // cycle. When the reader wakes and calls pull(), BeforePull() flushes + // the accumulated data, and the subsequent EntryRead() extends the + // flow control window. We do NOT notify on every callback — that + // would defeat coalescing by flushing tiny amounts each time. + reader_->NotifyPull(); + } } void Stream::ReceiveStopSending(QuicError error) { @@ -1509,10 +1791,10 @@ void Stream::ReceiveStopSending(QuicError error) { // writable side has already been shut down (e.g. we already sent // RESET_STREAM ourselves or finished sending with FIN) there is // nothing more to do here. The previous guard checked - // `state_->read_ended` which is unrelated to the writable side and + // `state()->read_ended` which is unrelated to the writable side and // suppressed STOP_SENDING handling whenever a sibling RESET_STREAM // frame had been processed first within the same packet. - if (state_->write_ended) return; + if (state()->write_ended) return; Debug(this, "Received stop sending with error %s", error); ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code()); EndWritable(); @@ -1528,7 +1810,7 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { "Received stream reset with final size %" PRIu64 " and error %s", final_size, error); - state_->reset_code = error.code(); + state()->reset_code = error.code(); EndReadable(final_size); EmitReset(error); } @@ -1536,10 +1818,10 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { // ============================================================================ void Stream::EmitBlocked() { - // state_->wants_block will be set from the javascript side if the + // state()->wants_block will be set from the javascript side if the // stream object has a handler for the blocked event. Debug(this, "Blocked"); - if (!env()->can_call_into_js() || !state_->wants_block) { + if (!env()->can_call_into_js() || !state()->wants_block) { return; } CallbackScope cb_scope(this); @@ -1556,7 +1838,7 @@ void Stream::UpdateWriteDesiredSize() { if (!outbound_ || !outbound_->is_streaming()) return; uint64_t available; - uint64_t hwm = state_->high_water_mark; + uint64_t hwm = state()->high_water_mark; if (is_pending()) { // Pending streams don't have a stream ID yet, so ngtcp2 can't @@ -1589,8 +1871,8 @@ void Stream::UpdateWriteDesiredSize() { uint32_t clamped = static_cast( std::min(desired, std::numeric_limits::max())); - uint32_t old_size = state_->write_desired_size; - state_->write_desired_size = clamped; + uint32_t old_size = state()->write_desired_size; + state()->write_desired_size = clamped; // Fire drain when transitioning from 0 to non-zero. // writeDesiredSize == 0 means the buffer is full or flow control is @@ -1609,28 +1891,45 @@ void Stream::EmitClose(const QuicError& error) { } void Stream::EmitHeaders() { - // state_->wants_headers will be set from the javascript side if the + STAT_RECORD_TIMESTAMP(Stats, received_at); + // state()->wants_headers will be set from the javascript side if the // stream object has a handler for the headers event. - if (!env()->can_call_into_js() || !state_->wants_headers) { + if (!env()->can_call_into_js() || !state()->wants_headers) { + headers_.clear(); return; } CallbackScope cb_scope(this); + auto& binding = BindingData::Get(env()); + size_t count = headers_.size() * 2; + LocalVector values(env()->isolate(), count); + + for (size_t i = 0; i < headers_.size(); i++) { + Local name; + Local value; + if (!headers_[i]->GetName(&binding).ToLocal(&name) || + !headers_[i]->GetValue(&binding).ToLocal(&value)) [[unlikely]] { + headers_.clear(); + return; + } + values[i * 2] = name; + values[i * 2 + 1] = value; + } + + headers_.clear(); + Local argv[] = { - Array::New(env()->isolate(), headers_.data(), headers_.size()), + Array::New(env()->isolate(), values.data(), count), Integer::NewFromUnsigned(env()->isolate(), static_cast(headers_kind_))}; - headers_.clear(); - - MakeCallback( - BindingData::Get(env()).stream_headers_callback(), arraysize(argv), argv); + MakeCallback(binding.stream_headers_callback(), arraysize(argv), argv); } void Stream::EmitReset(const QuicError& error) { - // state_->wants_reset will be set from the javascript side if the + // state()->wants_reset will be set from the javascript side if the // stream object has a handler for the reset event. - if (!env()->can_call_into_js() || !state_->wants_reset) { + if (!env()->can_call_into_js() || !state()->wants_reset) { return; } CallbackScope cb_scope(this); @@ -1641,9 +1940,9 @@ void Stream::EmitReset(const QuicError& error) { } void Stream::EmitWantTrailers() { - // state_->wants_trailers will be set from the javascript side if the + // state()->wants_trailers will be set from the javascript side if the // stream object has a handler for the trailers event. - if (!env()->can_call_into_js() || !state_->wants_trailers) { + if (!env()->can_call_into_js() || !state()->wants_trailers) { return; } CallbackScope cb_scope(this); diff --git a/src/quic/streams.h b/src/quic/streams.h index 0edeeed7a9209e..86cb36b2668985 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -15,11 +15,61 @@ #include "bindingdata.h" #include "data.h" +#include + namespace node::quic { class Session; class Stream; +// An elastic ring buffer used by Stream to coalesce received data before +// flushing it into the DataQueue. This avoids creating many small V8 +// BackingStore allocations from per-QUIC-frame ngtcp2 callbacks. Data is +// memcpy'd into the ring buffer and flushed as a single right-sized +// BackingStore when the reader pulls or the buffer reaches its flush +// threshold. The buffer starts at kMinCapacity and can grow up to a +// configured maximum (typically the stream flow control window). +class RecvAccumulator final { + public: + static constexpr size_t kMinCapacity = 64 * 1024; // 64 KB + static constexpr size_t kFlushThreshold = kMinCapacity; // flush at 64 KB + + explicit RecvAccumulator(size_t max_capacity); + ~RecvAccumulator() = default; + + DISALLOW_COPY_AND_MOVE(RecvAccumulator) + + // Append data. Returns the number of bytes written (may be less than + // len if the buffer is full). The caller should flush and retry with + // the remaining bytes. + size_t Write(const uint8_t* data, size_t len); + + // Flush accumulated data as a single DataQueue entry backed by a + // right-sized V8 BackingStore. Returns nullptr if nothing is + // accumulated. Resets the read/write cursors and shrinks the buffer + // back toward kMinCapacity if it was expanded. + std::unique_ptr Flush(Environment* env); + + // Current number of bytes awaiting flush. + size_t available() const { return len_; } + + // Number of bytes that can still be written before the buffer is full. + size_t remaining() const { return buf_.size() - len_; } + + // True if accumulated data has reached the flush threshold. + bool should_flush() const { return len_ >= kFlushThreshold; } + + // Grow the buffer capacity (doubles, up to max_capacity_). + void Grow(); + + private: + std::vector buf_; + size_t max_capacity_; + size_t read_pos_ = 0; + size_t write_pos_ = 0; + size_t len_ = 0; // write_pos_ - read_pos_, accounting for wrap +}; + using Ngtcp2Source = bob::SourceImpl; // When a request to open a stream is made before a Session is able to actually @@ -208,6 +258,11 @@ class Stream final : public AsyncWrap, Session& session() const; + // Returns the most recent activity timestamp for this stream in + // nanoseconds (uv_hrtime). Uses received_at if data has been received, + // otherwise falls back to created_at. Returns 0 if neither is set. + uint64_t last_activity_timestamp() const; + // True if this stream was created in a pending state and is still waiting // to be created. bool is_pending() const; @@ -253,6 +308,7 @@ class Stream final : public AsyncWrap, void EndWritable(); void EndReadable(std::optional maybe_final_size = std::nullopt); void EntryRead(size_t amount) override; + void BeforePull() override; // Pulls data from the internal outbound DataQueue configured for this stream. // This is called by the session/application when it is preparing to send @@ -293,7 +349,7 @@ class Stream final : public AsyncWrap, // Returns false if the header cannot be added. This will typically happen // if the application does not support headers, a maximum number of headers // have already been added, or the maximum total header length is reached. - bool AddHeader(const Header& header); + bool AddHeader(std::unique_ptr
                                            header); // TODO(@jasnell): Implement MemoryInfo to track outbound_, inbound_, // reader_, headers_, and pending_headers_queue_. @@ -304,12 +360,27 @@ class Stream final : public AsyncWrap, struct State; struct Stats; + // Typed accessors for arena-allocated state/stats. These are defined + // in streams.cc where State and Stats are complete types. + inline State* state() { return static_cast(state_slot_.ptr); } + inline const State* state() const { + return static_cast(state_slot_.ptr); + } + inline Stats* stats() { return static_cast(stats_slot_.ptr); } + inline const Stats* stats() const { + return static_cast(stats_slot_.ptr); + } + private: struct Impl; struct PendingHeaders; class Outbound; + // Flushes any data accumulated in the receive ring buffer into the + // inbound DataQueue as a single right-sized entry. + void FlushAccumulation(); + // Gets a reader for the data received for this stream from the peer, BaseObjectPtr get_reader(); @@ -362,12 +433,13 @@ class Stream final : public AsyncWrap, v8::Local headers, HeadersFlags flags); - AliasedStruct stats_; - AliasedStruct state_; + ArenaSlotBase stats_slot_; + ArenaSlotBase state_slot_; BaseObjectWeakPtr session_; std::unique_ptr outbound_; std::shared_ptr inbound_; BaseObjectWeakPtr reader_; + std::unique_ptr recv_accumulator_; // If the stream cannot be opened yet, it will be created in a pending state. // Once the owning session is able to, it will complete opening of the stream @@ -388,9 +460,11 @@ class Stream final : public AsyncWrap, const StoredPriority& stored_priority() const { return priority_; } // The headers_ field holds a block of headers that have been received and - // are being buffered for delivery to the JavaScript side. - // TODO(@jasnell): Use v8::Global instead of v8::Local here. - v8::LocalVector headers_; + // are being buffered for delivery to the JavaScript side. Headers are + // stored as C++ objects during collection (AddHeader) and converted to + // V8 strings only when emitted (EmitHeaders), avoiding StrongRootAllocator + // mutex contention on the per-header hot path. + std::vector> headers_; // The headers_kind_ field indicates the kind of headers that are being // buffered. diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 08ce7f9acd1c4c..c0a1610540ed3a 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -30,13 +30,16 @@ using ncrypto::SSLCtxPointer; using ncrypto::SSLPointer; using ncrypto::SSLSessionPointer; using ncrypto::X509Pointer; +using v8::Array; using v8::ArrayBuffer; +using v8::ArrayBufferView; using v8::Just; using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; using v8::Object; +using v8::String; using v8::Undefined; using v8::Value; @@ -47,7 +50,7 @@ namespace quic { namespace { // Temporarily wraps an SSL pointer but does not take ownership. -// Use by a few of the TLSSession methods that need access to the SSL* +// Used by a few of the TLSSession methods that need access to the SSL* // pointer held by the OSSLContext but cannot take ownership of it. class SSLPointerRef final { public: @@ -95,7 +98,7 @@ template Opt::*member> bool SetOption(Environment* env, Opt* options, const Local& object, - const Local& name) { + const Local& name) { Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; @@ -105,7 +108,7 @@ bool SetOption(Environment* env, if (value->IsArray()) { auto context = env->context(); - auto values = value.As(); + auto values = value.As(); uint32_t count = values->Length(); for (uint32_t n = 0; n < count; n++) { Local item; @@ -125,7 +128,7 @@ bool SetOption(Environment* env, } } else if constexpr (std::is_same::value) { if (item->IsArrayBufferView()) { - Store store = Store::CopyFrom(item.As()); + Store store = Store::CopyFrom(item.As()); (options->*member).push_back(std::move(store)); } else if (item->IsArrayBuffer()) { Store store = Store::CopyFrom(item.As()); @@ -154,7 +157,7 @@ bool SetOption(Environment* env, } } else if constexpr (std::is_same::value) { if (value->IsArrayBufferView()) { - Store store = Store::CopyFrom(value.As()); + Store store = Store::CopyFrom(value.As()); (options->*member).push_back(std::move(store)); } else if (value->IsArrayBuffer()) { Store store = Store::CopyFrom(value.As()); @@ -185,7 +188,7 @@ void OSSLContext::reset() { if (ctx_) { // The SSL object inside the ngtcp2 ctx may not have been set if // SSL creation failed. Guard against null before clearing app data. - if (SSL* ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ctx_); ssl != nullptr) { + if (SSL* ssl = *this; ssl != nullptr) { SSL_set_app_data(ssl, nullptr); } // connection_ is set during Initialize(). If Initialize() was @@ -222,7 +225,7 @@ void OSSLContext::Initialize(SSL* ssl, connection_ = connection; } -std::string OSSLContext::get_cipher_name() const { +std::string_view OSSLContext::get_cipher_name() const { return SSL_get_cipher_name(*this); } @@ -258,6 +261,18 @@ bool OSSLContext::set_hostname(std::string_view hostname) const { const_cast(name.c_str())) == 1; } +bool OSSLContext::set_verify_hostname(std::string_view hostname) const { + // SSL_set1_host tells OpenSSL to verify the peer certificate's + // subject name (SAN/CN) matches this hostname. This is separate + // from SSL_set_tlsext_host_name which only sets the SNI extension. + static const char* kDefaultHostname = "localhost"; + if (hostname.empty()) { + return SSL_set1_host(*this, kDefaultHostname) == 1; + } else { + return SSL_set1_host(*this, hostname.data()) == 1; + } +} + bool OSSLContext::set_early_data_enabled() const { return SSL_set_quic_tls_early_data_enabled(*this, 1) == 1; } @@ -497,6 +512,14 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { SSL_CTX_set_session_cache_mode( ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL); SSL_CTX_sess_set_new_cb(ctx.get(), OnNewSession); + + // In strict mode, set SSL_VERIFY_PEER so OpenSSL aborts the + // handshake if the server's certificate fails validation. In + // non-strict modes, verification still occurs but the handshake + // completes regardless — the result is surfaced to JS. + if (options_.verify_peer_strict) { + SSL_CTX_set_verify(ctx.get(), SSL_VERIFY_PEER, nullptr); + } break; } } @@ -703,6 +726,7 @@ Maybe TLSContext::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(verify_client) || !SET(reject_unauthorized) || + !SET(verify_hostname) || !SET(verify_peer_strict) || !SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) || !SET(servername) || !SET(ciphers) || !SET(groups) || !SET(verify_private_key) || !SET(keylog) || !SET(port) || @@ -727,6 +751,8 @@ std::string TLSContext::Options::ToString() const { (verify_client ? std::string("yes") : std::string("no")); res += prefix + "reject unauthorized: " + (reject_unauthorized ? std::string("yes") : std::string("no")); + res += prefix + "verify peer strict: " + + (verify_peer_strict ? std::string("yes") : std::string("no")); res += prefix + "enable early data: " + (enable_early_data ? std::string("yes") : std::string("no")); res += prefix + "enable_tls_trace: " + @@ -841,6 +867,14 @@ void TLSSession::Initialize( return; } + if (options.verify_hostname) { + if (!ossl_context_.set_verify_hostname(options.servername)) { + validation_error_ = "Failed to set verify hostname"; + ossl_context_.reset(); + return; + } + } + if (maybeSessionTicket.has_value()) { const auto& sessionTicket = *maybeSessionTicket; uv_buf_t buf = sessionTicket.ticket(); diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index 335f577e3994c5..ba502cf1f868df 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -45,12 +45,13 @@ class OSSLContext final { ngtcp2_conn* connection, SSL_CTX* ssl_ctx); - std::string get_cipher_name() const; + std::string_view get_cipher_name() const; std::string get_selected_alpn() const; std::string_view get_negotiated_group() const; bool set_alpn_protocols(std::string_view protocols) const; bool set_hostname(std::string_view hostname) const; + bool set_verify_hostname(std::string_view hostname) const; bool set_early_data_enabled() const; bool set_transport_params(const ngtcp2_vec& tp) const; @@ -207,6 +208,21 @@ class TLSContext final : public MemoryRetainer, // This option is only used by the server side. bool reject_unauthorized = true; + // When true, the client will set SSL_VERIFY_PEER so that OpenSSL + // aborts the handshake if the server's certificate fails validation. + // This is the "strict" verify_peer mode. When false (the default), + // the handshake completes regardless and VerifyPeerIdentity is + // called after to surface errors to JS. This option is only used + // by the client side. + bool verify_peer_strict = false; + + // When true, OpenSSL verifies that the server's certificate matches + // the servername (hostname verification via SSL_set1_host). Should + // be true for 'strict' and 'auto' verifyPeer modes, false for + // 'manual'. Without this, a valid certificate for any domain would + // be accepted. This option is only used by the client side. + bool verify_hostname = false; + // When true (the default), the server accepts 0-RTT early data // from clients with valid session tickets. When false, early data // is disabled and clients must complete a full handshake before diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index fb348b02e01b24..f9a429cef66279 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -170,10 +170,9 @@ ngtcp2_vec GenerateRetryToken(uint8_t* buffer, odcid, uv_hrtime()); DCHECK_GE(ret, 0); - DCHECK_LE(ret, RetryToken::kRetryTokenLen); DCHECK_EQ(buffer[0], RetryToken::kTokenMagic); // This shouldn't be possible but we handle it anyway just to be safe. - if (ret == 0) return {nullptr, 0}; + if (ret <= 0) return {nullptr, 0}; return {buffer, static_cast(ret)}; } @@ -189,10 +188,9 @@ ngtcp2_vec GenerateRegularToken(uint8_t* buffer, address.length(), uv_hrtime()); DCHECK_GE(ret, 0); - DCHECK_LE(ret, RegularToken::kRegularTokenLen); DCHECK_EQ(buffer[0], RegularToken::kTokenMagic); // This shouldn't be possible but we handle it anyway just to be safe. - if (ret == 0) return {nullptr, 0}; + if (ret <= 0) return {nullptr, 0}; return {buffer, static_cast(ret)}; } } // namespace diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 372e9dc0828a10..183ba973ac1823 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -18,9 +18,13 @@ namespace node { +using v8::BigInt; +using v8::Boolean; +using v8::DictionaryTemplate; using v8::Just; using v8::Local; using v8::Maybe; +using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::Value; @@ -339,6 +343,155 @@ void TransportParams::GeneratePreferredAddressToken(Session* session) { } } +v8::MaybeLocal TransportParams::ToObject(Environment* env) const { + auto& binding_data = BindingData::Get(env); + auto tmpl = binding_data.transport_params_template(); + static constexpr std::string_view names[] = { + "preferredAddressIpv4", + "preferredAddressIpv6", + "originalDCID", + "initialSCID", + "retrySCID", + "initialMaxStreamDataBidiLocal", + "initialMaxStreamDataBidiRemote", + "initialMaxStreamDataUni", + "initialMaxData", + "initialMaxStreamsBidi", + "initialMaxStreamsUni", + "maxIdleTimeout", + "activeConnectionIDLimit", + "ackDelayExponent", + "maxAckDelay", + "maxDatagramFrameSize", + "disableActiveMigration", + }; + if (tmpl.IsEmpty()) { + tmpl = DictionaryTemplate::New(env->isolate(), names); + binding_data.set_transport_params_template(tmpl); + } + + MaybeLocal values[] = { + Undefined(env->isolate()), // preferredAddressIpv4 + Undefined(env->isolate()), // preferredAddressIpv6 + Undefined(env->isolate()), // originalDCID + Undefined(env->isolate()), // initialSCID + Undefined(env->isolate()), // retrySCID + Undefined(env->isolate()), // initialMaxStreamDataBidiLocal + Undefined(env->isolate()), // initialMaxStreamDataBidiRemote + Undefined(env->isolate()), // initialMaxStreamDataUni + Undefined(env->isolate()), // initialMaxData + Undefined(env->isolate()), // initialMaxStreamsBidi + Undefined(env->isolate()), // initialMaxStreamsUni + Undefined(env->isolate()), // maxIdleTimeout + Undefined(env->isolate()), // activeConnectionIDLimit + Undefined(env->isolate()), // ackDelayExponent + Undefined(env->isolate()), // maxAckDelay + Undefined(env->isolate()), // maxDatagramFrameSize + Undefined(env->isolate()), // disableActiveMigration + }; + + static_assert(std::size(values) == std::size(names)); + + static constexpr size_t kPreferredAddressIpv4Index = 0; + static constexpr size_t kPreferredAddressIpv6Index = 1; + static constexpr size_t kOriginalDCIDIndex = 2; + static constexpr size_t kInitialSCIDIndex = 3; + static constexpr size_t kRetrySCIDIndex = 4; + static constexpr size_t kInitialMaxStreamDataBidiLocalIndex = 5; + static constexpr size_t kInitialMaxStreamDataBidiRemoteIndex = 6; + static constexpr size_t kInitialMaxStreamDataUniIndex = 7; + static constexpr size_t kInitialMaxDataIndex = 8; + static constexpr size_t kInitialMaxStreamsBidiIndex = 9; + static constexpr size_t kInitialMaxStreamsUniIndex = 10; + static constexpr size_t kMaxIdleTimeoutIndex = 11; + static constexpr size_t kActiveConnectionIDLimitIndex = 12; + static constexpr size_t kAckDelayExponentIndex = 13; + static constexpr size_t kMaxAckDelayIndex = 14; + static constexpr size_t kMaxDatagramFrameSizeIndex = 15; + static constexpr size_t kDisableActiveMigrationIndex = 16; + + if (ptr_ != nullptr) { + if (ptr_->preferred_addr_present) { + if (ptr_->preferred_addr.ipv4_present) { + auto address = std::make_shared( + reinterpret_cast(&ptr_->preferred_addr.ipv4)); + auto addr = SocketAddressBase::Create(env, std::move(address)); + if (!addr) return {}; + values[kPreferredAddressIpv4Index] = addr->object(); + } + + if (ptr_->preferred_addr.ipv6_present) { + auto address = std::make_shared( + reinterpret_cast(&ptr_->preferred_addr.ipv6)); + auto addr = SocketAddressBase::Create(env, std::move(address)); + if (!addr) return {}; + values[kPreferredAddressIpv6Index] = addr->object(); + } + // ngtcp2_preferred_addr preferred_addr; + } + + if (ptr_->original_dcid_present) { + CID cid(ptr_->original_dcid); + Local value; + if (!ToV8Value(env->context(), cid.ToString()).ToLocal(&value)) { + return {}; + } + values[kOriginalDCIDIndex] = value; + } + + if (ptr_->initial_scid_present) { + CID cid(ptr_->initial_scid); + Local value; + if (!ToV8Value(env->context(), cid.ToString()).ToLocal(&value)) { + return {}; + } + values[kInitialSCIDIndex] = value; + } + + if (ptr_->retry_scid_present) { + CID cid(ptr_->retry_scid); + Local value; + if (!ToV8Value(env->context(), cid.ToString()).ToLocal(&value)) { + return {}; + } + values[kRetrySCIDIndex] = value; + } + + values[kInitialMaxStreamDataBidiLocalIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->initial_max_stream_data_bidi_local); + values[kInitialMaxStreamDataBidiRemoteIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->initial_max_stream_data_bidi_remote); + values[kInitialMaxStreamDataUniIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->initial_max_stream_data_uni); + values[kInitialMaxDataIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->initial_max_data); + values[kInitialMaxStreamsBidiIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->initial_max_streams_bidi); + values[kInitialMaxStreamsUniIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->initial_max_streams_uni); + values[kMaxIdleTimeoutIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->max_idle_timeout / NGTCP2_SECONDS); + values[kActiveConnectionIDLimitIndex] = BigInt::NewFromUnsigned( + env->isolate(), ptr_->active_connection_id_limit); + values[kAckDelayExponentIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->ack_delay_exponent); + values[kMaxAckDelayIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->ack_delay_exponent); + values[kMaxAckDelayIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->max_ack_delay); + values[kMaxDatagramFrameSizeIndex] = + BigInt::NewFromUnsigned(env->isolate(), ptr_->max_datagram_frame_size); + values[kDisableActiveMigrationIndex] = + Boolean::New(env->isolate(), ptr_->disable_active_migration); + } + + auto obj = tmpl->NewInstance(env->context(), values); + if (obj->SetPrototypeV2(env->context(), Null(env->isolate())).IsNothing()) { + return {}; + } + return obj; +} + TransportParams::operator const ngtcp2_transport_params&() const { DCHECK_NOT_NULL(ptr_); return *ptr_; diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 1f3cd545cdd209..46724574611aff 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -164,6 +164,9 @@ class TransportParams final { size_t len, Version version = Version::V1) const; + // Returns a JavaScript object representing the transport parameters. + v8::MaybeLocal ToObject(Environment* env) const; + private: void SetPreferredAddress(const SocketAddress& address); void GeneratePreferredAddressToken(Session* session); diff --git a/src/stream_base.cc b/src/stream_base.cc index 6a631921341307..370b8f682eadae 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -296,14 +296,10 @@ int StreamBase::Writev(const FunctionCallbackInfo& args) { int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { CHECK(args[0]->IsObject()); + CHECK(args[1]->IsUint8Array()); Environment* env = Environment::GetCurrent(args); - if (!args[1]->IsUint8Array()) { - node::THROW_ERR_INVALID_ARG_TYPE(env, "Second argument must be a buffer"); - return 0; - } - Local req_wrap_obj = args[0].As(); uv_buf_t buf; buf.base = Buffer::Data(args[1]); diff --git a/src/string_bytes.cc b/src/string_bytes.cc index 865302bfd1b4de..1d4ee3a81803b2 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -671,6 +671,40 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, } } +MaybeLocal StringBytes::EncodeValidUtf8(Isolate* isolate, + const char* buf, + size_t buflen) { + CHECK_BUFLEN_IN_RANGE(buflen); + if (!buflen) return String::Empty(isolate); + buflen = keep_buflen_in_range(buflen); + + // ASCII fast path + if (!simdutf::validate_ascii_with_errors(buf, buflen).error) { + return ExternOneByteString::NewFromCopy(isolate, buf, buflen); + } + + if (buflen >= 32) { + size_t u16size = simdutf::utf16_length_from_utf8(buf, buflen); + if (u16size > static_cast(v8::String::kMaxLength)) { + isolate->ThrowException(ERR_STRING_TOO_LONG(isolate)); + return MaybeLocal(); + } + return EncodeTwoByteString( + isolate, u16size, [buf, buflen, u16size](uint16_t* dst) { + size_t written = simdutf::convert_valid_utf8_to_utf16( + buf, buflen, reinterpret_cast(dst)); + CHECK_EQ(written, u16size); + }); + } + + Local str; + if (!String::NewFromUtf8(isolate, buf, v8::NewStringType::kNormal, buflen) + .ToLocal(&str)) { + isolate->ThrowException(node::ERR_STRING_TOO_LONG(isolate)); + } + return str; +} + MaybeLocal StringBytes::Encode(Isolate* isolate, const uint16_t* buf, size_t buflen) { diff --git a/src/string_bytes.h b/src/string_bytes.h index 9949f508f83ffe..71aa9ff1f90a7c 100644 --- a/src/string_bytes.h +++ b/src/string_bytes.h @@ -83,6 +83,11 @@ class StringBytes { size_t buflen, enum encoding encoding); + // Like Encode(..., UTF8) but does not re-validate. Input must be valid UTF-8. + static v8::MaybeLocal EncodeValidUtf8(v8::Isolate* isolate, + const char* buf, + size_t buflen); + // Warning: This reverses endianness on BE platforms, even though the // signature using uint16_t implies that it should not. // However, the brokenness is already public API and can't therefore diff --git a/src/util-inl.h b/src/util-inl.h index d59e30a635b08b..e357d15a14496d 100644 --- a/src/util-inl.h +++ b/src/util-inl.h @@ -341,22 +341,6 @@ v8::Maybe FromV8Array(v8::Local context, return js_array->Iterate(context, PushItemToVector, &data); } -v8::MaybeLocal ToV8Value(v8::Local context, - std::string_view str, - v8::Isolate* isolate) { - if (isolate == nullptr) isolate = v8::Isolate::GetCurrent(); - if (str.size() >= static_cast(v8::String::kMaxLength)) [[unlikely]] { - // V8 only has a TODO comment about adding an exception when the maximum - // string size is exceeded. - ThrowErrStringTooLong(isolate); - return v8::MaybeLocal(); - } - - return v8::String::NewFromUtf8( - isolate, str.data(), v8::NewStringType::kNormal, str.size()) - .FromMaybe(v8::Local()); -} - v8::MaybeLocal ToV8Value(v8::Local context, std::u16string_view str, v8::Isolate* isolate) { diff --git a/src/util.cc b/src/util.cc index 1ea51cf7012963..317b8db0daac69 100644 --- a/src/util.cc +++ b/src/util.cc @@ -812,4 +812,15 @@ v8::Maybe GetValidFileMode(Environment* env, return v8::Just(mode); } +v8::MaybeLocal ToV8Value(v8::Local context, + std::string_view str, + v8::Isolate* isolate) { + if (isolate == nullptr) isolate = v8::Isolate::GetCurrent(); + if (str.size() >= static_cast(v8::String::kMaxLength)) [[unlikely]] { + ThrowErrStringTooLong(isolate); + return v8::MaybeLocal(); + } + return StringBytes::Encode(isolate, str.data(), str.size(), UTF8); +} + } // namespace node diff --git a/src/util.h b/src/util.h index 3dedeca4d227e9..48305bfdc13143 100644 --- a/src/util.h +++ b/src/util.h @@ -701,9 +701,9 @@ inline v8::Maybe FromV8Array(v8::Local context, v8::Local js_array, std::vector>* out); -inline v8::MaybeLocal ToV8Value(v8::Local context, - std::string_view str, - v8::Isolate* isolate = nullptr); +v8::MaybeLocal ToV8Value(v8::Local context, + std::string_view str, + v8::Isolate* isolate = nullptr); inline v8::MaybeLocal ToV8Value(v8::Local context, std::u16string_view str, v8::Isolate* isolate = nullptr); diff --git a/test/addons/new-context-inspector/binding.cc b/test/addons/new-context-inspector/binding.cc new file mode 100644 index 00000000000000..b5a025b18d0df6 --- /dev/null +++ b/test/addons/new-context-inspector/binding.cc @@ -0,0 +1,69 @@ +#include +#include + +namespace { + +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Script; +using v8::String; +using v8::Value; + +void MakeContext(const FunctionCallbackInfo& args) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope handle_scope(isolate); + Local context = isolate->GetCurrentContext(); + node::Environment* env = node::GetCurrentEnvironment(context); + assert(env); + + // Create a new context with Node.js-specific setup. + v8::MaybeLocal maybe_context = node::NewContext(isolate); + v8::Local new_context; + if (!maybe_context.ToLocal(&new_context)) { + return; + } + node::RegisterContext(env, new_context, "Addon Context", "addon://about"); + + // Return the global proxy object. + args.GetReturnValue().Set(new_context->Global()); +} + +void RunInContext(const FunctionCallbackInfo& args) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope handle_scope(isolate); + assert(args.Length() == 2); + + assert(args[0]->IsObject()); + Local global_proxy = args[0].As(); + v8::MaybeLocal maybe_context = global_proxy->GetCreationContext(); + v8::Local new_context; + if (!maybe_context.ToLocal(&new_context)) { + return; + } + Context::Scope context_scope(new_context); + + assert(args[1]->IsString()); + Local source = args[1].As(); + Local