From 312db1e3d24d9159fd337643facebc2ae8e09d8e Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Tue, 15 Sep 2026 04:45:00 +0900 Subject: [PATCH 01/83] perf_hooks: reuse buffer for uv metrics Reuse an aliased Int32Array to transfer uv metrics from C++ to JavaScript instead of allocating a new V8 array on every access. Assisted-by: Codex Signed-off-by: HoonDongKang PR-URL: https://github.com/nodejs/node/pull/65985 Reviewed-By: James M Snell Reviewed-By: Daeyeon Jeong Reviewed-By: Chengzhong Wu --- lib/internal/perf/nodetiming.js | 9 ++--- src/node_perf.cc | 35 +++++++++++++------ src/node_perf_common.h | 3 ++ src/node_snapshotable.cc | 3 ++ .../fixtures/test-nodetiming-uvmetricsinfo.js | 10 +++++- typings/internalBinding/performance.d.ts | 3 +- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/lib/internal/perf/nodetiming.js b/lib/internal/perf/nodetiming.js index a9e0c3f252ce..5de5e3e6644f 100644 --- a/lib/internal/perf/nodetiming.js +++ b/lib/internal/perf/nodetiming.js @@ -29,6 +29,7 @@ const { }, loopIdleTime, uvMetricsInfo, + uvMetricsBuffer, } = internalBinding('performance'); class PerformanceNodeTiming { @@ -129,11 +130,11 @@ class PerformanceNodeTiming { enumerable: true, configurable: true, get: () => { - const metrics = uvMetricsInfo(); + uvMetricsInfo(); return { - loopCount: metrics[0], - events: metrics[1], - eventsWaiting: metrics[2], + loopCount: uvMetricsBuffer[0], + events: uvMetricsBuffer[1], + eventsWaiting: uvMetricsBuffer[2], }; }, }, diff --git a/src/node_perf.cc b/src/node_perf.cc index f63e5f288cce..b4c74e9a09a7 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -14,7 +14,6 @@ namespace node { namespace performance { -using v8::Array; using v8::Context; using v8::DontDelete; using v8::Function; @@ -57,7 +56,12 @@ PerformanceState::PerformanceState(Isolate* isolate, offsetof(performance_state_internal, observers), NODE_PERFORMANCE_ENTRY_TYPE_INVALID, root, - MAYBE_FIELD_PTR(info, observers)) { + MAYBE_FIELD_PTR(info, observers)), + uv_metrics(isolate, + offsetof(performance_state_internal, uv_metrics), + 3, + root, + MAYBE_FIELD_PTR(info, uv_metrics)) { if (info == nullptr) { // For performance states initialized from scratch, reset // all the milestones and initialize the time origin. @@ -81,9 +85,15 @@ PerformanceState::SerializeInfo PerformanceState::Serialize( // We'll re-initialize them after deserialization. ResetMilestones(); + // Do not retain runtime metrics in the snapshot. + for (size_t i = 0; i < uv_metrics.Length(); ++i) { + uv_metrics[i] = 0; + } + SerializeInfo info{root.Serialize(context, creator), milestones.Serialize(context, creator), - observers.Serialize(context, creator)}; + observers.Serialize(context, creator), + uv_metrics.Serialize(context, creator)}; return info; } @@ -105,6 +115,7 @@ void PerformanceState::Deserialize(v8::Local context, root.Deserialize(context); milestones.Deserialize(context); observers.Deserialize(context); + uv_metrics.Deserialize(context); // Re-initialize the time origin and timestamp i.e. the process start time. Initialize(time_origin, time_origin_timestamp); @@ -116,6 +127,7 @@ std::ostream& operator<<(std::ostream& o, << " " << i.root << ", // root\n" << " " << i.milestones << ", // milestones\n" << " " << i.observers << ", // observers\n" + << " " << i.uv_metrics << ", // uv_metrics\n" << "}"; return o; } @@ -265,17 +277,13 @@ void LoopIdleTime(const FunctionCallbackInfo& args) { void UvMetricsInfo(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); uv_metrics_t metrics; // uv_metrics_info always return 0 CHECK_EQ(uv_metrics_info(env->event_loop(), &metrics), 0); - Local data[] = { - Integer::New(isolate, metrics.loop_count), - Integer::New(isolate, metrics.events), - Integer::New(isolate, metrics.events_waiting), - }; - Local arr = Array::New(env->isolate(), data, arraysize(data)); - args.GetReturnValue().Set(arr); + AliasedInt32Array& buffer = env->performance_state()->uv_metrics; + buffer[0] = static_cast(metrics.loop_count); + buffer[1] = static_cast(metrics.events); + buffer[2] = static_cast(metrics.events_waiting); } void CreateELDHistogram(const FunctionCallbackInfo& args) { @@ -367,6 +375,11 @@ void CreatePerContextProperties(Local target, target->Set(context, FIXED_ONE_BYTE_STRING(isolate, "milestones"), state->milestones.GetJSArray()).Check(); + target + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "uvMetricsBuffer"), + state->uv_metrics.GetJSArray()) + .Check(); Local constants = Object::New(isolate); diff --git a/src/node_perf_common.h b/src/node_perf_common.h index 01e7f35241ac..aa84ba55b08e 100644 --- a/src/node_perf_common.h +++ b/src/node_perf_common.h @@ -62,6 +62,7 @@ class PerformanceState { AliasedBufferIndex root; AliasedBufferIndex milestones; AliasedBufferIndex observers; + AliasedBufferIndex uv_metrics; }; explicit PerformanceState(v8::Isolate* isolate, @@ -78,6 +79,7 @@ class PerformanceState { AliasedUint8Array root; AliasedFloat64Array milestones; AliasedUint32Array observers; + AliasedInt32Array uv_metrics; uint64_t performance_last_gc_start_mark = 0; uint16_t current_gc_type = 0; @@ -92,6 +94,7 @@ class PerformanceState { // doubles first so that they are always sizeof(double)-aligned double milestones[NODE_PERFORMANCE_MILESTONE_INVALID]; uint32_t observers[NODE_PERFORMANCE_ENTRY_TYPE_INVALID]; + int32_t uv_metrics[3]; }; }; diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index cecdd4873611..fa1de1917e92 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -407,6 +407,7 @@ size_t SnapshotSerializer::Write(const ImmediateInfo::SerializeInfo& data) { // [ 4/8 bytes ] snapshot index of root // [ 4/8 bytes ] snapshot index of milestones // [ 4/8 bytes ] snapshot index of observers +// [ 4/8 bytes ] snapshot index of uv_metrics template <> performance::PerformanceState::SerializeInfo SnapshotDeserializer::Read() { Debug("Read()\n"); @@ -415,6 +416,7 @@ performance::PerformanceState::SerializeInfo SnapshotDeserializer::Read() { result.root = ReadArithmetic(); result.milestones = ReadArithmetic(); result.observers = ReadArithmetic(); + result.uv_metrics = ReadArithmetic(); if (is_debug) { std::string str = ToStr(result); Debug("Read() %s\n", str); @@ -433,6 +435,7 @@ size_t SnapshotSerializer::Write( size_t written_total = WriteArithmetic(data.root); written_total += WriteArithmetic(data.milestones); written_total += WriteArithmetic(data.observers); + written_total += WriteArithmetic(data.uv_metrics); Debug("Write() wrote %d bytes\n", written_total); diff --git a/test/fixtures/test-nodetiming-uvmetricsinfo.js b/test/fixtures/test-nodetiming-uvmetricsinfo.js index 59b1cc8ebf11..038ca8b79904 100644 --- a/test/fixtures/test-nodetiming-uvmetricsinfo.js +++ b/test/fixtures/test-nodetiming-uvmetricsinfo.js @@ -40,7 +40,15 @@ function safeMetricsInfo(cb) { fs.open(__filename, 'r', (err) => { assert.ifError(err); }); + + const saved = { ...info }; + safeMetricsInfo((nextInfo) => { + assert.notStrictEqual(nextInfo, info); + assert.ok(nextInfo.loopCount > saved.loopCount); + // Updating the shared buffer must not change earlier results. + assert.deepStrictEqual(info, saved); + }); } safeMetricsInfo(openFile); -} \ No newline at end of file +} diff --git a/typings/internalBinding/performance.d.ts b/typings/internalBinding/performance.d.ts index 5f6f4c88022c..cf3ef0a664f0 100644 --- a/typings/internalBinding/performance.d.ts +++ b/typings/internalBinding/performance.d.ts @@ -145,6 +145,7 @@ export interface PerformanceBinding { samplePerIteration: boolean, ): InternalPerformanceBinding.ELDHistogram; markBootstrapComplete(): void; - uvMetricsInfo(): [number, number, number]; + uvMetricsInfo(): void; + uvMetricsBuffer: Int32Array; now(): number; } From 27cdfbe13be5eff04522cb5918bbf409dd5f771c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 8 Sep 2026 02:16:50 +0000 Subject: [PATCH 02/83] util: implement debounce I found myself using debounce quite a bit recently while testing some recent other additions (quic and dtls testing, perf_hooks improvements, etc). I was using an npm dependency right up until I realized just how generally useful it is to actually have it Just There. So, since it was a holiday and I just felt like it... util.debounce(...) Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/65899 Reviewed-By: Matteo Collina --- doc/api/util.md | 98 +++++++ lib/internal/util/debounce.js | 237 +++++++++++++++++ lib/util.js | 9 + test/parallel/test-util-debounce.js | 381 ++++++++++++++++++++++++++++ 4 files changed, 725 insertions(+) create mode 100644 lib/internal/util/debounce.js create mode 100644 test/parallel/test-util-debounce.js diff --git a/doc/api/util.md b/doc/api/util.md index 81ccff315d1a..0ad0c966d0b8 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -387,6 +387,104 @@ The `--throw-deprecation` command-line flag and `process.throwDeprecation` property take precedence over `--trace-deprecation` and `process.traceDeprecation`. +## `util.debounce(fn, wait[, options])` + + + +* `fn` {Function} The function to debounce. +* `wait` {integer} The number of milliseconds to delay `fn`. +* `options` {Object} + * `leading` {boolean} When `true`, invokes `fn` immediately when a new + debounce window begins. **Default:** `false`. + * `rejectOnCancel` {boolean} When `true`, a call superseded by a later call + rejects with an `AbortError`. **Default:** `false`. + * `signal` {AbortSignal} An `AbortSignal` that cancels pending calls and + prevents future calls when aborted. +* Returns: {Function} The debounced function. + +Creates a function that delays calling `fn` until `wait` milliseconds have +elapsed since the most recent invocation. The debounced function returns a +{Promise} for the value returned by `fn`. If `fn` throws or returns a rejected +promise, the returned promise is rejected with the same reason. + +When the debounced function is called more than once before the delay expires, +`fn` receives the arguments from the most recent call. By default, the promises +from all calls resolve or reject with the result of that invocation. If +`options.rejectOnCancel` is `true`, the promises from superseded calls reject +with an `AbortError` instead. + +When `options.leading` is `true`, the first call in a debounce window invokes +`fn` immediately. Calls made during that window are delayed until `wait` +milliseconds have elapsed since the most recent call. A trailing invocation +only occurs if the debounced function was called again during the window. +The window begins before `fn` is invoked, so recursive calls and calls made +while an asynchronous `fn` is pending are part of the same window if they occur +before the delay expires. This also applies to calls made after a synchronous +`fn` returns but before the delay expires. + +If `options.signal` is aborted, pending and future calls reject with an +`AbortError`, with the signal's reason set as the error's `cause`, and `fn` is +not invoked by those calls. If the signal is already aborted, `debounce()` +throws an `AbortError`. + +The returned function has the following properties: + +* `cancel([reason])` cancels the current debounce window. Its pending promises + reject with an `AbortError`. If provided, `reason` is set as the error's + `cause`. +* `flush()` cancels the delay and invokes `fn` immediately. It has no effect if + no invocation is pending. +* `pending` {Promise|null} is the promise returned by the most recent call in + the current debounce window, or `null` if no invocation is pending. +* `pendingCount` {integer} is the number of calls awaiting the invocation in + the current debounce window. +* `ref()` makes the pending and future timeout keep the Node.js event loop + active. Returns the debounced function. +* `unref()` allows the event loop to exit while a timeout is pending. This also + applies to future timeouts. Returns the debounced function. + +When invoked, `fn` has the debounced function as its `this` value. After a +trailing invocation, a new debounce window can begin even if a promise returned +by `fn` is still pending. The debounced function preserves the `name` and +`length` of `fn`. + +```mjs +import { setTimeout as wait } from 'node:timers/promises'; +import { debounce } from 'node:util'; + +const fn = debounce(async (value) => { + await wait(100); + return value; +}, 50); + +const first = fn(1); +const second = fn(2); + +console.log(await first); // 2 +console.log(await second); // 2 +``` + +A debounced function can be used to trigger an action after a period of +inactivity. Each call resets the timeout: + +```cjs +const { debounce } = require('node:util'); + +const onInactivity = debounce(() => { + console.log('No activity for 5 seconds'); +}, 5_000).unref(); + +process.stdin.on('data', (data) => { + console.log(`Received ${data.length} bytes`); + onInactivity(); +}); + +// Start the initial inactivity timeout. +onInactivity(); +``` + ## `util.diff(actual, expected)` + +* `fn` {Function} The function to throttle. +* `limit` {integer} The maximum number of times to invoke `fn` during an + interval. Must be greater than `0`. +* `interval` {integer} The length of each interval in milliseconds. +* `options` {Object} + * `concurrency` {number} The maximum number of invocations of `fn` whose + return values may be unsettled at once. Must be a positive integer or + `Infinity`. **Default:** `Infinity`. + * `maxPending` {number} The maximum number of calls that may be queued when + `overflow` is `'queue'`. Must be a non-negative integer or `Infinity`. + **Default:** `Infinity`. + * `overflow` {string} Determines how calls exceeding the limit are handled. + **Default:** `'queue'`. + * `'queue'`: Queue calls in the order received. + * `'drop'`: Reject calls immediately without queueing them. + * `signal` {AbortSignal} An `AbortSignal` that cancels pending calls and + prevents future calls when aborted. + * `strict` {boolean} When `true`, ensures that `limit` is not exceeded during + any rolling interval. **Default:** `false`. +* Returns: {Function} The throttled function. + +Creates a function that limits how often `fn` is invoked. By default, calls that +exceed the limit are queued in the order received rather than discarded. The +throttled function returns a {Promise} for the value returned by `fn`. If `fn` +throws or returns a rejected promise, the returned promise is rejected with the +same reason. + +An invocation starts only when both rate and concurrency capacity are +available. Rate capacity is consumed when `fn` starts, not when a call enters +the queue. Concurrency capacity is released when the value returned by `fn` +settles. Non-promise values settle during the next microtask. + +When `options.overflow` is `'drop'`, calls made without available rate or +concurrency capacity are rejected immediately. When `options.overflow` is +`'queue'` and `options.maxPending` calls are already queued, additional calls +are also rejected immediately. `maxPending` has no effect when `overflow` is +`'drop'`. + +In both cases, rejected calls return a promise rejected with an +`ERR_THROTTLED` error. The rejected promise is marked as handled, so ignoring it +does not emit an `'unhandledRejection'` event. Awaiting or explicitly handling +the promise still observes the rejection. Rejected calls do not consume rate +or concurrency capacity, enter the queue, or schedule a timeout. + +By default, the interval begins when the first call in a new window invokes +`fn`. Up to `limit` calls can invoke `fn` during that window. Queued calls are +processed in groups of up to `limit` as each subsequent window begins. This +windowed behavior can result in calls occurring close together at a window +boundary. + +When `options.strict` is `true`, invocation times are tracked individually. +This ensures that no more than `limit` calls begin during any rolling interval, +at the cost of additional bookkeeping. + +If `options.signal` is aborted, pending and future calls reject with an +`AbortError`, with the signal's reason set as the error's `cause`, and `fn` is +not invoked by those calls. If the signal is already aborted, `throttle()` +throws an `AbortError`. + +The returned function has the following properties: + +* `cancel([reason])` cancels all queued calls and resets the current throttle + window. The queued promises reject with an `AbortError`. If provided, + `reason` is set as the error's `cause`. Does not cancel invocations that have + already started. +* `hasImmediateCapacity()` returns `true` if a call made at that moment could + invoke `fn` without being queued or rejected. The check does not reserve + capacity, and the throttled function always checks again when called. It + returns `false` while calls are queued to preserve their order. Callers can + avoid creating a timeout by only calling the throttled function when this + method returns `true`. +* `pending` {Promise|null} is the promise returned by the most recently queued + call, or `null` if no invocation is queued. +* `pendingCount` {integer} is the number of calls awaiting invocation. +* `activeCount` {integer} is the number of invocations whose return values have + not settled. +* `ref()` makes the pending and future timeout keep the Node.js event loop + active. Returns the throttled function. +* `unref()` allows the event loop to exit while a timeout is pending. This also + applies to future timeouts. Returns the throttled function. + +Calls that have already invoked `fn` are not affected by `cancel()` or by an +aborted signal. When invoked, `fn` has the throttled function as its `this` +value. The throttled function preserves the `name` and `length` of `fn`. + +```mjs +import { throttle } from 'node:util'; + +const request = throttle(async (id) => { + const response = await fetch(`https://example.com/items/${id}`); + return response.json(); +}, 2, 1_000); + +// At most two requests begin during each one-second interval. All other calls +// remain queued and retain their original arguments. +const results = await Promise.all([ + request(1), + request(2), + request(3), + request(4), +]); +``` + ## `util.diff(actual, expected)` * `path` {string|null} Path to a dynamic library, or `null` to resolve symbols @@ -221,6 +226,13 @@ Loads a dynamic library and resolves the requested function definitions. On Windows passing `null` is not supported. +A `path` inside a mounted [virtual file system][] is supported: the +operating system's dynamic loader cannot open a virtual path, so the +library's bytes are read from the VFS and loaded from a private, +self-cleaning temporary image instead, while `lib.path` keeps reporting +the virtual path. Libraries on the real file system are unaffected and +load directly. + When `definitions` is omitted, `functions` is returned as an empty object until symbols are resolved explicitly. @@ -302,6 +314,14 @@ Represents a loaded dynamic library. ### `new DynamicLibrary(path)` + + * `path` {string|null} Path to a dynamic library, or `null` to resolve symbols from the current process image. @@ -309,6 +329,9 @@ Loads the dynamic library without resolving any functions eagerly. On Windows passing `null` is not supported. +A `path` inside a mounted [virtual file system][] loads the same way as +with [`ffi.dlopen()`][]. + ```cjs const { DynamicLibrary, suffix } = require('node:ffi'); @@ -798,7 +821,9 @@ and keep callback and pointer lifetimes explicit on the native side. [Permission Model]: permissions.md#permission-model [`--allow-ffi`]: cli.md#--allow-ffi +[`ffi.dlopen()`]: #ffidlopenpath-definitions [`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy [`library.functions`]: #libraryfunctions [`using`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using [type names]: #type-names +[virtual file system]: vfs.md diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 6f67a85ad8a8..40998b9a548c 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -425,6 +425,12 @@ addon's bytes are read from the VFS and loaded from a private, self-cleaning temporary image instead. Addons on the real file system are unaffected and load directly. +Shared libraries opened through [`ffi.dlopen()`][] (or +[`new ffi.DynamicLibrary()`][]) work the same way: a library path inside a +mounted VFS is detected, its bytes are read from the VFS, and the library is +loaded from a private, self-cleaning image while `library.path` keeps +reporting the virtual path. Libraries on the real file system load directly. + ## Use with Single Executable Applications When running as a [Single Executable Application][] built with @@ -634,9 +640,11 @@ fields use synthetic but stable values: [`VirtualFileSystem`]: #class-virtualfilesystem [`VirtualProvider`]: #class-virtualprovider [`ZipProvider`]: #class-zipprovider +[`ffi.dlopen()`]: ffi.md#ffidlopenpath-definitions [`fs.BigIntStats`]: fs.md#class-fsstats [`fs.Stats`]: fs.md#class-fsstats [`import.meta.resolve()`]: esm.md#importmetaresolvespecifier +[`new ffi.DynamicLibrary()`]: ffi.md#new-dynamiclibrarypath [`node:fs`]: fs.md [`require()`]: modules.md#requireid [`require.resolve()`]: modules.md#requireresolverequest-options diff --git a/lib/ffi.js b/lib/ffi.js index ce8345f155fb..5cd7c4b354ab 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -9,6 +9,7 @@ const { ObjectGetOwnPropertyDescriptor, ObjectKeys, ObjectPrototypeToString, + ReflectConstruct, SafeWeakMap, SafeWeakRef, SymbolDispose, @@ -38,7 +39,7 @@ const { emitExperimentalWarning('FFI'); const { - DynamicLibrary, + DynamicLibrary: NativeDynamicLibrary, getInt8, getUint8, getInt16, @@ -119,6 +120,37 @@ function wrapFFIFunction(rawFn, owner) { return wrapped; } +const { getVfsLibraryReader } = require('internal/ffi/vfs'); + +// A thin constructor in front of the native class so that a library inside +// a mounted virtual file system loads transparently: its bytes are read +// from the VFS and handed to the native constructor, which loads them from +// a private, self-cleaning image - the same way require() handles a native +// addon in a VFS. The reader is installed by the VFS while it is mounted +// (see internal/ffi/vfs), so no VFS code is ever loaded from here. The +// wrapper shares the native prototype, so instances and instanceof behave +// as if the native class were exposed directly. +function DynamicLibrary(path) { + if (new.target === undefined) { + // Let the native constructor produce its usual error. + return FunctionPrototypeCall(NativeDynamicLibrary, this, path); + } + const readVirtualLibrary = getVfsLibraryReader(); + const binary = + readVirtualLibrary === null || typeof path !== 'string' ? + undefined : readVirtualLibrary(path); + return ReflectConstruct(NativeDynamicLibrary, + binary === undefined ? [path] : [path, binary], + new.target); +} +DynamicLibrary.prototype = NativeDynamicLibrary.prototype; +ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', { + __proto__: null, + configurable: true, + value: DynamicLibrary, + writable: true, +}); + const rawGetFunction = DynamicLibrary.prototype.getFunction; const rawGetFunctions = DynamicLibrary.prototype.getFunctions; const rawClose = DynamicLibrary.prototype.close; diff --git a/lib/internal/ffi/vfs.js b/lib/internal/ffi/vfs.js new file mode 100644 index 000000000000..eb6342ec0452 --- /dev/null +++ b/lib/internal/ffi/vfs.js @@ -0,0 +1,27 @@ +'use strict'; + +// Seam between node:ffi and the virtual file system, mirroring the fs +// handler integration in internal/fs/utils: the VFS hook installer sets a +// library reader while at least one VFS is mounted and clears it when the +// last one unmounts, and DynamicLibrary consults it before every load. The +// dependency points from the VFS into ffi: ffi never loads any VFS code, +// and pays only a null check while no VFS is mounted. + +// When reader is null, no VFS is active (zero overhead). Otherwise it is +// (path) => Buffer|undefined: the library's bytes for a path inside a +// mounted VFS, or undefined for a path the dynamic loader should open +// itself. +let vfsLibraryReader = null; + +function setVfsLibraryReader(reader) { + vfsLibraryReader = reader; +} + +function getVfsLibraryReader() { + return vfsLibraryReader; +} + +module.exports = { + getVfsLibraryReader, + setVfsLibraryReader, +}; diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 3e183e422831..3d624d5af293 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -968,10 +968,38 @@ function installAddonLoader() { const { dlopenBinary } = internalBinding('process_methods'); return dlopenBinary(module, filename, flags, readFileSync(filename)); } + // Do not forward a missing flags argument as `undefined`: + // process.dlopen() coerces it to 0, which is not a valid dlopen(2) + // mode, instead of applying the default flags. + if (flags === undefined) return originalDlopen(module, filename); return originalDlopen(module, filename, flags); }; } +/** + * Reads the bytes of a file that lives in a mounted VFS. Returns undefined + * for a path outside the reserved VFS root - the caller should open the + * path itself - and throws ENOENT for a path under the root that no + * mounted VFS serves, since no real file can exist there. Installed into + * node:ffi while hooks are installed, so DynamicLibrary can load a + * VFS-resident library from a private image, the same way the module + * loader handles a native addon in a VFS. + * @param {string} pathStr The path of the library + * @returns {Buffer|undefined} The library's bytes, or undefined + */ +function readVirtualBinary(pathStr) { + const normalized = normalizeMountedPath(pathStr); + if (!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) { + return undefined; + } + const layerId = getLayerIdFromPath(normalized); + const vfs = layerId === -1 ? undefined : activeVFSLayers.get(layerId); + if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) { + throw createENOENT('open', pathStr); + } + return vfs.readFileSync(normalized); +} + /** * Install all VFS hooks: module loader overrides and fs handlers. */ @@ -981,6 +1009,8 @@ function installHooks() { normalizedVfsRootPrefix = getNormalizedVfsRoot() + sep; installModuleLoaderOverrides(); installAddonLoader(); + const { setVfsLibraryReader } = require('internal/ffi/vfs'); + setVfsLibraryReader(readVirtualBinary); vfsHandlerObj = createVfsHandlers(); setVfsHandlers(vfsHandlerObj); hooksInstalled = true; @@ -998,6 +1028,8 @@ function uninstallHooks() { setLoaderOverrides(); setVfsHandlers(null); vfsHandlerObj = undefined; + const { setVfsLibraryReader } = require('internal/ffi/vfs'); + setVfsLibraryReader(null); process.dlopen = originalDlopen; hooksInstalled = false; } diff --git a/src/node_binding.cc b/src/node_binding.cc index e5cebd5fe385..b93e4200f299 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -8,7 +8,9 @@ #include "permission/permission.h" #include "util.h" +#include #include +#include #include #ifdef _WIN32 @@ -17,7 +19,6 @@ #include #include #include -#include #if defined(__linux__) #include #include @@ -475,64 +476,60 @@ int NodeMemfdCreate(const char* name, unsigned int flags) { return static_cast(syscall(SYS_memfd_create, name, flags)); } #endif // __linux__ +#else // _WIN32 + +// Windows refuses to unlink a file that backs a mapped image section: neither +// delete-on-close, nor DeleteFile(), nor a POSIX-semantics disposition can +// remove it while the DLL is loaded. A materialized image therefore has to +// outlive its load, and the only moment it can go is once the module is +// unloaded again. Node keeps addons loaded for the life of the process, so +// that moment is process exit: each image is kept here with the module it was +// loaded as, and released together at exit. +struct RetainedAddonImage { + HMODULE module; + std::wstring path; +}; +Mutex g_retained_addon_images_mutex; +std::vector* g_retained_addon_images = nullptr; + +// Unloads the images this process materialized -- and only those; addons loaded +// from a real path are left alone -- so that each file can finally be deleted. +// This has to happen after everything that might still call into an addon, so +// it is registered during static initialisation below: atexit() runs handlers +// last-registered-first, so registering before main() puts this behind every +// handler that is registered while running. +void ReleaseRetainedAddonImages() { + Mutex::ScopedLock lock(g_retained_addon_images_mutex); + if (g_retained_addon_images == nullptr) return; + for (auto it = g_retained_addon_images->rbegin(); + it != g_retained_addon_images->rend(); + ++it) { + // Deleting first doubles as the test for whether the image is still + // mapped, because that is the only thing that can stop it: an FFI library + // the caller already close()d is gone by now, and unloading it a second + // time through a stale module handle would be wrong. + if (DeleteFileW(it->path.c_str())) continue; + if (it->module != nullptr) FreeLibrary(it->module); + DeleteFileW(it->path.c_str()); + } + g_retained_addon_images->clear(); +} + +// Arms the hook before main() rather than at the first load; see above. +const struct RetainedAddonImageExitHook { + RetainedAddonImageExitHook() { atexit(ReleaseRetainedAddonImages); } +} g_retained_addon_image_exit_hook; + #endif // !_WIN32 -// Materializes native-addon bytes into a form dlopen()/LoadLibrary() can load, -// with the smallest, most private on-disk footprint each platform allows: -// Linux: an anonymous in-memory memfd, loaded via /proc/self/fd/N - -// the bytes never touch the filesystem. -// other POSIX: a 0700 mkdtemp() directory plus an O_EXCL|O_NOFOLLOW file, -// unlink()ed right after the load (the mapping keeps it alive). -// Windows: a temp file opened FILE_FLAG_DELETE_ON_CLOSE; its handle is -// retained for the process lifetime so the file is removed -// automatically once the process (and the loaded DLL) exit. -// Used for an addon that lives somewhere dlopen() cannot open by path, such as -// a virtual file system. -class AddonImage { - public: - AddonImage() = default; - ~AddonImage(); - AddonImage(const AddonImage&) = delete; - AddonImage& operator=(const AddonImage&) = delete; - - // The directory a temporary image would be written to, with a trailing - // separator; empty when it cannot be determined. Names the resource for the - // file-system permission check. - static std::string TempDir(); - - // On success sets path() to a real, loadable path for `data`. - bool Materialize(const char* data, size_t len); - const std::string& path() const { return path_; } - const std::string& errmsg() const { return errmsg_; } - - // Call exactly once, right after DLib::Open(); `opened` says whether the load - // succeeded. Releases the transient resources that are no longer needed (a - // successful load holds its own mapping): on POSIX closes the memfd or - // unlinks the temp file; on Windows retains the delete-on-close handle for - // the process lifetime when opened, or closes it (deleting the file) on - // failure. - void AfterOpen(bool opened); +} // namespace - private: - std::string path_; - std::string errmsg_; - bool consumed_ = false; -#ifdef _WIN32 - HANDLE handle_ = INVALID_HANDLE_VALUE; -#else - bool MaterializeTempFile(const char* data, size_t len); - int fd_ = -1; - std::string temp_dir_; // non-empty only for the temp-file (non-memfd) path -#endif -}; +// AddonImage is declared in node_binding.h so that the other loader of +// dynamically shared objects, node_ffi.cc, can reuse it; see the header for +// the platform-by-platform description. #ifdef _WIN32 -// Delete-on-close handles kept alive until process exit so their temp files -// outlive the loaded DLLs and are removed once the process ends. -Mutex g_retained_addon_handles_mutex; -std::vector* g_retained_addon_handles = nullptr; - // static std::string AddonImage::TempDir() { wchar_t dir[MAX_PATH + 1]; @@ -559,18 +556,22 @@ bool AddonImage::Materialize(const char* data, size_t len) { errmsg_ = "could not create a temporary file name"; return false; } - // Reopen the just-created file delete-on-close, sharing delete so the loader - // can map it while it is delete-pending; the file is removed when this handle - // and the loader's section are both released (i.e. at process exit). - handle_ = CreateFileW(file, - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, - nullptr); - if (handle_ == INVALID_HANDLE_VALUE) { + // Write the image and close it again: nothing may still hold the file open + // when the loader gets to it. Sharing is checked in both directions, and the + // loader opens a DLL for read and execute while sharing read alone, so any + // handle of ours holding write access fails the load with + // ERROR_SHARING_VIOLATION however permissive this side's share mode is. + HANDLE writer = + CreateFileW(file, + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY, + nullptr); + if (writer == INVALID_HANDLE_VALUE) { errmsg_ = "could not create a temporary file for the native addon"; + DeleteFileW(file); return false; } size_t off = 0; @@ -578,48 +579,52 @@ bool AddonImage::Materialize(const char* data, size_t len) { DWORD chunk = len - off > MAXDWORD ? MAXDWORD : static_cast(len - off); DWORD written = 0; - if (!WriteFile(handle_, data + off, chunk, &written, nullptr)) { + if (!WriteFile(writer, data + off, chunk, &written, nullptr)) { errmsg_ = "could not write the native addon to a temporary file"; - CloseHandle(handle_); - handle_ = INVALID_HANDLE_VALUE; + CloseHandle(writer); + DeleteFileW(file); return false; } off += written; } + CloseHandle(writer); + int utf8_len = WideCharToMultiByte(CP_UTF8, 0, file, -1, nullptr, 0, nullptr, nullptr); if (utf8_len <= 0) { errmsg_ = "could not encode the temporary file path"; - CloseHandle(handle_); - handle_ = INVALID_HANDLE_VALUE; + DeleteFileW(file); return false; } path_.resize(utf8_len - 1); WideCharToMultiByte( CP_UTF8, 0, file, -1, path_.data(), utf8_len, nullptr, nullptr); + wpath_ = file; return true; } -void AddonImage::AfterOpen(bool opened) { +void AddonImage::AfterOpen(bool opened, void* module) { consumed_ = true; - if (handle_ == INVALID_HANDLE_VALUE) return; + if (wpath_.empty()) return; if (!opened) { - CloseHandle(handle_); // delete-on-close removes the file - handle_ = INVALID_HANDLE_VALUE; + DeleteFileW(wpath_.c_str()); // nothing mapped it, so it can go now + wpath_.clear(); return; } - Mutex::ScopedLock lock(g_retained_addon_handles_mutex); - if (g_retained_addon_handles == nullptr) { - g_retained_addon_handles = new std::vector(); + // The load mapped it, so it has to stay until that module is unloaded again. + Mutex::ScopedLock lock(g_retained_addon_images_mutex); + if (g_retained_addon_images == nullptr) { + g_retained_addon_images = new std::vector(); } - g_retained_addon_handles->push_back(handle_); - handle_ = INVALID_HANDLE_VALUE; + g_retained_addon_images->push_back( + {static_cast(module), std::move(wpath_)}); + wpath_.clear(); } AddonImage::~AddonImage() { - // Materialized but Open() was never reached (e.g. an exception in between): - // closing the delete-on-close handle removes the file. - if (!consumed_ && handle_ != INVALID_HANDLE_VALUE) CloseHandle(handle_); + // Materialized but the load was never reached (e.g. an exception in + // between): nothing mapped the file, so remove it now. + if (!consumed_ && !wpath_.empty()) DeleteFileW(wpath_.c_str()); } #else // !_WIN32 @@ -701,10 +706,12 @@ bool AddonImage::MaterializeTempFile(const char* data, size_t len) { return true; } -void AddonImage::AfterOpen(bool opened) { +void AddonImage::AfterOpen(bool opened, void* module) { consumed_ = true; - // The right cleanup is the same whether or not the load worked. + // The right cleanup is the same whether or not the load worked, and the + // module never has to be unloaded: the name is already gone by now. (void)opened; + (void)module; // memfd: the load's mapping (or nothing, on failure) owns it from here. if (fd_ != -1) { close(fd_); @@ -731,8 +738,6 @@ AddonImage::~AddonImage() { #endif // _WIN32 -} // namespace - // Shared by process.dlopen() and the internal dlopenBinary(). `allow_binary` // says whether args[3] may carry the addon's bytes; it is false for // process.dlopen(), whose signature stays (module, filename[, flags]). @@ -815,7 +820,7 @@ static void DLOpenImpl(const FunctionCallbackInfo& args, Mutex::ScopedLock lock(dlib_load_mutex); const bool is_opened = dlib->Open(); - image.AfterOpen(is_opened); + image.AfterOpen(is_opened, is_opened ? dlib->handle_ : nullptr); // Objects containing v14 or later modules will have registered themselves // on the pending list. Activate all of them now. At present, only one diff --git a/src/node_binding.h b/src/node_binding.h index b931bf9d7389..ee30117d2068 100644 --- a/src/node_binding.h +++ b/src/node_binding.h @@ -7,6 +7,8 @@ #include #endif +#include + #include "node.h" #include "node_api.h" #include "quic/guard.h" @@ -171,6 +173,60 @@ void GetLinkedBinding(const v8::FunctionCallbackInfo& args); void DLOpen(const v8::FunctionCallbackInfo& args); void DLOpenBinary(const v8::FunctionCallbackInfo& args); +// Materializes the bytes of a dynamically shared object into a form +// dlopen()/LoadLibrary() can load, with the smallest, most private on-disk +// footprint each platform allows: +// Linux: an anonymous in-memory memfd, loaded via /proc/self/fd/N - +// the bytes never touch the filesystem. +// other POSIX: a 0700 mkdtemp() directory plus an O_EXCL|O_NOFOLLOW file, +// unlink()ed right after the load (the mapping keeps it alive). +// Windows: a temp file, written and closed before the load because the +// loader shares read alone. It cannot be unlinked while its +// image is mapped, so it is kept with the module it loaded as +// and both are released at process exit. +// Used for a native addon or an FFI library that lives somewhere the dynamic +// loader cannot open by path, such as a virtual file system. Call exactly one +// of Materialize()+AfterOpen() around the load; a destroyed image that never +// reached AfterOpen() cleans up after itself. +class AddonImage { + public: + AddonImage() = default; + ~AddonImage(); + AddonImage(const AddonImage&) = delete; + AddonImage& operator=(const AddonImage&) = delete; + + // The directory a temporary image would be written to, with a trailing + // separator; empty when it cannot be determined. Names the resource for the + // file-system permission check. + static std::string TempDir(); + + // On success sets path() to a real, loadable path for `data`. + bool Materialize(const char* data, size_t len); + const std::string& path() const { return path_; } + const std::string& errmsg() const { return errmsg_; } + + // Call exactly once, right after the load; `opened` says whether the load + // succeeded and `module` is the module handle it produced. Releases what is + // no longer needed: on POSIX closes the memfd or unlinks the temp file, which + // a successful load keeps alive through its own mapping. Windows cannot + // unlink a mapped image, so there the file is removed at once only when the + // load failed; otherwise it is kept, with `module`, until process exit, where + // the module is unloaded and the file finally deleted. + void AfterOpen(bool opened, void* module); + + private: + std::string path_; + std::string errmsg_; + bool consumed_ = false; +#ifdef _WIN32 + std::wstring wpath_; // the path of the image, to delete it again at exit +#else + bool MaterializeTempFile(const char* data, size_t len); + int fd_ = -1; + std::string temp_dir_; +#endif +}; + } // namespace binding } // namespace node diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 2555e0aa7968..13f1ab384aa6 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -10,6 +10,7 @@ #include "ffi/data.h" #include "ffi/fast.h" #include "ffi/types.h" +#include "node_binding.h" #include "node_errors.h" namespace node { @@ -525,9 +526,44 @@ void DynamicLibrary::New(const FunctionCallbackInfo& args) { library_path = lib->path_.c_str(); } + // On the internal path args[1] carries the library's bytes, for a library + // that lives somewhere the dynamic loader cannot open by path (a virtual + // file system). Materialize them into a private, self-cleaning image - the + // same mechanism process.dlopen() uses for such native addons - and load + // that, while still reporting the library's own path in `library.path` and + // any error. + binding::AddonImage image; + if (args.Length() > 1 && !args[1]->IsUndefined()) { + if (!args[1]->IsArrayBufferView()) { + THROW_ERR_INVALID_ARG_TYPE( + env, "Library binary must be a Buffer, TypedArray, or DataView"); + return; + } + // Loading from bytes materializes them into an image in the temporary + // directory, so this needs write access there on top of the FFI + // permission checked above. The check does not depend on whether the + // image actually reaches the file system on this platform (Linux uses an + // anonymous memfd): what a program must be granted should not vary by + // platform. + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, + permission::PermissionScope::kFileSystemWrite, + binding::AddonImage::TempDir()); + ArrayBufferViewContents binary(args[1]); + if (!image.Materialize(binary.data(), binary.length())) { + THROW_ERR_FFI_CALL_FAILED( + env, "dlopen failed: %s: %s", image.errmsg().c_str(), library_path); + return; + } + library_path = image.path().c_str(); + } + CHECK(lib->is_closed()); // Open the library - if (uv_dlopen(library_path, &lib->lib_) != 0) { + const bool opened = uv_dlopen(library_path, &lib->lib_) == 0; + image.AfterOpen(opened, + opened ? static_cast(lib->lib_.handle) : nullptr); + if (!opened) { THROW_ERR_FFI_CALL_FAILED(env, "dlopen failed: %s", uv_dlerror(&lib->lib_)); return; } diff --git a/test/ffi/test-ffi-vfs.js b/test/ffi/test-ffi-vfs.js new file mode 100644 index 000000000000..884bd9f0d5da --- /dev/null +++ b/test/ffi/test-ffi-vfs.js @@ -0,0 +1,94 @@ +// Flags: --experimental-vfs +'use strict'; +const common = require('../common'); +common.skipIfFFIMissing(); +const assert = require('node:assert'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { test } = require('node:test'); +const ffi = require('node:ffi'); +const vfs = require('node:vfs'); +const { fixtureSymbols, libraryPath } = require('./ffi-test-common'); + +// A library inside a mounted VFS loads transparently: the dynamic loader +// cannot open the reserved mount path, so its bytes are read from the VFS +// and loaded from a private, self-cleaning image - the same way require() +// handles a native addon in a VFS. +const libraryName = path.basename(libraryPath); +const myVfs = vfs.create(); +myVfs.writeFileSync(`/${libraryName}`, fs.readFileSync(libraryPath)); +const mountPoint = myVfs.mount(); +const virtualPath = path.join(mountPoint, libraryName); + +test('ffi.dlopen() loads a library from a mounted VFS', () => { + const before = new Set(fs.readdirSync(os.tmpdir())); + const { lib, functions } = ffi.dlopen(virtualPath, { + add_i32: fixtureSymbols.add_i32, + }); + + try { + assert.ok(lib instanceof ffi.DynamicLibrary); + // The library reports its own (virtual) path, not the image's. + assert.strictEqual(lib.path, virtualPath); + assert.strictEqual(functions.add_i32(2, 40), 42); + } finally { + lib.close(); + } + + // On Linux the image is an in-memory memfd that never touches the + // filesystem; on other POSIX it is unlinked right after loading. + // (Windows keeps a delete-on-close file until exit, so skip there.) + if (!common.isWindows) { + const leaked = fs.readdirSync(os.tmpdir()) + .filter((f) => f.startsWith('node-addon') && !before.has(f)); + assert.deepStrictEqual(leaked, [], `image not cleaned up: ${leaked}`); + } +}); + +test('new ffi.DynamicLibrary() loads from a mounted VFS', () => { + const lib = new ffi.DynamicLibrary(virtualPath); + + try { + assert.strictEqual(lib.path, virtualPath); + const addU8 = lib.getFunction('add_u8', fixtureSymbols.add_u8); + assert.strictEqual(addU8(19, 23), 42); + } finally { + lib.close(); + } +}); + +test('a missing library inside the VFS throws ENOENT', () => { + assert.throws(() => { + ffi.dlopen(path.join(mountPoint, 'no-such-library.so')); + }, { code: 'ENOENT' }); +}); + +test('libraries on the real file system still load directly', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + add_i32: fixtureSymbols.add_i32, + }); + + try { + assert.strictEqual(lib.path, libraryPath); + assert.strictEqual(functions.add_i32(-1, 2), 1); + } finally { + lib.close(); + } +}); + +test('a library loaded from a VFS outlives the mount', () => { + const otherVfs = vfs.create(); + otherVfs.writeFileSync(`/${libraryName}`, fs.readFileSync(libraryPath)); + const otherMount = otherVfs.mount(); + const { lib, functions } = ffi.dlopen(path.join(otherMount, libraryName), { + add_i32: fixtureSymbols.add_i32, + }); + + try { + otherVfs.unmount(); + assert.strictEqual(functions.add_i32(20, 22), 42); + } finally { + lib.close(); + } +}); diff --git a/test/parallel/test-dlopen-binary-image-cleanup.js b/test/parallel/test-dlopen-binary-image-cleanup.js new file mode 100644 index 000000000000..dabaeabaa0cc --- /dev/null +++ b/test/parallel/test-dlopen-binary-image-cleanup.js @@ -0,0 +1,90 @@ +// Flags: --expose-internals +'use strict'; + +// Loading an addon from bytes materializes them into a private image so the +// dynamic loader has a real path to open. That image is transient and must not +// outlive the process that loaded it. How it is held differs by platform, so +// this checks both halves of the contract: +// +// Linux: an anonymous memfd loaded through /proc/self/fd - nothing ever +// reaches the filesystem, and AfterOpen() closes the descriptor +// once the load owns its mapping, so repeated loads must not +// accumulate open descriptors. +// other POSIX: a mkdtemp() directory unlinked and rmdir()ed right after the +// load, so nothing is left even while the process runs. +// Windows: the loader maps the file by path for the DLL's lifetime, so +// the image has to stay put; it is retained with the module it +// loaded as, and both are released at process exit. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const tmpdir = require('../common/tmpdir'); + +const addonPath = path.join( + __dirname, '..', 'addons', 'hello-world', 'build', 'Release', 'binding.node'); +if (!fs.existsSync(addonPath)) common.skip('the hello-world addon is not built'); + +tmpdir.refresh(); + +// Where a temp-file image would land: GetTempPathW() reads TMP/TEMP and +// TempDir() reads TMPDIR, so pointing all three at a directory this test owns +// keeps any image the child writes somewhere it can inspect afterwards. Linux +// normally uses a memfd and never writes here at all. +const imageDir = tmpdir.resolve('addon-images'); +fs.mkdirSync(imageDir, { recursive: true }); + +const child = ` + const fs = require('fs'); + const { internalBinding } = require('internal/test/binding'); + const { dlopenBinary } = internalBinding('process_methods'); + const bytes = fs.readFileSync(${JSON.stringify(addonPath)}); + // A path that does not exist on disk, as a VFS-resident addon would be, so + // the load can only come from the bytes and their materialized image. + const virtualPath = ${JSON.stringify(path.join(addonPath, '..', 'nowhere', 'binding.node'))}; + + // On Linux the image is a descriptor rather than a file, so count them: each + // load must hand its fd to the mapping and close it, leaving no growth. + const fdDir = '/proc/self/fd'; + const countFds = () => { + try { return fs.readdirSync(fdDir).length; } catch { return -1; } + }; + const before = countFds(); + + // Load repeatedly: each load materializes its own image, so a leak of an + // image, a descriptor or a retained handle shows up as growth. + for (let i = 0; i < 5; i++) { + const m = { exports: {} }; + // flags undefined: keep the default dlopen(2) mode - 0 is not valid + // everywhere (glibc rejects it with EINVAL). + dlopenBinary(m, virtualPath, undefined, bytes); + if (m.exports.hello() !== 'world') throw new Error('addon did not load'); + } + + const after = countFds(); + if (before !== -1 && after > before) { + throw new Error(\`descriptor leak: \${before} -> \${after} after 5 loads\`); + } + process.exit(0); +`; + +const res = spawnSync(process.execPath, ['--expose-internals', '-e', child], { + env: { ...process.env, TMPDIR: imageDir, TMP: imageDir, TEMP: imageDir }, + encoding: 'utf8', +}); + +// The load itself must succeed. On Windows a retained writable handle makes the +// loader fail with ERROR_SHARING_VIOLATION ("The process cannot access the file +// because it is being used by another process"). +assert.strictEqual(res.status, 0, `child failed:\n${res.stderr}`); + +// Nothing an image left behind may outlive the process that created it. Match +// the shapes the two on-disk paths produce rather than requiring the directory +// to be empty, so an unrelated temp file cannot fail this. +const leftovers = fs.readdirSync(imageDir).filter( + (name) => /^nod.*\.tmp$/i.test(name) || name.startsWith('node-addon-')); +assert.deepStrictEqual( + leftovers, [], + `materialized addon image outlived the process that loaded it: ${leftovers}`); diff --git a/test/parallel/test-vfs-addon.js b/test/parallel/test-vfs-addon.js index 1c138a187d0a..7ea8fdbf7943 100644 --- a/test/parallel/test-vfs-addon.js +++ b/test/parallel/test-vfs-addon.js @@ -34,4 +34,12 @@ if (process.platform !== 'win32') { assert.deepStrictEqual(leaked, [], `addon temp not cleaned up: ${leaked}`); } +// Regression check: while a VFS is mounted, process.dlopen() of a +// real-file-system addon without a flags argument must keep the default +// flags rather than forwarding `undefined`, which coerces to 0 - not a +// valid dlopen(2) mode. +const realMod = { exports: {} }; +process.dlopen(realMod, addonPath); +assert.strictEqual(realMod.exports.hello(), 'world'); + myVfs.unmount(); From 26dbe11ed371ad2ddabdee2d6c67e6787c15e888 Mon Sep 17 00:00:00 2001 From: greenhead Date: Sun, 13 Sep 2026 09:05:04 +0900 Subject: [PATCH 08/83] test: fix stderr Buffer assertion in exec encoding test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: greenhead PR-URL: https://github.com/nodejs/node/pull/66008 Refs: https://github.com/nodejs/node/pull/10919 Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón --- test/parallel/test-child-process-exec-encoding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-child-process-exec-encoding.js b/test/parallel/test-child-process-exec-encoding.js index 21ab207fca8c..78a9b55b9dfc 100644 --- a/test/parallel/test-child-process-exec-encoding.js +++ b/test/parallel/test-child-process-exec-encoding.js @@ -41,7 +41,7 @@ if (process.argv[2] === 'child') { [undefined, null, 'buffer', 'invalid'].forEach((encoding) => { run({ encoding }, common.mustCall((stdout, stderr) => { assert(stdout instanceof Buffer); - assert(stdout instanceof Buffer); + assert(stderr instanceof Buffer); assert.strictEqual(stdout.toString(), expectedStdout); assert.strictEqual(stderr.toString(), expectedStderr); })); From 11ed32572b396084264f793f21a6e74e347576e3 Mon Sep 17 00:00:00 2001 From: Tim Perry <1526883+pimterry@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:29:34 +0200 Subject: [PATCH 09/83] quic: fix readable stream truncation on stop-sending, abort & timeout Signed-off-by: Tim Perry PR-URL: https://github.com/nodejs/node/pull/63967 Reviewed-By: James M Snell --- doc/api/quic.md | 24 +++ lib/internal/blob.js | 6 +- lib/internal/errors.js | 2 - lib/internal/quic/quic.js | 130 +++++++++---- lib/internal/quic/state.js | 35 +++- src/quic/streams.cc | 46 +++-- src/quic/streams.h | 12 +- test/common/quic.mjs | 79 ++++++++ .../test-quic-stream-iteration-destroyed.mjs | 37 ---- .../test-quic-stream-iteration-reset.mjs | 64 ------ .../test-quic-stream-setbody-errors.mjs | 8 +- ...st-quic-stream-truncated-reads-destroy.mjs | 72 +++++++ ...st-quic-stream-truncated-reads-timeout.mjs | 35 ++++ .../test-quic-stream-truncated-reads.mjs | 184 ++++++++++++++++++ 14 files changed, 556 insertions(+), 178 deletions(-) delete mode 100644 test/parallel/test-quic-stream-iteration-destroyed.mjs delete mode 100644 test/parallel/test-quic-stream-iteration-reset.mjs create mode 100644 test/parallel/test-quic-stream-truncated-reads-destroy.mjs create mode 100644 test/parallel/test-quic-stream-truncated-reads-timeout.mjs create mode 100644 test/parallel/test-quic-stream-truncated-reads.mjs diff --git a/doc/api/quic.md b/doc/api/quic.md index 19995000b20e..dfb3aea062de 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -3438,6 +3438,30 @@ value, PING frames will be sent automatically to keep the connection alive before the idle timeout fires. The value should be less than the effective idle timeout (`maxIdleTimeout` transport parameter) to be useful. +#### `sessionOptions.truncatedReads` + +* Type: {string} One of `'error'` or `'ignore'`. +* **Default:** `'error'` + +Controls how reading a stream reports a truncated read. A stream's read side +can end without receiving a QUIC FIN, meaning the peer never signalled that +the whole stream had been sent and the data received may be incomplete. This +selects how the stream's async iterator reports this: + +* `'error'` - The default. Peers are expected to always send a FIN to end + their data explicitly, and so any truncation is an error. The iterator yields + the data that did arrive and then throws, so an incomplete stream can never + be mistaken for a complete one. Incomplete streams will either throw a + `ERR_QUIC_STREAM_RESET` carrying the peer's error code, a connection error, + or `ERR_QUIC_STREAM_ABORTED` for other cases. + +* `'ignore'` - The truncation itself is ignored: only a stream or connection + error is reported, and any clean abort/cancellation or similar simply ends + the stream. A non-zero peer reset, non-zero local stop-sending or connection + error still fails, but a truncation with no error at all (an idle timeout, + a graceful close, or a plain `stopSending()`) ends the read cleanly with the + data received. This matches `stream.closed`, which rejects only on an error. + #### `sessionOptions.verifyPeer` (client only) * Type: {string} One of `'strict'`, `'auto'`, or `'manual'`. diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 8e26e40b9367..6d359c5adf7e 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -631,7 +631,7 @@ const kMaxBatchChunks = 16; const kDefaultMaxBatchBytes = 65536; async function* createBlobReaderIterable(reader, options = kEmptyObject) { - const { getReadError, maxBatchBytes = kDefaultMaxBatchBytes } = options; + const { maxBatchBytes = kDefaultMaxBatchBytes } = options; let wakeup = PromiseWithResolvers(); let immediate; reader.setWakeup(() => { @@ -664,9 +664,7 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { break; } if (pullResult.status < 0) { - error = typeof getReadError === 'function' ? - getReadError(pullResult.status) : - new ERR_INVALID_STATE('The reader is not readable'); + error = new ERR_INVALID_STATE('The reader is not readable'); break; } if (pullResult.status === 2) { diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 21613e423de5..135174847410 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1718,8 +1718,6 @@ 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_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error); E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parent, locations) { let message = 'require() cannot be used on an ESM graph with top-level await. Use import() instead.'; diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 33e419002495..2ee2529b1c81 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -13,7 +13,6 @@ const { ErrorCaptureStackTrace, FunctionPrototypeBind, FunctionPrototypeCall, - Number, ObjectDefineProperties, ObjectKeys, PromisePrototypeThen, @@ -117,7 +116,6 @@ const { ERR_QUIC_ENDPOINT_CLOSED, ERR_QUIC_OPEN_STREAM_FAILED, ERR_QUIC_STREAM_ABORTED, - ERR_QUIC_STREAM_RESET, ERR_QUIC_VERSION_NEGOTIATION_ERROR, }, } = require('internal/errors'); @@ -401,6 +399,7 @@ const endpointRegistry = new SafeSet(); * @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 {'error'|'ignore'} [truncatedReads] Truncated read policy * @property {ApplicationOptions} [application] The application options * @property {TransportParams} [transportParams] The transport parameters * @property {string} [servername] The server name identifier (client only) @@ -1600,6 +1599,9 @@ class QuicStream { state: undefined, stats: undefined, pendingClose: undefined, + destroyError: undefined, + stopSendingCode: undefined, + truncatedReads: undefined, reader: undefined, destroying: false, iteratorLocked: false, @@ -1648,9 +1650,10 @@ class QuicStream { * @param {object} handle * @param {QuicSession} session * @param {number} direction - * @param {boolean} [isLocal] + * @param {boolean} isLocal + * @param {'error'|'ignore'} truncatedReads */ - constructor(privateSymbol, handle, session, direction, isLocal) { + constructor(privateSymbol, handle, session, direction, isLocal, truncatedReads) { assertPrivateSymbol(privateSymbol); this.#handle = handle; @@ -1659,6 +1662,7 @@ class QuicStream { inner.session = session; inner.direction = direction; inner.isLocal = isLocal; + inner.truncatedReads = truncatedReads; inner.state = new QuicStreamState( kPrivateConstructor, handle.state, handle.stateByteOffset); @@ -1690,34 +1694,49 @@ class QuicStream { inner.iteratorLocked = true; inner.reader ??= this.#handle?.getReader(); - // Non-readable stream (outbound-only unidirectional, or closed) - if (!inner.reader) return; - - yield* createBlobReaderIterable(inner.reader, { - getReadError: () => { - // The read side ends for one of three reasons: - // * Clean FIN received from the peer (state.finReceived - // === true). The iterator stops without calling this; - // fall through to the generic state error if it does. - // * Peer sent us a RESET_STREAM. The C++ side records the - // code in state.resetCode regardless of whether the JS - // onreset handler was attached. state.finReceived stays - // false because no FIN was seen. - // * We aborted locally via stream.resetStream() or - // stream.stopSending(). Both paths run EndReadable in - // C++, setting state.readEnded without setting - // state.finReceived. There is no peer code to surface. - 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)); - } - return new ERR_QUIC_STREAM_ABORTED( - 'Stream aborted before FIN was received'); - } - return new ERR_INVALID_STATE('The stream is not readable'); - }, - }); + // No reader means either an outbound-only unidirectional stream, or a + // stream already destroyed (data gone, but truncation must still be + // checked below). + if (inner.reader) { + yield* createBlobReaderIterable(inner.reader); + } + + if (inner.state.readEnded && !inner.state.finReceived) { + // The readable has been truncated - ended with no clean FIN. We expose + // this in different ways depending on the truncatedReads option. + + // If we cancelled ourselves, check our own stop-sending code (not the + // result mirrored by the remote peer) + if (inner.stopSendingCode > 0n) { + throw new QuicError('Stream aborted before FIN was received', + { __proto__: null, + errorCode: inner.stopSendingCode }); + } + + // Non-zero reset is always an error: + const peerResetCode = inner.state.resetCode; + if (peerResetCode > 0n) { + throw new QuicError( + `The QUIC stream was reset by the peer with error code ${peerResetCode}`, + { __proto__: null, + code: 'ERR_QUIC_STREAM_RESET', + errorCode: peerResetCode }); + } + + // If stream teardown has started (stats.destroyedAt is set) then a + // close event confirming a final error/clean close will settle + // imminently (might be settled already). We await here to rethrow + // any errors if the connection has failed. + if (this.destroyed || this.stats.destroyedAt !== 0n) { + await this.closed; + } + + // Clean abort is truncation, but not necessarily an error: + if (inner.truncatedReads === 'error') { + throw new QuicError('Stream aborted before FIN was received', + { __proto__: null, errorCode: peerResetCode ?? 0n }); + } + } } /** @@ -2099,9 +2118,11 @@ class QuicStream { if (error !== undefined && typeof inner.onerror === 'function') { invokeOnerror(inner.onerror, error); } - const handle = this.#handle; - this[kFinishClose](error); - handle.destroy(); + // handle.destroy() kicks off all the cleanup internals, eventually + // including [kFinishClose] which needs this original destroy error: + inner.destroyError = error; + this.#handle.destroy(); + this[kFinishClose](error); // A no-op, unless destroy() failed } /** @@ -2519,7 +2540,9 @@ class QuicStream { stopSending(code = 0n) { assertIsQuicStream(this); if (this.destroyed) return; - this.#handle.stopSending(BigInt(code)); + const abortCode = BigInt(code); + this.#inner.stopSendingCode = abortCode; + this.#handle.stopSending(abortCode); } /** @@ -2612,6 +2635,9 @@ class QuicStream { if (this.destroyed) { return inner.pendingClose.promise; } + // Prefer an error staged by destroy() (the original object the caller + // passed) over the error delivered by the native close callback. + error = inner.destroyError ?? error; if (error !== undefined) { inner.pendingClose.reject(error); } else { @@ -2649,6 +2675,7 @@ class QuicStream { inner.session = undefined; inner.pendingClose.reject = undefined; inner.pendingClose.resolve = undefined; + inner.destroyError = undefined; inner.onblocked = undefined; inner.onreset = undefined; inner.onstopsending = undefined; @@ -2859,6 +2886,7 @@ class QuicSession { // because server-side cert validation is handled by rejectUnauthorized // at the C++ level. verifyPeer: 'manual', + truncatedReads: 'error', handshakeInfo: undefined, /** @type {QuicSessionPath|undefined} */ path: undefined, @@ -2892,8 +2920,9 @@ class QuicSession { * @param {symbol} privateSymbol * @param {object} handle * @param {QuicEndpoint} endpoint + * @param {{ truncatedReads?: 'error'|'ignore' }} [options] */ - constructor(privateSymbol, handle, endpoint) { + constructor(privateSymbol, handle, endpoint, options = kEmptyObject) { // Instances of QuicSession can only be created internally. assertPrivateSymbol(privateSymbol); @@ -2902,6 +2931,8 @@ class QuicSession { const inner = this.#inner; inner.endpoint = endpoint; + const { truncatedReads } = options; + if (truncatedReads !== undefined) inner.truncatedReads = truncatedReads; // Move any qlog entries that arrived before the wrapper existed. if (handle._pendingQlog !== undefined) { inner.pendingQlog = handle._pendingQlog; @@ -3447,7 +3478,8 @@ class QuicSession { } const stream = new QuicStream( - kPrivateConstructor, handle, this, direction, true /* isLocal */); + kPrivateConstructor, handle, this, direction, true /* isLocal */, + inner.truncatedReads); inner.streams.add(stream); if (typeof this.#inner.onerror === 'function') { markPromiseAsHandled(stream.closed); @@ -4232,7 +4264,7 @@ class QuicSession { [kNewStream](handle, direction) { const inner = this.#inner; const stream = new QuicStream(kPrivateConstructor, handle, this, direction, - false /* isLocal */); + false /* isLocal */, inner.truncatedReads); // Set the default byte budget for received streams. stream.budget = kDefaultBudget; @@ -4349,6 +4381,7 @@ class QuicEndpoint { sessions: new SafeSet(), stat: undefined, stats: undefined, + truncatedReads: undefined, onsession: undefined, sessionCallbacks: undefined, }; @@ -4481,8 +4514,8 @@ class QuicEndpoint { }; } - #newSession(handle) { - const session = new QuicSession(kPrivateConstructor, handle, this); + #newSession(handle, options) { + const session = new QuicSession(kPrivateConstructor, handle, this, options); this.#inner.sessions.add(session); // Set default pending datagram queue size. session.maxPendingDatagrams = kDefaultMaxPendingDatagrams; @@ -4656,9 +4689,13 @@ class QuicEndpoint { ontrailers, oninfo, onwanttrailers, + // Stored on the endpoint and applied to each incoming session. + truncatedReads, ...rest } = options; + inner.truncatedReads = truncatedReads; + // Store session and stream callbacks to apply to each new incoming session. inner.sessionCallbacks = { __proto__: null, @@ -4700,6 +4737,7 @@ class QuicEndpoint { validateObject(options, 'options'); const { sessionTicket, + truncatedReads, ...rest } = options; @@ -4708,7 +4746,7 @@ class QuicEndpoint { if (handle === undefined) { throw new ERR_QUIC_CONNECTION_FAILED(); } - const session = this.#newSession(handle); + const session = this.#newSession(handle, { __proto__: null, truncatedReads }); // Set callbacks before any async work to avoid missing events // that fire during or immediately after the handshake. applyCallbacks(session, options); @@ -4942,7 +4980,8 @@ class QuicEndpoint { const inner = this.#inner; assert(typeof inner.onsession === 'function', 'onsession callback not specified'); - const session = this.#newSession(handle); + const session = this.#newSession(handle, + { __proto__: null, truncatedReads: inner.truncatedReads }); // Apply session callbacks stored at listen time before notifying // the onsession callback, to avoid missing events that fire // during or immediately after the handshake. @@ -5434,6 +5473,7 @@ function processSessionOptions(options, config = kEmptyObject) { maxDatagramSendAttempts = 5, streamIdleTimeout, verifyPeer = 'auto', + truncatedReads = 'error', // HTTP/3 application-specific options. Nested under `application` // to separate protocol-specific settings from transport-level ones. application = kEmptyObject, @@ -5485,6 +5525,9 @@ function processSessionOptions(options, config = kEmptyObject) { validateOneOf(verifyPeer, 'options.verifyPeer', ['strict', 'auto', 'manual']); + validateOneOf(truncatedReads, 'options.truncatedReads', + ['error', 'ignore']); + validateInteger(drainingPeriodMultiplier, 'options.drainingPeriodMultiplier', 3, 255); @@ -5547,6 +5590,7 @@ function processSessionOptions(options, config = kEmptyObject) { verifyHostname: verifyPeer !== 'manual', }, verifyPeer, + truncatedReads, qlog, maxPayloadSize, unacknowledgedPacketThreshold, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index 21db0f0e3d59..0a47e2e7adc8 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -8,7 +8,9 @@ const { DataView, DataViewPrototypeGetBigInt64, DataViewPrototypeGetBigUint64, + DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, DataViewPrototypeGetUint16, DataViewPrototypeGetUint32, DataViewPrototypeGetUint8, @@ -17,6 +19,9 @@ const { DataViewPrototypeSetUint8, JSONStringify, Number, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeSlice, + Uint8Array, } = primordials; const { @@ -107,6 +112,7 @@ const { IDX_STATE_STREAM_WRITE_DESIRED_SIZE, IDX_STATE_STREAM_BUDGET, IDX_STATE_STREAM_RESET_CODE, + IDX_STATE_STREAM_SIZE, } = internalBinding('quic'); assert(IDX_STATE_SESSION_LISTENER_FLAGS !== undefined); @@ -723,8 +729,8 @@ class QuicStreamState { #handle; /** @type {number} */ #offset = 0; - /** @type {bigint|undefined} */ - #id = undefined; + /** @type {boolean} */ + #disconnected = false; /** * @param {symbol} privateSymbol @@ -746,7 +752,7 @@ class QuicStreamState { /** @type {bigint} */ get id() { const handle = this.#handle; - if (handle === undefined) return this.#id; + if (handle === undefined) return undefined; return DataViewPrototypeGetBigInt64(handle, this.#offset + IDX_STATE_STREAM_ID, kIsLittleEndian); } @@ -943,7 +949,7 @@ class QuicStreamState { } [kInspect](depth, options) { - if (this.#handle === undefined || + if (this.#disconnected || this.#handle === undefined || DataViewPrototypeGetByteLength(this.#handle) === 0) { return 'QuicStreamState { }'; } @@ -998,9 +1004,24 @@ class QuicStreamState { } [kFinishClose]() { - // Cache the stream ID since the buffer will be zeroed out and the ID will be lost. - this.#id = this.id; - this.#handle = undefined; + // Copy the final values out of the shared buffer (zeroed when the + // underlying stream goes away) so getters keep reporting the final + // state, the same way QuicStreamStats does. + const handle = this.#handle; + if (handle === undefined || + DataViewPrototypeGetByteLength(handle) < + this.#offset + IDX_STATE_STREAM_SIZE) { + this.#handle = undefined; + this.#disconnected = true; + return; + } + const copy = TypedArrayPrototypeSlice(new Uint8Array( + DataViewPrototypeGetBuffer(handle), + DataViewPrototypeGetByteOffset(handle) + this.#offset, + IDX_STATE_STREAM_SIZE)); + this.#handle = new DataView(TypedArrayPrototypeGetBuffer(copy)); + this.#offset = 0; + this.#disconnected = true; } } diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 0a5a783711ba..aae72740f871 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -1008,6 +1008,9 @@ void Stream::InitPerContext(Realm* realm, Local target) { #undef V NODE_DEFINE_CONSTANT(target, IDX_STATS_STREAM_COUNT); + + constexpr auto IDX_STATE_STREAM_SIZE = sizeof(Stream::State); + NODE_DEFINE_CONSTANT(target, IDX_STATE_STREAM_SIZE); } Stream* Stream::From(void* stream_user_data) { @@ -1321,13 +1324,6 @@ BaseObjectPtr Stream::get_reader() { return reader; } -void Stream::set_final_size(uint64_t final_size) { - DCHECK_IMPLIES(state()->fin_received == 1, - final_size <= STAT_GET(Stats, final_size)); - state()->fin_received = 1; - STAT_SET(Stats, final_size, final_size); -} - void Stream::set_outbound(std::shared_ptr source) { if (!source || !is_writable()) return; Debug(this, "Setting the outbound data source"); @@ -1471,7 +1467,7 @@ void Stream::FlushAccumulation() { return; } // Should be unreachable: append() only fails once the queue has been capped, - // EndReadable() flushes before capping, and ReceiveData() accumulates + // CapReadable() flushes before capping, and ReceiveData() accumulates // nothing once read_ended is set. Reaching here means received stream data // is being dropped on the floor, so say so and at least do not also leak // the flow control credit for it. @@ -1536,13 +1532,26 @@ void Stream::EndWritable() { state()->write_ended = 1; } -void Stream::EndReadable(std::optional maybe_final_size) { +void Stream::FinishReadable() { + if (!is_readable()) return; + state()->fin_received = 1; + CapReadable(std::nullopt); +} + +void Stream::TruncateReadable(std::optional maybe_final_size) { if (!is_readable()) return; + CapReadable(maybe_final_size); +} + +void Stream::CapReadable(std::optional maybe_final_size) { + DCHECK(is_readable()); 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)); + const uint64_t final_size = + maybe_final_size.value_or(STAT_GET(Stats, bytes_received)); + STAT_SET(Stats, final_size, final_size); + inbound_->cap(final_size); // Notify the JS reader so it can see EOS. The subsequent pull observes // the now-capped DataQueue and returns EOS. if (reader_) reader_->NotifyPull(); @@ -1570,14 +1579,15 @@ void Stream::Destroy(QuicError error) { // End the writable before marking as destroyed. EndWritable(); - // Also end the readable side if it isn't already. - EndReadable(); + // Also end the readable side if it isn't already. If not already ended, + // this will eventually surface as an error, since the data is truncated. + TruncateReadable(); // We are going to release our reference to the outbound_ queue here. outbound_.reset(); application_state_.reset(); - // EndReadable() above already flushed accumulated data. Just release + // TruncateReadable() above already flushed accumulated data. Just release // the ring buffer memory. recv_accumulator_.reset(); @@ -1632,7 +1642,7 @@ void Stream::ReceiveData(const uint8_t* data, // 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 (flags.fin) EndReadable(); + if (flags.fin) FinishReadable(); // Nothing will ever consume these bytes, so return the connection-level // credit ngtcp2 charged for them. The stream window is deliberately left // alone: there is no point inviting more data onto a stream we have @@ -1715,7 +1725,7 @@ void Stream::ReceiveData(const uint8_t* data, if (flags.fin) { FlushAccumulation(); - EndReadable(); + FinishReadable(); } 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 @@ -1746,7 +1756,7 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { final_size, error); state()->reset_code = error.code(); - EndReadable(final_size); + TruncateReadable(final_size); EmitReset(error); } @@ -1769,7 +1779,7 @@ void Stream::DoStreamReset(error_code code) { } void Stream::SendStopSending(error_code code) { - EndReadable(); + TruncateReadable(); if (!is_pending()) { // If the stream is a local unidirectional there's nothing to do here. diff --git a/src/quic/streams.h b/src/quic/streams.h index 182237c6334d..38e8f2ae978e 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -325,7 +325,12 @@ class Stream final : public AsyncWrap, void UpdateWriteDesiredSize(); void EndWritable(); - void EndReadable(std::optional maybe_final_size = std::nullopt); + // The read side ended cleanly with a peer FIN: the content is complete. + void FinishReadable(); + // The read side ended without a FIN (a reset, a local abort, or the session + // being torn down) so the content is truncated. + void TruncateReadable( + std::optional maybe_final_size = std::nullopt); void EntryRead(size_t amount) override; void BeforePull() override; @@ -416,10 +421,13 @@ class Stream final : public AsyncWrap, // consumer or dropped before reaching one. void CreditConsumedBytes(uint64_t amount); + // Common tail of FinishReadable()/TruncateReadable(): marks the read side + // ended, caps it at the final size, and notifies the reader. + void CapReadable(std::optional maybe_final_size); + // Gets a reader for the data received for this stream from the peer, BaseObjectPtr get_reader(); - void set_final_size(uint64_t amount); void set_outbound(std::shared_ptr source); // Streaming outbound support diff --git a/test/common/quic.mjs b/test/common/quic.mjs index dc4b094cf900..72effd8b6528 100644 --- a/test/common/quic.mjs +++ b/test/common/quic.mjs @@ -7,6 +7,7 @@ // listen/connect that apply default options suitable for most tests. import * as fixtures from '../common/fixtures.mjs'; +import { setTimeout } from 'node:timers/promises'; const { createPrivateKey } = await import('node:crypto'); const quic = await import('node:quic'); @@ -98,6 +99,81 @@ function hashBytes(buf) { return h >>> 0; } +/** + * Write `size` bytes to the stream and wait until the peer has + * acknowledged them. + */ +async function writeAndAwaitAck(stream, size) { + stream.writer.write(new Uint8Array(size).fill(7)); + while (stream.stats.maxOffsetAcknowledged < BigInt(size)) await setTimeout(5); +} + +/** + * Send `size` bytes and then stall forever without a FIN + * @yields {Uint8Array} + */ +async function* stallingBody(size) { + yield new Uint8Array(size).fill(7); + await new Promise(() => {}); +} + +/** + * Do a full single stream-read scenario, with the given client options and + * server body, and two hooks available to tweak behaviour, returning the + * details of the result after completion. + * @param {Function} serverBody + * @param {object} [options] + * @param {object} [options.clientOptions] Options forwarded to connect(). + * @param {Function} [options.beforeIterate] + * @param {Function} [options.onFirstChunk] + * @returns {Promise<{received: number, threw: any, closedError: any}>} + * Bytes received, the iteration error if any, and the client stream's + * closed rejection if any. + */ +async function readStream(serverBody, options = {}) { + const { clientOptions, beforeIterate, onFirstChunk } = options; + + const serverEndpoint = await listen((session) => { + session.closed.catch(() => {}); + session.onstream = (stream) => { + stream.closed.catch(() => {}); + serverBody(stream, session); + }; + }); + + const session = await connect(serverEndpoint.address, clientOptions); + await session.opened; + session.closed.catch(() => {}); + + const stream = await session.createBidirectionalStream(); + await stream.writer.write(new Uint8Array([1])); + + let closedError; + const closedSettled = stream.closed.catch((err) => { closedError = err; }); + + await beforeIterate?.({ stream, session }); + + let received = 0; + let threw; + let firstChunk = true; + try { + for await (const chunk of stream) { + for (const c of chunk) received += c.byteLength; + if (firstChunk) { + firstChunk = false; + await onFirstChunk?.({ stream, session }); + } + } + } catch (err) { + threw = err; + } + + session.close(); + await closedSettled; + await serverEndpoint.close(); + return { received, threw, closedError }; +} + export { key, cert, @@ -105,4 +181,7 @@ export { connect, makePayload, hashBytes, + readStream, + stallingBody, + writeAndAwaitAck, }; diff --git a/test/parallel/test-quic-stream-iteration-destroyed.mjs b/test/parallel/test-quic-stream-iteration-destroyed.mjs deleted file mode 100644 index 585be6d4a813..000000000000 --- a/test/parallel/test-quic-stream-iteration-destroyed.mjs +++ /dev/null @@ -1,37 +0,0 @@ -// Flags: --experimental-quic --no-warnings - -// Test: destroyed stream returns finished iterator. - -import { hasQuic, skip, mustCall } from '../common/index.mjs'; -import * as assert from 'node:assert'; - -if (!hasQuic) { - skip('QUIC is not enabled'); -} - -const { listen, connect } = await import('../common/quic.mjs'); - -const encoder = new TextEncoder(); - -const serverEndpoint = await listen(mustCall(async (serverSession) => { - await serverSession.closed; -})); - -const clientSession = await connect(serverEndpoint.address); -await clientSession.opened; - -const stream = await clientSession.createBidirectionalStream({ - body: encoder.encode('destroy test'), -}); - -// Destroy the stream immediately. -stream.destroy(); - -// Iterating a destroyed stream should immediately finish. -const iter = stream[Symbol.asyncIterator](); -const { done } = await iter.next(); -assert.strictEqual(done, true); - -await stream.closed; -await clientSession.close(); -await serverEndpoint.destroy(); diff --git a/test/parallel/test-quic-stream-iteration-reset.mjs b/test/parallel/test-quic-stream-iteration-reset.mjs deleted file mode 100644 index 6df571bd9ea7..000000000000 --- a/test/parallel/test-quic-stream-iteration-reset.mjs +++ /dev/null @@ -1,64 +0,0 @@ -// Flags: --experimental-quic --experimental-stream-iter --no-warnings - -// Test: peer RESET_STREAM causes iterator to error. -// When the server resets the stream, the client's async iterator -// should throw or return early. - -import { hasQuic, skip, mustCall } from '../common/index.mjs'; -import * as assert from 'node:assert'; - -if (!hasQuic) { - skip('QUIC is not enabled'); -} - -const { listen, connect } = await import('../common/quic.mjs'); - -const encoder = new TextEncoder(); - -const serverReady = Promise.withResolvers(); - -const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.onstream = mustCall(async (stream) => { - // Reset the stream from the server side. - stream.resetStream(42n); - await assert.rejects(stream.closed, mustCall((err) => { - assert.ok(err); - return true; - })); - serverReady.resolve(); - await serverSession.closed; - }); -}), { transportParams: { maxIdleTimeout: 1 } }); - -const clientSession = await connect(serverEndpoint.address, { - transportParams: { maxIdleTimeout: 1 }, -}); -await clientSession.opened; - -const stream = await clientSession.createBidirectionalStream({ - body: encoder.encode('will be reset by server'), -}); - -// Set up the closed handler before the reset to avoid unhandled rejection. -const closedPromise = assert.rejects(stream.closed, mustCall((err) => { - assert.ok(err); - return true; -})); - -await serverReady.promise; - -// The async iterator should either throw or return early when the -// peer resets the readable side. -try { - for await (const batch of stream) { - // May receive some data before the reset arrives. - assert.ok(Array.isArray(batch)); - } -} catch { - // The iterator may throw when the reset arrives mid-iteration. -} - -// Either way, the stream should close. -await closedPromise; -await clientSession.closed; -await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-setbody-errors.mjs b/test/parallel/test-quic-stream-setbody-errors.mjs index a15f2347171e..da045d764c2f 100644 --- a/test/parallel/test-quic-stream-setbody-errors.mjs +++ b/test/parallel/test-quic-stream-setbody-errors.mjs @@ -53,7 +53,13 @@ await clientSession.opened; message: /writer already accessed/, }); - for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars + // The server handles only the first stream and then closes its session, so + // this stream is never answered and never receives a FIN. Reading it + // therefore surfaces the truncation rather than ending cleanly. + await assert.rejects((async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of stream) { /* drain */ } + })(), { code: 'ERR_QUIC_STREAM_ABORTED' }); await stream.closed; } diff --git a/test/parallel/test-quic-stream-truncated-reads-destroy.mjs b/test/parallel/test-quic-stream-truncated-reads-destroy.mjs new file mode 100644 index 000000000000..01eddd3b0f0f --- /dev/null +++ b/test/parallel/test-quic-stream-truncated-reads-destroy.mjs @@ -0,0 +1,72 @@ +// Flags: --experimental-quic --no-warnings + +// Test: truncation is still reported when the stream is torn down locally +// before the iterator finishes draining. The stream's final read state and +// close error are persisted at close, so a consumer slower than the teardown +// (or one that only starts reading after it) still observes the truncation +// rather than a clean end that would make an incomplete stream look complete. + +import { hasQuic, skip } from '../common/index.mjs'; +import { setTimeout } from 'node:timers/promises'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { readStream, stallingBody } = await import('../common/quic.mjs'); + +// The server sends 1000 bytes then stalls without a FIN, so the client's +// read side can only end by truncation. +const serve = (stream) => { stream.setBody(stallingBody(1000)); }; + +// The session is destroyed with an error while the consumer is mid-iteration: +// the iterator delivers the data that arrived, then throws that same error +// object. +{ + const boom = new Error('boom'); + const { received, threw } = await readStream(serve, { + onFirstChunk: ({ session }) => session.destroy(boom), + }); + assert.ok(received > 0); + assert.strictEqual(threw, boom); +} + +// The session is destroyed without an error mid-iteration: an errorless +// truncation, reported as aborted under the default policy. +{ + const { received, threw } = await readStream(serve, { + onFirstChunk: ({ session }) => session.destroy(), + }); + assert.ok(received > 0); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); + assert.strictEqual(threw.errorCode, 0n); +} + +// The session is destroyed before iteration even starts: the buffered data +// is gone, but the truncation is still reported rather than a clean empty +// end. +{ + const { received, threw } = await readStream(serve, { + beforeIterate: async ({ stream, session }) => { + while (stream.stats.bytesReceived === 0n) await setTimeout(5); + session.destroy(); + }, + }); + assert.strictEqual(received, 0); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); +} + +// Destroying the stream itself (rather than the session) before iterating +// reports the truncation the same way, with closed resolving cleanly. +{ + const { received, threw, closedError } = await readStream(serve, { + beforeIterate: async ({ stream }) => { + while (stream.stats.bytesReceived === 0n) await setTimeout(5); + stream.destroy(); + }, + }); + assert.strictEqual(received, 0); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); + assert.strictEqual(closedError, undefined); +} diff --git a/test/parallel/test-quic-stream-truncated-reads-timeout.mjs b/test/parallel/test-quic-stream-truncated-reads-timeout.mjs new file mode 100644 index 000000000000..2ff19f463c3c --- /dev/null +++ b/test/parallel/test-quic-stream-truncated-reads-timeout.mjs @@ -0,0 +1,35 @@ +// Flags: --experimental-quic --no-warnings + +// Test: a readable truncated by the connection idle timeout delivers the +// data it received, then ends per the truncatedReads policy: an error under +// the default (so an incomplete stream can never look complete), and a clean +// end under 'ignore' (an idle timeout carries no error). + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { readStream, stallingBody } = await import('../common/quic.mjs'); + +// The server sends 1000 bytes then stalls without a FIN, so the short (1s) +// connection idle timeout is the only thing that ends the read side. +const serve = (stream) => { stream.setBody(stallingBody(1000)); }; +const transportParams = { maxIdleTimeout: 1 }; + +{ + const { received, threw } = + await readStream(serve, { clientOptions: { transportParams } }); + assert.strictEqual(received, 1000); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); + assert.strictEqual(threw.errorCode, 0n); +} +{ + const { received, threw } = await readStream(serve, { + clientOptions: { transportParams, truncatedReads: 'ignore' }, + }); + assert.strictEqual(received, 1000); + assert.strictEqual(threw, undefined); +} diff --git a/test/parallel/test-quic-stream-truncated-reads.mjs b/test/parallel/test-quic-stream-truncated-reads.mjs new file mode 100644 index 000000000000..e5c381a8d679 --- /dev/null +++ b/test/parallel/test-quic-stream-truncated-reads.mjs @@ -0,0 +1,184 @@ +// Flags: --experimental-quic --no-warnings + +// Test: a stream read that ends without a FIN is a truncation. What the read +// reports is decided in this order: +// +// 1. the peer reset the stream with a non-zero code: ERR_QUIC_STREAM_RESET +// carrying that code, under either policy; +// 2. the stream was already being torn down and its closed promise rejected: +// that same error, under either policy; +// 3. otherwise the truncation carries no error of its own and the +// truncatedReads policy decides - 'error' (the default) throws +// ERR_QUIC_STREAM_ABORTED so an incomplete stream can never look +// complete, 'ignore' ends the read cleanly. +// +// This file covers rules 1 and 3 on a live connection: peer resets and local +// aborts. Rule 2 needs the stream to already be tearing down when the reader +// resumes, which is covered by the sibling +// test-quic-stream-truncated-reads-destroy test, and truncation by idle +// timeout by test-quic-stream-truncated-reads-timeout. + +import { hasQuic, skip } from '../common/index.mjs'; +import assert from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { connect, listen, readStream, stallingBody, writeAndAwaitAck } = + await import('../common/quic.mjs'); + +// Sends 1000 bytes, waits for them to land, then resets with the given code. +const resetWith = (code) => async (stream) => { + await writeAndAwaitAck(stream, 1000); + stream.resetStream(code); +}; + +// Sends 1000 bytes and then stalls without a FIN, so only the client's own +// abort can end the read. +const stall = (stream) => { stream.setBody(stallingBody(1000)); }; + +// A peer reset with code 0 is a clean abort: a truncation, but not an error. +// Rule 3 - the default reports it, 'ignore' treats it as a clean end. +{ + const { received, threw } = await readStream(resetWith(0n)); + assert.strictEqual(received, 1000); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); + assert.strictEqual(threw.errorCode, 0n); +} +{ + const { received, threw } = + await readStream(resetWith(0n), { clientOptions: { truncatedReads: 'ignore' } }); + assert.strictEqual(received, 1000); + assert.strictEqual(threw, undefined); +} + +// A peer reset with a nonzero code is rule 1: an error under either policy, +// carrying the peer's code. It also rejects the closed promise on both sides. +{ + const serverClosed = Promise.withResolvers(); + const { received, threw, closedError } = await readStream(async (stream) => { + await writeAndAwaitAck(stream, 1000); + stream.resetStream(42n); + // Our own reset closes the server-side stream with an error too. + serverClosed.resolve(stream.closed.then(() => undefined, (err) => err)); + }); + assert.strictEqual(received, 1000); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_RESET'); + assert.strictEqual(threw.errorCode, 42n); + assert.strictEqual(threw.message, + 'The QUIC stream was reset by the peer with error code 42'); + assert.strictEqual(closedError?.code, 'ERR_QUIC_APPLICATION_ERROR'); + assert.strictEqual(closedError.errorCode, 42n); + const serverClosedError = await serverClosed.promise; + assert.strictEqual(serverClosedError?.code, 'ERR_QUIC_APPLICATION_ERROR'); + assert.strictEqual(serverClosedError.errorCode, 42n); +} +{ + const { received, threw } = + await readStream(resetWith(42n), { clientOptions: { truncatedReads: 'ignore' } }); + assert.strictEqual(received, 1000); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_RESET'); + assert.strictEqual(threw.errorCode, 42n); +} + +// Aborting our own read with stopSending() is rule 3, not rule 1: we asked for +// the truncation, so it is not an error the peer inflicted on us, and the code +// we send does not come back as one. The read still stops short, so the +// default policy reports it and 'ignore' does not. +{ + const { received, threw, closedError } = await readStream(stall, { + onFirstChunk: ({ stream }) => stream.stopSending(0n), + }); + assert.ok(received > 0); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); + assert.strictEqual(threw.errorCode, 0n); + assert.strictEqual(closedError, undefined); +} +{ + const { received, threw } = await readStream(stall, { + clientOptions: { truncatedReads: 'ignore' }, + onFirstChunk: ({ stream }) => stream.stopSending(0n), + }); + assert.ok(received > 0); + assert.strictEqual(threw, undefined); +} + +// A nonzero stopSending code is an error this end raised, so it is reported +// under either policy and carries that code. The peer answers our +// STOP_SENDING with a RESET_STREAM echoing it, which is what rejects closed - +// but the read reports our own abort rather than attributing it to the peer, +// and does so whether or not that answer has arrived yet. +for (const truncatedReads of ['error', 'ignore']) { + for (const awaitEcho of [false, true]) { + const peerReset = Promise.withResolvers(); + const { received, threw, closedError } = await readStream(stall, { + clientOptions: { truncatedReads }, + beforeIterate: ({ stream }) => { stream.onreset = () => peerReset.resolve(); }, + onFirstChunk: async ({ stream }) => { + stream.stopSending(7n); + // For the 2nd pass, wait until we receive the corresponding reset: + if (awaitEcho) await peerReset.promise; + }, + }); + assert.ok(received > 0); + assert.strictEqual(threw?.code, 'ERR_QUIC_STREAM_ABORTED'); + assert.strictEqual(threw.errorCode, 7n); + assert.strictEqual(closedError?.code, 'ERR_QUIC_APPLICATION_ERROR'); + assert.strictEqual(closedError.errorCode, 7n); + } +} + +// A peer abruptly destroying its session truncates the read too. That +// currently reaches us as an implicit reset, so which rule applies depends on +// the code the peer's teardown puts on the wire - not part of this contract, +// and deliberately not pinned here. What must hold either way is the default +// policy's promise: the incomplete stream never looks complete. +{ + const { received, threw } = await readStream(async (stream, session) => { + await writeAndAwaitAck(stream, 1000); + session.destroy(new Error('connection boom')); + }); + assert.strictEqual(received, 1000); + assert.ok(threw); +} + +// Check the option in the server case as well: +{ + const serverRead = Promise.withResolvers(); + const serverEndpoint = await listen((session) => { + session.closed.catch(() => {}); + session.onstream = async (stream) => { + stream.closed.catch(() => {}); + let received = 0; + try { + for await (const chunk of stream) { + for (const c of chunk) received += c.byteLength; + } + serverRead.resolve({ received, threw: undefined }); + } catch (err) { + serverRead.resolve({ received, threw: err }); + } + }; + }, { truncatedReads: 'ignore' }); + + const session = await connect(serverEndpoint.address); + await session.opened; + session.closed.catch(() => {}); + + const stream = await session.createBidirectionalStream(); + stream.closed.catch(() => {}); + await writeAndAwaitAck(stream, 100); + stream.resetStream(0n); + + const { threw } = await serverRead.promise; + assert.strictEqual(threw, undefined); + + session.close(); + await serverEndpoint.close(); +} + +// The option is validated. +await assert.rejects(connect('127.0.0.1:1234', { truncatedReads: 'nope' }), { + code: 'ERR_INVALID_ARG_VALUE', +}); From 67868c7e1cf8f84358eb66d169bcd99a54b625f0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 13 Sep 2026 11:58:50 +0200 Subject: [PATCH 10/83] tools: make checkout credential use explicit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disable credential persistence for CodeQL. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66013 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón --- .github/workflows/codeql.yml | 2 ++ .github/workflows/commit-queue.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0b83c888ecdc..10a9e299145b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 600dadc17bc4..8f29a390c678 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -188,6 +188,7 @@ jobs: # to be set here because `checkout` configures GitHub authentication # for push as well. token: ${{ secrets.GH_USER_TOKEN }} + persist-credentials: true - name: Start the Commit Queue if: steps.get_mergeable_prs.outputs.numbers != '' From fe40b317efea35a3f7a22308111a53ed093021e9 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 13 Sep 2026 11:58:50 +0200 Subject: [PATCH 11/83] tools: correct Slack action version comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66013 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón --- .github/workflows/notify-on-push.yml | 4 ++-- .github/workflows/notify-on-review-wanted.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/notify-on-push.yml b/.github/workflows/notify-on-push.yml index 16bd91bccd2a..25421b8447d8 100644 --- a/.github/workflows/notify-on-push.yml +++ b/.github/workflows/notify-on-push.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04-arm steps: - name: Slack Notification - uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 env: SLACK_COLOR: '#DE512A' SLACK_ICON: https://github.com/nodejs.png?size=48 @@ -50,7 +50,7 @@ jobs: COMMITS: ${{ toJSON(github.event.commits) }} - name: Slack Notification if: ${{ failure() && steps.commit-check.conclusion == 'failure' && github.repository == 'nodejs/node' }} - uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 env: SLACK_COLOR: '#DE512A' SLACK_ICON: https://github.com/nodejs.png?size=48 diff --git a/.github/workflows/notify-on-review-wanted.yml b/.github/workflows/notify-on-review-wanted.yml index 2f1f3af8139b..effc6c209eb1 100644 --- a/.github/workflows/notify-on-review-wanted.yml +++ b/.github/workflows/notify-on-review-wanted.yml @@ -34,7 +34,7 @@ jobs: fi - name: Slack Notification - uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 env: MSG_MINIMAL: actions url SLACK_COLOR: '#3d85c6' From 96e769e36d776a2611de806a0a51c3c777594d12 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 13 Sep 2026 11:58:51 +0200 Subject: [PATCH 12/83] tools: use self-repository references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve local actions and reusable workflows from the running workflow commit with $/ references, independent of checkout paths. Remove the tarball job's action-only checkout and the WPT action checkout/copy workaround, which are no longer needed. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66013 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón --- .github/workflows/build-tarball.yml | 7 +----- .../workflows/coverage-linux-without-intl.yml | 2 +- .github/workflows/coverage-linux.yml | 2 +- .github/workflows/daily-wpt-fyi.yml | 22 ++----------------- .github/workflows/stress-test.yml | 2 +- .github/workflows/test-internet.yml | 2 +- .github/workflows/test-linux-perfetto.yml | 2 +- .github/workflows/test-linux-quic.yml | 2 +- .github/workflows/test-linux.yml | 2 +- .github/workflows/test-shared.yml | 4 ++-- 10 files changed, 12 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml index e173d4a544e0..045bd3ae93c2 100644 --- a/.github/workflows/build-tarball.yml +++ b/.github/workflows/build-tarball.yml @@ -104,13 +104,8 @@ jobs: SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} SCCACHE_IDLE_TIMEOUT: '0' steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: .github/actions/install-clang - sparse-checkout-cone-mode: false - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/coverage-linux-without-intl.yml b/.github/workflows/coverage-linux-without-intl.yml index 7a73d69b70ec..a98492ec668e 100644 --- a/.github/workflows/coverage-linux-without-intl.yml +++ b/.github/workflows/coverage-linux-without-intl.yml @@ -54,7 +54,7 @@ jobs: with: persist-credentials: false - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml index 79c6eaaebc91..4ba373b790a2 100644 --- a/.github/workflows/coverage-linux.yml +++ b/.github/workflows/coverage-linux.yml @@ -54,7 +54,7 @@ jobs: with: persist-credentials: false - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/daily-wpt-fyi.yml b/.github/workflows/daily-wpt-fyi.yml index 043de271faa9..8901ecbcee42 100644 --- a/.github/workflows/daily-wpt-fyi.yml +++ b/.github/workflows/daily-wpt-fyi.yml @@ -105,33 +105,15 @@ jobs: else echo "UNDICI_WPT=legacy" >> $GITHUB_ENV fi - # Checkout composite actions from the default branch since the - # version-specific checkout above overwrites .github/actions/ - - name: Checkout undici WPT actions - if: ${{ env.WPT_REPORT != '' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - sparse-checkout: | - .github/actions/undici-wpt-current - .github/actions/undici-wpt-legacy - sparse-checkout-cone-mode: false - persist-credentials: false - path: _wpt_actions - clean: false - - name: Place undici WPT actions - if: ${{ env.WPT_REPORT != '' }} - run: | - mkdir -p .github/actions - cp -r _wpt_actions/.github/actions/undici-wpt-* .github/actions/ - name: Run undici WPT (current) if: ${{ env.UNDICI_WPT == 'current' }} - uses: ./.github/actions/undici-wpt-current + uses: $/.github/actions/undici-wpt-current with: undici-version: ${{ env.UNDICI_VERSION }} wpt-report: ${{ env.WPT_REPORT }} - name: Run undici WPT (legacy) if: ${{ env.UNDICI_WPT == 'legacy' }} - uses: ./.github/actions/undici-wpt-legacy + uses: $/.github/actions/undici-wpt-legacy with: undici-version: ${{ env.UNDICI_VERSION }} wpt-report: ${{ env.WPT_REPORT }} diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml index a33cf81eaed4..0f17615671cd 100644 --- a/.github/workflows/stress-test.yml +++ b/.github/workflows/stress-test.yml @@ -60,7 +60,7 @@ jobs: path: node - name: Install Clang ${{ env.CLANG_VERSION }} if: runner.os == 'Linux' - uses: ./node/.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Set up Xcode ${{ env.XCODE_VERSION }} diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml index 497d5156fec9..672893a999aa 100644 --- a/.github/workflows/test-internet.yml +++ b/.github/workflows/test-internet.yml @@ -59,7 +59,7 @@ jobs: with: persist-credentials: false - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/test-linux-perfetto.yml b/.github/workflows/test-linux-perfetto.yml index 3dc533720d43..e300ee59aa86 100644 --- a/.github/workflows/test-linux-perfetto.yml +++ b/.github/workflows/test-linux-perfetto.yml @@ -42,7 +42,7 @@ jobs: persist-credentials: false path: node - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./node/.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Install Rust ${{ env.RUSTC_VERSION }} diff --git a/.github/workflows/test-linux-quic.yml b/.github/workflows/test-linux-quic.yml index 7c77d4668dc6..eca8521a310e 100644 --- a/.github/workflows/test-linux-quic.yml +++ b/.github/workflows/test-linux-quic.yml @@ -52,7 +52,7 @@ jobs: persist-credentials: false path: node - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./node/.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Install Rust ${{ env.RUSTC_VERSION }} diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index acd1e04a04ab..65aead07b923 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -65,7 +65,7 @@ jobs: persist-credentials: false path: node - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./node/.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Install Rust ${{ env.RUSTC_VERSION }} diff --git a/.github/workflows/test-shared.yml b/.github/workflows/test-shared.yml index f7483103016f..be3c22face09 100644 --- a/.github/workflows/test-shared.yml +++ b/.github/workflows/test-shared.yml @@ -159,7 +159,7 @@ jobs: - runner: macos-latest system: aarch64-darwin name: '${{ matrix.system }}: with shared libraries${{ matrix.perfetto && '' and perfetto'' || '''' }}' - uses: ./.github/workflows/build-shared.yml + uses: $/.github/workflows/build-shared.yml with: runner: ${{ matrix.runner }} with-sccache: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} @@ -250,7 +250,7 @@ jobs: matrix: openssl: ${{ fromJSON(needs.build-aarch64-linux-v8.outputs.matrix) }} name: 'aarch64-linux: with shared ${{ matrix.openssl.name }}' - uses: ./.github/workflows/build-shared.yml + uses: $/.github/workflows/build-shared.yml with: runner: ubuntu-24.04-arm v8-nar: ${{ needs.build-aarch64-linux-v8.outputs.local-cache && 'libv8-aarch64-linux.nar' }} From 7203d9bebea7a765ad4352445574d2b62253a6d0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 13 Sep 2026 11:58:51 +0200 Subject: [PATCH 13/83] tools: avoid workflow shell interpolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass input, output, and version values through environment variables instead of expanding them into shell source. Split benchmark categories and PR numbers into arrays to retain separate arguments. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66013 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón --- .github/workflows/auto-start-ci.yml | 4 +++- .github/workflows/benchmark.yml | 17 ++++++++++++----- .github/workflows/commit-queue.yml | 4 +++- .github/workflows/daily-wpt-fyi.yml | 7 +++++-- .github/workflows/timezone-update.yml | 4 ++-- .github/workflows/tools.yml | 2 +- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/auto-start-ci.yml b/.github/workflows/auto-start-ci.yml index 8c01b0592c2e..dc827d62ba71 100644 --- a/.github/workflows/auto-start-ci.yml +++ b/.github/workflows/auto-start-ci.yml @@ -72,7 +72,9 @@ jobs: - name: Start the CI run: | + read -r -a numbers <<< "$PULL_REQUESTS" curl -fsSL "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/tools/actions/start-ci.sh" \ - | sh -s -- ${{ needs.get-prs-for-ci.outputs.numbers }} + | sh -s -- "${numbers[@]}" env: GH_TOKEN: ${{ github.token }} + PULL_REQUESTS: ${{ needs.get-prs-for-ci.outputs.numbers }} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 924feb42725e..7f10917e238a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -47,7 +47,9 @@ jobs: steps: - name: Mark token input as sensitive if: inputs.token != '' - run: echo "::add-mask::${{ inputs.token }}" + run: printf '::add-mask::%s\n' "$COMMENT_TOKEN" + env: + COMMENT_TOKEN: ${{ inputs.token }} - name: Add link to the current run id: comment run: | @@ -155,7 +157,7 @@ jobs: run: | nix-shell \ -I nixpkgs=./tools/nix/pkgs.nix \ - --pure --keep FILTER --keep LC_ALL --keep LANG \ + --pure --keep CATEGORIES --keep FILTER --keep RUNS --keep LC_ALL --keep LANG \ --arg loadJSBuiltinsDynamically false \ --arg ccache 'null' \ --arg icu 'null' \ @@ -163,11 +165,12 @@ jobs: --arg devTools '[]' \ --run ' set -o pipefail + read -r -a categories <<< "$CATEGORIES" ./base_node benchmark/compare.js \ --filter "$FILTER" \ - --runs ${{ inputs.runs }} \ + --runs "$RUNS" \ --old ./base_node --new ./node \ - -- ${{ inputs.category }} \ + -- "${categories[@]}" \ | tee /dev/stderr \ > ${{ matrix.system }}.csv echo "> [!WARNING] " @@ -185,7 +188,9 @@ jobs: echo "> using a dedicated machine, e.g. Jenkins CI." ' | tee /dev/stderr >> "$GITHUB_STEP_SUMMARY" env: + CATEGORIES: ${{ inputs.category }} FILTER: ${{ inputs.filter }} + RUNS: ${{ inputs.runs }} - name: Upload raw benchmark results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -246,7 +251,9 @@ jobs: ' | tee /dev/stderr ${{ inputs.post-comment && 'body.txt' || '' }} >> "$GITHUB_STEP_SUMMARY" - name: Mark token input as sensitive if: inputs.token != '' - run: echo "::add-mask::${{ inputs.token }}" + run: printf '::add-mask::%s\n' "$COMMENT_TOKEN" + env: + COMMENT_TOKEN: ${{ inputs.token }} - name: Edit comment if: inputs.post-comment run: | diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 8f29a390c678..83f921838b30 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -196,6 +196,8 @@ jobs: git config --local user.email "github-bot@iojs.org" git config --local user.name "Node.js GitHub Bot" ncu-config set token "$GH_TOKEN" - ./tools/actions/commit-queue.sh ${{ steps.get_mergeable_prs.outputs.numbers }} + read -r -a numbers <<< "$PULL_REQUESTS" + ./tools/actions/commit-queue.sh "${numbers[@]}" env: GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} + PULL_REQUESTS: ${{ steps.get_mergeable_prs.outputs.numbers }} diff --git a/.github/workflows/daily-wpt-fyi.yml b/.github/workflows/daily-wpt-fyi.yml index 8901ecbcee42..d15f21010cd9 100644 --- a/.github/workflows/daily-wpt-fyi.yml +++ b/.github/workflows/daily-wpt-fyi.yml @@ -122,7 +122,9 @@ jobs: - name: Clone report for upload if: ${{ env.WPT_REPORT != '' }} working-directory: out/wpt - run: cp wptreport.json wptreport-${{ steps.setup-node.outputs.node-version }}.json + run: cp wptreport.json "wptreport-$NODE_VERSION.json" + env: + NODE_VERSION: ${{ steps.setup-node.outputs.node-version }} - name: Upload GitHub Actions artifact if: ${{ env.WPT_REPORT != '' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -133,12 +135,13 @@ jobs: - name: Upload WPT Report to wpt.fyi API if: ${{ env.WPT_REPORT != '' }} env: + NODE_VERSION: ${{ steps.setup-node.outputs.node-version }} WPT_FYI_USERNAME: ${{ vars.WPT_FYI_USERNAME }} WPT_FYI_PASSWORD: ${{ secrets.WPT_FYI_PASSWORD }} working-directory: out/wpt run: | gzip wptreport.json - echo "## Node.js ${{ steps.setup-node.outputs.node-version }}" >> $GITHUB_STEP_SUMMARY + echo "## Node.js $NODE_VERSION" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "WPT Revision: [\`${WPT_REVISION:0:7}\`](https://github.com/web-platform-tests/wpt/commits/$WPT_REVISION)" >> $GITHUB_STEP_SUMMARY for WPT_FYI_ENDPOINT in "https://wpt.fyi/api/results/upload" "https://staging.wpt.fyi/api/results/upload" diff --git a/.github/workflows/timezone-update.yml b/.github/workflows/timezone-update.yml index 481c20da4e05..abe4516db4a7 100644 --- a/.github/workflows/timezone-update.yml +++ b/.github/workflows/timezone-update.yml @@ -40,14 +40,14 @@ jobs: - name: Compare versions run: | - echo "Comparing current version ${{ env.current_version }} to new version ${{ env.new_version }}" + echo "Comparing current version $current_version to new version $new_version" - run: ./tools/dep_updaters/update-timezone.mjs if: ${{ env.new_version != env.current_version }} - name: Update the expected timezone version in test if: ${{ env.new_version != env.current_version }} - run: echo "${{ env.new_version }}" > test/fixtures/tz-version.txt + run: printf '%s\n' "$new_version" > test/fixtures/tz-version.txt - name: Open Pull Request if: ${{ env.new_version != env.current_version }} diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index e204c6b43140..b70bc4c34f70 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -329,7 +329,7 @@ jobs: - name: Generate commit message if not set if: env.COMMIT_MSG == '' && (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) run: | - echo "COMMIT_MSG=${{ matrix.subsystem }}: update ${{ matrix.id }} to ${{ env.NEW_VERSION }}" >> "$GITHUB_ENV" + echo "COMMIT_MSG=${{ matrix.subsystem }}: update ${{ matrix.id }} to $NEW_VERSION" >> "$GITHUB_ENV" - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 if: github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id # Creates a PR or update the Action's existing PR, or From fa10566e6f148ee2a14c26e437642bc21498b86a Mon Sep 17 00:00:00 2001 From: John Finnerty Date: Wed, 16 Sep 2026 00:36:03 +1200 Subject: [PATCH 14/83] doc: clarify QUIC async write backpressure Signed-off-by: John Finnerty <297514060+johnfinnerty-nz@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/65947 Reviewed-By: James M Snell Reviewed-By: Xuguang Mei --- doc/api/quic.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index dfb3aea062de..bd737ae230f0 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -321,10 +321,17 @@ There are two ways to write data to a stream: up front or can be expressed as an iterable. * **Writer** — access [`stream.writer`][] to push data incrementally. The writer exposes synchronous methods (`writeSync()`, `writevSync()`, - `endSync()`) that return immediately, as well as async equivalents - (`write()`, `writev()`, `end()`) that wait for drain when backpressured. + `endSync()`) that return immediately, as well as asynchronous counterparts + (`write()`, `writev()`, `end()`). The asynchronous `write()` and `writev()` + methods use the stream/iter strict backpressure policy: when the write buffer + is full, they reject with `ERR_INVALID_STATE` instead of waiting for capacity. + If a drain is already pending, `end()` waits for it before closing. Check + `writer.canWrite` before writing. To wait for capacity, use `ondrain()` from + `node:stream/iter`, then retry the write. The stream's `onblocked` callback + reports that transport flow control has blocked progress, but does not + signal that writer capacity is available again. `writeSync()` returns `false` when the write buffer is full; the caller - should wait for drain before retrying. + should wait with `ondrain()` before retrying. These two approaches are mutually exclusive for a given stream. @@ -2449,12 +2456,16 @@ The Writer has the following methods: * `writeSync(chunk)` — Synchronous write. Returns `true` if accepted, `false` if flow-controlled. Data is NOT accepted on `false`. -* `write(chunk[, options])` — Async write with drain wait. `options.signal` - is checked at entry but not observed during the write. +* `write(chunk[, options])` — Async write. Rejects with `ERR_INVALID_STATE` + when the stream is flow-controlled rather than waiting for capacity. + `options.signal` is checked at entry but not observed during the write. * `writevSync(chunks)` — Synchronous vectored write. All-or-nothing. -* `writev(chunks[, options])` — Async vectored write. +* `writev(chunks[, options])` — Async vectored write. Rejects with + `ERR_INVALID_STATE` when the stream is flow-controlled rather than waiting + for capacity. * `endSync()` — Synchronous close. Returns total bytes or `-1`. -* `end([options])` — Async close. +* `end([options])` — Async close. If a drain is already pending, waits for it + before closing. * `fail(reason)` — Errors the stream (sends `RESET_STREAM` to peer). When `reason` is a [`QuicError`][], its [`error.errorCode`][] is used as the wire code on the resulting `RESET_STREAM` frame; otherwise @@ -2464,7 +2475,20 @@ The Writer has the following methods: See [`stream.destroy()`][] for a full-stream abort that also resets the readable side via `STOP_SENDING`. * `canWrite` — `true` if writes will be accepted, `false` if at capacity, - or `null` if closed/errored. + or `null` if closed/errored. When `writeSync()` returns `false`, use + `ondrain()` from `node:stream/iter` to wait before retrying. If `ondrain()` + returns `null`, no drain wait is available and the write should not be + retried. + +```mjs +import { ondrain } from 'node:stream/iter'; + +while (!writer.writeSync(chunk)) { + const drain = ondrain(writer); + if (drain === null) break; + await drain; +} +``` The bytes from each `writeSync()` / `writevSync()` / `write()` / `writev()` input chunk are copied into an internal buffer, so the caller's source From dbfa70a2ee1ee1a722d81de26fdcae20eb57c16d Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 15 Sep 2026 10:00:45 -0400 Subject: [PATCH 15/83] deps: update googletest to 8eff9e336692fc95961e096564f1044c600b881d PR-URL: https://github.com/nodejs/node/pull/66009 Reviewed-By: Filip Skokan Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- .../include/gtest/gtest-death-test.h | 52 +--- .../include/gtest/internal/gtest-port.h | 32 +-- deps/googletest/src/gtest-internal-inl.h | 21 +- deps/googletest/src/gtest-port.cc | 243 ++---------------- deps/googletest/src/gtest.cc | 2 +- 5 files changed, 43 insertions(+), 307 deletions(-) diff --git a/deps/googletest/include/gtest/gtest-death-test.h b/deps/googletest/include/gtest/gtest-death-test.h index afd7b3a4685a..337313ea2093 100644 --- a/deps/googletest/include/gtest/gtest-death-test.h +++ b/deps/googletest/include/gtest/gtest-death-test.h @@ -105,54 +105,10 @@ GTEST_API_ bool InDeathTestChild(); // // On the regular expressions used in death tests: // -// On POSIX-compliant systems (*nix), we use the library, -// which uses the POSIX extended regex syntax. -// -// On other platforms (e.g. Windows or Mac), we only support a simple regex -// syntax implemented as part of Google Test. This limited -// implementation should be enough most of the time when writing -// death tests; though it lacks many features you can find in PCRE -// or POSIX extended regex syntax. For example, we don't support -// union ("x|y"), grouping ("(xy)"), brackets ("[xy]"), and -// repetition count ("x{5,7}"), among others. -// -// Below is the syntax that we do support. We chose it to be a -// subset of both PCRE and POSIX extended regex, so it's easy to -// learn wherever you come from. In the following: 'A' denotes a -// literal character, period (.), or a single \\ escape sequence; -// 'x' and 'y' denote regular expressions; 'm' and 'n' are for -// natural numbers. -// -// c matches any literal character c -// \\d matches any decimal digit -// \\D matches any character that's not a decimal digit -// \\f matches \f -// \\n matches \n -// \\r matches \r -// \\s matches any ASCII whitespace, including \n -// \\S matches any character that's not a whitespace -// \\t matches \t -// \\v matches \v -// \\w matches any letter, _, or decimal digit -// \\W matches any character that \\w doesn't match -// \\c matches any literal character c, which must be a punctuation -// . matches any single character except \n -// A? matches 0 or 1 occurrences of A -// A* matches 0 or many occurrences of A -// A+ matches 1 or many occurrences of A -// ^ matches the beginning of a string (not that of each line) -// $ matches the end of a string (not that of each line) -// xy matches x followed by y -// -// If you accidentally use PCRE or POSIX extended regex features -// not implemented by us, you will get a run-time failure. In that -// case, please try to rewrite your regular expression within the -// above syntax. -// -// This implementation is *not* meant to be as highly tuned or robust -// as a compiled regex library, but should perform well enough for a -// death test, which already incurs significant overhead by launching -// a child process. +// Depending on the platform, this may use RE2, the POSIX library, +// the C++11 standard library's engine with ECMAScript syntax, or +// another similar engine. Regular expressions should be simple and portable +// enough to work across the engines of interest. // // Known caveats: // diff --git a/deps/googletest/include/gtest/internal/gtest-port.h b/deps/googletest/include/gtest/internal/gtest-port.h index 154be3c16029..3b2947b852e7 100644 --- a/deps/googletest/include/gtest/internal/gtest-port.h +++ b/deps/googletest/include/gtest/internal/gtest-port.h @@ -176,7 +176,7 @@ // GTEST_USES_POSIX_RE - enhanced POSIX regex is used. Do not confuse with // GTEST_HAS_POSIX_RE (see above) which users can // define themselves. -// GTEST_USES_SIMPLE_RE - our own simple regex is used; +// GTEST_USES_STD_RE - std::regex from the C++ standard library is used; // the above RE\b(s) are mutually exclusive. // GTEST_HAS_ABSL - Google Test is compiled with Abseil. @@ -438,8 +438,9 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION; #include // NOLINT #define GTEST_USES_POSIX_RE 1 #else -// Use our own simple regex implementation. -#define GTEST_USES_SIMPLE_RE 1 +// Use std::regex from the C++ standard library. +#include // NOLINT +#define GTEST_USES_STD_RE 1 #endif #ifndef GTEST_HAS_EXCEPTIONS @@ -992,12 +993,11 @@ class GTEST_API_ [[nodiscard]] RE { RE2 regex_; }; -#elif defined(GTEST_USES_POSIX_RE) || defined(GTEST_USES_SIMPLE_RE) +#elif defined(GTEST_USES_POSIX_RE) || defined(GTEST_USES_STD_RE) GTEST_DISABLE_MSC_WARNINGS_PUSH_(4251 \ /* class A needs to have dll-interface to be used by clients of class B */) -// A simple C++ wrapper for . It uses the POSIX Extended -// Regular Expression syntax. +// A simple C++ wrapper for or . class GTEST_API_ [[nodiscard]] RE { public: // A copy constructor is required by the Standard to initialize object @@ -1037,9 +1037,9 @@ class GTEST_API_ [[nodiscard]] RE { regex_t full_regex_; // For FullMatch(). regex_t partial_regex_; // For PartialMatch(). -#else // GTEST_USES_SIMPLE_RE +#else // GTEST_USES_STD_RE - std::string full_pattern_; // For FullMatch(); + std::regex regex_; #endif }; @@ -1755,14 +1755,16 @@ class [[nodiscard]] MutexBase { #define GTEST_DECLARE_STATIC_MUTEX_(mutex) \ extern ::testing::internal::MutexBase mutex +#if defined(PTHREAD_NULL) +#define GTEST_INTERNAL_PTHREAD_NULL PTHREAD_NULL +#else +#define GTEST_INTERNAL_PTHREAD_NULL (pthread_t{}) +#endif + // Defines and statically (i.e. at link time) initializes a static mutex. -// The initialization list here does not explicitly initialize each field, -// instead relying on default initialization for the unspecified fields. In -// particular, the owner_ field (a pthread_t) is not explicitly initialized. -// This allows initialization to work whether pthread_t is a scalar or struct. -// The flag -Wmissing-field-initializers must not be specified for this to work. -#define GTEST_DEFINE_STATIC_MUTEX_(mutex) \ - ::testing::internal::MutexBase mutex = {PTHREAD_MUTEX_INITIALIZER, false, 0} +#define GTEST_DEFINE_STATIC_MUTEX_(mutex) \ + ::testing::internal::MutexBase mutex = {PTHREAD_MUTEX_INITIALIZER, false, \ + GTEST_INTERNAL_PTHREAD_NULL} // The Mutex class can only be used for mutexes created at runtime. It // shares its API with MutexBase otherwise. diff --git a/deps/googletest/src/gtest-internal-inl.h b/deps/googletest/src/gtest-internal-inl.h index 4bebca1bc656..5a6332a755ae 100644 --- a/deps/googletest/src/gtest-internal-inl.h +++ b/deps/googletest/src/gtest-internal-inl.h @@ -980,26 +980,7 @@ inline UnitTestImpl* GetUnitTestImpl() { return UnitTest::GetInstance()->impl(); } -#ifdef GTEST_USES_SIMPLE_RE - -// Internal helper functions for implementing the simple regular -// expression matcher. -GTEST_API_ bool IsInSet(char ch, const char* str); -GTEST_API_ bool IsAsciiDigit(char ch); -GTEST_API_ bool IsAsciiPunct(char ch); -GTEST_API_ bool IsRepeat(char ch); -GTEST_API_ bool IsAsciiWhiteSpace(char ch); -GTEST_API_ bool IsAsciiWordChar(char ch); -GTEST_API_ bool IsValidEscape(char ch); -GTEST_API_ bool AtomMatchesChar(bool escaped, char pattern, char ch); -GTEST_API_ bool ValidateRegex(const char* regex); -GTEST_API_ bool MatchRegexAtHead(const char* regex, const char* str); -GTEST_API_ bool MatchRepetitionAndRegexAtHead(bool escaped, char ch, - char repeat, const char* regex, - const char* str); -GTEST_API_ bool MatchRegexAnywhere(const char* regex, const char* str); - -#endif // GTEST_USES_SIMPLE_RE + // Parses the command line for Google Test flags, without initializing // other parts of Google Test. diff --git a/deps/googletest/src/gtest-port.cc b/deps/googletest/src/gtest-port.cc index be5b16e76d3e..68f77f71247b 100644 --- a/deps/googletest/src/gtest-port.cc +++ b/deps/googletest/src/gtest-port.cc @@ -766,249 +766,46 @@ void RE::Init(const char* regex) { delete[] full_pattern; } -#elif defined(GTEST_USES_SIMPLE_RE) - -// Returns true if and only if ch appears anywhere in str (excluding the -// terminating '\0' character). -bool IsInSet(char ch, const char* str) { - return ch != '\0' && strchr(str, ch) != nullptr; -} - -// Returns true if and only if ch belongs to the given classification. -// Unlike similar functions in , these aren't affected by the -// current locale. -bool IsAsciiDigit(char ch) { return '0' <= ch && ch <= '9'; } -bool IsAsciiPunct(char ch) { - return IsInSet(ch, "^-!\"#$%&'()*+,./:;<=>?@[\\]_`{|}~"); -} -bool IsRepeat(char ch) { return IsInSet(ch, "?*+"); } -bool IsAsciiWhiteSpace(char ch) { return IsInSet(ch, " \f\n\r\t\v"); } -bool IsAsciiWordChar(char ch) { - return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || - ('0' <= ch && ch <= '9') || ch == '_'; -} - -// Returns true if and only if "\\c" is a supported escape sequence. -bool IsValidEscape(char c) { - return (IsAsciiPunct(c) || IsInSet(c, "dDfnrsStvwW")); -} - -// Returns true if and only if the given atom (specified by escaped and -// pattern) matches ch. The result is undefined if the atom is invalid. -bool AtomMatchesChar(bool escaped, char pattern_char, char ch) { - if (escaped) { // "\\p" where p is pattern_char. - switch (pattern_char) { - case 'd': - return IsAsciiDigit(ch); - case 'D': - return !IsAsciiDigit(ch); - case 'f': - return ch == '\f'; - case 'n': - return ch == '\n'; - case 'r': - return ch == '\r'; - case 's': - return IsAsciiWhiteSpace(ch); - case 'S': - return !IsAsciiWhiteSpace(ch); - case 't': - return ch == '\t'; - case 'v': - return ch == '\v'; - case 'w': - return IsAsciiWordChar(ch); - case 'W': - return !IsAsciiWordChar(ch); - } - return IsAsciiPunct(pattern_char) && pattern_char == ch; - } - - return (pattern_char == '.' && ch != '\n') || pattern_char == ch; -} - -// Helper function used by ValidateRegex() to format error messages. -static std::string FormatRegexSyntaxError(const char* regex, int index) { - return (Message() << "Syntax error at index " << index - << " in simple regular expression \"" << regex << "\": ") - .GetString(); -} - -// Generates non-fatal failures and returns false if regex is invalid; -// otherwise returns true. -bool ValidateRegex(const char* regex) { - if (regex == nullptr) { - ADD_FAILURE() << "NULL is not a valid simple regular expression."; - return false; - } - - bool is_valid = true; - - // True if and only if ?, *, or + can follow the previous atom. - bool prev_repeatable = false; - for (int i = 0; regex[i]; i++) { - if (regex[i] == '\\') { // An escape sequence - i++; - if (regex[i] == '\0') { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i - 1) - << "'\\' cannot appear at the end."; - return false; - } - - if (!IsValidEscape(regex[i])) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i - 1) - << "invalid escape sequence \"\\" << regex[i] << "\"."; - is_valid = false; - } - prev_repeatable = true; - } else { // Not an escape sequence. - const char ch = regex[i]; - - if (ch == '^' && i > 0) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) - << "'^' can only appear at the beginning."; - is_valid = false; - } else if (ch == '$' && regex[i + 1] != '\0') { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) - << "'$' can only appear at the end."; - is_valid = false; - } else if (IsInSet(ch, "()[]{}|")) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) << "'" << ch - << "' is unsupported."; - is_valid = false; - } else if (IsRepeat(ch) && !prev_repeatable) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) << "'" << ch - << "' can only follow a repeatable token."; - is_valid = false; - } - - prev_repeatable = !IsInSet(ch, "^$?*+"); - } - } - - return is_valid; -} - -// Matches a repeated regex atom followed by a valid simple regular -// expression. The regex atom is defined as c if escaped is false, -// or \c otherwise. repeat is the repetition meta character (?, *, -// or +). The behavior is undefined if str contains too many -// characters to be indexable by size_t, in which case the test will -// probably time out anyway. We are fine with this limitation as -// std::string has it too. -bool MatchRepetitionAndRegexAtHead(bool escaped, char c, char repeat, - const char* regex, const char* str) { - const size_t min_count = (repeat == '+') ? 1 : 0; - const size_t max_count = (repeat == '?') ? 1 : static_cast(-1) - 1; - // We cannot call numeric_limits::max() as it conflicts with the - // max() macro on Windows. - - for (size_t i = 0; i <= max_count; ++i) { - // We know that the atom matches each of the first i characters in str. - if (i >= min_count && MatchRegexAtHead(regex, str + i)) { - // We have enough matches at the head, and the tail matches too. - // Since we only care about *whether* the pattern matches str - // (as opposed to *how* it matches), there is no need to find a - // greedy match. - return true; - } - if (str[i] == '\0' || !AtomMatchesChar(escaped, c, str[i])) return false; - } - return false; -} - -// Returns true if and only if regex matches a prefix of str. regex must -// be a valid simple regular expression and not start with "^", or the -// result is undefined. -bool MatchRegexAtHead(const char* regex, const char* str) { - if (*regex == '\0') // An empty regex matches a prefix of anything. - return true; - - // "$" only matches the end of a string. Note that regex being - // valid guarantees that there's nothing after "$" in it. - if (*regex == '$') return *str == '\0'; - - // Is the first thing in regex an escape sequence? - const bool escaped = *regex == '\\'; - if (escaped) ++regex; - if (IsRepeat(regex[1])) { - // MatchRepetitionAndRegexAtHead() calls MatchRegexAtHead(), so - // here's an indirect recursion. It terminates as the regex gets - // shorter in each recursion. - return MatchRepetitionAndRegexAtHead(escaped, regex[0], regex[1], regex + 2, - str); - } else { - // regex isn't empty, isn't "$", and doesn't start with a - // repetition. We match the first atom of regex with the first - // character of str and recurse. - return (*str != '\0') && AtomMatchesChar(escaped, *regex, *str) && - MatchRegexAtHead(regex + 1, str + 1); - } -} - -// Returns true if and only if regex matches any substring of str. regex must -// be a valid simple regular expression, or the result is undefined. -// -// The algorithm is recursive, but the recursion depth doesn't exceed -// the regex length, so we won't need to worry about running out of -// stack space normally. In rare cases the time complexity can be -// exponential with respect to the regex length + the string length, -// but usually it's must faster (often close to linear). -bool MatchRegexAnywhere(const char* regex, const char* str) { - if (regex == nullptr || str == nullptr) return false; - - if (*regex == '^') return MatchRegexAtHead(regex + 1, str); - - // A successful match can be anywhere in str. - do { - if (MatchRegexAtHead(regex, str)) return true; - } while (*str++ != '\0'); - return false; -} - -// Implements the RE class. +#elif defined(GTEST_USES_STD_RE) RE::~RE() = default; // Returns true if and only if regular expression re matches the entire str. bool RE::FullMatch(const char* str, const RE& re) { - return re.is_valid_ && MatchRegexAnywhere(re.full_pattern_.c_str(), str); + if (!re.is_valid_ || str == nullptr) return false; + return std::regex_match(str, re.regex_); } // Returns true if and only if regular expression re matches a substring of // str (including str itself). bool RE::PartialMatch(const char* str, const RE& re) { - return re.is_valid_ && MatchRegexAnywhere(re.pattern_.c_str(), str); + if (!re.is_valid_ || str == nullptr) return false; + return std::regex_search(str, re.regex_); } // Initializes an RE from its string representation. void RE::Init(const char* regex) { - full_pattern_.clear(); - pattern_.clear(); + pattern_ = regex == nullptr ? "" : regex; + is_valid_ = false; - if (regex != nullptr) { - pattern_ = regex; - } - - is_valid_ = ValidateRegex(regex); - if (!is_valid_) { - // No need to calculate the full pattern when the regex is invalid. + if (regex == nullptr) { + ADD_FAILURE() << "NULL is not a valid regular expression."; return; } - // Reserves enough bytes to hold the regular expression used for a - // full match: we need space to prepend a '^' and append a '$'. - full_pattern_.reserve(pattern_.size() + 2); - - if (pattern_.empty() || pattern_.front() != '^') { - full_pattern_.push_back('^'); // Makes sure full_pattern_ starts with '^'. +#if GTEST_HAS_EXCEPTIONS + try { + regex_ = std::regex(regex, std::regex_constants::ECMAScript); + } catch (const std::regex_error& e) { + ADD_FAILURE() << "Regular expression \"" << regex + << "\" is not a valid regular expression: " << e.what(); + return; } +#else + regex_ = std::regex(regex, std::regex_constants::ECMAScript); +#endif - full_pattern_.append(pattern_); - - if (pattern_.empty() || pattern_.back() != '$') { - full_pattern_.push_back('$'); // Makes sure full_pattern_ ends with '$'. - } + is_valid_ = true; } #endif // GTEST_USES_POSIX_RE diff --git a/deps/googletest/src/gtest.cc b/deps/googletest/src/gtest.cc index 47c60da22916..3772f18552dc 100644 --- a/deps/googletest/src/gtest.cc +++ b/deps/googletest/src/gtest.cc @@ -4648,7 +4648,7 @@ std::string JsonUnitTestResultPrinter::EscapeJson(const std::string& str) { m << "\\r"; break; default: - if (ch < ' ') { + if (static_cast(ch) < ' ' || ch == '\x7F') { m << "\\u00" << String::FormatByte(static_cast(ch)); } else { m << ch; From 84e91744b9b0115443b70340e9d2739183a4e16d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 15 Sep 2026 18:05:54 +0200 Subject: [PATCH 16/83] test: deflake util.throttle tests Short throttle windows can expire between synchronous calls when the process is descheduled. Delayed timer dispatch can also release two queued calls together, invalidating the strict test's spacing assertion. Move timing checks to a separate test with mocked timers and a matching libuv clock. Cover both punctual and delayed dispatch, and keep real async_hooks timer-allocation checks with windows canceled before expiry. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66034 Refs: https://github.com/nodejs/node/pull/65899 Refs: https://ci.nodejs.org/job/node-test-commit-linux-containered/nodes=ubuntu2404_sharedlibs_openssl111_x64/59189/ Refs: https://ci.nodejs.org/job/node-test-commit-linuxone/nodes=rhel8-s390x/58602/ Reviewed-By: James M Snell Reviewed-By: Stefan Stojanovic Reviewed-By: Matteo Collina --- test/parallel/test-util-throttle-timing.js | 170 +++++++++++++++++++++ test/parallel/test-util-throttle.js | 124 ++------------- 2 files changed, 181 insertions(+), 113 deletions(-) create mode 100644 test/parallel/test-util-throttle-timing.js diff --git a/test/parallel/test-util-throttle-timing.js b/test/parallel/test-util-throttle-timing.js new file mode 100644 index 000000000000..56b6c7716a92 --- /dev/null +++ b/test/parallel/test-util-throttle-timing.js @@ -0,0 +1,170 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { mock } = require('node:test'); +const { setImmediate } = require('node:timers/promises'); +const { internalBinding } = require('internal/test/binding'); + +// Throttle uses the libuv clock as well as timers. Control both before loading +// the implementation, which captures setTimeout and clearTimeout. +mock.timers.enable({ apis: ['Date', 'setTimeout'] }); +mock.method(internalBinding('timers'), 'getLibuvNow', () => Date.now()); +const { throttle } = require('node:util'); + +process.on('unhandledRejection', common.mustNotCall()); + +(async () => { + { + const values = []; + const times = []; + const start = Date.now(); + const throttled = throttle(common.mustCall(function(value) { + assert.strictEqual(this, throttled); + values.push(value); + times.push(Date.now() - start); + return value; + }, 5), 2, 40); + + assert.strictEqual(typeof throttled.cancel, 'function'); + assert.strictEqual(typeof throttled.hasImmediateCapacity, 'function'); + assert.strictEqual(typeof throttled.ref, 'function'); + assert.strictEqual(typeof throttled.unref, 'function'); + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(throttled.activeCount, 0); + assert.strictEqual(throttled.hasImmediateCapacity(), true); + + const first = throttled(1); + const second = throttled(2); + const third = throttled(3); + const fourth = throttled(4); + const fifth = throttled(5); + + assert(first instanceof Promise); + assert(second instanceof Promise); + assert.notStrictEqual(first, second); + assert.deepStrictEqual(values, [1, 2]); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + assert.strictEqual(throttled.pending, fifth); + assert.strictEqual(throttled.pendingCount, 3); + assert.strictEqual(throttled.unref(), throttled); + assert.strictEqual(throttled.ref(), throttled); + + mock.timers.tick(39); + assert.deepStrictEqual(values, [1, 2]); + mock.timers.tick(1); + assert.deepStrictEqual(values, [1, 2, 3, 4]); + assert.strictEqual(throttled.pendingCount, 1); + mock.timers.tick(39); + assert.deepStrictEqual(values, [1, 2, 3, 4]); + mock.timers.tick(1); + + assert.deepStrictEqual( + await Promise.all([first, second, third, fourth, fifth]), + [1, 2, 3, 4, 5], + ); + assert.deepStrictEqual(values, [1, 2, 3, 4, 5]); + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(throttled.activeCount, 0); + assert.deepStrictEqual(times, [0, 0, 40, 40, 80]); + } + + { + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + return value; + }, 2), 1, 20, { maxPending: 1 }); + const first = throttled(1); + const second = throttled(2); + const dropped = throttled(3); + + assert.deepStrictEqual(values, [1]); + assert.strictEqual(throttled.pendingCount, 1); + await setImmediate(); + await assert.rejects(dropped, { code: 'ERR_THROTTLED' }); + mock.timers.tick(19); + assert.deepStrictEqual(values, [1]); + mock.timers.tick(1); + assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); + assert.deepStrictEqual(values, [1, 2]); + } + + { + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + return value; + }, 3), 2, 30, { overflow: 'drop' }); + const first = throttled(1); + const second = throttled(2); + await assert.rejects(throttled(3), { code: 'ERR_THROTTLED' }); + assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); + + mock.timers.tick(29); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + await assert.rejects(throttled(4), { code: 'ERR_THROTTLED' }); + mock.timers.tick(1); + assert.strictEqual(throttled.hasImmediateCapacity(), true); + assert.strictEqual(await throttled(5), 5); + assert.deepStrictEqual(values, [1, 2, 5]); + } + + // A rolling window releases one slot at a time when timers run on schedule. + // If dispatch is delayed until both slots expire, both calls may run together. + for (const delayed of [false, true]) { + const times = []; + const start = Date.now(); + const throttled = throttle(common.mustCall((value) => { + times.push(Date.now() - start); + return value; + }, 4), 2, 80, { strict: true }); + + const first = throttled(1); + mock.timers.tick(40); + const second = throttled(2); + const third = throttled(3); + const fourth = throttled(4); + + mock.timers.tick(39); + assert.deepStrictEqual(times, [0, 40]); + if (delayed) { + mock.timers.tick(41); + } else { + mock.timers.tick(1); + assert.deepStrictEqual(times, [0, 40, 80]); + mock.timers.tick(39); + assert.deepStrictEqual(times, [0, 40, 80]); + mock.timers.tick(1); + } + + assert.deepStrictEqual( + await Promise.all([first, second, third, fourth]), + [1, 2, 3, 4], + ); + assert.deepStrictEqual(times, delayed ? [0, 40, 120, 120] : [0, 40, 80, 120]); + } + + { + let recursive; + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + if (value === 1) recursive = throttled(2); + return value; + }, 2), 1, 20); + + const first = throttled(1); + assert.deepStrictEqual(values, [1]); + assert.strictEqual(throttled.pending, recursive); + assert.strictEqual(throttled.pendingCount, 1); + mock.timers.tick(19); + assert.deepStrictEqual(values, [1]); + mock.timers.tick(1); + assert.deepStrictEqual(await Promise.all([first, recursive]), [1, 2]); + assert.deepStrictEqual(values, [1, 2]); + } +})().then(common.mustCall()).finally(() => mock.reset()); diff --git a/test/parallel/test-util-throttle.js b/test/parallel/test-util-throttle.js index 550df43e67fe..b22fb4630e7e 100644 --- a/test/parallel/test-util-throttle.js +++ b/test/parallel/test-util-throttle.js @@ -4,7 +4,7 @@ const common = require('../common'); const assert = require('node:assert'); const { createHook } = require('node:async_hooks'); -const { setImmediate, setTimeout } = require('node:timers/promises'); +const { setImmediate } = require('node:timers/promises'); const { throttle } = require('node:util'); const { TIMEOUT_MAX } = require('internal/timers'); @@ -95,55 +95,9 @@ throttle(() => {}, 1, 1, { ); } +// Keep windows open across synchronous assertions, even if the process is +// descheduled. Window expiration is covered in test-util-throttle-timing.js. (async () => { - { - const values = []; - const times = []; - const start = Date.now(); - const throttled = throttle(common.mustCall(function(value) { - assert.strictEqual(this, throttled); - values.push(value); - times.push(Date.now() - start); - return value; - }, 5), 2, 40); - - assert.strictEqual(typeof throttled.cancel, 'function'); - assert.strictEqual(typeof throttled.hasImmediateCapacity, 'function'); - assert.strictEqual(typeof throttled.ref, 'function'); - assert.strictEqual(typeof throttled.unref, 'function'); - assert.strictEqual(throttled.pending, null); - assert.strictEqual(throttled.pendingCount, 0); - assert.strictEqual(throttled.activeCount, 0); - assert.strictEqual(throttled.hasImmediateCapacity(), true); - - const first = throttled(1); - const second = throttled(2); - const third = throttled(3); - const fourth = throttled(4); - const fifth = throttled(5); - - assert(first instanceof Promise); - assert(second instanceof Promise); - assert.notStrictEqual(first, second); - assert.deepStrictEqual(values, [1, 2]); - assert.strictEqual(throttled.hasImmediateCapacity(), false); - assert.strictEqual(throttled.pending, fifth); - assert.strictEqual(throttled.pendingCount, 3); - assert.strictEqual(throttled.unref(), throttled); - assert.strictEqual(throttled.ref(), throttled); - - assert.deepStrictEqual( - await Promise.all([first, second, third, fourth, fifth]), - [1, 2, 3, 4, 5], - ); - assert.deepStrictEqual(values, [1, 2, 3, 4, 5]); - assert.strictEqual(throttled.pending, null); - assert.strictEqual(throttled.pendingCount, 0); - assert.strictEqual(throttled.activeCount, 0); - assert(times[2] - times[0] >= 30); - assert(times[4] - times[2] >= 30); - } - { let running = 0; let maxRunning = 0; @@ -220,24 +174,6 @@ throttle(() => {}, 1, 1, { assert.strictEqual(throttled.activeCount, 0); } - { - const values = []; - const throttled = throttle(common.mustCall((value) => { - values.push(value); - return value; - }, 2), 1, 20, { maxPending: 1 }); - const first = throttled(1); - const second = throttled(2); - const dropped = throttled(3); - - assert.deepStrictEqual(values, [1]); - assert.strictEqual(throttled.pendingCount, 1); - await setImmediate(); - await assert.rejects(dropped, { code: 'ERR_THROTTLED' }); - assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); - assert.deepStrictEqual(values, [1, 2]); - } - { let timeoutCount = 0; const hook = createHook({ @@ -249,7 +185,7 @@ throttle(() => {}, 1, 1, { const throttled = throttle(common.mustCall((value) => { values.push(value); return value; - }, 3), 2, 30, { overflow: 'drop' }); + }, 3), 2, TIMEOUT_MAX, { overflow: 'drop' }); hook.enable(); const first = throttled(1); @@ -265,37 +201,14 @@ throttle(() => {}, 1, 1, { await assert.rejects(dropped, { code: 'ERR_THROTTLED' }); assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); - await setTimeout(30); + throttled.cancel(); assert.strictEqual(await throttled(4), 4); assert.deepStrictEqual(values, [1, 2, 4]); } - { - const times = []; - const start = Date.now(); - const throttled = throttle(common.mustCall((value) => { - times.push(Date.now() - start); - return value; - }, 4), 2, 80, { strict: true }); - - const first = throttled(1); - await setTimeout(40); - const second = throttled(2); - const third = throttled(3); - const fourth = throttled(4); - - assert.deepStrictEqual( - await Promise.all([first, second, third, fourth]), - [1, 2, 3, 4], - ); - assert(times[2] - times[0] >= 65); - assert(times[3] - times[1] >= 65); - assert(times[3] - times[2] >= 25); - } - { const reason = new Error('cancelled'); - const throttled = throttle(common.mustCall((value) => value, 2), 1, 100); + const throttled = throttle(common.mustCall((value) => value, 2), 1, TIMEOUT_MAX); const first = throttled(1); const second = throttled(2); const third = throttled(3); @@ -322,7 +235,7 @@ throttle(() => {}, 1, 1, { { const reason = new Error('stop'); const controller = new AbortController(); - const throttled = throttle(common.mustCall((value) => value), 1, 100, { + const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX, { signal: controller.signal, }); const first = throttled(1); @@ -363,23 +276,6 @@ throttle(() => {}, 1, 1, { await assert.rejects(throttled(), (error) => error === expected); } - { - let recursive; - const values = []; - const throttled = throttle(common.mustCall((value) => { - values.push(value); - if (value === 1) recursive = throttled(2); - return value; - }, 2), 1, 20); - - const first = throttled(1); - assert.deepStrictEqual(values, [1]); - assert.strictEqual(throttled.pending, recursive); - assert.strictEqual(throttled.pendingCount, 1); - assert.deepStrictEqual(await Promise.all([first, recursive]), [1, 2]); - assert.deepStrictEqual(values, [1, 2]); - } - { function original(first, second) { return first + second; @@ -409,7 +305,7 @@ throttle(() => {}, 1, 1, { if (type === 'Timeout') timeoutCount++; }, }); - const throttled = throttle(common.mustCall((value) => value), 1, 100); + const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX); hook.enable(); assert.strictEqual(throttled.hasImmediateCapacity(), true); @@ -430,7 +326,7 @@ throttle(() => {}, 1, 1, { if (type === 'Timeout') timeoutCount++; }, }); - const throttled = throttle(common.mustCall((value) => value), 1, 100); + const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX); hook.enable(); const first = throttled(1); const second = throttled(2); @@ -440,6 +336,8 @@ throttle(() => {}, 1, 1, { const thirdRejection = assert.rejects(third, { code: 'ABORT_ERR' }); assert.strictEqual(timeoutCount, 1); + assert.strictEqual(throttled.unref(), throttled); + assert.strictEqual(throttled.ref(), throttled); throttled.cancel(); assert.strictEqual(await first, 1); await Promise.all([secondRejection, thirdRejection]); From e13c138a2c4c60da78769c77cd29defa4b29c2c9 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 15 Sep 2026 18:06:11 +0200 Subject: [PATCH 17/83] http2: settle pending write callbacks on destroy When a stream is destroyed while a write is still in flight, nghttp2 may have handed the data to the socket and never report the write completion once the stream or session tears down. That leaves the write callback unresolved, the Writable stuck, and the event loop never drains, which times out test-http2-close-while-writing on macOS. Settle the in-flight write in Http2Stream._destroy by invoking its callback with the destroy error and resetting writePending. A later native completion callback is then a no-op because writeCb is null and writePending is 0, so the settle is idempotent. Passing the error rather than null also drains any buffered writes. Fixes: https://github.com/nodejs/node/issues/58252 Signed-off-by: Matteo Collina Assisted-by: pi-coding-agent PR-URL: https://github.com/nodejs/node/pull/66016 Fixes: https://github.com/nodejs/node/issues/58252 Reviewed-By: James M Snell Reviewed-By: Xuguang Mei --- lib/internal/http2/core.js | 9 +++++++++ test/parallel/test-http2-close-while-writing.js | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index e0a7c6555c9c..1774808da137 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -2730,6 +2730,15 @@ class Http2Stream extends Duplex { }); } } + + // A write in flight may never get its native completion callback once the + // stream tears down; settle it so the Writable can clean up. + if (state.writeCb !== null) { + const writeCb = state.writeCb; + state.writeCb = null; + state.writePending = 0; + writeCb(err); + } callback(err); } // The Http2Stream can be destroyed if it has closed and if the readable diff --git a/test/parallel/test-http2-close-while-writing.js b/test/parallel/test-http2-close-while-writing.js index 17931005dc6c..c78386c85f23 100644 --- a/test/parallel/test-http2-close-while-writing.js +++ b/test/parallel/test-http2-close-while-writing.js @@ -29,11 +29,21 @@ server.on('session', common.mustCall(function(session) { stream.on('error', common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ABORTED'); })); - stream.resume(); + + // Every write dispatched before close must have its callback invoked. + let writes = 0; + let writeCallbacks = 0; stream.on('data', function() { - this.write(Buffer.alloc(1)); + writes++; + this.write(Buffer.alloc(1), () => { + writeCallbacks++; + }); process.nextTick(() => client_stream.destroy()); }); + stream.on('close', common.mustCall(() => { + assert.strictEqual(writeCallbacks, writes); + })); + stream.resume(); })); })); From cfdb7e6c5a958a43f0933fb21a915c6b3f63314e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 15 Sep 2026 18:06:21 +0200 Subject: [PATCH 18/83] src: ffi: create fast-call metadata Symbols lazily The FFI fast-call wrappers key per-function metadata on raw FFI functions using two per-isolate Symbols (kFastArguments / kFastBufferInvoke) that were declared in src/env_properties.h. Everything in env_properties.h is allocated while the startup snapshot is built, so each Symbol advances the isolate's identity-hash RNG before Object.prototype / Function.prototype receive their snapshot identity hashes. In the snapshot produced for Node 26.4.0+ this shifted those hashes so a function map (a function whose `length` was redefined) and a plain-object map (an object literal with an accessor) collide in V8's 64-slot NormalizedMapCache. Every store into such objects then misses the inline cache, and the repro reported in the linked issue is roughly 7x slower. Create the two Symbols lazily in the FFI binding's Initialize, on the first run of internalBinding('ffi') at runtime, instead of declaring them in env_properties.h. They are therefore not allocated during snapshot serialization and no longer bias the snapshot's prototype identity hashes. Their export, property layout, and the fast-call feature behavior are unchanged. Refs: https://github.com/nodejs/node/issues/66011 Signed-off-by: Matteo Collina Assisted-by: Pi PR-URL: https://github.com/nodejs/node/pull/66015 Reviewed-By: Paolo Insogna Reviewed-By: Xuguang Mei Reviewed-By: James M Snell --- src/env-inl.h | 34 ++++++++++++++++++++++++++++++++++ src/env.h | 20 ++++++++++++++++++++ src/env_properties.h | 2 -- src/node_ffi.cc | 17 ++++++++++++++++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/env-inl.h b/src/env-inl.h index e41afd25ffc3..a194751e8b31 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -866,6 +866,24 @@ void Environment::set_process_exit_handler( #undef V #undef VM + inline v8::Local IsolateData::ffi_fast_arguments_symbol() const { + return ffi_fast_arguments_symbol_.Get(isolate_); + } + inline void IsolateData::set_ffi_fast_arguments_symbol( + v8::Local value) { + CHECK(ffi_fast_arguments_symbol_.IsEmpty()); + ffi_fast_arguments_symbol_.Set(isolate_, value); + } + inline v8::Local IsolateData::ffi_fast_buffer_invoke_symbol() + const { + return ffi_fast_buffer_invoke_symbol_.Get(isolate_); + } + inline void IsolateData::set_ffi_fast_buffer_invoke_symbol( + v8::Local value) { + CHECK(ffi_fast_buffer_invoke_symbol_.IsEmpty()); + ffi_fast_buffer_invoke_symbol_.Set(isolate_, value); + } + #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) #define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) @@ -881,6 +899,22 @@ void Environment::set_process_exit_handler( #undef VY #undef VP + inline v8::Local Environment::ffi_fast_arguments_symbol() const { + return isolate_data()->ffi_fast_arguments_symbol(); + } + inline void Environment::set_ffi_fast_arguments_symbol( + v8::Local value) { + isolate_data()->set_ffi_fast_arguments_symbol(value); + } + inline v8::Local Environment::ffi_fast_buffer_invoke_symbol() + const { + return isolate_data()->ffi_fast_buffer_invoke_symbol(); + } + inline void Environment::set_ffi_fast_buffer_invoke_symbol( + v8::Local value) { + isolate_data()->set_ffi_fast_buffer_invoke_symbol(value); + } + #define V(Name, label, _, __) \ inline v8::Local Environment::Name##_permission_string() const { \ return isolate_data()->Name##_permission_string(); \ diff --git a/src/env.h b/src/env.h index 9149b91fe4bb..cb701b197df9 100644 --- a/src/env.h +++ b/src/env.h @@ -214,6 +214,17 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { inline v8::Local async_wrap_provider(int index) const; + // Symbols used by the FFI fast-call API to key per-function metadata on raw + // FFI functions. Kept out of env_properties.h so they are created lazily at + // runtime, not while the startup snapshot is built (allocating Symbols during + // serialization advances the isolate's identity-hash RNG, which can shift the + // snapshot hashes for Object.prototype/Function.prototype and make a function + // map and a plain-object map collide in V8's NormalizedMapCache). + inline v8::Local ffi_fast_arguments_symbol() const; + inline void set_ffi_fast_arguments_symbol(v8::Local value); + inline v8::Local ffi_fast_buffer_invoke_symbol() const; + inline void set_ffi_fast_buffer_invoke_symbol(v8::Local value); + size_t max_young_gen_size = 1; std::unordered_map> static_str_map; @@ -254,6 +265,9 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { PERMISSIONS(V) #undef V + v8::Eternal ffi_fast_arguments_symbol_; + v8::Eternal ffi_fast_buffer_invoke_symbol_; + // Keep a list of all Persistent strings used for AsyncWrap Provider types. std::array, AsyncWrap::PROVIDERS_LENGTH> async_wrap_providers_; @@ -963,6 +977,12 @@ class Environment final : public MemoryRetainer { #undef VY #undef VP + // Runtime-created FFI fast-call API Symbols (see IsolateData). + inline v8::Local ffi_fast_arguments_symbol() const; + inline void set_ffi_fast_arguments_symbol(v8::Local value); + inline v8::Local ffi_fast_buffer_invoke_symbol() const; + inline void set_ffi_fast_buffer_invoke_symbol(v8::Local value); + #define V(Name, label, _, __) \ inline v8::Local Name##_permission_string() const; PERMISSIONS(V) diff --git a/src/env_properties.h b/src/env_properties.h index 3e7fb2c20d9b..152d4bca86ae 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -50,8 +50,6 @@ V(ffi_sb_invoke_slow_symbol, "ffi_sb_invoke_slow_symbol") \ V(ffi_sb_arguments_symbol, "ffi_sb_arguments_symbol") \ V(ffi_sb_return_symbol, "ffi_sb_return_symbol") \ - V(ffi_fast_arguments_symbol, "ffi_fast_arguments_symbol") \ - V(ffi_fast_buffer_invoke_symbol, "ffi_fast_buffer_invoke_symbol") \ V(constructor_key_symbol, "constructor_key_symbol") \ V(handle_onclose_symbol, "handle_onclose") \ V(no_message_symbol, "no_message_symbol") \ diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 13f1ab384aa6..495e59ba5cce 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -1424,7 +1424,22 @@ static void Initialize(Local target, env->ffi_sb_return_symbol()) .Check(); // Fast API wrappers use separate metadata Symbols so pointer-conversion - // routing does not depend on SharedBuffer internals. + // routing does not depend on SharedBuffer internals. These are created here + // (at runtime, on first `internalBinding('ffi')`) instead of being declared + // in env_properties.h, so they are not allocated while the startup snapshot + // is being built. Allocating Symbols during snapshot serialization advances + // the isolate's identity-hash RNG and shifts the identity hashes baked into + // the snapshot for Object.prototype / Function.prototype, which can make a + // function map and a plain-object map collide in V8's NormalizedMapCache. + if (env->ffi_fast_arguments_symbol().IsEmpty()) { + env->set_ffi_fast_arguments_symbol(v8::Symbol::New( + isolate, FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_arguments_symbol"))); + } + if (env->ffi_fast_buffer_invoke_symbol().IsEmpty()) { + env->set_ffi_fast_buffer_invoke_symbol(v8::Symbol::New( + isolate, + FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_buffer_invoke_symbol"))); + } target ->Set(context, FIXED_ONE_BYTE_STRING(isolate, "kFastArguments"), From ea51c141d5fe20b415d5aa1ae996fb14283c7354 Mon Sep 17 00:00:00 2001 From: Philipp Dunkel Date: Tue, 15 Sep 2026 18:06:32 +0200 Subject: [PATCH 19/83] vfs: close the fs hook gaps for mounted paths Several `node:fs` entry points behave differently for a mounted path than for a real one, because of how the call reaches the VFS hooks. Make them behave as they do for a real path: * Add the `watchFile`, `unwatchFile` and `promisesWatch` handlers, backed by the provider's stat watcher and async watcher; those calls threw a TypeError before. Have `watch` refuse a path that does not exist with ENOENT instead of handing back a watcher that polls forever and keeps the process alive. * Convert timestamps and validate arguments before the hook runs in `utimes`, `lutimes` and `readdir` (sync, callback and promise forms), so a mounted path gets the same ERR_INVALID_ARG_* errors and the same seconds-since-epoch numbers as a real one. * Pass the mode and times through to the `fchmod` and `futimes` hooks and route them to the handle's entry, so descriptor operations take effect like their path forms instead of being no-ops; the memory handle validates the way a FileHandle would since one calls it directly. * Treat a `mkdtemp` prefix as text rather than a path when it ends in a separator, so the directory is created inside the intended parent. * Map the first directory a recursive `mkdir` created back under the mount point instead of returning the provider-relative path. * Make disposing an already closed virtual `Dir` a no-op, as on the native `Dir`, instead of rejecting with ERR_DIR_CLOSED. test-vfs-fs-hook-gaps adds a test per gap, stating the real-fs outcome as the expectation. The existing file handle test asserted that `chmod()` and `utimes()` without arguments were no-ops; they now validate and apply, so it exercises that instead. Signed-off-by: Philipp Dunkel PR-URL: https://github.com/nodejs/node/pull/65852 Reviewed-By: James M Snell Reviewed-By: Matteo Collina Reviewed-By: Trivikram Kamat Reviewed-By: Filip Skokan --- lib/fs.js | 75 ++++++------- lib/internal/fs/promises.js | 31 +++--- lib/internal/vfs/dir.js | 7 +- lib/internal/vfs/file_handle.js | 54 ++++++++- lib/internal/vfs/file_system.js | 48 +++++--- lib/internal/vfs/setup.js | 56 +++++++++- test/parallel/test-vfs-file-handle.js | 11 +- test/parallel/test-vfs-fs-matches-real-fs.js | 111 +++++++++++++++++++ 8 files changed, 304 insertions(+), 89 deletions(-) create mode 100644 test/parallel/test-vfs-fs-matches-real-fs.js diff --git a/lib/fs.js b/lib/fs.js index da6b1d341c5d..3df908c88477 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1863,9 +1863,6 @@ function readdir(path, options, callback) { options = undefined; } - const h = vfsState.handlers; - if (h !== null && vfsResult(h.readdir(path, options), callback)) return; - callback = makeCallback(callback); options = getOptions(options); path = getValidatedPath(path); @@ -1873,6 +1870,9 @@ function readdir(path, options, callback) { validateBoolean(options.recursive, 'options.recursive'); } + const h = vfsState.handlers; + if (h !== null && vfsResult(h.readdir(path, options), callback)) return; + if (options.recursive) { readdirRecursive(path, options, callback); return; @@ -1909,17 +1909,18 @@ function readdir(path, options, callback) { * @returns {string | Buffer[] | Dirent[]} */ function readdirSync(path, options) { - const h = vfsState.handlers; - if (h !== null) { - const result = h.readdirSync(path, options); - if (result !== undefined) return result; - } options = getOptions(options); path = getValidatedPath(path); if (options.recursive != null) { validateBoolean(options.recursive, 'options.recursive'); } + const h = vfsState.handlers; + if (h !== null) { + const result = h.readdirSync(path, options); + if (result !== undefined) return result; + } + if (options.recursive) { return readdirSyncRecursive(path, options); } @@ -2422,7 +2423,7 @@ function fchmod(fd, mode, callback) { callback = makeCallback(callback); const h = vfsState.handlers; - if (h !== null && vfsVoid(h.fchmod(fd), callback)) return; + if (h !== null && vfsVoid(h.fchmod(fd, mode), callback)) return; if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.')); @@ -2441,19 +2442,18 @@ function fchmod(fd, mode, callback) { * @returns {void} */ function fchmodSync(fd, mode) { + mode = parseFileMode(mode, 'mode'); + const h = vfsState.handlers; if (h !== null) { - const result = h.fchmodSync(fd); + const result = h.fchmodSync(fd, mode); if (result !== undefined) return; } if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.'); } - binding.fchmod( - fd, - parseFileMode(mode, 'mode'), - ); + binding.fchmod(fd, mode); } /** @@ -2692,18 +2692,15 @@ function chownSync(path, uid, gid) { function utimes(path, atime, mtime, callback) { callback = makeCallback(callback); path = getValidatedPath(path); + atime = toUnixTimestamp(atime); + mtime = toUnixTimestamp(mtime); const h = vfsState.handlers; if (h !== null && vfsVoid(h.utimes(path, atime, mtime), callback)) return; const req = new FSReqCallback(); req.oncomplete = callback; - binding.utimes( - path, - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - req, - ); + binding.utimes(path, atime, mtime, req); } /** @@ -2716,6 +2713,8 @@ function utimes(path, atime, mtime, callback) { */ function utimesSync(path, atime, mtime) { path = getValidatedPath(path); + atime = toUnixTimestamp(atime); + mtime = toUnixTimestamp(mtime); const h = vfsState.handlers; if (h !== null) { @@ -2723,11 +2722,7 @@ function utimesSync(path, atime, mtime) { if (result !== undefined) return; } - binding.utimes( - path, - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - ); + binding.utimes(path, atime, mtime); } /** @@ -2745,7 +2740,7 @@ function futimes(fd, atime, mtime, callback) { callback = makeCallback(callback); const h = vfsState.handlers; - if (h !== null && vfsVoid(h.futimes(fd), callback)) return; + if (h !== null && vfsVoid(h.futimes(fd, atime, mtime), callback)) return; if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.')); @@ -2767,9 +2762,12 @@ function futimes(fd, atime, mtime, callback) { * @returns {void} */ function futimesSync(fd, atime, mtime) { + atime = toUnixTimestamp(atime, 'atime'); + mtime = toUnixTimestamp(mtime, 'mtime'); + const h = vfsState.handlers; if (h !== null) { - const result = h.futimesSync(fd); + const result = h.futimesSync(fd, atime, mtime); if (result !== undefined) return; } @@ -2777,11 +2775,7 @@ function futimesSync(fd, atime, mtime) { throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'); } - binding.futimes( - fd, - toUnixTimestamp(atime, 'atime'), - toUnixTimestamp(mtime, 'mtime'), - ); + binding.futimes(fd, atime, mtime); } /** @@ -2796,18 +2790,15 @@ function futimesSync(fd, atime, mtime) { function lutimes(path, atime, mtime, callback) { callback = makeCallback(callback); path = getValidatedPath(path); + atime = toUnixTimestamp(atime); + mtime = toUnixTimestamp(mtime); const h = vfsState.handlers; if (h !== null && vfsVoid(h.lutimes(path, atime, mtime), callback)) return; const req = new FSReqCallback(); req.oncomplete = callback; - binding.lutimes( - path, - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - req, - ); + binding.lutimes(path, atime, mtime, req); } /** @@ -2820,6 +2811,8 @@ function lutimes(path, atime, mtime, callback) { */ function lutimesSync(path, atime, mtime) { path = getValidatedPath(path); + atime = toUnixTimestamp(atime); + mtime = toUnixTimestamp(mtime); const h = vfsState.handlers; if (h !== null) { @@ -2827,11 +2820,7 @@ function lutimesSync(path, atime, mtime) { if (result !== undefined) return; } - binding.lutimes( - path, - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - ); + binding.lutimes(path, atime, mtime); } function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) { diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index b27d27e1c4b7..41d5350bea70 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -1725,17 +1725,18 @@ async function readdirRecursiveWithPermissionModel(basePath, options) { } async function readdir(path, options) { - const h = vfsState.handlers; - if (h !== null) { - const promise = h.readdir(path, options); - if (promise !== undefined) return await promise; - } options = getOptions(options); // Make shallow copy to prevent mutating options from affecting results options = copyObject(options); path = getValidatedPath(path); + + const h = vfsState.handlers; + if (h !== null) { + const promise = h.readdir(path, options); + if (promise !== undefined) return await promise; + } if (options.recursive) { return readdirRecursive(path, options); } @@ -2011,6 +2012,8 @@ async function chown(path, uid, gid) { async function utimes(path, atime, mtime) { path = getValidatedPath(path); + atime = toUnixTimestamp(atime); + mtime = toUnixTimestamp(mtime); const h = vfsState.handlers; if (h !== null) { @@ -2019,12 +2022,7 @@ async function utimes(path, atime, mtime) { } return await PromisePrototypeThen( - binding.utimes( - path, - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - kUsePromises, - ), + binding.utimes(path, atime, mtime, kUsePromises), undefined, handleErrorFromBinding, ); @@ -2044,6 +2042,10 @@ async function futimes(handle, atime, mtime) { } async function lutimes(path, atime, mtime) { + path = getValidatedPath(path); + atime = toUnixTimestamp(atime); + mtime = toUnixTimestamp(mtime); + const h = vfsState.handlers; if (h !== null) { const promise = h.lutimes(path, atime, mtime); @@ -2051,12 +2053,7 @@ async function lutimes(path, atime, mtime) { } return await PromisePrototypeThen( - binding.lutimes( - getValidatedPath(path), - toUnixTimestamp(atime), - toUnixTimestamp(mtime), - kUsePromises, - ), + binding.lutimes(path, atime, mtime, kUsePromises), undefined, handleErrorFromBinding, ); diff --git a/lib/internal/vfs/dir.js b/lib/internal/vfs/dir.js index 803aeb404531..3b0a6140b1ee 100644 --- a/lib/internal/vfs/dir.js +++ b/lib/internal/vfs/dir.js @@ -94,10 +94,15 @@ class VirtualDir { this.closeSync(); } } + + async [SymbolAsyncDispose]() { + if (!this.#closed) { + this.closeSync(); + } + } } VirtualDir.prototype[SymbolAsyncIterator] = VirtualDir.prototype.entries; -VirtualDir.prototype[SymbolAsyncDispose] = VirtualDir.prototype.close; module.exports = { VirtualDir, diff --git a/lib/internal/vfs/file_handle.js b/lib/internal/vfs/file_handle.js index a65fe5f99386..4d28abde535e 100644 --- a/lib/internal/vfs/file_handle.js +++ b/lib/internal/vfs/file_handle.js @@ -20,6 +20,8 @@ const { const { createEBADF, } = require('internal/vfs/errors'); +const { stringToFlags, toUnixTimestamp } = require('internal/fs/utils'); +const { parseFileMode } = require('internal/validators'); // Private symbols const kPath = Symbol('kPath'); @@ -29,7 +31,6 @@ const kPosition = Symbol('kPosition'); const kClosed = Symbol('kClosed'); const kAccess = Symbol('kAccess'); -const { stringToFlags } = require('internal/fs/utils'); const { fs: { O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY }, } = internalBinding('constants'); @@ -288,10 +289,17 @@ class VirtualFileHandle { } /** - * No-op chmod - VFS files don't have real permissions. + * @param {number} mode The new permission bits + */ + chmodSync(mode) {} + + /** + * @param {number} mode The new permission bits * @returns {Promise} */ - async chmod() {} + async chmod(mode) { + this.chmodSync(mode); + } /** * No-op chown - VFS files don't have real ownership. @@ -300,10 +308,19 @@ class VirtualFileHandle { async chown() {} /** - * No-op utimes - timestamps are handled by the provider. + * @param {Date|number|string} atime The new access time + * @param {Date|number|string} mtime The new modification time + */ + utimesSync(atime, mtime) {} + + /** + * @param {Date|number|string} atime The new access time + * @param {Date|number|string} mtime The new modification time * @returns {Promise} */ - async utimes() {} + async utimes(atime, mtime) { + this.utimesSync(atime, mtime); + } /** * No-op datasync - VFS is in-memory. @@ -681,6 +698,33 @@ class MemoryFileHandle extends VirtualFileHandle { throw new ERR_INVALID_STATE('stats not available'); } + /** + * @param {number} mode The new permission bits + */ + chmodSync(mode) { + this.#checkClosed('fchmod'); + mode = parseFileMode(mode, 'mode'); + if (this.#entry) { + this.#entry.mode = (this.#entry.mode & ~0o7777) | (mode & 0o7777); + this.#entry.ctime = DateNow(); + } + } + + /** + * @param {Date|number|string} atime The new access time + * @param {Date|number|string} mtime The new modification time + */ + utimesSync(atime, mtime) { + this.#checkClosed('futimes'); + const atimeMs = toUnixTimestamp(atime, 'atime') * 1000; + const mtimeMs = toUnixTimestamp(mtime, 'mtime') * 1000; + if (this.#entry) { + this.#entry.atime = atimeMs; + this.#entry.mtime = mtimeMs; + this.#entry.ctime = DateNow(); + } + } + /** * Gets file stats. * @param {object} [options] Options diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index 574c076c426d..afb5fab3eb73 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -63,6 +63,17 @@ function normalizeMountedPath(inputPath) { return toNamespacedPath(resolvePath(inputPath)); } +const kTempChars = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + +function randomSuffix() { + let suffix = ''; + for (let i = 0; i < 6; i++) { + suffix += kTempChars[(MathRandom() * kTempChars.length) | 0]; + } + return suffix; +} + let registerVFS; let deregisterVFS; @@ -359,7 +370,8 @@ class VirtualFileSystem { */ mkdirSync(dirPath, options) { const providerPath = this.#toProviderPath(dirPath); - return this[kProvider].mkdirSync(providerPath, options); + const created = this[kProvider].mkdirSync(providerPath, options); + return created === undefined ? undefined : this.#toMountedPath(created); } /** @@ -557,17 +569,25 @@ class VirtualFileSystem { * @returns {string} The full path of the created directory */ mkdtempSync(prefix) { - const providerPrefix = this.#toProviderPath(prefix); - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let suffix = ''; - for (let i = 0; i < 6; i++) { - suffix += chars[(MathRandom() * chars.length) | 0]; - } - const dirPath = providerPrefix + suffix; + const dirPath = this.#toProviderPrefix(prefix) + randomSuffix(); this[kProvider].mkdirSync(dirPath); return this.#toMountedPath(dirPath); } + /** + * Converts a mkdtemp prefix to a provider-relative one, keeping a + * trailing separator. + * @param {string} prefix The mounted prefix + * @returns {string} + */ + #toProviderPrefix(prefix) { + const last = prefix[prefix.length - 1]; + const trailing = last === '/' || last === sep; + const providerPrefix = this.#toProviderPath(prefix); + if (!trailing) return providerPrefix; + return providerPrefix === '/' ? '/' : `${providerPrefix}/`; + } + /** * Opens a directory synchronously. * @param {string} dirPath The directory path @@ -1106,6 +1126,7 @@ class VirtualFileSystem { // Arrow functions capture `this` for private method access. const toProviderPath = (p) => this.#toProviderPath(p); + const toProviderPrefix = (p) => this.#toProviderPrefix(p); const toMountedPath = (p) => this.#toMountedPath(p); return ObjectFreeze({ @@ -1141,7 +1162,8 @@ class VirtualFileSystem { async mkdir(dirPath, options) { const providerPath = toProviderPath(dirPath); - return provider.mkdir(providerPath, options); + const created = await provider.mkdir(providerPath, options); + return created === undefined ? undefined : toMountedPath(created); }, async rmdir(dirPath) { @@ -1235,13 +1257,7 @@ class VirtualFileSystem { }, async mkdtemp(prefix) { - const providerPrefix = toProviderPath(prefix); - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let suffix = ''; - for (let i = 0; i < 6; i++) { - suffix += chars[(MathRandom() * chars.length) | 0]; - } - const dirPath = providerPrefix + suffix; + const dirPath = toProviderPrefix(prefix) + randomSuffix(); await provider.mkdir(dirPath); return toMountedPath(dirPath); }, diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 3d624d5af293..a589883bf4ad 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -534,9 +534,17 @@ function createVfsHandlers() { if (vfd) { vfd.entry.truncateSync(len); return true; } return undefined; }, - fchmodSync: noopFdSync, + fchmodSync(fd, mode) { + const vfd = getVirtualFd(fd); + if (vfd) { vfd.entry.chmodSync(mode); return true; } + return undefined; + }, fchownSync: noopFdSync, - futimesSync: noopFdSync, + futimesSync(fd, atime, mtime) { + const vfd = getVirtualFd(fd); + if (vfd) { vfd.entry.utimesSync(atime, mtime); return true; } + return undefined; + }, fdatasyncSync: noopFdSync, fsyncSync: noopFdSync, readvSync(fd, buffers, position) { @@ -593,9 +601,17 @@ function createVfsHandlers() { if (!vfd) return undefined; return vfd.entry.truncate(len).then(() => true); }, - fchmod: noopFd, + fchmod(fd, mode) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.chmod(mode).then(() => true); + }, fchown: noopFd, - futimes: noopFd, + futimes(fd, atime, mtime) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.utimes(atime, mtime).then(() => true); + }, fdatasync: noopFd, fsync: noopFd, @@ -628,10 +644,40 @@ function createVfsHandlers() { const pathStr = toPathStr(filename); if (pathStr !== null) { const r = findVFSForPath(pathStr); - if (r !== null) return r.vfs.watch(pathStr, options, listener); + if (r !== null) { + if (!r.vfs.existsSync(pathStr)) throw createENOENT('watch', pathStr); + return r.vfs.watch(pathStr, options, listener); + } } return undefined; }, + watchFile(filename, options, listener) { + const pathStr = toPathStr(filename); + if (pathStr === null) return undefined; + const r = findVFSForPath(pathStr); + if (r === null) return undefined; + if (options === null || typeof options !== 'object') { + listener = options; + options = kEmptyObject; + } + return r.vfs.watchFile(pathStr, options, listener); + }, + unwatchFile(filename, listener) { + const pathStr = toPathStr(filename); + if (pathStr === null) return undefined; + const r = findVFSForPath(pathStr); + if (r === null) return undefined; + r.vfs.unwatchFile(pathStr, listener); + return true; + }, + promisesWatch(filename, options) { + const pathStr = toPathStr(filename); + if (pathStr === null) return undefined; + const r = findVFSForPath(pathStr); + if (r === null) return undefined; + if (!r.vfs.existsSync(pathStr)) throw createENOENT('watch', pathStr); + return r.vfs.promises.watch(pathStr, options); + }, readdir(path, options) { const promise = vfsOp(path, (vfs, n) => vfs.promises.readdir(n, options)); diff --git a/test/parallel/test-vfs-file-handle.js b/test/parallel/test-vfs-file-handle.js index d9d919446b8e..4b86714fbc53 100644 --- a/test/parallel/test-vfs-file-handle.js +++ b/test/parallel/test-vfs-file-handle.js @@ -42,10 +42,17 @@ myVfs.writeFileSync('/file.txt', 'hello world'); assert.strictEqual(b1.toString(), 'hello'); assert.strictEqual(b2.toString(), ' world'); + // Metadata methods reach the entry the way fchmod(2)/futimes(2) do, and + // validate their arguments the way a FileHandle would. + await handle.chmod(0o600); + assert.strictEqual((await handle.stat()).mode & 0o777, 0o600); + await handle.utimes(1000, 2000); + assert.strictEqual((await handle.stat()).mtimeMs, 2000 * 1000); + await assert.rejects(handle.chmod(), { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects(handle.utimes(), { code: 'ERR_INVALID_ARG_TYPE' }); + // no-op metadata methods - await handle.chmod(); await handle.chown(); - await handle.utimes(); await handle.datasync(); await handle.sync(); diff --git a/test/parallel/test-vfs-fs-matches-real-fs.js b/test/parallel/test-vfs-fs-matches-real-fs.js new file mode 100644 index 000000000000..a500d796599d --- /dev/null +++ b/test/parallel/test-vfs-fs-matches-real-fs.js @@ -0,0 +1,111 @@ +// Flags: --experimental-vfs +'use strict'; + +// `node:fs` entry points route mounted paths through the VFS hooks. Where a +// hook is missing, runs before argument validation, or ignores the +// descriptor form of an operation, the same call behaves differently from a +// real path. Each case states the real-fs outcome as the expectation. Cases +// are independent so the runner reports each one. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); +const { test } = require('node:test'); + +function mount(populate) { + const layer = vfs.create(); + populate?.(layer); + return layer.mount(); +} + +test('watchFile on a mounted path installs a stat watcher', () => { + const file = path.join(mount((l) => l.writeFileSync('/f', 'x')), 'f'); + fs.watchFile(file, { interval: 10 }, common.mustNotCall()); + fs.unwatchFile(file); +}); + +test('fs.promises.watch on a mounted directory yields events', async () => { + const dir = path.join(mount((l) => l.mkdirSync('/d')), 'd'); + const ac = new AbortController(); + const watcher = fs.promises.watch(dir, { signal: ac.signal }); + setTimeout(() => fs.writeFileSync(path.join(dir, 'x'), '1'), 20); + for await (const event of watcher) { + assert.strictEqual(event.filename, 'x'); + ac.abort(); + break; + } +}); + +test('watch on a missing mounted path throws ENOENT', () => { + const dir = mount(); + // Should the call return a watcher instead, it polls forever, so it is + // closed to let the process exit. + let watcher; + try { + assert.throws(() => { watcher = fs.watch(path.join(dir, 'nope')); }, + { code: 'ENOENT' }); + } finally { + watcher?.close(); + } +}); + +test('utimesSync accepts numeric strings as seconds', () => { + const file = path.join(mount((l) => l.writeFileSync('/f', 'x')), 'f'); + fs.utimesSync(file, '1000', '2000'); + assert.strictEqual(fs.statSync(file).mtimeMs, 2000 * 1000); +}); + +test('utimesSync rejects an invalid time argument', () => { + const file = path.join(mount((l) => l.writeFileSync('/f', 'x')), 'f'); + assert.throws(() => fs.utimesSync(file, {}, {}), { code: 'ERR_INVALID_ARG_TYPE' }); +}); + +test('readdirSync rejects an invalid encoding', () => { + const dir = mount(); + assert.throws(() => fs.readdirSync(dir, { encoding: 'nope' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +}); + +test('futimesSync updates the timestamps through a descriptor', () => { + const file = path.join(mount((l) => l.writeFileSync('/f', 'x')), 'f'); + const fd = fs.openSync(file, 'r+'); + try { + fs.futimesSync(fd, 1000, 2000); + } finally { + fs.closeSync(fd); + } + assert.strictEqual(fs.statSync(file).mtimeMs, 2000 * 1000); +}); + +test('fchmodSync changes the mode through a descriptor', () => { + const file = path.join(mount((l) => l.writeFileSync('/f', 'x')), 'f'); + const fd = fs.openSync(file, 'r+'); + try { + fs.fchmodSync(fd, 0o600); + } finally { + fs.closeSync(fd); + } + assert.strictEqual(fs.statSync(file).mode & 0o777, 0o600); +}); + +test('mkdtempSync with a trailing separator creates the directory inside the prefix', () => { + const dir = path.join(mount((l) => l.mkdirSync('/dir')), 'dir'); + const created = fs.mkdtempSync(dir + path.sep); + assert.ok(created.startsWith(dir + path.sep), `${created} is not inside ${dir}`); + assert.strictEqual(fs.statSync(created).isDirectory(), true); +}); + +test('mkdirSync({ recursive: true }) returns the first directory created', () => { + const dir = mount(); + const created = fs.mkdirSync(path.join(dir, 'a', 'b'), { recursive: true }); + assert.strictEqual(created, path.join(dir, 'a')); +}); + +test('a closed Dir can be disposed asynchronously', async () => { + const dir = mount((l) => l.mkdirSync('/d')); + const handle = fs.opendirSync(dir); + handle.closeSync(); + await handle[Symbol.asyncDispose](); +}); From efc612d89180ab3dba02b0d14c0be0b0687f134f Mon Sep 17 00:00:00 2001 From: Abhinandan Kumar <181508976+abhi128nandan@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:13:38 +0530 Subject: [PATCH 20/83] test: cover cpSync fast path timestamp preservation Add test coverage for the native cpSync fast path to ensure it matches the existing timestamp preservation behavior. Signed-off-by: Abhinandan Kumar PR-URL: https://github.com/nodejs/node/pull/65678 Reviewed-By: LiviaMedeiros --- ...est-fs-cp-sync-preserve-timestamps-dir.mjs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs b/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs index 20d889075988..06d1c3521478 100644 --- a/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs +++ b/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs @@ -1,5 +1,6 @@ -// This tests that cpSync with a filter preserves directory timestamps -// when preserveTimestamps is true. +// This tests that cpSync preserves directory timestamps +// when preserveTimestamps is true, both on the JS fallback path (with filter) +// and the native fast path (without filter). import '../common/index.mjs'; import { nextdir } from '../common/fs.js'; import assert from 'node:assert'; @@ -40,3 +41,21 @@ assert.strictEqual(srcDirStat.mtime.getTime(), destDirStat.mtime.getTime()); const srcRootStat = statSync(src); const destRootStat = statSync(dest); assert.strictEqual(srcRootStat.mtime.getTime(), destRootStat.mtime.getTime()); + +// Copy with preserveTimestamps and NO filter (to exercise the native fast path). +const destFast = nextdir(); +cpSync(src, destFast, { + recursive: true, + preserveTimestamps: true, +}); + +// Verify file timestamps are preserved. +const destFastFileStat = statSync(join(destFast, 'subdir', 'file.txt')); +assert.strictEqual(srcFileStat.mtime.getTime(), destFastFileStat.mtime.getTime()); + +// Verify directory timestamps are preserved. +const destFastDirStat = statSync(join(destFast, 'subdir')); +assert.strictEqual(srcDirStat.mtime.getTime(), destFastDirStat.mtime.getTime()); + +const destFastRootStat = statSync(destFast); +assert.strictEqual(srcRootStat.mtime.getTime(), destFastRootStat.mtime.getTime()); From 5c5bd227b0fea6861b1d75d772dac402d79b10da Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 15 Sep 2026 21:11:35 +0200 Subject: [PATCH 21/83] test: prevent parser reuse across close scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both cases replace parser cleanup methods. Faster socket cleanup can return a modified parser to the shared pool and close it before the other request uses it. Run the immediate and deferred close cases in separate test files so each gets its own process and parser pool. Preserve both cleanup paths and all call-count assertions. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66017 Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón Reviewed-By: Luigi Pinca --- ...ver-connection-list-when-close-deferred.js | 35 ++++++++++++++++ ...-http-server-connection-list-when-close.js | 42 +++++-------------- 2 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 test/parallel/test-http-server-connection-list-when-close-deferred.js diff --git a/test/parallel/test-http-server-connection-list-when-close-deferred.js b/test/parallel/test-http-server-connection-list-when-close-deferred.js new file mode 100644 index 000000000000..af87e3561618 --- /dev/null +++ b/test/parallel/test-http-server-connection-list-when-close-deferred.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); + +// Keep this case in a separate process from the immediate-close case so +// their modified parsers cannot be reused across cases. + +function request(server) { + http.get({ + agent: false, + port: server.address().port, + path: '/', + }, (res) => { + res.resume(); + }); +} + +const server = http.createServer(common.mustCallAtLeast((req, res) => { + // See `freeParser` in _http_common.js + const { parser } = req.socket; + parser.free = common.mustCall(() => { + setImmediate(common.mustCall(() => { + parser.close(); + })); + }); + req.socket.on('close', common.mustCall(() => { + setImmediate(common.mustCall(() => { + server.close(); + })); + })); + res.end('ok'); +})).listen(0, common.mustCall(() => { + request(server); +})); diff --git a/test/parallel/test-http-server-connection-list-when-close.js b/test/parallel/test-http-server-connection-list-when-close.js index 0c8308b63c53..305755b14eb3 100644 --- a/test/parallel/test-http-server-connection-list-when-close.js +++ b/test/parallel/test-http-server-connection-list-when-close.js @@ -13,36 +13,14 @@ function request(server) { }); } -{ - const server = http.createServer(common.mustCallAtLeast((req, res) => { - // Hack to not remove parser out of server.connectionList - // See `freeParser` in _http_common.js - req.socket.parser.free = common.mustCall(); - req.socket.on('close', common.mustCall(() => { - server.close(); - })); - res.end('ok'); - })).listen(0, common.mustCall(() => { - request(server); +const server = http.createServer(common.mustCallAtLeast((req, res) => { + // Hack to not remove parser out of server.connectionList + // See `freeParser` in _http_common.js + req.socket.parser.free = common.mustCall(); + req.socket.on('close', common.mustCall(() => { + server.close(); })); -} - -{ - const server = http.createServer(common.mustCallAtLeast((req, res) => { - // See `freeParser` in _http_common.js - const { parser } = req.socket; - parser.free = common.mustCall(() => { - setImmediate(common.mustCall(() => { - parser.close(); - })); - }); - req.socket.on('close', common.mustCall(() => { - setImmediate(common.mustCall(() => { - server.close(); - })); - })); - res.end('ok'); - })).listen(0, common.mustCall(() => { - request(server); - })); -} + res.end('ok'); +})).listen(0, common.mustCall(() => { + request(server); +})); From 18a9ba509ad3bb31209264857f8a9b67b760c56b Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 17 Aug 2026 13:42:18 -0700 Subject: [PATCH 22/83] build, doc: move to redesign Signed-off-by: Aviv Keller PR-URL: https://github.com/nodejs/node/pull/62045 Reviewed-By: James M Snell Reviewed-By: Filip Skokan Reviewed-By: Tim Perry --- .github/dependabot.yml | 3 +- .github/workflows/build-tarball.yml | 5 + Makefile | 25 +- doc/node.1 | 12 +- test/doctool/test-make-doc.mjs | 74 - tools/doc/api-links.doc-kit.config.mjs | 17 + tools/doc/package-lock.json | 1962 ++++++++++++++++-------- tools/doc/package.json | 5 +- tools/doc/web.doc-kit.config.mjs | 53 + vcbuild.bat | 16 +- 10 files changed, 1393 insertions(+), 779 deletions(-) delete mode 100644 test/doctool/test-make-doc.mjs create mode 100644 tools/doc/api-links.doc-kit.config.mjs create mode 100644 tools/doc/web.doc-kit.config.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c63475dc60f5..aa8c3f9a028d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -53,7 +53,8 @@ updates: semver-minor-days: 5 semver-patch-days: 5 exclude: - - '@node-core/doc-kit' + - '@doc-kit/*' + - '@node-core/*' commit-message: prefix: tools open-pull-requests-limit: 10 diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml index 045bd3ae93c2..9091e2165281 100644 --- a/.github/workflows/build-tarball.yml +++ b/.github/workflows/build-tarball.yml @@ -67,6 +67,7 @@ env: PYTHON_VERSION: '3.14' FLAKY_TESTS: keep_retrying CLANG_VERSION: '19' + NODE_VERSION: lts/* permissions: contents: read @@ -84,6 +85,10 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} allow-prereleases: true + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ env.NODE_VERSION }} - name: Make tarball run: | export DISTTYPE=nightly diff --git a/Makefile b/Makefile index 8dec67750f0c..75bed45906f5 100644 --- a/Makefile +++ b/Makefile @@ -389,7 +389,7 @@ ifeq ($(OSTYPE),os400) DOCBUILDSTAMP_PREREQS := $(DOCBUILDSTAMP_PREREQS) out/$(BUILDTYPE)/node.exp endif -DOC_KIT ?= tools/doc/node_modules/@node-core/doc-kit/bin/cli.mjs +DOC_KIT ?= tools/doc/node_modules/@doc-kit/cli/bin/cli.mjs node_use_openssl_and_icu = $(call available-node,"-p" \ "process.versions.openssl != undefined && process.versions.icu != undefined") @@ -858,7 +858,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/llms.txt 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.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. @@ -895,15 +895,10 @@ $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json &: $(a else \ $(call available-node, \ $(DOC_KIT) generate \ - -t legacy-html-all \ - -t legacy-json-all \ - -i doc/api/*.md \ - --ignore $(skip_apidoc_files) \ - -o out/doc/api \ - -c ./CHANGELOG.md \ + --log-level debug \ + --config-file tools/doc/web.doc-kit.config.mjs \ -v $(VERSION) \ - --index doc/api/index.md \ - --type-map doc/type-map.json \ + $(if $(JOBS),-p $(JOBS)) \ ) \ fi endif @@ -914,13 +909,10 @@ out/doc/llms.txt: $(apidoc_sources) tools/doc/node_modules | out/doc else \ $(call available-node, \ $(DOC_KIT) generate \ + --config-file tools/doc/web.doc-kit.config.mjs \ -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 @@ -930,12 +922,9 @@ out/doc/apilinks.json: $(wildcard lib/*.js) tools/doc/node_modules | out/doc else \ $(call available-node, \ $(DOC_KIT) generate \ - -t api-links \ - -i lib/*.js \ + --config-file tools/doc/api-links.doc-kit.config.mjs \ -o $(@D) \ - -c ./CHANGELOG.md \ -v $(VERSION) \ - --type-map doc/type-map.json \ ) \ fi diff --git a/doc/node.1 b/doc/node.1 index 894a69e150b7..f4560cbab90d 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -1,9 +1,9 @@ .\" -.\" This file was generated automatically by the @node-core/doc-kit tool. +.\" This file was generated automatically by Node.js's doc-kit tool. .\" Please do not edit this file manually. Make any updates to cli.md .\" and regenerate the file afterward. .\" -.\" To regenerate this file, run `make doc/node.1`. +.\" To regenerate this file, run `make node.1`. .\" .\"====================================================================== .Dd $Mdocdate$ @@ -236,7 +236,7 @@ Error: connect ERR_ACCESS_DENIED Access to this API has been restricted. Use --a .It Fl -allow-openssl-store When using the Permission Model, the process will not be able to use OpenSSL STORE loaders by default, for example to load a private key from a -\fB\fR passed to \fBcrypto.createPrivateKey()\fR. Attempts to do so will throw +\fB{URL}\fR passed to \fBcrypto.createPrivateKey()\fR. Attempts to do so will throw an \fBERR_ACCESS_DENIED\fR unless the user explicitly passes the \fB--allow-openssl-store\fR flag. This permission can be dropped at runtime via \fBpermission.drop()\fR. @@ -408,11 +408,11 @@ creation behavior. The following options are currently supported: .Bl -bullet .It -\fBbuilder\fR \fB\fR Required. Provides the name to the script that is executed -before building the snapshot, as if \fB--build-snapshot\fR had been passed +\fBbuilder\fR \fB{string}\fR Required. Provides the name to the script that is executed +before building the snapshot, as if \fB--build-snapshot\fR had been passed with \fBbuilder\fR as the main script name. .It -\fBwithoutCodeCache\fR \fB\fR Optional. Including the code cache reduces the +\fBwithoutCodeCache\fR \fB{boolean}\fR Optional. Including the code cache reduces the time spent on compiling functions included in the snapshot at the expense of a bigger snapshot size and potentially breaking portability of the snapshot. diff --git a/test/doctool/test-make-doc.mjs b/test/doctool/test-make-doc.mjs deleted file mode 100644 index 59e681707dd4..000000000000 --- a/test/doctool/test-make-doc.mjs +++ /dev/null @@ -1,74 +0,0 @@ -import * as common from '../common/index.mjs'; - -import assert from 'assert'; -import fs from 'fs'; -import path from 'path'; - -if (common.isWindows) { - common.skip('`make doc` does not run on Windows'); -} - -// This tests that `make doc` generates the documentation properly. -// Note that for this test to pass, `make doc` must be run first. - -const apiURL = new URL('../../out/doc/api/', import.meta.url); -const mdURL = new URL('../../doc/api/', import.meta.url); -const allMD = fs.readdirSync(mdURL); -const allDocs = fs.readdirSync(apiURL); -assert.ok(allDocs.includes('index.html')); - -const actualDocs = allDocs.filter( - (name) => { - const extension = path.extname(name); - return extension === '.html' || extension === '.json'; - }, -); - -for (const name of actualDocs) { - if (name.startsWith('all.') || name === 'apilinks.json') continue; - - assert.ok( - allMD.includes(name.replace(/\.\w+$/, '.md')), - `Unexpected output: out/doc/api/${name}, remove and rerun.`, - ); -} - -const toc = fs.readFileSync(new URL('./index.html', apiURL), 'utf8'); -const re = /href=("([^/]+\.html)"|([^/]+\.html))/; -const globalRe = new RegExp(re, 'g'); -const links = toc.match(globalRe); -assert.notStrictEqual(links, null); - -// Filter out duplicate links, leave just filenames, add expected JSON files. -const linkedHtmls = [...new Set(links)].map((link) => link.match(re)[1]) - .concat(['index.html']); -const expectedJsons = linkedHtmls - .map((name) => name.replace('.html', '.json')); -const expectedDocs = linkedHtmls.concat(expectedJsons); -const renamedDocs = ['policy.json', 'policy.html']; -const skipedDocs = ['dtls.json', 'dtls.html', 'quic.json', 'quic.html']; - -// Test that all the relative links in the TOC match to the actual documents. -for (const expectedDoc of expectedDocs) { - if (skipedDocs.includes(expectedDoc)) continue; - assert.ok(actualDocs.includes(expectedDoc), `${expectedDoc} does not exist`); -} - -// Test that all the actual documents match to the relative links in the TOC -// and that they are not empty files. -for (const actualDoc of actualDocs) { - // When renaming the documentation, the old url is lost - // Unless the old file is still available pointing to the correct location - // 301 redirects are not yet automated. So keeping the old URL is a - // reasonable workaround. - if (renamedDocs.includes(actualDoc) || skipedDocs.includes(actualDoc) || - actualDoc === 'apilinks.json') continue; - assert.ok( - expectedDocs.includes(actualDoc), `${actualDoc} does not match TOC`); - - assert.notStrictEqual( - fs.statSync(new URL(`./${actualDoc}`, apiURL)).size, - 0, - `${actualDoc} is empty`, - ); -} diff --git a/tools/doc/api-links.doc-kit.config.mjs b/tools/doc/api-links.doc-kit.config.mjs new file mode 100644 index 000000000000..5039baceea42 --- /dev/null +++ b/tools/doc/api-links.doc-kit.config.mjs @@ -0,0 +1,17 @@ +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const fromRoot = (path) => + pathToFileURL(join(import.meta.dirname, '..', '..', path)).href; + +export default { + extends: '@node-core/doc-kit/config', + + target: ['api-links'], + + global: { + input: ['lib/*.js'], + + changelog: fromRoot('CHANGELOG.md'), + }, +}; diff --git a/tools/doc/package-lock.json b/tools/doc/package-lock.json index 20912317efc8..5959e5398cdb 100644 --- a/tools/doc/package-lock.json +++ b/tools/doc/package-lock.json @@ -6,7 +6,20 @@ "": { "name": "doc", "dependencies": { - "@node-core/doc-kit": "1.4.1" + "@doc-kit/cli": "1.0.2", + "@doc-kit/generator-react": "0.3.0", + "@node-core/doc-kit": "2.0.2", + "@node-core/doc-kit-legacy": "1.0.2" + } + }, + "node_modules/@11ty/is-land": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@11ty/is-land/-/is-land-5.0.1.tgz", + "integrity": "sha512-Rh/sLhE4vrc2JaSjeY385v2UxnDY9BhnQtitETb3SKyr0A48Q5Vn06q2AvDBHObtk9+dcFWsoZX4jhT+O9g+xQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" } }, "node_modules/@actions/core": { @@ -44,63 +57,156 @@ "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "license": "MIT" }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@doc-kit/cli": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@doc-kit/cli/-/cli-1.0.2.tgz", + "integrity": "sha512-O++n6R2+N1WfWXRdJsLDBXWBnDTBvQBH9RRzQ99fYeD/xsANHPp8SrJ82sa+zWAPVL8+xhIeDNecLQKiPzsRnA==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@doc-kit/core": "1.1.0", + "commander": "^15.0.0" + }, + "bin": { + "doc-kit": "bin/cli.mjs" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "node_modules/@doc-kit/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@doc-kit/core/-/core-1.1.0.tgz", + "integrity": "sha512-r+4do3F7wxQ9OHvIhEbr4sZTqKWWWa9EAV7OTIWRvpICmhe8LSPCmPhzcE3fv2kxCiG3yE82iLGh9bDFFa0Gmg==", + "license": "MIT", + "dependencies": { + "@actions/core": "^3.0.0", + "@node-core/rehype-shiki": "^1.5.0", + "@swc/html-wasm": "^1.15.46", + "@swc/wasm": "^1.15.46", + "acorn": "^8.17.0", + "cosmiconfig": "^9.0.2", + "dedent": "^1.7.2", + "github-slugger": "^2.0.0", + "glob-parent": "^6.0.2", + "hastscript": "^9.0.1", + "piscina": "^5.3.1", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-mdx": "^3.1.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-stringify": "^11.0.0", + "semver": "^7.8.5", + "shiki": "^4.4.3", + "tinyglobby": "^0.2.17", + "unified": "^11.0.5", + "unist-builder": "^4.0.0", + "unist-util-find-after": "^5.0.0", + "unist-util-position": "^5.0.0", + "unist-util-remove": "^4.0.0", + "unist-util-select": "^5.1.0", + "unist-util-visit": "^5.1.0", + "yaml": "^2.9.0" + }, + "peerDependencies": { + "@doc-kit/generator-react": ">=0.1.0", + "@node-core/doc-kit": ">=2.0.0", + "@node-core/doc-kit-legacy": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@doc-kit/generator-react": { + "optional": true + }, + "@node-core/doc-kit": { + "optional": true + }, + "@node-core/doc-kit-legacy": { + "optional": true + } + } + }, + "node_modules/@doc-kit/generator-react": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@doc-kit/generator-react/-/generator-react-0.3.0.tgz", + "integrity": "sha512-Tmyt7Y0/a20V/B61/jJk5yaJoyKkill9vN8DETTyc4N4oz6igW9MLseJpuIAdhk2WAXtwmkqMXmYNr82yGb2vg==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@11ty/is-land": "^5.0.1", + "@doc-kit/core": "1.1.0", + "@fontsource-variable/open-sans": "^5.3.0", + "@fontsource/ibm-plex-mono": "^5.3.0", + "@heroicons/react": "^2.2.0", + "@node-core/rehype-shiki": "^1.5.0", + "@node-core/ui-components": "^1.7.6", + "@orama/orama": "^3.1.18", + "@orama/ui": "^1.5.4", + "estree-util-to-js": "^2.0.0", + "github-slugger": "^2.0.0", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "mdast-util-slice-markdown": "^2.0.1", + "preact": "^10.29.7", + "preact-render-to-string": "^6.7.0", + "reading-time": "^1.5.0", + "recma-jsx": "^1.0.1", + "recma-stringify": "^1.0.0", + "rehype-raw": "^7.0.0", + "rehype-recma": "^1.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "semver": "^7.8.5", + "unified": "^11.0.5", + "unist-builder": "^4.0.0", + "unist-util-visit": "^5.1.0", + "vite": "~8.2.2" } }, "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.6" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -108,11 +214,29 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@fontsource-variable/open-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/open-sans/-/open-sans-5.3.0.tgz", + "integrity": "sha512-VOfhZfLIOBVa6K/dTEu7scqQXYdGa4jBX8quFpQ0S6574B+W2EOBJf9PkqyVtMEI7WWIWNEbpvPc3vY1mN2mmQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", + "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@heroicons/react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", @@ -122,12 +246,6 @@ "react": ">= 16 || ^19.0.0-rc" } }, - "node_modules/@minify-html/wasm": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@minify-html/wasm/-/wasm-0.18.1.tgz", - "integrity": "sha512-GBkBOJxe7duO+z2b00SP83EewOI+Qm4MsnajXHw4yT7/J+TuG3jLEatBHKnT59Zq4CgXBRpdkv/2hlCGnyqAzg==", - "license": "MIT" - }, "node_modules/@napi-rs/nice": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", @@ -454,24 +572,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -486,72 +586,41 @@ } }, "node_modules/@node-core/doc-kit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@node-core/doc-kit/-/doc-kit-1.4.1.tgz", - "integrity": "sha512-4S9cIGW6+3U3wWsCHRABJg5rjkTLU90Mdyb9v53ljf622A9u8sGQgSzVH9FgQHcKw5z3ofK25h12E/eDGi5SVA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-core/doc-kit/-/doc-kit-2.0.2.tgz", + "integrity": "sha512-ZzWQOqIGqa25wW63aoAJtHL8wMuCUf6bdXiNmmO9J3EspeUtvFHqCdmZXeff5I57WPKpd9hbtAax/9EyJABlIA==", + "license": "MIT", "dependencies": { - "@actions/core": "^3.0.0", - "@heroicons/react": "^2.2.0", - "@minify-html/wasm": "^0.18.1", - "@node-core/rehype-shiki": "^1.4.1", - "@node-core/ui-components": "^1.7.0", - "@orama/orama": "^3.1.18", - "@orama/ui": "^1.5.4", - "@rollup/plugin-virtual": "^3.0.2", - "@swc/html-wasm": "^1.15.40", - "acorn": "^8.16.0", - "commander": "^14.0.3", + "@doc-kit/core": "1.1.0", "dedent": "^1.7.2", - "estree-util-to-js": "^2.0.0", "estree-util-visit": "^2.0.0", - "github-slugger": "^2.0.0", - "glob-parent": "^6.0.2", - "hast-util-to-string": "^3.0.1", + "unist-util-visit": "^5.1.0" + } + }, + "node_modules/@node-core/doc-kit-legacy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@node-core/doc-kit-legacy/-/doc-kit-legacy-1.0.2.tgz", + "integrity": "sha512-vtYy/Qz9Jky2yxQ767aOC7/hNEByzEAezDbvNInfros7bYVLftkNAU3SWF5P4FE1aLlF4iAEAy3tR2nkU9IzuQ==", + "license": "MIT", + "dependencies": { + "@doc-kit/core": "1.1.0", "hastscript": "^9.0.1", - "lightningcss-wasm": "^1.32.0", - "mdast-util-slice-markdown": "^2.0.1", - "piscina": "^5.2.0", - "preact": "^10.29.2", - "preact-render-to-string": "^6.7.0", - "reading-time": "^1.5.0", - "recma-jsx": "^1.0.1", - "rehype-raw": "^7.0.0", - "rehype-recma": "^1.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-mdx": "^3.1.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-stringify": "^11.0.0", - "rolldown": "1.0.2", - "semver": "^7.8.0", - "shiki": "^4.1.0", - "tinyglobby": "^0.2.15", - "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find-after": "^5.0.0", - "unist-util-position": "^5.0.0", - "unist-util-remove": "^4.0.0", - "unist-util-select": "^5.1.0", - "unist-util-visit": "^5.1.0", - "yaml": "^2.9.0" - }, - "bin": { - "doc-kit": "bin/cli.mjs" + "unist-util-visit": "^5.1.0" } }, "node_modules/@node-core/rehype-shiki": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@node-core/rehype-shiki/-/rehype-shiki-1.4.2.tgz", - "integrity": "sha512-H2T4bi9EZg0s0HLlcdANXzozrxeCfXzhZvbqUEygkggrv5pzJX9MVFU5K0/zGJfiqhzUu6TXMrNgpLqX1pylyw==", - "dependencies": { - "@shikijs/core": "^4.0.2", - "@shikijs/engine-javascript": "^4.0.2", - "@shikijs/engine-oniguruma": "^4.0.2", - "@shikijs/twoslash": "^4.0.2", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@node-core/rehype-shiki/-/rehype-shiki-1.5.0.tgz", + "integrity": "sha512-G/NtRi2NPveDc6fXgNvSbqq92lL4GKZmilmwouQL+fIHcOzuicwJQPP1tedYmUnhexO7PykYA0ckxHD3BOl5IQ==", + "dependencies": { + "@shikijs/core": "^4.3.1", + "@shikijs/engine-javascript": "^4.3.1", + "@shikijs/engine-oniguruma": "^4.3.1", + "@shikijs/twoslash": "^4.3.1", "classnames": "~2.5.1", "hast-util-to-string": "^3.0.1", - "shiki": "~4.0.2", + "shiki": "~4.3.1", "typescript": "5.9.3", "unist-util-visit": "^5.1.0" }, @@ -560,13 +629,13 @@ } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", - "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz", + "integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==", "license": "MIT", "dependencies": { - "@shikijs/primitive": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/primitive": "4.3.1", + "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" @@ -576,26 +645,26 @@ } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/engine-javascript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", - "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz", + "integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "oniguruma-to-es": "^4.3.6" }, "engines": { "node": ">=20" } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/engine-oniguruma": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", - "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz", + "integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -603,24 +672,24 @@ } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/langs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", - "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz", + "integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.3.1" }, "engines": { "node": ">=20" } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/primitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", - "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz", + "integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -629,21 +698,21 @@ } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/themes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", - "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz", + "integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.3.1" }, "engines": { "node": ">=20" } }, "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz", + "integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -654,17 +723,17 @@ } }, "node_modules/@node-core/rehype-shiki/node_modules/shiki": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", - "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz", + "integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.0.2", - "@shikijs/engine-javascript": "4.0.2", - "@shikijs/engine-oniguruma": "4.0.2", - "@shikijs/langs": "4.0.2", - "@shikijs/themes": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/core": "4.3.1", + "@shikijs/engine-javascript": "4.3.1", + "@shikijs/engine-oniguruma": "4.3.1", + "@shikijs/langs": "4.3.1", + "@shikijs/themes": "4.3.1", + "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -673,24 +742,24 @@ } }, "node_modules/@node-core/ui-components": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@node-core/ui-components/-/ui-components-1.7.1.tgz", - "integrity": "sha512-i1un8y0yth6WdT0OXpAQeL8XIkohcZ2P8khSC0oQstWG8H7PCRqX+mdZlJAuVUKq3MaQnC2fO34Q3j7L4TenXg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@node-core/ui-components/-/ui-components-1.7.6.tgz", + "integrity": "sha512-4HUQKbcSAAXF/wBVHdfGrf0FW2krKfdtVDMRWWR/cELEEgVWV5TLBVqvYA9+NbiOnNvPkpTgBlMjjCJpVh7tAg==", "dependencies": { "@heroicons/react": "^2.2.0", "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", - "@radix-ui/react-avatar": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-avatar": "^1.2.3", + "@radix-ui/react-dialog": "^1.1.20", + "@radix-ui/react-dropdown-menu": "^2.1.21", + "@radix-ui/react-label": "^2.1.12", + "@radix-ui/react-select": "^2.3.4", + "@radix-ui/react-separator": "^1.1.12", + "@radix-ui/react-tabs": "^1.1.18", + "@radix-ui/react-tooltip": "^1.2.13", "@vcarl/remark-headings": "~0.1.0", "classnames": "~2.5.1", - "react": "^19.2.6" + "react": "^19.2.8" }, "engines": { "node": ">=20" @@ -759,33 +828,33 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@radix-ui/number": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", - "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", - "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.11.tgz", - "integrity": "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -803,16 +872,17 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.1.tgz", - "integrity": "sha512-+8PWoLLZv3AVb5m0pvoiOca/bQGzc9vPVb+982HB2x3Un0DpYEPM3zLMl4oqRpBsocJuNqLkiv/HXTnTrlwr4g==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -830,15 +900,15 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.11.tgz", - "integrity": "sha512-djW9+zeg137KQdlPtmE8xnaD+K2rcXXMWFrSg0hsmYZ6HRbdTA7tDHFgpaW9+huWVEu0RCabL+985T4TA0BE7g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -856,9 +926,9 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -871,9 +941,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", - "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -886,23 +956,24 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.18.tgz", - "integrity": "sha512-apa28mldjMgORmE6g/w3sCcA0Y9UAVeeDVoozN4i7kOw12mLl9RBchfzK3Nn6qxOWjrZhK1Lfy7f07kyzxtnBw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -922,9 +993,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -937,16 +1008,16 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.14.tgz", - "integrity": "sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-effect-event": "0.0.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", @@ -964,18 +1035,18 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.19.tgz", - "integrity": "sha512-HZccBkbK0LOi8nYKIp5jll/zIRW0cCOmG6WWyqsSpmXCU+ZlcBbTqIwlBvPCu886C5RVu6c/kHV7xSP8IgYNHw==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.19", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -993,9 +1064,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", - "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1008,14 +1079,14 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.11.tgz", - "integrity": "sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1033,12 +1104,12 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1051,12 +1122,12 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.11.tgz", - "integrity": "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -1074,27 +1145,27 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.19.tgz", - "integrity": "sha512-Mht9BVd1AIsNFVQr4KG3bIK7XQn5IXF0TL/2ObsrzOdc1loaly/+kBDL5roSCYn8j8XZkvpOD0WYLz2FQtH1Eg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -1114,21 +1185,21 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.2.tgz", - "integrity": "sha512-3QXNeMkdshed1MR3LNoiCirBywRFPkD8ETJa/HlPuLwSajaQixf2ro+isoDNJlGABg9ug41XuZpINZJIle4XWg==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-rect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/rect": "1.1.2" + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -1146,13 +1217,13 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz", - "integrity": "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1170,12 +1241,12 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", - "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1193,12 +1264,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -1216,20 +1287,22 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.14.tgz", - "integrity": "sha512-8Qcnx9447tx/aCBgw6Jenfqg4Skq+vqab9mCBmuGNipIS5YXvL275wbKEu7+ICYHIlAPgCduUMJH1XOYewKF6Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3" + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1247,31 +1320,31 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.2.tgz", - "integrity": "sha512-brXD6C/V0fVK0DDbscLVw6LsXrjQ+ay8jdOBaN+tLb4vsHsAMm6Gt6eT77wHX1Eq8GPtD5rJ+RxFtfDozsb4+Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.11", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, @@ -1291,12 +1364,12 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", - "integrity": "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -1314,12 +1387,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -1332,19 +1405,19 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.16.tgz", - "integrity": "sha512-v3Ab2l7z6U7tRB4xA0IyKdq0OsqaO1o9ZjsIEoKKnSZ/l96mZz8aCTX0NCXw+YVHJXr8Km4d+Mn6/Q8YjXa+gw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -1362,23 +1435,24 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.11.tgz", - "integrity": "sha512-8XZ6Py3y3W2nEzAUGCN5cfVKaUi+CVApcz1d6lrNVVf2hvYEixMRkq8k9ggPKnQUpRRuOV5avt8uvxViH2jLwA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.14", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-visually-hidden": "1.2.7" + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", @@ -1396,9 +1470,9 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1411,13 +1485,14 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1430,12 +1505,12 @@ } }, "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1448,9 +1523,9 @@ } }, "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1463,9 +1538,9 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1478,9 +1553,9 @@ } }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", - "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1493,12 +1568,12 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", - "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.2" + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -1511,12 +1586,12 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", - "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1529,12 +1604,12 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.7.tgz", - "integrity": "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -1552,15 +1627,31 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", - "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", "cpu": [ "arm64" ], @@ -1574,9 +1665,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", "cpu": [ "arm64" ], @@ -1590,9 +1681,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", "cpu": [ "x64" ], @@ -1606,9 +1697,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", "cpu": [ "x64" ], @@ -1622,9 +1713,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", "cpu": [ "arm" ], @@ -1638,9 +1729,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", "cpu": [ "arm64" ], @@ -1657,9 +1748,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", "cpu": [ "arm64" ], @@ -1676,9 +1767,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", "cpu": [ "ppc64" ], @@ -1695,9 +1786,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", "cpu": [ "s390x" ], @@ -1714,9 +1805,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", "cpu": [ "x64" ], @@ -1733,9 +1824,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", "cpu": [ "x64" ], @@ -1752,9 +1843,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", "cpu": [ "arm64" ], @@ -1767,28 +1858,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", "cpu": [ "arm64" ], @@ -1802,9 +1875,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", "cpu": [ "x64" ], @@ -1823,46 +1896,29 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, - "node_modules/@rollup/plugin-virtual": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", - "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@shikijs/core": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz", - "integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==", - "license": "MIT", - "dependencies": { - "@shikijs/primitive": "4.3.1", - "@shikijs/types": "4.3.1", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/engine-javascript": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz", - "integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.3.1", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" }, @@ -1871,12 +1927,12 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz", - "integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.3.1", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -1884,51 +1940,51 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz", - "integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.3.1" + "@shikijs/types": "4.4.3" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz", - "integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.3.1", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz", - "integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.3.1" + "@shikijs/types": "4.4.3" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/twoslash": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/twoslash/-/twoslash-4.3.1.tgz", - "integrity": "sha512-xK8inH/gK++1V4rTxrwCwjvaNwkkJ7oDjOIpdqONVxIpAFnVC3gzqjH5KiXGTelUcxpUJ3PtOKWct1YQ0kAloA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/twoslash/-/twoslash-4.4.3.tgz", + "integrity": "sha512-m7HNzunEIHRk1jCya3ngGsO3+8pYxrPIIxtdJewg/W8ceW/+m/mSsm4jM3L9DvYYNa8Rvbu7Dabt3BOpCclz8Q==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.3.1", - "@shikijs/types": "4.3.1", + "@shikijs/core": "4.4.3", + "@shikijs/types": "4.4.3", "twoslash": "^0.3.9" }, "engines": { @@ -1939,13 +1995,13 @@ } }, "node_modules/@shikijs/types": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz", - "integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" @@ -1958,20 +2014,16 @@ "license": "MIT" }, "node_modules/@swc/html-wasm": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.43.tgz", - "integrity": "sha512-tw1DpLmh54BDNPDBY/tHLDFcLnxzji242Fu/8Y8nrbSBdDndO0Lv56g869mRgrsmZBvsoS31JqgDkf6YYtvi3g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.16.2.tgz", + "integrity": "sha512-sMOfqcr1r4h+SgDxEzXd/bEV17zxWM8FwIjWDJm4vbooK3YISFFvk9m8Na+KUYJ+hNwUBPNM2zN0uXFJGf/hRQ==", "license": "Apache-2.0" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "node_modules/@swc/wasm": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.16.2.tgz", + "integrity": "sha512-X7Y8nRO6YrDx/2Q3ERTVpRDtxuuw+zlzMxa7VYIHt8P1S0hBT9CYit5b85BwdwGoLtOe3zpLvRSFVyXSCdW9rQ==", + "license": "Apache-2.0" }, "node_modules/@types/debug": { "version": "4.1.13", @@ -1998,9 +2050,9 @@ } }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -2022,9 +2074,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "license": "MIT", "peer": true, "dependencies": { @@ -2050,9 +2102,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", - "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "license": "ISC" }, "node_modules/@vcarl/remark-headings": { @@ -2114,9 +2166,9 @@ } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2134,6 +2186,12 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -2171,6 +2229,15 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2238,12 +2305,38 @@ } }, "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.12.0" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/css-selector-parser": { @@ -2322,6 +2415,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -2353,6 +2455,24 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -2472,6 +2592,20 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -2717,6 +2851,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -2729,88 +2879,370 @@ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" + "node": ">= 12.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-wasm": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-wasm/-/lightningcss-wasm-1.32.0.tgz", - "integrity": "sha512-SteAkCtRuSCDYPGHKhLV/dDs5Bk+7I4QUxWxfk4xwsTI1rQk8MQyYtpGcd3NECsUGzK0q2/KqoVS+YHCqKHUTQ==", - "bundleDependencies": [ - "napi-wasm" + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" ], "license": "MPL-2.0", - "dependencies": { - "napi-wasm": "^1.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">= 12.0.0" }, @@ -2819,9 +3251,10 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-wasm/node_modules/napi-wasm": { - "version": "1.1.3", - "inBundle": true, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/longest-streak": { @@ -3901,6 +4334,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -3930,6 +4381,18 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -3955,6 +4418,24 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -3967,10 +4448,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", "engines": { "node": ">=12" @@ -3980,9 +4467,9 @@ } }, "node_modules/piscina": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", - "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.3.2.tgz", + "integrity": "sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==", "license": "MIT", "engines": { "node": ">=20.x" @@ -3991,14 +4478,50 @@ "@napi-rs/nice": "^1.0.4" } }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/preact": { - "version": "10.29.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.4.tgz", - "integrity": "sha512-GMpwh9+NJ8tSmqwIaVyFRQkiKfBEzQ+k7r7tle4W+kaJ+7wJiB9hFz9BixAomMtenPPSBfM4bZhXozGxhf0uFQ==", + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } } }, "node_modules/preact-render-to-string": { @@ -4030,25 +4553,25 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "license": "MIT", "peer": true, "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.3.0" } }, "node_modules/react-markdown": { @@ -4354,13 +4877,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4370,27 +4902,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" } }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT", "peer": true }, @@ -4407,19 +4939,19 @@ } }, "node_modules/shiki": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz", - "integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.3.1", - "@shikijs/engine-javascript": "4.3.1", - "@shikijs/engine-oniguruma": "4.3.1", - "@shikijs/langs": "4.3.1", - "@shikijs/themes": "4.3.1", - "@shikijs/types": "4.3.1", + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" @@ -4434,6 +4966,15 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -4572,9 +5113,9 @@ } }, "node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -4824,6 +5365,83 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/tools/doc/package.json b/tools/doc/package.json index 9d59db21ef0e..06362c13693d 100644 --- a/tools/doc/package.json +++ b/tools/doc/package.json @@ -2,6 +2,9 @@ "name": "doc", "private": true, "dependencies": { - "@node-core/doc-kit": "1.4.1" + "@doc-kit/cli": "1.0.2", + "@doc-kit/generator-react": "0.3.0", + "@node-core/doc-kit": "2.0.2", + "@node-core/doc-kit-legacy": "1.0.2" } } diff --git a/tools/doc/web.doc-kit.config.mjs b/tools/doc/web.doc-kit.config.mjs new file mode 100644 index 000000000000..ad8a9ee21be0 --- /dev/null +++ b/tools/doc/web.doc-kit.config.mjs @@ -0,0 +1,53 @@ +import { createRequire } from 'node:module'; +import { totalmem } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const require = createRequire(import.meta.url); + +// Gate HTML generation for high-memory machines +// +// TODO(@avivkeller): Lower the amount of memory +// we use. +const hasEnoughMemory = totalmem() > 5 * (1024 ** 3); + +// The HTML generator bundles its CSS with Lightning CSS, which ships as a +// native binding that is not available on every platform we build on. +// Probe for it up front and skip HTML generation when it cannot be loaded, +// rather than failing partway through the build. +// +// TODO(@avivkeller): Fall back to WASM on machines +// without native implementations of our dependencies +const canLoadLightningCSS = () => { + try { + require('lightningcss'); + return true; + } catch (error) { + console.warn(`Skipping HTML generation: unable to load lightningcss (${error.message.split('\n')[0]})`); + return false; + } +}; + +const fromRoot = (path) => + pathToFileURL(join(import.meta.dirname, '..', '..', path)).href; + +export default { + extends: '@node-core/doc-kit/config', + + target: [ + 'legacy-json-all', + hasEnoughMemory && canLoadLightningCSS() && 'section-pages', + ].filter(Boolean), + + global: { + input: ['doc/api/*.md'], + ignore: ['doc/api/quic.md'], + output: 'out/doc/api', + + changelog: fromRoot('CHANGELOG.md'), + }, + + metadata: { + typeMap: fromRoot('doc/type-map.json'), + }, +}; diff --git a/vcbuild.bat b/vcbuild.bat index 681c92d285fb..9f6a4884dc86 100644 --- a/vcbuild.bat +++ b/vcbuild.bat @@ -229,7 +229,7 @@ if defined package set stage_package=1 set "node_exe=%config%\node.exe" set "node_gyp_exe="%node_exe%" deps\npm\node_modules\node-gyp\bin\node-gyp" set "npm_exe="%~dp0%node_exe%" %~dp0deps\npm\bin\npm-cli.js" -set "doc_kit_exe="%~dp0%node_exe%" %~dp0tools\doc\node_modules\@node-core\doc-kit\bin\cli.mjs" +set "doc_kit_exe="%~dp0%node_exe%" %~dp0tools\doc\node_modules\@doc-kit\cli\bin\cli.mjs" if "%target_env%"=="vs2022" set "node_gyp_exe=%node_gyp_exe% --msvs_version=2022" if "%target_env%"=="vs2026" set "node_gyp_exe=%node_gyp_exe% --msvs_version=2026" @@ -701,13 +701,15 @@ robocopy /e doc\api %config%\doc\api %doc_kit_exe% ^ generate ^ - -t legacy-html-all legacy-json-all api-links ^ - -i doc/api/*.md ^ - -i lib/*.js ^ + --config-file "%~dp0tools\doc\web.doc-kit.config.mjs" ^ -o %config%/doc/api/ ^ - -c file://%~dp0\CHANGELOG.md ^ - -v %NODE_VERSION% ^ - --type-map "file://%~dp0doc\type-map.json" + -v %NODE_VERSION% + +%doc_kit_exe% ^ + generate ^ + --config-file "%~dp0tools\doc\api-links.doc-kit.config.mjs" ^ + -o %config%/doc/api/ ^ + -v %NODE_VERSION% :run @rem Run tests if requested. From bd2b98f549b1062c3a779834f49bcbaa27601911 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 23 Aug 2026 01:02:22 +0000 Subject: [PATCH 23/83] lib,src: apply multiple updates to dtls implementation Signed-off-by: James M Snell Assisted-by: Opecode PR-URL: https://github.com/nodejs/node/pull/65511 Reviewed-By: Matteo Collina Reviewed-By: Filip Skokan --- doc/api/dtls.md | 822 ++++++++++++++-- lib/dtls.js | 4 + lib/internal/dtls/dtls.js | 877 ++++++++++++++++-- lib/internal/dtls/state.js | 14 + lib/internal/dtls/stats.js | 18 + lib/internal/dtls/symbols.js | 6 + src/dtls/dtls.cc | 1 + src/dtls/dtls.h | 6 +- src/dtls/dtls_context.cc | 715 +++++++++++++- src/dtls/dtls_context.h | 75 +- src/dtls/dtls_endpoint.cc | 322 ++++++- src/dtls/dtls_endpoint.h | 69 +- src/dtls/dtls_session.cc | 788 +++++++++++++--- src/dtls/dtls_session.h | 123 ++- src/node_sockaddr.cc | 29 +- src/node_sockaddr.h | 15 + test/cctest/test_environment.cc | 15 +- test/cctest/test_sockaddr.cc | 87 ++ test/parallel/test-dtls-accessors.mjs | 63 +- test/parallel/test-dtls-alpn-validation.mjs | 87 ++ test/parallel/test-dtls-alpn.mjs | 67 +- .../test-dtls-authorized-before-handshake.mjs | 121 +++ test/parallel/test-dtls-authorized.mjs | 100 ++ test/parallel/test-dtls-default-ca.mjs | 9 +- test/parallel/test-dtls-error-queue.mjs | 84 ++ test/parallel/test-dtls-errors.mjs | 25 +- ...dtls-export-keying-material-validation.mjs | 90 ++ test/parallel/test-dtls-handshake-timeout.mjs | 212 +++++ test/parallel/test-dtls-ipv6.mjs | 98 ++ test/parallel/test-dtls-ipv6only.mjs | 77 ++ test/parallel/test-dtls-keylog.mjs | 81 +- test/parallel/test-dtls-max-sessions.mjs | 137 +++ .../test-dtls-message-listener-gate.mjs | 84 ++ .../parallel/test-dtls-mtu-record-framing.mjs | 89 ++ test/parallel/test-dtls-opened-settles.mjs | 190 ++++ test/parallel/test-dtls-options.mjs | 161 +++- test/parallel/test-dtls-passphrase.mjs | 140 +++ .../test-dtls-peer-x509-certificate.mjs | 163 ++++ test/parallel/test-dtls-psk.mjs | 468 ++++++++++ test/parallel/test-dtls-request-cert.mjs | 141 +++ ...st-dtls-resumption-reject-unauthorized.mjs | 129 +++ test/parallel/test-dtls-robustness.mjs | 47 +- test/parallel/test-dtls-secure-context.mjs | 182 ++++ test/parallel/test-dtls-send-error.mjs | 87 ++ test/parallel/test-dtls-send.mjs | 120 ++- .../test-dtls-server-handshake-error.mjs | 166 ++++ .../test-dtls-servername-validation.mjs | 83 ++ .../parallel/test-dtls-session-id-context.mjs | 71 ++ .../parallel/test-dtls-session-properties.mjs | 2 +- .../parallel/test-dtls-session-resumption.mjs | 280 ++++++ .../test-dtls-session-table-cleanup.mjs | 16 +- test/parallel/test-dtls-sni-binding.mjs | 109 +++ test/parallel/test-dtls-sni.mjs | 459 +++++++++ test/parallel/test-dtls-socket-options.mjs | 110 +++ test/parallel/test-dtls-stats.mjs | 52 ++ test/parallel/test-permission-net-dtls.mjs | 9 +- 56 files changed, 8194 insertions(+), 401 deletions(-) create mode 100644 test/parallel/test-dtls-alpn-validation.mjs create mode 100644 test/parallel/test-dtls-authorized-before-handshake.mjs create mode 100644 test/parallel/test-dtls-authorized.mjs create mode 100644 test/parallel/test-dtls-error-queue.mjs create mode 100644 test/parallel/test-dtls-export-keying-material-validation.mjs create mode 100644 test/parallel/test-dtls-handshake-timeout.mjs create mode 100644 test/parallel/test-dtls-ipv6.mjs create mode 100644 test/parallel/test-dtls-ipv6only.mjs create mode 100644 test/parallel/test-dtls-max-sessions.mjs create mode 100644 test/parallel/test-dtls-message-listener-gate.mjs create mode 100644 test/parallel/test-dtls-mtu-record-framing.mjs create mode 100644 test/parallel/test-dtls-opened-settles.mjs create mode 100644 test/parallel/test-dtls-passphrase.mjs create mode 100644 test/parallel/test-dtls-peer-x509-certificate.mjs create mode 100644 test/parallel/test-dtls-psk.mjs create mode 100644 test/parallel/test-dtls-request-cert.mjs create mode 100644 test/parallel/test-dtls-resumption-reject-unauthorized.mjs create mode 100644 test/parallel/test-dtls-secure-context.mjs create mode 100644 test/parallel/test-dtls-send-error.mjs create mode 100644 test/parallel/test-dtls-server-handshake-error.mjs create mode 100644 test/parallel/test-dtls-servername-validation.mjs create mode 100644 test/parallel/test-dtls-session-id-context.mjs create mode 100644 test/parallel/test-dtls-session-resumption.mjs create mode 100644 test/parallel/test-dtls-sni-binding.mjs create mode 100644 test/parallel/test-dtls-sni.mjs create mode 100644 test/parallel/test-dtls-socket-options.mjs diff --git a/doc/api/dtls.md b/doc/api/dtls.md index df0e5b50a428..f012e6ef316e 100644 --- a/doc/api/dtls.md +++ b/doc/api/dtls.md @@ -6,7 +6,7 @@ added: REPLACEME -> Stability: 1 - Experimental +> Stability: 1.1 - Active Development @@ -74,20 +74,73 @@ added: REPLACEME * `options` {Object} * `cert` {string|Buffer} Server certificate in PEM format. **Required.** * `key` {string|Buffer} Server private key in PEM format. **Required.** + * `secureContext` {DTLSSecureContext} A context from + [`dtls.createSecureContext()`][] to use instead of building one from the + credential options below. Must have been created with `isServer: true`. + Cannot be combined with any option the context already carries. + * `sni` {Object|Function} Server Name Indication. A map of host names to the + identity to serve them with, or a function returning one. Cannot be + combined with `secureContext`; set it on the context instead. See + [Server Name Indication][]. + * `passphrase` {string} Passphrase to decrypt `key`, if it is encrypted. + Ignored when `key` is not encrypted. Unlike `key` and `cert`, this must be + a string, matching [`tls.createSecureContext()`][]. * `port` {number} Port to bind to. **Required.** * `host` {string} Address to bind to. **Default:** `'0.0.0.0'`. * `ca` {string|Buffer|string\[]|Buffer\[]} CA certificates in PEM format. * `ciphers` {string} OpenSSL cipher list string. - * `alpn` {string\[]|Buffer} ALPN protocol names. + * `alpn` {string\[]|Buffer} ALPN protocol names. Each name must be between + 1 and 255 bytes. A `Buffer` must already be in ALPN wire format: one + length byte followed by that many bytes, repeated. * `srtp` {string} Colon-separated SRTP protection profile names (e.g., `'SRTP_AES128_CM_SHA1_80:SRTP_AEAD_AES_128_GCM'`). - * `requestCert` {boolean} Request client certificate. **Default:** `false`. - * `mtu` {number} Maximum transmission unit for DTLS records. - **Default:** `1200`. + * `requestCert` {boolean} Request a certificate from the client. + **Default:** `false`. + * `rejectUnauthorized` {boolean} Only has an effect together with + `requestCert`. When `true`, a client that presents no certificate, or one + that does not chain to a trusted CA, is rejected during the handshake and + receives a TLS alert. When `false`, the certificate is still requested and + verified but the handshake completes regardless, leaving the decision to + the application via [`session.authorized`][]. **Default:** `true`. + * `mtu` {number} Maximum size in bytes of a DTLS datagram. **Default:** + `1200`. + * `handshakeTimeout` {number} Milliseconds a handshake may take before it is + abandoned. `0` disables it. **Default:** `60000`. See + [Handshake timeout][]. + * `ipv6Only` {boolean} When `true`, an IPv6 endpoint serves IPv6 only. When + `false`, binding `'::'` also accepts IPv4 peers, which arrive with mapped + addresses such as `'::ffff:203.0.113.1'` -- anything keyed on the peer + address, including `maxSessionsPerHost`, sees them in that form. Has no + effect on an IPv4 endpoint. **Default:** `false`. + * `reusePort` {boolean} When `true`, sets `SO_REUSEPORT`, so several + processes may bind the same port and the kernel spreads arriving + datagrams between them. Every one of them must set it. **Default:** + `false`. + * `udpReceiveBufferSize` {number} Size in bytes for the socket's receive + buffer (`SO_RCVBUF`). Raising it gives the endpoint room for bursts that + the default would drop. The kernel clamps this to its own maximum. + **Default:** the system default. + * `udpSendBufferSize` {number} Size in bytes for the socket's send buffer + (`SO_SNDBUF`). Clamped as above. **Default:** the system default. + * `udpTTL` {number} IP time-to-live for outgoing datagrams, from `1` to + `255`. **Default:** the system default. + * `maxSessions` {number} The maximum number of concurrent sessions the + endpoint will hold. Set to `0` for no limit. **Default:** `10000`. + * `maxSessionsPerHost` {number} The maximum number of concurrent sessions + from any single source IP address, ignoring port. Set to `0` for no limit. + **Default:** `1000`. + * `sessionIdContext` {string} Opaque identifier scoping resumable sessions + to this server, at most 32 bytes. **Default:** a value derived from + `process.argv`, as in `tls.createServer()`. * Returns: {DTLSEndpoint} Creates a DTLS server bound to the specified address and port. The server -uses automatic HMAC-based cookie exchange for DoS protection. +uses automatic HMAC-based cookie exchange for DoS protection. See +[Denial of service][]. + +Binding failures are thrown with the code the operating system gave, as in +`net` and `dgram`: an address already in use throws an error whose `code` is +`'EADDRINUSE'`, with `errno` and `syscall` set. ```mjs import { listen } from 'node:dtls'; @@ -117,26 +170,50 @@ console.log('DTLS server listening on', endpoint.address); added: REPLACEME --> -* `host` {string} Remote host to connect to. +* `host` {string} Remote host to connect to, as an IPv4 or IPv6 literal. + Host names are not resolved. * `port` {number} Remote port to connect to. * `options` {Object} * `ca` {string|Buffer|string\[]|Buffer\[]} CA certificates in PEM format. * `cert` {string|Buffer} Client certificate in PEM format. * `key` {string|Buffer} Client private key in PEM format. + * `secureContext` {DTLSSecureContext} A context from + [`dtls.createSecureContext()`][] to use instead of building one from the + credential options below. Must **not** have been created with + `isServer: true`. Cannot be combined with any option the context already + carries. + * `psk` {Object|Function} A pre-shared key as `{ identity, key }`, or a + function returning one. See [Pre-shared keys][]. + * `session` {Buffer} A session from [`session.session`][] on an earlier + connection, to resume rather than handshake in full. See + [Session resumption][]. + * `passphrase` {string} Passphrase to decrypt `key`, if it is encrypted. + Ignored when `key` is not encrypted. Unlike `key` and `cert`, this must be + a string, matching [`tls.createSecureContext()`][]. * `rejectUnauthorized` {boolean} When `true`, the server's certificate must both chain to a trusted CA and match the expected identity (`servername`, or `host` when `servername` is not set); otherwise the handshake is - aborted and `session.opened` rejects. When `false`, the certificate is not - verified. **Default:** `true`. + aborted and `session.opened` rejects. When `false`, the certificate is + still verified and the handshake completes regardless, leaving the + decision to the application via [`session.authorized`][] and + [`session.authorizationError`][]. **Default:** `true`. * `servername` {string} Server name used for the SNI (Server Name Indication) extension and as the identity checked during certificate verification. **Default:** the `host` argument. Set to `''` to disable SNI. SNI is never sent for IP address literals. - * `bindHost` {string} Local bind address. **Default:** `'0.0.0.0'`. + * `bindHost` {string} Local bind address. **Default:** `'::'` when `host` is an + IPv6 literal, otherwise `'0.0.0.0'`. The local socket must be in the same + address family as the peer. * `bindPort` {number} Local bind port. **Default:** `0` (ephemeral). - * `alpn` {string\[]|Buffer} ALPN protocol names. + * `alpn` {string\[]|Buffer} ALPN protocol names. Each name must be between + 1 and 255 bytes. A `Buffer` must already be in ALPN wire format: one + length byte followed by that many bytes, repeated. * `srtp` {string} SRTP protection profile names. - * `mtu` {number} Maximum transmission unit. **Default:** `1200`. + * `mtu` {number} Maximum size in bytes of a DTLS datagram. **Default:** + `1200`. + * `handshakeTimeout` {number} Milliseconds a handshake may take before it is + abandoned and `session.opened` rejects. `0` disables it. **Default:** + `60000`. See [Handshake timeout][]. * Returns: {DTLSSession} Connects to a DTLS server. Returns a `DTLSSession` whose `opened` property @@ -146,7 +223,7 @@ is a `Promise` that resolves when the handshake completes. import { connect } from 'node:dtls'; import { readFileSync } from 'node:fs'; -const session = connect('localhost', 4433, { +const session = connect('127.0.0.1', 4433, { ca: [readFileSync('ca-cert.pem')], }); @@ -158,6 +235,419 @@ session.onmessage = (data) => { }; ``` +## `dtls.createSecureContext([options])` + + + +* `options` {Object} + * `alpn` {string\[]} ALPN protocols. + * `ca` {string|Buffer|Array} CA certificates in PEM format. When omitted, + the bundled default certificate authorities are used. + * `cert` {string|Buffer} Certificate in PEM format. + * `ciphers` {string} OpenSSL cipher suite list. + * `ecdhCurve` {string} Named curve or curve list for ECDH. + * `isServer` {boolean} Build a context for a server. **Default:** `false`. + * `key` {string|Buffer} Private key in PEM format. + * `passphrase` {string} Passphrase for `key`, if it is encrypted. + * `rejectUnauthorized` {boolean} Verification behaviour, as for + [`dtls.listen()`][] and [`dtls.connect()`][]. + * `requestCert` {boolean} Request a certificate from the peer. Servers only. + * `sessionIdContext` {string} Session id context. Servers only. + * `sni` {Object|Function} Server Name Indication. Servers only. See + [Server Name Indication][]. + * `psk` {Object|Function} Pre-shared keys. See [Pre-shared keys][]. + * `pskIdentityHint` {string} Identity hint to advertise, naming which key a + client should pick. Requires `psk`. Servers only. + * `srtp` {string} SRTP profile list. + * `ticketKeys` {Buffer} Session ticket keys, for resuming sessions across + endpoints and restarts. Servers only. See [Session resumption][]. +* Returns: {DTLSSecureContext} + +Options marked "Servers only" require `isServer: true`. Passing one to a +client context throws `ERR_INVALID_ARG_VALUE`, rather than being ignored or +applied where it can have no effect. + +Creates a reusable secure context. Pass it to [`dtls.listen()`][] or +[`dtls.connect()`][] as `secureContext` in place of the credential options. + +A context holds a parsed certificate and key and, when `ca` is given, its own +certificate store; roughly 28 KiB in total. Building one per connection is +therefore expensive in memory rather than in time -- two thousand of them cost +about 54 MiB, against 2 MiB when a single context is shared. Clients opening +many connections should build the context once. + +The peer identity checked during verification is **not** part of the context. +It is bound to each connection from `servername` (or the host), so one context +can be used against different peers and still reject the wrong certificate. + +`isServer` is fixed when the context is created, because it selects the +underlying OpenSSL method. Passing a server context to [`dtls.connect()`][], +or a client context to [`dtls.listen()`][], throws. + +```mjs +import { connect, createSecureContext, listen } from 'node:dtls'; +import { readFileSync } from 'node:fs'; + +const serverContext = createSecureContext({ + cert: readFileSync('server-cert.pem'), + key: readFileSync('server-key.pem'), + isServer: true, +}); + +// One context, several endpoints. +const a = listen(onsession, { secureContext: serverContext, port: 5684 }); +const b = listen(onsession, { secureContext: serverContext, port: 5685 }); + +const clientContext = createSecureContext({ + ca: readFileSync('ca-cert.pem'), +}); + +// One context, many connections, each verified against its own name. +const s1 = connect('192.0.2.1', 5684, { + secureContext: clientContext, + servername: 'a.example.com', +}); +const s2 = connect('192.0.2.2', 5684, { + secureContext: clientContext, + servername: 'b.example.com', +}); +``` + +## Server Name Indication + +An endpoint can serve more than one identity by giving `listen()` an `sni` +map, or a function. Each key of a map is a host name and each value is either +a +[`DTLSSecureContext`][] created with `isServer: true`, or a plain object of +the same options [`dtls.createSecureContext()`][] takes: + +```mjs +import { createSecureContext, listen } from 'node:dtls'; +import { readFileSync } from 'node:fs'; + +const endpoint = listen(onsession, { + cert: readFileSync('default-cert.pem'), + key: readFileSync('default-key.pem'), + port: 5684, + sni: { + 'api.example.com': { + cert: readFileSync('api-cert.pem'), + key: readFileSync('api-key.pem'), + }, + 'www.example.com': createSecureContext({ + cert: readFileSync('www-cert.pem'), + key: readFileSync('www-key.pem'), + isServer: true, + }), + '*': { + cert: readFileSync('default-cert.pem'), + key: readFileSync('default-key.pem'), + }, + }, +}); +``` + +The `'*'` key is the fallback, used when the client's name matches nothing and +when the client sends no name at all. **Without it, an unmatched name is +refused with an `unrecognized_name` alert** rather than falling back to the +endpoint's own `cert` and `key`; providing an `sni` map is taken to mean that +only the names in it are served. [`tls.createServer()`][] differs here: its +`SNICallback` falls back to the default identity silently. + +Verification follows the selected identity, so an entry carrying its own `ca` +accepts only client certificates issued under it. `requestCert` and +`rejectUnauthorized` are not per-identity: they belong to the endpoint and +apply to every name it serves. + +A function may be given instead of a map, for identities that are chosen +rather than enumerated: + +```mjs +listen(onsession, { + port: 5684, + cert, + key, + sni: (servername) => contexts.get(servername), +}); +``` + +It is called with the name the client asked for, or `undefined` if the client +sent no SNI extension, and returns what a map entry holds: a +[`dtls.createSecureContext()`][] result or the options to build one. Returning +nothing declines the name, which is refused exactly as an unmatched map with no +`'*'` entry is, rather than falling back to the endpoint's own certificate. + +The function runs during the handshake and must return synchronously, so it +cannot consult a database. Returning a prepared context is worth doing: +building one from options parses the certificate again on every handshake. + +An exception thrown by the function fails that handshake and is reported to the +session's error handler, like any other handshake failure. It does not reach +the process as an uncaught exception. + +The certificate and the cipher list both follow the selected context. +Pre-shared keys do not. OpenSSL installs the PSK callbacks on the connection +when it is created, before any name is known, and selecting an identity does +not replace them, so the keys a server accepts are always the endpoint's own. +A `psk` given on an SNI identity is never consulted, and an identity cannot be +served over PSK alone. + +`sni` belongs to the secure context rather than to the endpoint, so it can be +given to [`dtls.createSecureContext()`][] and cannot be combined with a +`secureContext` that already exists. Applying it to a prepared context would +reconfigure that context for every endpoint sharing it, and the identities a +server serves are part of what its context is. + +A connection refused for an unrecognized name still reaches the `listen()` +callback: the session exists once the client's address is validated, which +happens before the name is examined. It then fails like any other handshake +failure. + +## Denial of service + +Cookie exchange proves a peer can receive at its claimed address, but it does +not limit how many sessions that peer may then establish, and each session +holds a TLS state machine, two buffers and a timer. `maxSessions` bounds the +total; `maxSessionsPerHost` is what prevents one peer from taking all of it. +A peer refused by either cap is answered with silence rather than an alert, +because replying to an address that has not completed cookie exchange would +create an amplification vector; a legitimate client retransmits and is +admitted once there is room. Refusals are counted by +[`endpointStats.serverRefusedCount`][]. + +Deployments serving many clients behind a single NAT may need to raise +`maxSessionsPerHost`. + +## Handshake timeout + +A handshake that never finishes is abandoned after `handshakeTimeout` +milliseconds, and its session error is `DTLS handshake timeout`. + +OpenSSL already gives up on its own, but only after twelve retransmits on a +doubling backoff capped at 60 seconds -- around eight minutes in total. Until +then the session holds its place against `maxSessions` (see +[Denial of service][]), +so handshakes that are started and abandoned can occupy an endpoint for the +cost of starting them. That needs no spoofing: the peer completes the cookie +exchange and then simply stops. + +The two limits coexist and whichever comes first ends the handshake. The +retransmit schedule itself is untouched, deliberately -- compressing it to +force earlier failure would cause spurious retransmissions on exactly the +lossy links DTLS is meant for. + +The timeout covers resumed and PSK handshakes as well, and stops applying once +the handshake completes; it is not an idle timeout. + +A handshake can stall without either peer being at fault or aware. +DTLS discards records it cannot authenticate rather than answering them +(RFC 6347 section 4.1.2.1), so a mismatched pre-shared key or a cipher list +with nothing in common produces silence rather than an alert. This timeout is +what ends those. + +## Pre-shared keys + +DTLS can authenticate with a key both peers already hold instead of a +certificate (RFC 4279). This is how it is usually deployed to constrained +devices, which frequently have no certificate at all. + +A server gives the identities it accepts; a client gives the one it is. No +certificate is needed on either side: + +```mjs +import { connect, listen } from 'node:dtls'; + +const endpoint = listen(onsession, { + port: 5684, + psk: { 'device-42': deviceKey }, +}); + +const client = connect('192.0.2.1', 5684, { + psk: { identity: 'device-42', key: deviceKey }, +}); +``` + +Either side may pass a function instead, for keys that are looked up or +derived rather than known up front. A server's is called with the identity the +client offered and returns the key, or nothing to refuse it. A client's is +called with the server's identity hint, if it sent one, and returns +`{ identity, key }`: + +```mjs +listen(onsession, { + port: 5684, + psk: (identity) => deriveKey(masterSecret, identity), +}); +``` + +The callback runs during the handshake and must return synchronously, so it +cannot consult a database. Where both are given, the map is checked first and +the callback is only reached when the map has no answer -- a configuration +using only the map never runs JavaScript inside the handshake. + +An exception thrown by the callback fails that handshake and is reported to +the session's error handler. It does not reach the process as an uncaught +exception. + +### Cipher suites + +The default cipher list excludes PSK, so giving `psk` without `ciphers` +enables the PSK suites. Supplying `ciphers` disables that and uses exactly +what was asked for. + +A server keeps the certificate suites as well, since it may serve both kinds +of client on one port. A client does not: a client that configured a +pre-shared key and no CA wants the key, and leaving the certificate suites +enabled would let a server choose one, failing the handshake while verifying a +certificate the caller never meant to rely on. + +Forward-secret PSK key exchanges are preferred over plain PSK of the same +strength. Plain PSK derives its keys from the shared secret alone, so anyone +who later learns that key can decrypt traffic they recorded earlier. `RSA-PSK` +is excluded: it needs a certificate and adds no forward secrecy. + +CoAP requires `TLS_PSK_WITH_AES_128_CCM_8` (RFC 7252), whose 64-bit +authentication tag OpenSSL rejects at security level 1 and above. Node.js +default is above it, so that suite has to be asked for explicitly and with the +security level lowered: + +```mjs +listen(onsession, { port: 5684, psk, ciphers: 'PSK-AES128-CCM8@SECLEVEL=0' }); +``` + +### Failure modes + +A wrong key does not produce an error. The identity only names the key, so the +handshake proceeds and the two sides derive different secrets; the first +record that fails authentication is then discarded rather than answered, since +DTLS discards invalid records instead of replying to them (RFC 6347 section +4.1.2.1). Neither peer is told anything and both retransmit. + +A cipher list with nothing in common behaves the same way, which is what makes +the `CCM8` case above present as a stall rather than a rejection. Both are +ended by [`handshakeTimeout`][], after 60 seconds by default. + +An identity the server does not recognise is refused outright, and the client +sees the handshake fail. + +## Session resumption + +A resumed handshake skips the server's certificate, which matters more here +than it does over TCP: the `Certificate` flight is fragmented across several +datagrams, and losing any one of them costs a retransmission timeout. Measured +on loopback, a full handshake has the server send 1850 bytes in 4 packets +against 280 bytes in 3 for a resumed one. + +A client reads [`session.session`][] once the session is open and passes it to +a later [`dtls.connect()`][]: + +```mjs +import { connect } from 'node:dtls'; + +const first = connect('192.0.2.1', 5684, { ca, servername: 'device.example' }); +await first.opened; +const ticket = first.session; // Buffer. +await first.close(); + +const second = connect('192.0.2.1', 5684, { + ca, + servername: 'device.example', + session: ticket, +}); +await second.opened; +console.log(second.reused); // True. +``` + +A session that the server will not accept -- expired, or issued by a different +endpoint -- is not an error. The handshake simply proceeds in full, and +[`session.reused`][] is `false`. + +The cookie exchange still happens for a resumed handshake, so resumption is not +a way around the address validation described under [Denial of service][]. + +### Binding to the authenticated host + +A session may only be resumed against the identity it was authenticated for -- +the `servername`, or the host when there is none. Reusing it for anything else +throws. + +This is not a convenience check. A resumed handshake does not re-send or +re-verify the peer's certificate; it inherits the authenticated identity of the +original session. Replaying a session against a different host would therefore +skip verification while appearing to succeed. For the same reason a `session` +that did not come from [`session.session`][] is rejected outright: nothing +records which identity it belongs to, so it cannot be checked. + +### Resuming under `rejectUnauthorized` + +A session carries the verification result it was established with, so a session +established with `rejectUnauthorized: false` cannot be resumed by a connection +that asked for a verified peer. The handshake fails: + +```mjs +import { connect } from 'node:dtls'; + +// Connected without verifying anything. +const first = connect('192.0.2.1', 5684, { rejectUnauthorized: false }); +await first.opened; +console.log(first.authorized); // False. +const ticket = first.session; +await first.close(); + +const second = connect('192.0.2.1', 5684, { + rejectUnauthorized: true, + session: ticket, +}); +await second.opened; // Rejects: verification failed. +``` + +The host is the same in both, so binding the session to its authenticated +identity does not cover this on its own; what differs is whether the caller +asked for the peer to be verified. Because a resumed handshake runs no +verification of its own, the recorded result is re-checked once it completes, +and a session whose peer never verified is refused wherever verification is +required. [`session.authorized`][] and [`session.authorizationError`][] report +the recorded result on a resumed session either way. + +### Ticket keys + +The key that encrypts session tickets is generated at random for each context, +so by default a ticket is only good for the endpoint that issued it and only +until the process restarts. Give every endpoint the same `ticketKeys` to let +tickets be resumed across a restart or a cluster: + +```mjs +import { listen } from 'node:dtls'; +import { randomBytes } from 'node:crypto'; + +const ticketKeys = randomBytes(80); // Share this between processes. +const endpoint = listen(onsession, { cert, key, port: 5684, ticketKeys }); +``` + +The length is OpenSSL's: a key name followed by an HMAC key and an AES key. It +differs from the 48 bytes [`tls.createServer()`][] uses, which is a layout +`node:tls` defines for itself. Supplying the wrong length throws and reports +the length expected. + +Ticket keys are long-lived secrets. Anyone holding them can decrypt tickets and +recover the sessions they protect, so treat them as key material and rotate +them. + +## Class: `DTLSSecureContext` + + + +An opaque, reusable bundle of credentials and TLS settings, created by +[`dtls.createSecureContext()`][]. It cannot be constructed directly. + +### `secureContext.isServer` + +* Returns: {boolean} `true` if the context was created for a server. + ## Class: `DTLSEndpoint` + +* Type: {bigint} The number of datagrams discarded before a handshake was + attempted because they could not be a ClientHello. Read only. + +Datagrams arriving at a listening endpoint that do not match an existing +session are screened for the shape of a DTLS ClientHello record before any +state is allocated for them. A steadily rising value indicates junk or scan +traffic rather than failing clients, which are counted as sessions that never +complete. + +### `endpointStats.serverRefusedCount` + + + +* Type: {bigint} The number of otherwise valid handshake attempts refused + because the endpoint was at `maxSessions` or the peer was at + `maxSessionsPerHost`. Read only. + ### `endpointStats.isConnected` + +* Returns: {X509Certificate|undefined} The peer's certificate, or `undefined` + if the peer sent none. + +An [`X509Certificate`][] for the peer's leaf certificate. The issuer chain is +reachable through its `issuerCertificate` property, and the parsed fields -- +`subject`, `issuer`, `validFrom`, `validTo`, `fingerprint256`, `serialNumber` +and the rest -- are properties of that object. + +Where [`tls.TLSSocket.getPeerCertificate()`][] returns a plain dictionary with +`valid_from`, `valid_to` and a chain walked through `issuerCertificate`, this +returns the same `X509Certificate` class that +[`tls.TLSSocket.getPeerX509Certificate()`][] does. Call `toLegacyObject()` on +it to get the dictionary form. + +The same object is returned on every access once the peer's certificate is +available. + +### `session.session` + + + +* Returns: {Buffer|undefined} An opaque session for resuming this connection + later, or `undefined` on a server session or before the handshake completes. + +Pass it as the `session` option to a later [`dtls.connect()`][]. It is bound to +the host this connection authenticated against and is refused elsewhere; see +[Session resumption][]. + +Server sessions return `undefined`: a server has no identity to bind the value +to, and it is the client that carries a session between connections. + +### `session.reused` + + + +* Returns: {boolean} `true` if this connection resumed an earlier session + rather than performing a full handshake. + +Like [`session.authorized`][], this reads `false` once the session is closed. + +### `session.authorized` + + + +* Returns: {boolean} `true` if the peer presented a certificate chain that + verified against the configured certificate authorities, and, for a client, + matched the requested identity. `false` before the handshake completes. + +### `session.authorizationError` + + + +* Returns: {string|undefined} The short X509 verification error code, for + example `'CERT_HAS_EXPIRED'` or `'HOSTNAME_MISMATCH'`, or `undefined` if the + peer's chain verified. + +A peer that presented no certificate at all reports +`'UNABLE_TO_GET_ISSUER_CERT'`, so this can be used to distinguish "no +certificate" from "a certificate that failed to verify". + +The chain is verified even when `rejectUnauthorized` is `false`; the result is +simply not enforced. That makes these two properties the way to apply a custom +authorization policy: + +```mjs +import { connect } from 'node:dtls'; + +const session = connect('192.0.2.1', 4433, { + ca: [caCert], + servername: 'example.com', + rejectUnauthorized: false, +}); + +await session.opened; + +if (!session.authorized && session.authorizationError !== 'CERT_HAS_EXPIRED') { + await session.close(); +} +``` ### `session.alpnProtocol` -* Returns: {string|undefined} The negotiated ALPN protocol. +* Returns: {string|undefined} The negotiated ALPN protocol, or `undefined` if + ALPN was not used. + +If a server has `alpn` configured and a client offers only protocols the +server does not support, the server sends a fatal `no_application_protocol` +alert and the handshake fails, as required by [RFC 7301][] section 3.2. A +server with no `alpn` configured declines the extension instead, and the +handshake completes with no protocol negotiated. ### `session.srtpProfile` @@ -388,7 +1043,8 @@ live and updated as data flows through the session. ### `session.exportKeyingMaterial(length, label[, context])` -* `length` {number} Number of bytes to export. +* `length` {number} Number of bytes to export. Must be an integer between + `1` and `65536`. * `label` {string} The label for the exported keying material. * `context` {Buffer} Optional context value. * Returns: {Buffer} @@ -397,6 +1053,45 @@ Exports keying material from the DTLS session, as defined in [RFC 5705][]. This is commonly used with DTLS-SRTP to derive encryption keys for media streams. +Throws `ERR_OUT_OF_RANGE` if `length` is outside the accepted range. The upper +bound is not imposed by [RFC 5705][]; it exists so that a caller cannot request +an arbitrarily large allocation, and is far above what any defined exporter +needs (DTLS-SRTP uses 60 bytes). + +### Callback properties + +#### `session.onmessage` + +* {Function} + * `data` {Buffer} + +Set to receive application data from the peer. + +#### `session.onerror` + +* {Function} + * `error` {Error} + +Set to receive error notifications. + +#### `session.onhandshake` + +* {Function} + * `protocol` {string} + +Set to receive handshake completion notifications. + +#### `session.onkeylog` + +* {Function} + * `line` {string} + +Set to receive TLS key log lines (for debugging with Wireshark). + +### `session[Symbol.asyncDispose]()` + +Equivalent to calling `session.close()`. + ## Class: `DTLSSession.Stats` + +* `path` {string|Buffer|URL} +* `options` {Object} + * `type` {string} An optional mime type for the blob. +* Returns: {Blob} + +For detailed information, see the documentation of the Promise-returning +version of this API: [`fs.openAsBlob()`][]. + ### `fs.opendirSync(path[, options])` + > Stability: 1.0 - Early Development @@ -286,7 +286,7 @@ run().compose(names).pipe(process.stdout); ## `createRunner([options])` * `options` {Object} @@ -309,7 +309,7 @@ value passed to `createRunner()`. ## `bench([name][, options], fn)` * `name` {string} The benchmark name. **Default:** The `name` property of `fn`, @@ -379,7 +379,7 @@ the samples. ### `bench.skip([name][, options], fn)` Shorthand for `bench(name, { ...options, skip: true }, fn)`. @@ -387,7 +387,7 @@ Shorthand for `bench(name, { ...options, skip: true }, fn)`. ### `bench.only([name][, options], fn)` Shorthand for `bench(name, { ...options, only: true }, fn)`. @@ -395,7 +395,7 @@ Shorthand for `bench(name, { ...options, only: true }, fn)`. ## `suite([name][, options], fn)` * `name` {string} The suite name. **Default:** The `name` property of `fn`, or @@ -421,7 +421,7 @@ functions are awaited before benchmark execution begins. ## `describe([name][, options], fn)` Alias for `suite()`. @@ -429,7 +429,7 @@ Alias for `suite()`. ## `before(fn)` * `fn` {Function|AsyncFunction} The hook function. @@ -439,7 +439,7 @@ Registers a hook that runs once before the benchmarks in the current suite. ## `after(fn)` * `fn` {Function|AsyncFunction} The hook function. @@ -449,7 +449,7 @@ Registers a hook that runs once after the benchmarks in the current suite. ## `beforeEach(fn)` * `fn` {Function|AsyncFunction} The hook function. It receives an object with @@ -462,7 +462,7 @@ the benchmark function before `context.start()` or `context.record()`. ## `afterEach(fn)` * `fn` {Function|AsyncFunction} The hook function. It receives an object with @@ -475,7 +475,7 @@ in the benchmark function after `context.end()` or `context.record()`. ## `run([options])` * `options` {Object} @@ -517,7 +517,7 @@ for await (const { type, data } of run()) { ## `runFile(path[, options])` * `path` {string|Buffer|URL} The path of one benchmark module. @@ -571,7 +571,7 @@ instance is created for every warmup and measured sample. ### `context.index` * {number} @@ -582,7 +582,7 @@ measured samples have separate index sequences. ### `context.name` * {string} @@ -592,7 +592,7 @@ The benchmark name. ### `context.params` * {Object} @@ -602,7 +602,7 @@ The benchmark's canonicalized parameter metadata. ### `context.phase` * {string} @@ -613,7 +613,7 @@ and `'measurement'` for a measured invocation. ### `context.signal` * {AbortSignal} @@ -624,7 +624,7 @@ finishes. ### `context.start()` Starts the measured region using `process.hrtime.bigint()`. Calling `start()` @@ -633,7 +633,7 @@ more than once is an error. ### `context.end(operations[, options])` * `operations` {number} The number of completed operations. Must be a positive @@ -654,7 +654,7 @@ region. ### `context.record(sample)` * `sample` {Object} @@ -676,7 +676,7 @@ message transport from the duration. `record()` is mutually exclusive with ### `context.diagnostic(message[, options])` * `message` {any} A structured-cloneable diagnostic value. With CLI process @@ -704,7 +704,7 @@ arguments or an uncloneable message or detail violate the sample contract. ### `context.done()` Requests successful benchmark completion after the current measured sample. diff --git a/doc/api/cli.md b/doc/api/cli.md index 3e3f2e4416f6..1d47a032a3c9 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -271,7 +271,7 @@ process.permission.has('fs.read', 'custom-require-2.js'); // true ### `--allow-fs-vfs` > Stability: 1.1 - Active development @@ -475,7 +475,7 @@ Error: Access to this API has been restricted ### `--bench` > Stability: 1 - Experimental @@ -503,7 +503,7 @@ This flag cannot be combined with `--test`, `--watch`, `--watch-path`, ### `--bench-isolation=mode` > Stability: 1 - Experimental @@ -522,7 +522,7 @@ The supported modes are `'process'` and `'none'`. ### `--bench-name-pattern=pattern` > Stability: 1 - Experimental @@ -533,7 +533,7 @@ regular expression `pattern`. Non-matching benchmarks are reported as skipped. ### `--bench-reporter-destination=destination` > Stability: 1 - Experimental @@ -545,7 +545,7 @@ can be `stdout`, `stderr`, or a file path. A single reporter defaults to ### `--bench-reporter=reporter` > Stability: 1 - Experimental @@ -561,7 +561,7 @@ have a corresponding `--bench-reporter-destination`. The default reporter is ### `--bench-samples=count` > Stability: 1 - Experimental @@ -573,7 +573,7 @@ selected benchmark. A benchmark may finish earlier by calling ### `--bench-warmup=count` > Stability: 1 - Experimental @@ -1022,7 +1022,7 @@ Node.js must be built against a FIPS-capable OpenSSL. ### `--enable-fips-indicator-events` Publish OpenSSL FIPS indicator results to the @@ -1223,7 +1223,7 @@ Enable experimental import support for `.node` addons. ### `--experimental-bench` > Stability: 1 - Experimental @@ -1404,7 +1404,7 @@ configuration file. ### `--experimental-dtls` > Stability: 1 - Experimental @@ -1713,7 +1713,7 @@ Enable experimental WebAssembly System Interface (WASI) support. ### `--experimental-web-worker` Enable experimental support for the Web Worker API. @@ -1743,7 +1743,7 @@ Disable loading native addons that are not [context-aware][]. @@ -2442,7 +2442,7 @@ Silence all process warnings (including deprecations). ### `--no-worker-snapshot` > Stability: 1 - Experimental @@ -2866,7 +2866,7 @@ forked processes, or clustered processes. diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 89e167547e07..b0b61ed52155 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2546,7 +2546,7 @@ or `'private'` for private (asymmetric) keys. ## Class: `Mac` * Extends: {stream.Transform} @@ -2591,7 +2591,7 @@ console.log(mac.final('hex')); ### `mac.final([outputEncoding])` * `outputEncoding` {string} The [encoding][] of the return value. @@ -2611,7 +2611,7 @@ including when finalization fails. Later calls to `mac.update()` or ### `mac.update(data[, inputEncoding])` * `data` {string|Buffer|TypedArray|DataView} @@ -3664,7 +3664,7 @@ operations. The specific constants currently defined are described in > Stability: 1.2 - Release candidate @@ -5202,7 +5202,7 @@ mode][]. > Stability: 1.2 - Release candidate @@ -5292,7 +5292,7 @@ added: - v21.7.0 - v20.12.0 changes: - - version: REPLACEME + - version: v26.9.0 pr-url: https://github.com/nodejs/node/pull/65484 description: Hash algorithms exposed by OpenSSL providers are now supported. The `functionName` and `customization` options diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index e63f23829f90..1509c830b99e 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1573,7 +1573,7 @@ passed to `console.error()`. #### Crypto > Stability: 1 - Experimental diff --git a/doc/api/dtls.md b/doc/api/dtls.md index f012e6ef316e..50f23e8444d8 100644 --- a/doc/api/dtls.md +++ b/doc/api/dtls.md @@ -1,10 +1,10 @@ # DTLS - + > Stability: 1.1 - Active Development @@ -65,7 +65,7 @@ DTLS is designed for UDP transport and differs from TLS in several key ways: ## `dtls.listen(callback, options)` * `callback` {Function} Called for each new DTLS session accepted by the @@ -167,7 +167,7 @@ console.log('DTLS server listening on', endpoint.address); ## `dtls.connect(host, port[, options])` * `host` {string} Remote host to connect to, as an IPv4 or IPv6 literal. @@ -651,7 +651,7 @@ An opaque, reusable bundle of credentials and TLS settings, created by ## Class: `DTLSEndpoint` Manages a UDP socket and multiplexes DTLS sessions. @@ -665,7 +665,7 @@ The local address the endpoint is bound to. ### `endpoint.stats` * Type: {DTLSEndpoint.Stats} @@ -706,7 +706,7 @@ Equivalent to calling `endpoint.close()`. ## Class: `DTLSEndpoint.Stats` A view of the collected statistics for an endpoint. @@ -714,7 +714,7 @@ A view of the collected statistics for an endpoint. ### `endpointStats.createdAt` * Type: {bigint} A timestamp indicating when the endpoint was created. Read only. @@ -722,7 +722,7 @@ added: REPLACEME ### `endpointStats.destroyedAt` * Type: {bigint} A timestamp indicating when the endpoint was destroyed. Read only. @@ -730,7 +730,7 @@ added: REPLACEME ### `endpointStats.bytesReceived` * Type: {bigint} The total number of bytes received by this endpoint. Read only. @@ -738,7 +738,7 @@ added: REPLACEME ### `endpointStats.bytesSent` * Type: {bigint} The total number of bytes sent by this endpoint. Read only. @@ -746,7 +746,7 @@ added: REPLACEME ### `endpointStats.packetsReceived` * Type: {bigint} The total number of UDP packets received by this endpoint. Read only. @@ -754,7 +754,7 @@ added: REPLACEME ### `endpointStats.packetsSent` * Type: {bigint} The total number of UDP packets sent by this endpoint. Read only. @@ -762,7 +762,7 @@ added: REPLACEME ### `endpointStats.serverSessions` * Type: {bigint} The total number of peer-initiated sessions accepted by this @@ -771,7 +771,7 @@ added: REPLACEME ### `endpointStats.clientSessions` * Type: {bigint} The total number of sessions initiated by this endpoint. Read only. @@ -779,7 +779,7 @@ added: REPLACEME ### `endpointStats.serverBusyCount` * Type: {bigint} The total number of incoming connections rejected because the @@ -813,7 +813,7 @@ added: REPLACEME ### `endpointStats.isConnected` * Type: {boolean} @@ -824,7 +824,7 @@ Once the endpoint is destroyed, the stats become a stale snapshot. ## Class: `DTLSSession` Represents a DTLS association with a single remote peer. @@ -1033,7 +1033,7 @@ handshake completes with no protocol negotiated. ### `session.stats` * Type: {DTLSSession.Stats} @@ -1095,7 +1095,7 @@ Equivalent to calling `session.close()`. ## Class: `DTLSSession.Stats` A view of the collected statistics for a session. @@ -1103,7 +1103,7 @@ A view of the collected statistics for a session. ### `sessionStats.createdAt` * Type: {bigint} A timestamp indicating when the session was created. Read only. @@ -1111,7 +1111,7 @@ added: REPLACEME ### `sessionStats.destroyedAt` * Type: {bigint} A timestamp indicating when the session was destroyed. Read only. @@ -1119,7 +1119,7 @@ added: REPLACEME ### `sessionStats.closingAt` * Type: {bigint} A timestamp indicating when `close()` was called. Read only. @@ -1127,7 +1127,7 @@ added: REPLACEME ### `sessionStats.handshakeCompletedAt` * Type: {bigint} A timestamp indicating when the DTLS handshake completed. Read only. @@ -1135,7 +1135,7 @@ added: REPLACEME ### `sessionStats.bytesReceived` * Type: {bigint} The total number of application data bytes received. Read only. @@ -1143,7 +1143,7 @@ added: REPLACEME ### `sessionStats.bytesSent` * Type: {bigint} The total number of application data bytes sent. Read only. @@ -1151,7 +1151,7 @@ added: REPLACEME ### `sessionStats.messagesReceived` * Type: {bigint} The total number of application messages received. Read only. @@ -1159,7 +1159,7 @@ added: REPLACEME ### `sessionStats.messagesSent` * Type: {bigint} The total number of application messages sent. Read only. @@ -1167,7 +1167,7 @@ added: REPLACEME ### `sessionStats.retransmitCount` * Type: {bigint} The total number of DTLS handshake retransmissions. Read only. @@ -1175,7 +1175,7 @@ added: REPLACEME ### `sessionStats.isConnected` * Type: {boolean} diff --git a/doc/api/errors.md b/doc/api/errors.md index b8695156d645..36d8a2ac22ec 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1083,7 +1083,7 @@ The given crypto key object's type is invalid for the attempted operation. ### `ERR_CRYPTO_INVALID_MAC` An invalid MAC algorithm was specified. @@ -1166,7 +1166,7 @@ OpenSSL with KEM support. ### `ERR_CRYPTO_MAC_FINALIZED` An operation was attempted on a `Mac` object after finalization was attempted @@ -1177,7 +1177,7 @@ or an underlying MAC update failed. ### `ERR_CRYPTO_MAC_NOT_SUPPORTED` Node.js was built without support for the OpenSSL `EVP_MAC` API. @@ -1187,7 +1187,7 @@ Node.js was built without support for the OpenSSL `EVP_MAC` API. ### `ERR_CRYPTO_MAC_UPDATE_FAILED` [`mac.update()`][] failed for an unspecified reason. diff --git a/doc/api/fs.md b/doc/api/fs.md index 0d850cc888af..f1d6e799d6c4 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1424,7 +1424,7 @@ behavior is similar to `cp dir1/ dir2/`. > Stability: 1 - Experimental. Enable this API with the diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 77a484a62cf6..69a4d6613b9b 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1094,7 +1094,7 @@ before Node.js receives the first byte of the response from the server. ### `performanceResourceTiming.finalResponseHeadersStart` * Type: {number} @@ -1106,7 +1106,7 @@ as opposed to an interim response. ### `performanceResourceTiming.firstInterimResponseStart` * Type: {number} @@ -1122,7 +1122,7 @@ added: - v18.2.0 - v16.17.0 changes: - - version: REPLACEME + - version: v26.9.0 pr-url: https://github.com/nodejs/node/pull/65017 description: This property now returns `firstInterimResponseStart` when it is non-zero. @@ -1217,7 +1217,7 @@ content-codings. ### `performanceResourceTiming.renderBlockingStatus` * Type: {string} @@ -1227,7 +1227,7 @@ The render blocking status of the resource. It is either `'blocking'` or `'non-b ### `performanceResourceTiming.contentType` * Type: {string} @@ -1238,7 +1238,7 @@ string if it cannot be determined. ### `performanceResourceTiming.contentEncoding` * Type: {string} @@ -1776,7 +1776,7 @@ console.log(snapshot.percentile(99)); ## `perf_hooks.importHistogram(data)` * `data` {Uint8Array} A CBOR-encoded histogram previously produced by @@ -2179,7 +2179,7 @@ loop delay threshold. ### `histogram.export()` * Returns: {Uint8Array} @@ -2370,7 +2370,7 @@ The mean of the recorded event loop delays. ### `histogram.meanCI([options])` * `options` {Object} diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 5083d16894d6..1032a14be6db 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -179,7 +179,7 @@ See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][], ### Virtual file system (VFS) for assets > Stability: 1.0 - Early development diff --git a/doc/api/v8.md b/doc/api/v8.md index a4373d1ac72d..fc69b311faa3 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1856,7 +1856,7 @@ console.log(profile); ## `v8.setHeapProfileNearHeapLimit(limit)` > Stability: 1 - Experimental diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 40998b9a548c..8172a7cf7e8d 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -117,7 +117,7 @@ added: v26.4.0 ### `vfs.mount()` * Returns: {string} The absolute mount point. @@ -172,7 +172,7 @@ fs.existsSync(`${mountPoint}/data.txt`); // false ### `vfs.unmount()` Unmounts the virtual file system. After unmounting, virtual files @@ -185,7 +185,7 @@ currently mounted has no effect. ### `vfs.mounted` * {boolean} @@ -195,7 +195,7 @@ added: REPLACEME ### `vfs.mountPoint` * {string | null} @@ -207,7 +207,7 @@ mounted. ### `vfs.mountPointURL` * {string | null} @@ -571,7 +571,7 @@ The resolved absolute path used as the root. ## Class: `ZipProvider` A provider that exposes the entries of a ZIP archive - either a @@ -612,7 +612,7 @@ main(); ### `new ZipProvider(source)` * `source` {zlib.ZipBuffer|zlib.ZipFile} An already-open archive. diff --git a/doc/changelogs/CHANGELOG_V26.md b/doc/changelogs/CHANGELOG_V26.md index 11c30aeef58b..825c6d1e9aff 100644 --- a/doc/changelogs/CHANGELOG_V26.md +++ b/doc/changelogs/CHANGELOG_V26.md @@ -8,6 +8,7 @@ +26.9.0
26.8.2
26.8.1
26.8.0
@@ -53,6 +54,235 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2026-09-16, Version 26.9.0 (Current), @aduh95 + +### Notable Changes + +* \[[`d414624dce`](https://github.com/nodejs/node/commit/d414624dce)] - **(SEMVER-MINOR)** **crypto**: add a generic MAC API (Filip Skokan) [#65553](https://github.com/nodejs/node/pull/65553) +* \[[`7ac458f802`](https://github.com/nodejs/node/commit/7ac458f802)] - **(SEMVER-MINOR)** **crypto**: discover ciphers from OpenSSL providers (Filip Skokan) [#65484](https://github.com/nodejs/node/pull/65484) +* \[[`7c7e95a3bd`](https://github.com/nodejs/node/commit/7c7e95a3bd)] - **(SEMVER-MINOR)** **crypto**: discover hashes from OpenSSL providers (Filip Skokan) [#65484](https://github.com/nodejs/node/pull/65484) +* \[[`2d2f4f6d1a`](https://github.com/nodejs/node/commit/2d2f4f6d1a)] - **(SEMVER-MINOR)** **ffi**: enable module by default (Matteo Collina) [#65475](https://github.com/nodejs/node/pull/65475) +* \[[`657415c6df`](https://github.com/nodejs/node/commit/657415c6df)] - **(SEMVER-MINOR)** **lib**: implement `node:bench` (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`ebcfec2f0c`](https://github.com/nodejs/node/commit/ebcfec2f0c)] - **(SEMVER-MINOR)** **perf\_hooks**: implement Histogram `meanCI` API (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`e1913630c3`](https://github.com/nodejs/node/commit/e1913630c3)] - **(SEMVER-MINOR)** **perf\_hooks**: add CBOR export/import for histogram exchange (James M Snell) [#65434](https://github.com/nodejs/node/pull/65434) +* \[[`8e9151c9f2`](https://github.com/nodejs/node/commit/8e9151c9f2)] - **(SEMVER-MINOR)** **src**: let embedders supply a builtin code cache without a snapshot (Shelley Vohr) [#65352](https://github.com/nodejs/node/pull/65352) +* \[[`889854f18b`](https://github.com/nodejs/node/commit/889854f18b)] - **(SEMVER-MINOR)** **src,lib**: implement experimental DTLS API (James M Snell) [#63182](https://github.com/nodejs/node/pull/63182) +* \[[`5198142c59`](https://github.com/nodejs/node/commit/5198142c59)] - **(SEMVER-MINOR)** **vfs**: integrate with CJS and ESM module loaders (Matteo Collina) [#63653](https://github.com/nodejs/node/pull/63653) +* \[[`5af9d72e7f`](https://github.com/nodejs/node/commit/5af9d72e7f)] - **(SEMVER-MINOR)** **worker**: add support for Web Workers (Aviv Keller) [#64894](https://github.com/nodejs/node/pull/64894) + +### Commits + +* \[[`59dcad3f44`](https://github.com/nodejs/node/commit/59dcad3f44)] - **(SEMVER-MINOR)** **benchmark**: implement node:bench version of bench tools (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`6bdb6dd8fa`](https://github.com/nodejs/node/commit/6bdb6dd8fa)] - **benchmark**: fix max-regressions detection in compare.js (James M Snell) [#65587](https://github.com/nodejs/node/pull/65587) +* \[[`49d40091ba`](https://github.com/nodejs/node/commit/49d40091ba)] - **benchmark**: add --analyze option to benchmark/scatter.js (James M Snell) [#65594](https://github.com/nodejs/node/pull/65594) +* \[[`bd40a9a19a`](https://github.com/nodejs/node/commit/bd40a9a19a)] - **benchmark**: add crypto class benchmarks (Filip Skokan) [#65518](https://github.com/nodejs/node/pull/65518) +* \[[`6bff2238f5`](https://github.com/nodejs/node/commit/6bff2238f5)] - **buffer**: pad aligned allocations by a multiple of 8 (Lazizbek Ergashev) [#65605](https://github.com/nodejs/node/pull/65605) +* \[[`37d12946dd`](https://github.com/nodejs/node/commit/37d12946dd)] - **buffer**: prevent abort on indexOf with lone surrogate needle (Rafael Gonzaga) [#65430](https://github.com/nodejs/node/pull/65430) +* \[[`02f8eb3b2d`](https://github.com/nodejs/node/commit/02f8eb3b2d)] - **build**: add `--shared-perfetto` flag (Antoine du Hamel) [#65614](https://github.com/nodejs/node/pull/65614) +* \[[`fe7032ec41`](https://github.com/nodejs/node/commit/fe7032ec41)] - **build**: enable V8 gdb/lldb plugin support (Chengzhong Wu) [#65786](https://github.com/nodejs/node/pull/65786) +* \[[`109f2caf80`](https://github.com/nodejs/node/commit/109f2caf80)] - **build**: allow linking shared dependencies in the GN build (Shelley Vohr) [#65797](https://github.com/nodejs/node/pull/65797) +* \[[`55213fd8a8`](https://github.com/nodejs/node/commit/55213fd8a8)] - **build**: enable the V8 sandbox in shared-cage builds (Shelley Vohr) [#62237](https://github.com/nodejs/node/pull/62237) +* \[[`120929a1b3`](https://github.com/nodejs/node/commit/120929a1b3)] - **build**: add `--shared-highway` configure flag (Antoine du Hamel) [#65686](https://github.com/nodejs/node/pull/65686) +* \[[`c99af0942b`](https://github.com/nodejs/node/commit/c99af0942b)] - **build**: add `--shared-abseil` configure flag (Antoine du Hamel) [#65682](https://github.com/nodejs/node/pull/65682) +* \[[`431c2bf0eb`](https://github.com/nodejs/node/commit/431c2bf0eb)] - **build**: define V8\_CONTIGUOUS\_COMPRESSED\_RO\_SPACE for shared cage (Shelley Vohr) [#65464](https://github.com/nodejs/node/pull/65464) +* \[[`b6b5764c6e`](https://github.com/nodejs/node/commit/b6b5764c6e)] - **build**: remove obsolete configure flags (Chengzhong Wu) [#65456](https://github.com/nodejs/node/pull/65456) +* \[[`a623ee29d9`](https://github.com/nodejs/node/commit/a623ee29d9)] - **build,src**: make --use-largepages a no-op (Joyee Cheung) [#65389](https://github.com/nodejs/node/pull/65389) +* \[[`2f494cb7aa`](https://github.com/nodejs/node/commit/2f494cb7aa)] - **crypto**: fix multi-prime RSA JWKs (Filip Skokan) [#65649](https://github.com/nodejs/node/pull/65649) +* \[[`501d8ac815`](https://github.com/nodejs/node/commit/501d8ac815)] - **crypto**: cache valid ECDH key pairs (Filip Skokan) [#65615](https://github.com/nodejs/node/pull/65615) +* \[[`3fd905ea8f`](https://github.com/nodejs/node/commit/3fd905ea8f)] - **crypto**: add strict mode to --force-fips (Filip Skokan) [#65645](https://github.com/nodejs/node/pull/65645) +* \[[`54b3cdb805`](https://github.com/nodejs/node/commit/54b3cdb805)] - **crypto**: add FIPS indicator diagnostics channel (Filip Skokan) [#65645](https://github.com/nodejs/node/pull/65645) +* \[[`3e6e931fc9`](https://github.com/nodejs/node/commit/3e6e931fc9)] - **crypto**: fix public PKCS8 export error (한국) [#65609](https://github.com/nodejs/node/pull/65609) +* \[[`d414624dce`](https://github.com/nodejs/node/commit/d414624dce)] - **(SEMVER-MINOR)** **crypto**: add a generic MAC API (Filip Skokan) [#65553](https://github.com/nodejs/node/pull/65553) +* \[[`eef579047c`](https://github.com/nodejs/node/commit/eef579047c)] - **crypto**: validate JWK usages before key\_ops (Filip Skokan) [#65550](https://github.com/nodejs/node/pull/65550) +* \[[`9a9b137a24`](https://github.com/nodejs/node/commit/9a9b137a24)] - **crypto**: fix private SPKI export error (Filip Skokan) [#65550](https://github.com/nodejs/node/pull/65550) +* \[[`9941fb5dab`](https://github.com/nodejs/node/commit/9941fb5dab)] - **crypto**: fix RSA-PSS oversized salt handling (Filip Skokan) [#65550](https://github.com/nodejs/node/pull/65550) +* \[[`09b431596a`](https://github.com/nodejs/node/commit/09b431596a)] - **crypto**: correct CCM decryption FIPS error message (kyungrae) [#65450](https://github.com/nodejs/node/pull/65450) +* \[[`e6b34e6f7a`](https://github.com/nodejs/node/commit/e6b34e6f7a)] - **crypto**: harden X509Certificate state (Filip Skokan) [#65518](https://github.com/nodejs/node/pull/65518) +* \[[`a884b6ccbc`](https://github.com/nodejs/node/commit/a884b6ccbc)] - **crypto**: optimize key slot caching (Filip Skokan) [#65518](https://github.com/nodejs/node/pull/65518) +* \[[`fb512086d3`](https://github.com/nodejs/node/commit/fb512086d3)] - **crypto**: add null checks for OPENSSL\_INIT\_new() (Nora Dossche) [#63457](https://github.com/nodejs/node/pull/63457) +* \[[`656504ffc0`](https://github.com/nodejs/node/commit/656504ffc0)] - **crypto**: prevent Hmac.digest() from returning uninitialized memory (Matteo Collina) [#65112](https://github.com/nodejs/node/pull/65112) +* \[[`15e7884732`](https://github.com/nodejs/node/commit/15e7884732)] - **crypto**: avoid throwing CryptoKey brand checks (Filip Skokan) [#65503](https://github.com/nodejs/node/pull/65503) +* \[[`3302bec74a`](https://github.com/nodejs/node/commit/3302bec74a)] - **crypto**: avoid throwing KeyObject brand checks (Filip Skokan) [#65503](https://github.com/nodejs/node/pull/65503) +* \[[`7ac458f802`](https://github.com/nodejs/node/commit/7ac458f802)] - **(SEMVER-MINOR)** **crypto**: discover ciphers from OpenSSL providers (Filip Skokan) [#65484](https://github.com/nodejs/node/pull/65484) +* \[[`7c7e95a3bd`](https://github.com/nodejs/node/commit/7c7e95a3bd)] - **(SEMVER-MINOR)** **crypto**: discover hashes from OpenSSL providers (Filip Skokan) [#65484](https://github.com/nodejs/node/pull/65484) +* \[[`0539e90110`](https://github.com/nodejs/node/commit/0539e90110)] - **deps**: update googletest to 283c17563fe7a1111cd7f581aa5d541e8baeff2f (Node.js GitHub Bot) [#65833](https://github.com/nodejs/node/pull/65833) +* \[[`e34af820da`](https://github.com/nodejs/node/commit/e34af820da)] - **deps**: update simdjson to 4.6.11 (Node.js GitHub Bot) [#65834](https://github.com/nodejs/node/pull/65834) +* \[[`5fb7ffebc2`](https://github.com/nodejs/node/commit/5fb7ffebc2)] - **deps**: update zlib to 1.3.2.1-motley-285e94b (Node.js GitHub Bot) [#65835](https://github.com/nodejs/node/pull/65835) +* \[[`05cdf1db62`](https://github.com/nodejs/node/commit/05cdf1db62)] - **deps**: V8: backport 1a0089053443 (Jakob Linke) [#65764](https://github.com/nodejs/node/pull/65764) +* \[[`8910f7914a`](https://github.com/nodejs/node/commit/8910f7914a)] - **deps**: V8: backport c9c0abfa51f0 (Jakob Linke) [#65764](https://github.com/nodejs/node/pull/65764) +* \[[`e7fd388424`](https://github.com/nodejs/node/commit/e7fd388424)] - **deps**: V8: backport ebd15783b7ba (Marja Hölttä) [#65764](https://github.com/nodejs/node/pull/65764) +* \[[`82715158b8`](https://github.com/nodejs/node/commit/82715158b8)] - **deps**: V8: backport f3d4d458fe59 (Olivier Flückiger) [#65702](https://github.com/nodejs/node/pull/65702) +* \[[`7003159885`](https://github.com/nodejs/node/commit/7003159885)] - **diagnostics\_channel**: lazily create tracing context (Romain Lanz) [#65513](https://github.com/nodejs/node/pull/65513) +* \[[`a97def782f`](https://github.com/nodejs/node/commit/a97def782f)] - **doc**: clarify node:bench significance policy (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`c74091647d`](https://github.com/nodejs/node/commit/c74091647d)] - **doc**: clarify isolation modes for node:bench (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`3ef81a676c`](https://github.com/nodejs/node/commit/3ef81a676c)] - **doc**: clarify measurement integrity details of node:bench (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`6b565d7b2c`](https://github.com/nodejs/node/commit/6b565d7b2c)] - **doc**: clarify security triage dispositions and permission boundaries (Rafael Gonzaga) [#65436](https://github.com/nodejs/node/pull/65436) +* \[[`724981287c`](https://github.com/nodejs/node/commit/724981287c)] - **(SEMVER-MINOR)** **doc**: move histogram.burnRate to correct location in doc (James M Snell) [#65434](https://github.com/nodejs/node/pull/65434) +* \[[`f8b627c09b`](https://github.com/nodejs/node/commit/f8b627c09b)] - **doc**: fix default limit of maxHeadersCount (Aryn H) [#65472](https://github.com/nodejs/node/pull/65472) +* \[[`aa3d94fc1d`](https://github.com/nodejs/node/commit/aa3d94fc1d)] - **doc**: fix return types for sync methods (Chiang Fong Lee) [#58575](https://github.com/nodejs/node/pull/58575) +* \[[`fe9f452ddd`](https://github.com/nodejs/node/commit/fe9f452ddd)] - **doc**: document REPL DEP0185 throws and uncaught-exception behavior (Adrián Estrada) [#64993](https://github.com/nodejs/node/pull/64993) +* \[[`6b84ee66fa`](https://github.com/nodejs/node/commit/6b84ee66fa)] - **errors**: validate constructor name (Christian Aurich) [#65607](https://github.com/nodejs/node/pull/65607) +* \[[`ed1b629901`](https://github.com/nodejs/node/commit/ed1b629901)] - **events**: fix weak listener retention overwrite (Aryan) [#64024](https://github.com/nodejs/node/pull/64024) +* \[[`7b4c17f723`](https://github.com/nodejs/node/commit/7b4c17f723)] - **ffi**: throw on missing memory helper arguments (Soul Lee) [#65500](https://github.com/nodejs/node/pull/65500) +* \[[`cf78d4260e`](https://github.com/nodejs/node/commit/cf78d4260e)] - **ffi**: include SharedArrayBuffer in error message (Donghoon Kang) [#65735](https://github.com/nodejs/node/pull/65735) +* \[[`2d2f4f6d1a`](https://github.com/nodejs/node/commit/2d2f4f6d1a)] - **(SEMVER-MINOR)** **ffi**: enable module by default (Matteo Collina) [#65475](https://github.com/nodejs/node/pull/65475) +* \[[`bdcb6def4e`](https://github.com/nodejs/node/commit/bdcb6def4e)] - **ffi**: validate DynamicLibrary getter receivers (Trivikram Kamat) [#65415](https://github.com/nodejs/node/pull/65415) +* \[[`5d24a8fe37`](https://github.com/nodejs/node/commit/5d24a8fe37)] - **fs**: copy directory trees for fs.cp() on the thread pool (Shelley Vohr) [#65488](https://github.com/nodejs/node/pull/65488) +* \[[`40bdf32d91`](https://github.com/nodejs/node/commit/40bdf32d91)] - **fs**: give directories created by cpSync the source directory's mode (Shelley Vohr) [#65488](https://github.com/nodejs/node/pull/65488) +* \[[`6ac1bc040f`](https://github.com/nodejs/node/commit/6ac1bc040f)] - **fs**: write files in one thread pool round trip (Shelley Vohr) [#65489](https://github.com/nodejs/node/pull/65489) +* \[[`b4b89c08c1`](https://github.com/nodejs/node/commit/b4b89c08c1)] - **fs**: handle recursive watch setup races (Filip Skokan) [#65699](https://github.com/nodejs/node/pull/65699) +* \[[`54a1311d9e`](https://github.com/nodejs/node/commit/54a1311d9e)] - **fs**: apply nocase to literal glob exclude patterns (Yusuke Hayashi) [#64817](https://github.com/nodejs/node/pull/64817) +* \[[`2130460831`](https://github.com/nodejs/node/commit/2130460831)] - **fs**: improve performance of recursive directory read (Aviv Keller) [#65524](https://github.com/nodejs/node/pull/65524) +* \[[`ab7f4c88b7`](https://github.com/nodejs/node/commit/ab7f4c88b7)] - **fs**: do not descend into symlinks for \*\* unless following symlinks (RafaelGSS) [#65435](https://github.com/nodejs/node/pull/65435) +* \[[`e990324e7a`](https://github.com/nodejs/node/commit/e990324e7a)] - **fs**: watch directories, not files, in recursive fs.watch fallback (Shelley Vohr) [#65486](https://github.com/nodejs/node/pull/65486) +* \[[`b6db21972a`](https://github.com/nodejs/node/commit/b6db21972a)] - **fs**: preserve directory timestamps in cp (Abhinandan Kumar) [#65540](https://github.com/nodejs/node/pull/65540) +* \[[`d418ca140e`](https://github.com/nodejs/node/commit/d418ca140e)] - **fs**: fix recursive watch error handling (Filip Skokan) [#65635](https://github.com/nodejs/node/pull/65635) +* \[[`ea372bf9c9`](https://github.com/nodejs/node/commit/ea372bf9c9)] - **fs**: cancel in-flight stat on abort (Mert Can Altin) [#63142](https://github.com/nodejs/node/pull/63142) +* \[[`458664073e`](https://github.com/nodejs/node/commit/458664073e)] - **fs**: fix rmSync error messages for non-ASCII paths (Yeaseen) [#61233](https://github.com/nodejs/node/pull/61233) +* \[[`34b38235c3`](https://github.com/nodejs/node/commit/34b38235c3)] - **fs**: add maxDepth option to glob (Alexander Lichter) [#64003](https://github.com/nodejs/node/pull/64003) +* \[[`e3cc0d9313`](https://github.com/nodejs/node/commit/e3cc0d9313)] - **http**: fast-forward teardown of unread messages (Matteo Collina) [#65732](https://github.com/nodejs/node/pull/65732) +* \[[`f9175a212c`](https://github.com/nodejs/node/commit/f9175a212c)] - **http**: coalesce chunked writes during auto-corking (GetThatCookie) [#64987](https://github.com/nodejs/node/pull/64987) +* \[[`b4a7cd3efb`](https://github.com/nodejs/node/commit/b4a7cd3efb)] - **http2**: fix write deadlock exposed by larger window sizes (Tim Perry) [#65440](https://github.com/nodejs/node/pull/65440) +* \[[`b7e97f6cfd`](https://github.com/nodejs/node/commit/b7e97f6cfd)] - **http2**: error for incomplete reads on RST, auto-drain, deprecate aborted (Tim Perry) [#63249](https://github.com/nodejs/node/pull/63249) +* \[[`d62989dbdf`](https://github.com/nodejs/node/commit/d62989dbdf)] - **http2**: fix async context loss when trailers carry END\_STREAM (Orgad Shaneh) [#63814](https://github.com/nodejs/node/pull/63814) +* \[[`8aba314f87`](https://github.com/nodejs/node/commit/8aba314f87)] - **https**: limit proxy CONNECT response headers (Matteo Collina) [#64545](https://github.com/nodejs/node/pull/64545) +* \[[`bea74e1cb5`](https://github.com/nodejs/node/commit/bea74e1cb5)] - **lib**: put node:bench behind an --experimental-bench flag (James M Snell) [#65920](https://github.com/nodejs/node/pull/65920) +* \[[`2835523960`](https://github.com/nodejs/node/commit/2835523960)] - **lib**: fixup node:bench handling of --require option (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`aae83d87a4`](https://github.com/nodejs/node/commit/aae83d87a4)] - **lib**: improve diagnostic message support (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`ba7baed423`](https://github.com/nodejs/node/commit/ba7baed423)] - **lib**: have runFile honor permissions and accept URL/Buffer paths (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`55103db988`](https://github.com/nodejs/node/commit/55103db988)] - **lib**: add runFile api to node:bench (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`0a4c50eb76`](https://github.com/nodejs/node/commit/0a4c50eb76)] - **lib**: add context.diagnostic api to node:bench (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`476e25e864`](https://github.com/nodejs/node/commit/476e25e864)] - **lib**: add `bench:plan` event to `node:bench` (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`67cf0e556f`](https://github.com/nodejs/node/commit/67cf0e556f)] - **lib**: clarify mean in node:bench docs (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`f85f72ad42`](https://github.com/nodejs/node/commit/f85f72ad42)] - **lib**: improve node:bench stream handling (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`b32b09f56b`](https://github.com/nodejs/node/commit/b32b09f56b)] - **lib**: add runId, fileRunId, entryFile, namePath to node:bench (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`4b9b53f958`](https://github.com/nodejs/node/commit/4b9b53f958)] - **(SEMVER-MINOR)** **lib**: add `node:bench` explicit createRunner (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`d9f18171f0`](https://github.com/nodejs/node/commit/d9f18171f0)] - **(SEMVER-MINOR)** **lib**: complete the implementation of node:bench and cli (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`c39e20e9ca`](https://github.com/nodejs/node/commit/c39e20e9ca)] - **(SEMVER-MINOR)** **lib**: implement bench/reporters (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`657415c6df`](https://github.com/nodejs/node/commit/657415c6df)] - **(SEMVER-MINOR)** **lib**: implement node:bench (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`46863a7f9c`](https://github.com/nodejs/node/commit/46863a7f9c)] - **lib**: use `Float16Array` from primordials (Antoine du Hamel) [#65702](https://github.com/nodejs/node/pull/65702) +* \[[`cbd0948138`](https://github.com/nodejs/node/commit/cbd0948138)] - **lib**: defer source map payload decoding until first use (Shelley Vohr) [#65490](https://github.com/nodejs/node/pull/65490) +* \[[`1ddb1c8934`](https://github.com/nodejs/node/commit/1ddb1c8934)] - **lib**: optimize async context frame activation (Tim Perry) [#65519](https://github.com/nodejs/node/pull/65519) +* \[[`02f6feef7e`](https://github.com/nodejs/node/commit/02f6feef7e)] - **lib**: use validateArray for array arguments (JunHwan Choi) [#65344](https://github.com/nodejs/node/pull/65344) +* \[[`962795ab8c`](https://github.com/nodejs/node/commit/962795ab8c)] - **lib**: apply minor dtls cleanups (James M Snell) [#63539](https://github.com/nodejs/node/pull/63539) +* \[[`c07b2ab8a9`](https://github.com/nodejs/node/commit/c07b2ab8a9)] - **lib,benchmark**: address multiple review issues (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`54a5e25f72`](https://github.com/nodejs/node/commit/54a5e25f72)] - **module**: derive builtinModules from enabled builtin set (Jungwon Sohn) [#65418](https://github.com/nodejs/node/pull/65418) +* \[[`c93a90cf12`](https://github.com/nodejs/node/commit/c93a90cf12)] - **net**: fix BlockList.fromJSON for IPv4-mapped IPv6 rules (Daijiro Wachi) [#64125](https://github.com/nodejs/node/pull/64125) +* \[[`506d23b76f`](https://github.com/nodejs/node/commit/506d23b76f)] - **net**: recognize bare IPv6 loopback addresses in isLoopback (Daijiro Wachi) [#63619](https://github.com/nodejs/node/pull/63619) +* \[[`81cd6d7b78`](https://github.com/nodejs/node/commit/81cd6d7b78)] - **net**: improve dtls cert verification (James M Snell) [#64314](https://github.com/nodejs/node/pull/64314) +* \[[`51d094e26b`](https://github.com/nodejs/node/commit/51d094e26b)] - **node-api**: make object property arrays const (Yilong Li) [#65621](https://github.com/nodejs/node/pull/65621) +* \[[`875c7ecee2`](https://github.com/nodejs/node/commit/875c7ecee2)] - **node-api**: enter env context for async callbacks (Shelley Vohr) [#65406](https://github.com/nodejs/node/pull/65406) +* \[[`ebcfec2f0c`](https://github.com/nodejs/node/commit/ebcfec2f0c)] - **(SEMVER-MINOR)** **perf\_hooks**: implement Histogram meanCI API (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`09b333bcc5`](https://github.com/nodejs/node/commit/09b333bcc5)] - **perf\_hooks**: add missing resource timing attributes (greenhead) [#65017](https://github.com/nodejs/node/pull/65017) +* \[[`e1913630c3`](https://github.com/nodejs/node/commit/e1913630c3)] - **(SEMVER-MINOR)** **perf\_hooks**: add CBOR export/import for histogram exchange (James M Snell) [#65434](https://github.com/nodejs/node/pull/65434) +* \[[`67a78317dd`](https://github.com/nodejs/node/commit/67a78317dd)] - **permission**: do not enforce fs and addons in audit mode (Issac) [#65659](https://github.com/nodejs/node/pull/65659) +* \[[`19e69e5f72`](https://github.com/nodejs/node/commit/19e69e5f72)] - **permission**: support URL and Uint8Array as has()/drop() reference (Seungmin Nam) [#65492](https://github.com/nodejs/node/pull/65492) +* \[[`168af4d8d7`](https://github.com/nodejs/node/commit/168af4d8d7)] - **permission**: block FileHandle fsync and fdatasync (Rafael Gonzaga) [#65431](https://github.com/nodejs/node/pull/65431) +* \[[`855fff682f`](https://github.com/nodejs/node/commit/855fff682f)] - **quic**: stop guarding ngtcp2\_recv\_stop\_sending callback field (René) [#65688](https://github.com/nodejs/node/pull/65688) +* \[[`d30df2a3e1`](https://github.com/nodejs/node/commit/d30df2a3e1)] - **quic**: reuse TLS pause machinery to drop event deferral & improve 0RTT (Tim Perry) [#65522](https://github.com/nodejs/node/pull/65522) +* \[[`fce1b6a482`](https://github.com/nodejs/node/commit/fce1b6a482)] - **quic**: remove unused fin flag from blob reader wakeup (trivenay) [#65315](https://github.com/nodejs/node/pull/65315) +* \[[`c721aabfb7`](https://github.com/nodejs/node/commit/c721aabfb7)] - **quic**: apply multiple fixes to flow control signaling (James M Snell) [#65309](https://github.com/nodejs/node/pull/65309) +* \[[`2c552cd32c`](https://github.com/nodejs/node/commit/2c552cd32c)] - **quic**: release stream arenas before cleanup (Trivikram Kamat) [#65410](https://github.com/nodejs/node/pull/65410) +* \[[`487662717b`](https://github.com/nodejs/node/commit/487662717b)] - **sea**: mount bundled assets as a virtual file system (Matteo Collina) [#65675](https://github.com/nodejs/node/pull/65675) +* \[[`0ec15a2a54`](https://github.com/nodejs/node/commit/0ec15a2a54)] - **sea**: keep ELF segments on separate pages in --build-sea output (Shelley Vohr) [#65564](https://github.com/nodejs/node/pull/65564) +* \[[`1627fd6561`](https://github.com/nodejs/node/commit/1627fd6561)] - **sqlite**: re-validate database state after reading options (Trevor Burnham) [#65595](https://github.com/nodejs/node/pull/65595) +* \[[`18c3867d19`](https://github.com/nodejs/node/commit/18c3867d19)] - **sqlite**: run backup completion in callback scope (Filip Skokan) [#65666](https://github.com/nodejs/node/pull/65666) +* \[[`a77d738683`](https://github.com/nodejs/node/commit/a77d738683)] - **sqlite**: copy changeset before applying it (Matteo Collina) [#65286](https://github.com/nodejs/node/pull/65286) +* \[[`b5f0558c84`](https://github.com/nodejs/node/commit/b5f0558c84)] - **sqlite**: reject closing a session from a callback (Trevor Burnham) [#65454](https://github.com/nodejs/node/pull/65454) +* \[[`5f95032859`](https://github.com/nodejs/node/commit/5f95032859)] - **sqlite**: keep sessions alive across SQLite callbacks (Trevor Burnham) [#65465](https://github.com/nodejs/node/pull/65465) +* \[[`2ce0e2eb10`](https://github.com/nodejs/node/commit/2ce0e2eb10)] - **sqlite**: throw on disposal of an in-use session (Guilherme Araújo) [#65449](https://github.com/nodejs/node/pull/65449) +* \[[`f66ed89136`](https://github.com/nodejs/node/commit/f66ed89136)] - **src**: fix use-after-free in CleanupHookThunkRun (Caleb Everett) [#65630](https://github.com/nodejs/node/pull/65630) +* \[[`14613fdc3e`](https://github.com/nodejs/node/commit/14613fdc3e)] - **(SEMVER-MINOR)** **src**: fixup histogram and options linting issues (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`6db3f662cf`](https://github.com/nodejs/node/commit/6db3f662cf)] - **src**: fix startup snapshot reproducibility of InternalFieldInfo (Chengzhong Wu) [#65684](https://github.com/nodejs/node/pull/65684) +* \[[`8e9151c9f2`](https://github.com/nodejs/node/commit/8e9151c9f2)] - **(SEMVER-MINOR)** **src**: let embedders supply a builtin code cache without a snapshot (Shelley Vohr) [#65352](https://github.com/nodejs/node/pull/65352) +* \[[`760337aa5b`](https://github.com/nodejs/node/commit/760337aa5b)] - **src**: fix live lock between environments with blocked requests (Ilyas Shabi) [#65520](https://github.com/nodejs/node/pull/65520) +* \[[`f2a588c385`](https://github.com/nodejs/node/commit/f2a588c385)] - **src**: apply IsolateSettings when using a snapshot (Shelley Vohr) [#65407](https://github.com/nodejs/node/pull/65407) +* \[[`2fd50ae915`](https://github.com/nodejs/node/commit/2fd50ae915)] - **src**: add re-entrancy guard to TriggerUncaughtException (Temuulen Undrakhbayar) [#64327](https://github.com/nodejs/node/pull/64327) +* \[[`e260fd146c`](https://github.com/nodejs/node/commit/e260fd146c)] - **src**: reuse cached strings in CompileSerializeMain (agape1225) [#65453](https://github.com/nodejs/node/pull/65453) +* \[[`0650dca863`](https://github.com/nodejs/node/commit/0650dca863)] - **src**: add missing vector include (Filip Skokan) [#65622](https://github.com/nodejs/node/pull/65622) +* \[[`6e9d44d1fe`](https://github.com/nodejs/node/commit/6e9d44d1fe)] - **src**: disable V8 external memory reasonable size check (Paul Bouchon) [#65589](https://github.com/nodejs/node/pull/65589) +* \[[`9c46e56303`](https://github.com/nodejs/node/commit/9c46e56303)] - **src**: report libuv error when openAsBlob cannot stat (Paul Bouchon) [#65517](https://github.com/nodejs/node/pull/65517) +* \[[`a0aaff6b07`](https://github.com/nodejs/node/commit/a0aaff6b07)] - **src**: list scripts when `--run` has no command (James Ross) [#64606](https://github.com/nodejs/node/pull/64606) +* \[[`469590c155`](https://github.com/nodejs/node/commit/469590c155)] - **src**: fixup manual new/delete usages (James M Snell) [#65348](https://github.com/nodejs/node/pull/65348) +* \[[`5902acf031`](https://github.com/nodejs/node/commit/5902acf031)] - **src**: make the options structs smaller with packed bits (James M Snell) [#65145](https://github.com/nodejs/node/pull/65145) +* \[[`0c24639a5a`](https://github.com/nodejs/node/commit/0c24639a5a)] - **(SEMVER-MINOR)** **src, lib**: add stats to dtls (James M Snell) [#63182](https://github.com/nodejs/node/pull/63182) +* \[[`652fbc286e`](https://github.com/nodejs/node/commit/652fbc286e)] - **(SEMVER-MINOR)** **src,lib**: add dtls interop tests (James M Snell) [#63182](https://github.com/nodejs/node/pull/63182) +* \[[`889854f18b`](https://github.com/nodejs/node/commit/889854f18b)] - **(SEMVER-MINOR)** **src,lib**: implement experimental DTLS API (James M Snell) [#63182](https://github.com/nodejs/node/pull/63182) +* \[[`76538d519c`](https://github.com/nodejs/node/commit/76538d519c)] - **stream**: use webidl validation semantics for args (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`649a4aba93`](https://github.com/nodejs/node/commit/649a4aba93)] - **stream**: ensure that stateful transforms preserve this (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`23220992c0`](https://github.com/nodejs/node/commit/23220992c0)] - **stream**: fix nested async flushing with infinite sources (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`3ad1338139`](https://github.com/nodejs/node/commit/3ad1338139)] - **stream**: ensure from() observes returned rejecting promise correctly (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`cafdee6369`](https://github.com/nodejs/node/commit/cafdee6369)] - **stream**: apply source normalization once at call time (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`edb83cb620`](https://github.com/nodejs/node/commit/edb83cb620)] - **stream**: make consumer signals on longer alter source precedence (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`56bdd765df`](https://github.com/nodejs/node/commit/56bdd765df)] - **stream**: make pipeTo source normalization independent of Writer (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`037c07af2b`](https://github.com/nodejs/node/commit/037c07af2b)] - **stream**: pre-aborted pipeTo now applies dest failure handling (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`7308492d48`](https://github.com/nodejs/node/commit/7308492d48)] - **stream**: ensure pre-existing writes drain before EOF and end() waits (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`756773477b`](https://github.com/nodejs/node/commit/756773477b)] - **stream**: ensure async dispoal after endSync awaits for drain (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`042b951312`](https://github.com/nodejs/node/commit/042b951312)] - **stream**: ensure factory signals remain active through closing (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`2dc09ca9ca`](https://github.com/nodejs/node/commit/2dc09ca9ca)] - **stream**: canWrite and ondrain now reflect physical capacity (James M Snell) [#65658](https://github.com/nodejs/node/pull/65658) +* \[[`9192fbb6dd`](https://github.com/nodejs/node/commit/9192fbb6dd)] - **stream**: skip unobserved 'readable' emission at EOF (Matteo Collina) [#65749](https://github.com/nodejs/node/pull/65749) +* \[[`dc25a61e7a`](https://github.com/nodejs/node/commit/dc25a61e7a)] - **stream**: avoid per-chunk promises in webstream adapters (Matteo Collina) [#65548](https://github.com/nodejs/node/pull/65548) +* \[[`82ae196af6`](https://github.com/nodejs/node/commit/82ae196af6)] - **stream**: address stream/iter review feedback (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`3b37b48592`](https://github.com/nodejs/node/commit/3b37b48592)] - **stream**: replace object sentinel with symbol (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`b6424b6612`](https://github.com/nodejs/node/commit/b6424b6612)] - **stream**: ensure pipeToSync requires synchronous close (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`830fc81acf`](https://github.com/nodejs/node/commit/830fc81acf)] - **stream**: cancel active stream/iter pulls (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`30606908de`](https://github.com/nodejs/node/commit/30606908de)] - **stream**: fix merge settlement tagging and falsy error tracking (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`7319198755`](https://github.com/nodejs/node/commit/7319198755)] - **stream**: ensure full-close semantics when closed (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`bf5acb783f`](https://github.com/nodejs/node/commit/bf5acb783f)] - **stream**: defend against re-entrancy in writev (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`b01594f93a`](https://github.com/nodejs/node/commit/b01594f93a)] - **stream**: ensure stability of stored metadata (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`7bfd81ab09`](https://github.com/nodejs/node/commit/7bfd81ab09)] - **stream**: fixup writer to terminate on consumer return/throw (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`dc2483c38d`](https://github.com/nodejs/node/commit/dc2483c38d)] - **stream**: ensure iterator cleanup on done, reject, etc (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`f6fac6f7f0`](https://github.com/nodejs/node/commit/f6fac6f7f0)] - **stream**: fixup cancelation handling in pull() (James M Snell) [#65652](https://github.com/nodejs/node/pull/65652) +* \[[`dfe0617e71`](https://github.com/nodejs/node/commit/dfe0617e71)] - **stream**: fix early drain after Utf8Stream reopen (Matteo Collina) [#65633](https://github.com/nodejs/node/pull/65633) +* \[[`900fae0a24`](https://github.com/nodejs/node/commit/900fae0a24)] - **test**: split test-bench-cli to try deflaking it (James M Snell) [#65919](https://github.com/nodejs/node/pull/65919) +* \[[`31f4bd73b2`](https://github.com/nodejs/node/commit/31f4bd73b2)] - **test**: make node:bench test samples survive a coarse clock (Shelley Vohr) [#65780](https://github.com/nodejs/node/pull/65780) +* \[[`e0fa1893c3`](https://github.com/nodejs/node/commit/e0fa1893c3)] - **test**: do not dump core in external memory limit test (Shelley Vohr) [#65780](https://github.com/nodejs/node/pull/65780) +* \[[`00ad283b8d`](https://github.com/nodejs/node/commit/00ad283b8d)] - **test**: zero-fill buffers before the string length limit check (Christian Aurich) [#65755](https://github.com/nodejs/node/pull/65755) +* \[[`70763fcc46`](https://github.com/nodejs/node/commit/70763fcc46)] - **test**: expand test coverage of node:bench (James M Snell) [#65631](https://github.com/nodejs/node/pull/65631) +* \[[`5a38e6b115`](https://github.com/nodejs/node/commit/5a38e6b115)] - **(SEMVER-MINOR)** **test**: fix node:bench test timing (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`536ae0e24b`](https://github.com/nodejs/node/commit/536ae0e24b)] - **(SEMVER-MINOR)** **test**: improve node:bench test coverage (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`eec66d529c`](https://github.com/nodejs/node/commit/eec66d529c)] - **(SEMVER-MINOR)** **test**: update bench tests to not fail on no-crypto (James M Snell) [#65606](https://github.com/nodejs/node/pull/65606) +* \[[`ce7b349de5`](https://github.com/nodejs/node/commit/ce7b349de5)] - **test**: bump WPT webidl and interfaces (Filip Skokan) [#65679](https://github.com/nodejs/node/pull/65679) +* \[[`ec96f8dc23`](https://github.com/nodejs/node/commit/ec96f8dc23)] - **test**: update WPT for url to c23755a144 (Node.js GitHub Bot) [#65651](https://github.com/nodejs/node/pull/65651) +* \[[`ebe2ac248b`](https://github.com/nodejs/node/commit/ebe2ac248b)] - **test**: fix recursive fs.watch error fixture (Filip Skokan) [#65683](https://github.com/nodejs/node/pull/65683) +* \[[`a19912ee8f`](https://github.com/nodejs/node/commit/a19912ee8f)] - **test**: update streams WPT (Jeong SeokChan) [#65638](https://github.com/nodejs/node/pull/65638) +* \[[`14b5dfc63d`](https://github.com/nodejs/node/commit/14b5dfc63d)] - **test**: remove `console.log` call in `node_run_list` (Antoine du Hamel) [#65572](https://github.com/nodejs/node/pull/65572) +* \[[`3f67df139c`](https://github.com/nodejs/node/commit/3f67df139c)] - **test**: expect node:ffi to be enabled by default (Matteo Collina) [#65636](https://github.com/nodejs/node/pull/65636) +* \[[`b9ee2e67bf`](https://github.com/nodejs/node/commit/b9ee2e67bf)] - **(SEMVER-MINOR)** **test**: use native builder for legacy SEA tests (Filip Skokan) [#65553](https://github.com/nodejs/node/pull/65553) +* \[[`53a29d0894`](https://github.com/nodejs/node/commit/53a29d0894)] - **test**: use common spawnSync helpers in more tests (greenhead) [#65552](https://github.com/nodejs/node/pull/65552) +* \[[`5c680a50f4`](https://github.com/nodejs/node/commit/5c680a50f4)] - **test**: add coverage for removeEventListener boolean capture (Lazizbek Ergashev) [#65245](https://github.com/nodejs/node/pull/65245) +* \[[`6f99474912`](https://github.com/nodejs/node/commit/6f99474912)] - **test**: deflake fastutf8stream destroy and reopen tests (Christian Aurich) [#65554](https://github.com/nodejs/node/pull/65554) +* \[[`2342b2bf30`](https://github.com/nodejs/node/commit/2342b2bf30)] - **test**: cover Readable.from() sync iterator errors (jakecastelli) [#65515](https://github.com/nodejs/node/pull/65515) +* \[[`1e455a4ffe`](https://github.com/nodejs/node/commit/1e455a4ffe)] - **test**: support inspecting WPTs in child processes (Filip Skokan) [#65510](https://github.com/nodejs/node/pull/65510) +* \[[`1f7cab588e`](https://github.com/nodejs/node/commit/1f7cab588e)] - **test**: document WPT runner workflows (Filip Skokan) [#65510](https://github.com/nodejs/node/pull/65510) +* \[[`d39a902cec`](https://github.com/nodejs/node/commit/d39a902cec)] - **test**: simplify test-worker-heap-profile.js (Donghoon Kang) [#65372](https://github.com/nodejs/node/pull/65372) +* \[[`90d1b863f7`](https://github.com/nodejs/node/commit/90d1b863f7)] - **test**: keep WPT backend checks alive (Filip Skokan) [#65320](https://github.com/nodejs/node/pull/65320) +* \[[`be60c34b52`](https://github.com/nodejs/node/commit/be60c34b52)] - **(SEMVER-MINOR)** **test**: enable multi-global WPTs (Filip Skokan) [#64894](https://github.com/nodejs/node/pull/64894) +* \[[`d04cfcda8e`](https://github.com/nodejs/node/commit/d04cfcda8e)] - **(SEMVER-MINOR)** **test**: add opt-in process WPT runner (Filip Skokan) [#64894](https://github.com/nodejs/node/pull/64894) +* \[[`715c21f805`](https://github.com/nodejs/node/commit/715c21f805)] - **(SEMVER-MINOR)** **test**: accomodate multi-global tests in WPT{Runner,TestSpec,Report} (Filip Skokan) [#64894](https://github.com/nodejs/node/pull/64894) +* \[[`9aa8489a01`](https://github.com/nodejs/node/commit/9aa8489a01)] - **test**: fix lint in dtls tests (Matteo Collina) [#64902](https://github.com/nodejs/node/pull/64902) +* \[[`6a3d8a6d9f`](https://github.com/nodejs/node/commit/6a3d8a6d9f)] - **tls**: read the peer certificate chain without consuming it (Tony Gies) [#65602](https://github.com/nodejs/node/pull/65602) +* \[[`2e519f8fef`](https://github.com/nodejs/node/commit/2e519f8fef)] - **tls,quic**: commonize TLS cert handling between tls, dtls & quic (Tim Perry) [#64711](https://github.com/nodejs/node/pull/64711) +* \[[`2266c14051`](https://github.com/nodejs/node/commit/2266c14051)] - **tools**: add GHA workflow to test vendored Perfetto (Antoine du Hamel) [#65614](https://github.com/nodejs/node/pull/65614) +* \[[`19779f121f`](https://github.com/nodejs/node/commit/19779f121f)] - **tools**: fix the list of globals in ESLint config files (Antoine du Hamel) [#65281](https://github.com/nodejs/node/pull/65281) +* \[[`b57db8dfd3`](https://github.com/nodejs/node/commit/b57db8dfd3)] - **typings**: add types for performance binding (Jungwon Sohn) [#65574](https://github.com/nodejs/node/pull/65574) +* \[[`371e2260f4`](https://github.com/nodejs/node/commit/371e2260f4)] - **typings**: add encodeIntoResults to EncodingBinding (greenhead) [#65350](https://github.com/nodejs/node/pull/65350) +* \[[`417cdd8a77`](https://github.com/nodejs/node/commit/417cdd8a77)] - **typings**: add typing for permission binding (Seungmin Nam) [#65385](https://github.com/nodejs/node/pull/65385) +* \[[`a82cc7097f`](https://github.com/nodejs/node/commit/a82cc7097f)] - **url**: align URLPatternInit dictionary conversion with WebIDL (Piyush Yadav) [#65498](https://github.com/nodejs/node/pull/65498) +* \[[`0e8f7b96b0`](https://github.com/nodejs/node/commit/0e8f7b96b0)] - **util**: canonicalize namespaced tags in inspect() (René) [#63257](https://github.com/nodejs/node/pull/63257) +* \[[`acd2bb10ce`](https://github.com/nodejs/node/commit/acd2bb10ce)] - **v8**: add setHeapProfileNearHeapLimit (Ilyas Shabi) [#64676](https://github.com/nodejs/node/pull/64676) +* \[[`d0535b1041`](https://github.com/nodejs/node/commit/d0535b1041)] - **vfs**: load native addons from a mounted file system (Philipp Dunkel) [#65680](https://github.com/nodejs/node/pull/65680) +* \[[`ea6f70ba41`](https://github.com/nodejs/node/commit/ea6f70ba41)] - **vfs**: fix rename over non-empty directory (Christian Aurich) [#65613](https://github.com/nodejs/node/pull/65613) +* \[[`b4f6b3798a`](https://github.com/nodejs/node/commit/b4f6b3798a)] - **vfs**: add ZipProvider (Philipp Dunkel) [#64915](https://github.com/nodejs/node/pull/64915) +* \[[`5198142c59`](https://github.com/nodejs/node/commit/5198142c59)] - **(SEMVER-MINOR)** **vfs**: integrate with CJS and ESM module loaders (Matteo Collina) [#63653](https://github.com/nodejs/node/pull/63653) +* \[[`38192126e2`](https://github.com/nodejs/node/commit/38192126e2)] - **worker**: start worker threads from the built-in snapshot (Shelley Vohr) [#65336](https://github.com/nodejs/node/pull/65336) +* \[[`56144ea80a`](https://github.com/nodejs/node/commit/56144ea80a)] - **worker**: add ref/unref to web workers (Aviv Keller) [#65507](https://github.com/nodejs/node/pull/65507) +* \[[`7231c13b8a`](https://github.com/nodejs/node/commit/7231c13b8a)] - **(SEMVER-MINOR)** **worker**: add wpt tests for Web Workers (Aviv Keller) [#64894](https://github.com/nodejs/node/pull/64894) +* \[[`5af9d72e7f`](https://github.com/nodejs/node/commit/5af9d72e7f)] - **(SEMVER-MINOR)** **worker**: add support for Web Workers (Aviv Keller) [#64894](https://github.com/nodejs/node/pull/64894) +* \[[`069a1b1200`](https://github.com/nodejs/node/commit/069a1b1200)] - **zlib**: avoid waiting for paused ZIP iterators (Trivikram Kamat) [#65278](https://github.com/nodejs/node/pull/65278) + ## 2026-09-09, Version 26.8.2 (Current), @aduh95 From ecd365e5fe7d053824396237ca412d1e6d534adc Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 16 Sep 2026 20:29:39 +0200 Subject: [PATCH 30/83] src: print exception thrown during primordial initialization Otherwise there's little information for debugging if the initialization fails due to a bug in local development. This should generally only happen during the build process, when the snapshot is built. Signed-off-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/65991 Reviewed-By: Matteo Collina Reviewed-By: Chengzhong Wu Reviewed-By: James M Snell --- src/node_realm.cc | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/node_realm.cc b/src/node_realm.cc index 91bb530dfd94..167264f797fc 100644 --- a/src/node_realm.cc +++ b/src/node_realm.cc @@ -17,6 +17,7 @@ using v8::MaybeLocal; using v8::Object; using v8::SnapshotCreator; using v8::String; +using v8::TryCatch; using v8::Value; Realm::Realm(Environment* env, v8::Local context, Kind kind) @@ -47,11 +48,22 @@ void Realm::CreateProperties() { Local ctx = context(); // Store primordials setup by the per-context script in the environment. - Local per_context_bindings = - GetPerContextExports(ctx, env_->isolate_data()).ToLocalChecked(); - Local primordials = - per_context_bindings->Get(ctx, env_->primordials_string()) - .ToLocalChecked(); + TryCatch try_catch(isolate_); + Local per_context_bindings; + Local primordials; + if (!GetPerContextExports(ctx, env_->isolate_data()) + .ToLocal(&per_context_bindings) || + !per_context_bindings->Get(ctx, env_->primordials_string()) + .ToLocal(&primordials)) { + // In general, this should only throw exceptions during local development + // when there's a temporary bug in the scripts. Print the exception here to + // facilitate debugging. + if (try_catch.HasCaught() && !try_catch.HasTerminated()) { + PrintCaughtException(isolate_, ctx, try_catch); + } + env_->Exit(ExitCode::kBootstrapFailure); + return; + } CHECK(primordials->IsObject()); set_primordials(primordials.As()); From a093ea5a926c984116cc318bcdd3d462682228b3 Mon Sep 17 00:00:00 2001 From: Christian Aurich Date: Wed, 16 Sep 2026 16:34:12 -0300 Subject: [PATCH 31/83] vfs: write RealFSProvider files to open fd Write RealFileHandle contents through the open file descriptor instead of reopening the original real path, mirroring 8cb83126b83 for reads. This keeps writes attached to the opened file across renames and makes them honor the handle's access mode. Preserve iterable support and filehandle.writeFile() current-position semantics. Signed-off-by: Christian Aurich PR-URL: https://github.com/nodejs/node/pull/65885 Refs: https://github.com/nodejs/node/pull/64104 Refs: https://github.com/nodejs/node/issues/64103 Refs: https://github.com/nodejs/node/pull/65854 Reviewed-By: Xuguang Mei Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- lib/internal/vfs/providers/real.js | 100 ++++++++++++-- .../parallel/test-vfs-real-provider-handle.js | 129 +++++++++++++++++- 2 files changed, 216 insertions(+), 13 deletions(-) diff --git a/lib/internal/vfs/providers/real.js b/lib/internal/vfs/providers/real.js index df9bd00ac1ad..2f25f582244c 100644 --- a/lib/internal/vfs/providers/real.js +++ b/lib/internal/vfs/providers/real.js @@ -2,6 +2,7 @@ const { ArrayPrototypePush, + MathMin, Promise, StringPrototypeStartsWith, } = primordials; @@ -11,11 +12,21 @@ const fs = require('fs'); const path = require('path'); const { VirtualProvider } = require('internal/vfs/provider'); const { VirtualFileHandle } = require('internal/vfs/file_handle'); -const { getValidatedPath } = require('internal/fs/utils'); +const { + constants: { kWriteFileMaxChunkSize }, + getOptions, + getValidatedPath, +} = require('internal/fs/utils'); +const { isIterable } = require('internal/streams/utils'); +const { isArrayBufferView } = require('internal/util/types'); const { setOwnProperty } = require('internal/util'); +const { parseFileMode, validateBoolean } = require('internal/validators'); const { - ERR_METHOD_NOT_IMPLEMENTED, -} = require('internal/errors').codes; + AbortError, + codes: { + ERR_METHOD_NOT_IMPLEMENTED, + }, +} = require('internal/errors'); const { createEACCES, createEBADF, @@ -24,6 +35,16 @@ const { const kReadFileUnknownBufferLength = 8192; +function isCustomIterable(obj) { + return isIterable(obj) && !isArrayBufferView(obj) && typeof obj !== 'string'; +} + +function checkAborted(signal) { + if (signal?.aborted) { + throw new AbortError(undefined, { cause: signal.reason }); + } +} + /** * A file handle that wraps a real file descriptor. */ @@ -33,7 +54,6 @@ const kReadFileUnknownBufferLength = 8192; // sync-opened handles can still share one underlying handle for async ops. class RealFileHandle extends VirtualFileHandle { #fd; - #realPath; #checkClosed(syscall) { if (this.closed) { @@ -60,12 +80,10 @@ class RealFileHandle extends VirtualFileHandle { * @param {string} flags The open flags * @param {number} mode The file mode * @param {number} fd The real file descriptor - * @param {string} realPath The real filesystem path */ - constructor(path, flags, mode, fd, realPath) { + constructor(path, flags, mode, fd) { super(path, flags, mode); this.#fd = fd; - this.#realPath = realPath; } readSync(buffer, offset, length, position) { @@ -175,12 +193,72 @@ class RealFileHandle extends VirtualFileHandle { writeFileSync(data, options) { this.#checkClosed('write'); - fs.writeFileSync(this.#realPath, data, options); + fs.writeFileSync(this.#fd, data, options); + } + + // Writes the whole buffer at the descriptor's current position, at most + // kWriteFileMaxChunkSize per call, the way writeFileHandle() does. + async #writeAll(buffer, signal) { + let written = 0; + while (written < buffer.byteLength) { + checkAborted(signal); + const { bytesWritten } = await this.write( + buffer, + written, + MathMin(kWriteFileMaxChunkSize, buffer.byteLength - written), + null); + written += bytesWritten; + } + } + + #fsync() { + this.#checkClosed('fsync'); + return new Promise((resolve, reject) => { + fs.fsync(this.#fd, (err) => { + if (err) reject(err); + else resolve(); + }); + }); } async writeFile(data, options) { this.#checkClosed('write'); - return fs.promises.writeFile(this.#realPath, data, options); + if (!isCustomIterable(data)) { + return new Promise((resolve, reject) => { + fs.writeFile(this.#fd, data, options, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + // The chunks are written one at a time through the descriptor, the way + // writeFileHandle() does. + // `flush` is not part of that: `filehandle.writeFile()` ignores it, but + // reopening the path used to fsync once at the end, so that is kept here + // instead of being multiplied by the number of chunks. + const opts = getOptions(options, { + encoding: 'utf8', + mode: 0o666, + flush: false, + }); + const flush = opts.flush ?? false; + validateBoolean(flush, 'options.flush'); + parseFileMode(opts.mode, 'mode', 0o666); + // An already aborted signal must not end up waiting on a source that + // never yields, so it is read before the first next(). + checkAborted(opts.signal); + + const encoding = opts.encoding || 'utf8'; + for await (const chunk of data) { + await this.#writeAll( + isArrayBufferView(chunk) ? chunk : Buffer.from(chunk, encoding), + opts.signal); + // An abort that arrived while the write was in flight must surface + // before the source is asked for another chunk. + checkAborted(opts.signal); + } + if (flush) await this.#fsync(); } statSync(options) { @@ -335,7 +413,7 @@ class RealFSProvider extends VirtualProvider { openSync(vfsPath, flags, mode) { const realPath = this.#resolvePath(vfsPath); const fd = fs.openSync(realPath, flags, mode); - return new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd, realPath); + return new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd); } async open(vfsPath, flags, mode) { @@ -343,7 +421,7 @@ class RealFSProvider extends VirtualProvider { return new Promise((resolve, reject) => { fs.open(realPath, flags, mode, (err, fd) => { if (err) reject(err); - else resolve(new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd, realPath)); + else resolve(new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd)); }); }); } diff --git a/test/parallel/test-vfs-real-provider-handle.js b/test/parallel/test-vfs-real-provider-handle.js index 50d31470b864..e7b5465a594d 100644 --- a/test/parallel/test-vfs-real-provider-handle.js +++ b/test/parallel/test-vfs-real-provider-handle.js @@ -34,12 +34,134 @@ const myVfs = vfs.create(new vfs.RealFSProvider(root)); assert.strictEqual(handle.statSync().isFile(), true); assert.strictEqual(handle.readFileSync('utf8'), 'zzllo world'); + // Like `filehandle.writeFile()`, this writes from the handle's current + // position rather than replacing the file, so a shorter write over an + // "r+" handle leaves the tail of the old content in place. handle.writeFileSync('replaced'); - assert.strictEqual(handle.readFileSync('utf8'), 'replaced'); + assert.strictEqual(handle.readFileSync('utf8'), 'replacedrld'); myVfs.closeSync(fd); } + // ===== writeFile goes through the file description, not the path ===== + { + fs.writeFileSync(path.join(root, 'renamed-away.txt'), 'aaaaaa'); + const handle = await myVfs.provider.open('/renamed-away.txt', 'r+'); + fs.renameSync(path.join(root, 'renamed-away.txt'), + path.join(root, 'renamed-to.txt')); + + handle.writeFileSync('bb'); + await handle.writeFile('cc'); + await handle.close(); + + assert.strictEqual( + fs.readFileSync(path.join(root, 'renamed-to.txt'), 'utf8'), 'bbccaa'); + assert.strictEqual(fs.existsSync(path.join(root, 'renamed-away.txt')), + false); + } + + // ===== writeFile takes the iterables filehandle.writeFile() takes ===== + { + const handle = await myVfs.provider.open('/iterable.txt', 'w'); + await handle.writeFile(['one ', 'two ']); + await handle.writeFile(async function* () { + yield 'three '; + yield Buffer.from('four'); + }()); + // A chunk that is not a view is converted the way writeFileHandle() does. + await handle.writeFile([[32, 65], Uint8Array.of(66).buffer]); + await handle.close(); + + assert.strictEqual( + fs.readFileSync(path.join(root, 'iterable.txt'), 'utf8'), + 'one two three four AB'); + } + + // ===== options are validated before the source is consumed ===== + { + const handle = await myVfs.provider.open('/opts.txt', 'w'); + + // The signal is read before the first next(), so a source that never + // yields cannot leave the write pending. + const neverYields = { + [Symbol.asyncIterator]: () => ({ next: () => new Promise(() => {}) }), + }; + await assert.rejects( + handle.writeFile(neverYields, { signal: AbortSignal.abort() }), + { name: 'AbortError' }); + + // The rest of `options` is validated there too, so a bad value is + // reported instead of waiting on a source that never produces. + await assert.rejects(handle.writeFile(neverYields, { mode: 'invalid' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + + await handle.close(); + } + + // ===== flush costs one fsync per call, not one per chunk ===== + { + const originalFsync = fs.fsync; + let fsyncs = 0; + fs.fsync = function fsync(...args) { + fsyncs++; + return originalFsync.apply(this, args); + }; + + try { + const handle = await myVfs.provider.open('/flushed.txt', 'w'); + await handle.writeFile(['a', 'b', 'c'], { flush: true }); + await handle.close(); + assert.strictEqual(fsyncs, 1); + assert.strictEqual( + fs.readFileSync(path.join(root, 'flushed.txt'), 'utf8'), 'abc'); + + } finally { + fs.fsync = originalFsync; + } + } + + // ===== an abort landing during a write stops the source ===== + { + const handle = await myVfs.provider.open('/abort-mid.txt', 'w'); + const ac = new AbortController(); + const originalWrite = fs.write; + // Abort as the write settles, which is the window the post-write check + // covers. Without it a source of one chunk resolves successfully. + fs.write = function write(fd, buf, off, len, pos, callback) { + return originalWrite.call(this, fd, buf, off, len, pos, (err, n) => { + ac.abort(); + callback(err, n); + }); + }; + + let pulled = 0; + try { + await assert.rejects(handle.writeFile(async function* () { + pulled++; + yield 'first'; + pulled++; + yield 'second'; + }(), { signal: ac.signal }), { name: 'AbortError' }); + } finally { + fs.write = originalWrite; + await handle.close(); + } + assert.strictEqual(pulled, 1); // The source was not asked for more + } + + // ===== writeFile on a handle that was not opened for writing ===== + { + fs.writeFileSync(path.join(root, 'ronly.txt'), 'untouched'); + const handle = await myVfs.provider.open('/ronly.txt', 'r'); + + assert.throws(() => handle.writeFileSync('x'), { code: 'EBADF' }); + await assert.rejects(handle.writeFile('x'), { code: 'EBADF' }); + await handle.close(); + + assert.strictEqual( + fs.readFileSync(path.join(root, 'ronly.txt'), 'utf8'), 'untouched'); + } + // ===== Async read/write/stat/truncate via provider.open ===== { await myVfs.promises.writeFile('/h2.txt', 'abcdef'); @@ -64,10 +186,13 @@ const myVfs = vfs.create(new vfs.RealFSProvider(root)); assert.ok(handle.readFileSync().length > 0); assert.ok((await handle.readFile()).length > 0); + // Each write starts where the previous one left the handle, so the + // second call appends rather than replacing what the first one wrote. handle.writeFileSync('OVERWRITTEN'); assert.strictEqual(handle.readFileSync('utf8'), 'OVERWRITTEN'); await handle.writeFile('async-overwrite'); - assert.strictEqual(await handle.readFile('utf8'), 'async-overwrite'); + assert.strictEqual(await handle.readFile('utf8'), + 'OVERWRITTENasync-overwrite'); handle.truncateSync(3); await handle.truncate(2); From 6f723ec6e724dba723f03b5c5754267038a664fe Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Wed, 16 Sep 2026 18:22:28 -0400 Subject: [PATCH 32/83] doc: fix crypto changes list sorting Signed-off-by: Chengzhong Wu PR-URL: https://github.com/nodejs/node/pull/66073 Reviewed-By: Luigi Pinca Reviewed-By: Tim Perry Reviewed-By: Antoine du Hamel --- doc/api/crypto.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index b0b61ed52155..cc20fd5c7f7c 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3664,15 +3664,15 @@ operations. The specific constants currently defined are described in + +* `source` {string} A directory or an archive file to mount and run. + +Requires [`--experimental-vfs`][]. May be given at most once. + +Mounts `source` exactly as [`--vfs-mount`][] does, and additionally runs the +entry point and all subsequent `require()`/`import` resolution against that +mount rather than the real file system. The entry point is taken from the mount +the same way `node ` takes one: the mount's own `package.json` +`"main"`, or `index.js`. Any positional command-line argument is the program's +own (available from `process.argv[2]` onward), never an entry-point override. + +`process.argv[1]` reports `source` rather than the reserved mount point, since +the mount point is an opaque implementation detail. + +Mounting the same source twice mounts it twice, at two separate mount points. +The entry point then comes from the mount `--vfs-load` itself contributed, not +from an earlier `--vfs-mount` of the same source. + +In worker threads `--vfs-load` mounts but does not load: a worker inherits the +same mounts, in the same order, and runs its own entry point. + +`--vfs-load` is not permitted in [`NODE_OPTIONS`][]: which entry point runs is +the command line's decision, and the environment must not be able to redirect +it. + +```console +$ node --experimental-vfs --vfs-load=app.zip +$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip +``` + +### `--vfs-mount=source` + + + +* `source` {string} A directory or an archive file to mount. + +Requires [`--experimental-vfs`][]. May be repeated to mount several sources. + +Mounts `source` as a virtual file system ([`node:vfs`][]). Each mount is placed +at a reserved mount point assigned by Node.js, so mounts never shadow real +paths and no target can be chosen. Mounting alone does not change the entry +point; use [`--vfs-load`][] for the source to run from. + +`--vfs-mount` and [`--vfs-load`][] mount in the order they are written, so + +```console +$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c +``` + +mounts `a`, `b` and `c` in that order and runs `b`. Mounts contributed by +[`NODE_OPTIONS`][] are mounted before the command line's. + +The provider backing a source is chosen from the source itself rather than from +its file name: + +* A directory is mounted with a [`RealFSProvider`][] rooted there. +* A file whose bytes are a ZIP archive is mounted with a [`ZipProvider`][], so + an archive can carry any name. + +Providers registered with `vfs.registerProvider()` (typically from a module +preloaded with [`--require`][] or [`--import`][]) are consulted first, in +reverse registration order, and may claim directories as well as files. If no +provider claims the source, Node.js exits with an error. + ### `--watch` + +* `entry` {Object} + * `name` {string} A short identifier, used in diagnostics. + * `canHandle` {Function} Called with the resolved path and its + [`fs.Stats`][]. Returns `true` if this provider should back the source. + * `create` {Function} Called with the resolved path and its [`fs.Stats`][]. + Returns the {VirtualProvider} backing the source. + +Registers a provider that [`--vfs-mount`][] can select for a source it +recognizes, so a file format Node.js has no built-in provider for can still be +mounted. + +A source is claimed by the first provider whose `canHandle()` returns `true`. +Registered providers are consulted before the built-in ones, newest +registration first, and are offered directories as well as files, so a +registered provider can back, wrap, or vet any source. If none claims the +source, the built-in providers handle it: a directory with +[`RealFSProvider`][], and a file whose bytes are a ZIP archive with +[`ZipProvider`][]. + +Providers must be registered before the mounts are created. Register from a +module preloaded with [`--require`][] or [`--import`][]: + +```cjs +// provider.js, preloaded with --require +const fs = require('node:fs'); +const vfs = require('node:vfs'); + +const MAGIC = Buffer.from('CUSTOMFMT'); + +vfs.registerProvider({ + name: 'customfmt', + canHandle(path, stats) { + if (!stats.isFile()) return false; + const head = Buffer.alloc(MAGIC.length); + const fd = fs.openSync(path, 'r'); + try { + fs.readSync(fd, head, 0, MAGIC.length, 0); + } finally { + fs.closeSync(fd); + } + return head.equals(MAGIC); + }, + create(path) { + return new MyCustomProvider(path); + }, +}); +``` + +```console +$ node --experimental-vfs --require ./provider.js \ + --vfs-load archive.customfmt +``` + ## Class: `VirtualFileSystem` + +* `input` {Buffer | ArrayBuffer | TypedArray} The bytes that would be decoded. +* `encoding` {string} The character encoding `input` would be decoded with. + **Default:** `'utf8'`. +* Returns: {integer} + +Returns the length, in UTF-16 code units, of the string that +`buf.toString(encoding)` would produce for the same bytes, without decoding +them. This is the counterpart of [`Buffer.byteLength()`][], which returns the +number of bytes a string would encode to. + +For `'utf8'`, invalid byte sequences are counted as they would be decoded: +each maximal invalid subsequence becomes one `U+FFFD` replacement character. +For every other encoding the result is computed from `input.byteLength` alone. + +A detached `ArrayBuffer`, or a `TypedArray` backed by one, is treated as empty. + +The result is not capped: compare it with +[`buffer.constants.MAX_STRING_LENGTH`][] before decoding to know whether the +decode can succeed at all. A string of `n` code units occupies between `n` and +`2 * n` bytes of memory. + +```mjs +import { Buffer, constants } from 'node:buffer'; + +const buf = Buffer.from('€ 100', 'utf8'); + +console.log(Buffer.stringLength(buf)); +// Prints: 5 +console.log(Buffer.stringLength(buf, 'hex')); +// Prints: 14 +console.log(Buffer.stringLength(buf) <= constants.MAX_STRING_LENGTH); +// Prints: true +``` + +```cjs +const { Buffer, constants } = require('node:buffer'); + +const buf = Buffer.from('€ 100', 'utf8'); + +console.log(Buffer.stringLength(buf)); +// Prints: 5 +console.log(Buffer.stringLength(buf, 'hex')); +// Prints: 14 +console.log(Buffer.stringLength(buf) <= constants.MAX_STRING_LENGTH); +// Prints: true +``` + ### Static method: `Buffer.compare(buf1, buf2)`