diff --git a/README.md b/README.md index 56ea162..78455a8 100644 --- a/README.md +++ b/README.md @@ -295,14 +295,21 @@ const logger = createLogger({ The `Error` class's `message` and `stack` properties [are not enumerable](https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify), so `JSON.stringify(new Error('message'))` returns `'{}'`. -Winston has special handling when an `Error` is the first or second argument to a log call: +Winston lifts `message`, `stack` and `cause` onto the record when an `Error` is the **second** argument to a log call: ```ts -logger.log(new Error('cause')) // { message: 'cause', stack: ... } logger.log('message', new Error('cause')) // { message: 'message cause', stack: ... } ``` -But when errors are nested inside structured log data, `message` and `stack` are lost: +It does nothing of the kind for the other two shapes. + +An `Error` passed **alone** becomes the record itself, and since `message`, `stack` and `name` are not own enumerable properties, any transport that spreads or enumerates it receives none of them: + +```ts +logger.error(new Error('cause')) // { ...info } was { level: 'error' } — no message, no stack +``` + +An `Error` **nested** in structured log data loses `message` and `stack` for the same reason: ```ts try { @@ -312,15 +319,17 @@ try { } ``` -`createLogger` solves this with two complementary mechanisms: +`createLogger` solves both with two complementary mechanisms: -- `serializeErrorFormat` runs at the logger level and walks the log info, replacing any `Error` instance (at any depth) with a plain, JSON-serializable object via the library's `serializeError`. This applies to every transport. +- `serializeErrorFormat` runs at the logger level and walks the log info, replacing any `Error` instance (at any depth, the record itself included) with a plain, JSON-serializable object via the library's `serializeError`. This applies to every transport. When the record is the error, `level` and winston's routing symbols are re-applied to the serialized object so routing still works. - `serializableErrorReplacer` is passed to the Console transport's final `format.json()` as a safety net — [logform](https://github.com/winstonjs/logform) uses [safe-stable-stringify](https://www.npmjs.com/package/safe-stable-stringify), which accepts a replacer, so any `Error` that slips through is still serialised correctly. ```ts format.json({ replacer: serializableErrorReplacer }) ``` +> **Upgrading from 2.0.** A record that is itself an `Error` now reaches transports as a plain object rather than an `Error` instance, so that it carries `name`, `message` and `stack` as ordinary properties. A custom transport that tested `info instanceof Error` should read those properties instead. + To plug in a custom transformation (for example, an `Error`-normalising function previously applied via a custom winston-transport), pass it via `errorSerializer` — it's threaded into both mechanisms: ```ts @@ -331,6 +340,20 @@ const logger = createLogger({ }) ``` +#### `DOMException` + +An `AbortSignal` carries a `DOMException` as its `reason`, and an operation cancelled through one rejects with that reason. A `DOMException` is therefore what a `catch` block receives whenever a fetch, a stream or a job is abandoned on a deadline. It needs no special treatment: log it like any other error. + +```ts +try { + await fetch(url, { signal: AbortSignal.timeout(5_000) }) +} catch (error) { + logger.error('delivery failed', { error }) // { error: { name: 'TimeoutError', message: ..., stack: ... } } +} +``` + +A `DOMException` keeps `message` and `name` as getter-only accessors on its prototype, which makes it the one error type a deep clone cannot rebuild. The library substitutes a serialized object before it clones the log record, using the configured `errorSerializer`, so `redactPaths` handles a `DOMException` like any other value. + For direct format usage, `serializeErrorFormat` accepts the same override and `createSerializableErrorReplacer(serializer)` builds a matching JSON replacer: ```ts diff --git a/package-lock.json b/package-lock.json index 9b35e44..dcff776 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@makerx/node-winston", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makerx/node-winston", - "version": "2.0.2", + "version": "2.0.3", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", @@ -39,7 +39,7 @@ "rimraf": "^6.1.3", "rollup": "4.60.2", "tslib": "^2.8.1", - "tsx": "4.21.0", + "tsx": "4.23.13", "typescript": "^6.0.2", "vitest": "^4.1.4" }, @@ -236,44 +236,10 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -288,9 +254,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -305,9 +271,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -322,9 +288,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -339,9 +305,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -356,9 +322,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -373,9 +339,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -390,9 +356,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -407,9 +373,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -424,9 +390,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -441,9 +407,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -458,9 +424,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -475,9 +441,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -492,9 +458,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -509,9 +475,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -526,9 +492,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -543,9 +509,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -560,9 +526,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -577,9 +543,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -594,9 +560,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -611,9 +577,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -628,9 +594,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -645,9 +611,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -662,9 +628,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -679,9 +645,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -696,9 +662,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -977,29 +943,10 @@ "node": ">=20.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", "dev": true, "license": "MIT", "funding": { @@ -1019,10 +966,27 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", "cpu": [ "arm64" ], @@ -1037,9 +1001,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", "cpu": [ "arm64" ], @@ -1054,9 +1018,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", "cpu": [ "x64" ], @@ -1071,9 +1035,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", "cpu": [ "x64" ], @@ -1088,9 +1052,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", "cpu": [ "arm" ], @@ -1105,13 +1069,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1122,13 +1089,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1139,13 +1109,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1156,13 +1129,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1173,13 +1149,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1190,13 +1169,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1207,9 +1189,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", "cpu": [ "arm64" ], @@ -1223,29 +1205,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", "cpu": [ "arm64" ], @@ -1260,9 +1223,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", "cpu": [ "x64" ], @@ -1277,9 +1240,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -1787,17 +1750,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2134,16 +2086,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -2619,9 +2571,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3425,9 +3377,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3438,32 +3390,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -3870,9 +3822,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "dev": true, "funding": [ { @@ -4132,19 +4084,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4907,10 +4846,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4986,9 +4935,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -5002,23 +4951,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -5037,9 +4986,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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" ], @@ -5058,9 +5007,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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" ], @@ -5079,9 +5028,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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" ], @@ -5100,9 +5049,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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" ], @@ -5121,13 +5070,16 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5142,13 +5094,16 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5163,13 +5118,16 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5184,13 +5142,16 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5205,9 +5166,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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" ], @@ -5226,9 +5187,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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" ], @@ -5550,9 +5511,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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==", "dev": true, "funding": [ { @@ -6113,9 +6074,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -6159,9 +6120,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -6179,7 +6140,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6381,16 +6342,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/rimraf": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", @@ -6422,16 +6373,16 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/rimraf/node_modules/glob": { @@ -6469,14 +6420,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -6485,21 +6436,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" } }, "node_modules/rollup": { @@ -6719,9 +6670,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, "license": "MIT", "engines": { @@ -7270,9 +7221,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -7327,14 +7278,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -7541,17 +7491,17 @@ } }, "node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "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" @@ -7567,7 +7517,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/package.json b/package.json index 0a21664..b079428 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makerx/node-winston", - "version": "2.0.2", + "version": "2.1.0", "private": false, "description": "A set of winston formats, console transport and logger creation functions", "author": "MakerX", @@ -65,7 +65,7 @@ "rimraf": "^6.1.3", "rollup": "4.60.2", "tslib": "^2.8.1", - "tsx": "4.21.0", + "tsx": "4.23.13", "typescript": "^6.0.2", "vitest": "^4.1.4" }, @@ -82,6 +82,6 @@ "yamlify-object-colors": "^1.0.3" }, "overrides": { - "postcss": "^8.5.10" + "postcss": "^8.5.23" } } diff --git a/src/index.spec.ts b/src/index.spec.ts index e6f919f..ea76ed5 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -383,6 +383,139 @@ describe('createLogger mapAuditLevelForOtel', () => { }) }) +describe('createLogger error as the whole record', () => { + // `logger.error(err)` makes the error the record. `message`, `stack` and `name` are not own + // enumerable properties, so a transport that spreads or enumerates it used to receive neither + // a message nor a stack. + it('gives a transport the message, name and stack', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ consoleOptions: { silent: true }, transports: [transport] }) + + logger.error(Object.assign(new TypeError('boom'), { code: 'E_BOOM' }) as unknown as string) + + expect(transport.logs[0]).toMatchObject({ + name: 'TypeError', + message: 'boom', + code: 'E_BOOM', + level: 'error', + }) + expect(transport.logs[0].stack).toContain('TypeError: boom') + }) + + it('still routes at the right level', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ + consoleOptions: { silent: true }, + transports: [transport], + loggerOptions: { level: 'warn' }, + }) + + logger.error(new TypeError('kept') as unknown as string) + logger.info(new TypeError('filtered out') as unknown as string) + + expect(transport.logs.map((l) => l.message)).toEqual(['kept']) + }) + + it('leaves a plain info object alone', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ consoleOptions: { silent: true }, transports: [transport] }) + + logger.log({ level: 'info', message: 'plain', stack: 'a string' }) + + expect(transport.logs[0]).toMatchObject({ message: 'plain', stack: 'a string' }) + }) +}) + +describe('createLogger DOMException', () => { + // An operation cancelled through an `AbortSignal` rejects with a `DOMException`, whose `message` and `name` are + // getter-only prototype accessors. A deep clone cannot rebuild one, so redaction used to throw a + // TypeError out of the log call itself, costing the caller the log line and everything after it. + const aborted = () => new DOMException('the operation timed out', 'TimeoutError') + + it('logs one held in metadata, at any depth', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ + consoleOptions: { silent: true }, + transports: [transport], + redactPaths: ['authorization'], + }) + + logger.error('flat', { error: aborted() }) + logger.error('nested', { context: { error: aborted() } }) + logger.error('in an array', { errors: [aborted()] }) + + expect(transport.logs.length).toBe(3) + expect(transport.logs[0].error).toMatchObject({ name: 'TimeoutError', message: 'the operation timed out' }) + expect(transport.logs[0].error.stack).toBeDefined() + expect(transport.logs[1].context.error).toMatchObject({ name: 'TimeoutError' }) + expect(transport.logs[2].errors[0]).toMatchObject({ name: 'TimeoutError' }) + }) + + // A separate winston code path: with a message and a non-plain second argument it lifts + // `message`, `stack` and `cause` onto a fresh info object and keeps the raw error under `SPLAT`, + // which is where the clone used to find it. + it('logs one passed as the second argument', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ + consoleOptions: { silent: true }, + transports: [transport], + redactPaths: ['authorization'], + }) + + logger.error('delivery failed', aborted() as unknown as string) + + expect(transport.logs[0].message).toBe('delivery failed the operation timed out') + expect(transport.logs[0].stack).toContain('TimeoutError') + }) + + it('logs one passed as the whole record', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ + consoleOptions: { silent: true }, + transports: [transport], + redactPaths: ['authorization'], + }) + + logger.error(aborted() as unknown as string) + + expect(transport.logs[0]).toMatchObject({ name: 'TimeoutError', message: 'the operation timed out' }) + }) + + it('logs one with every logger-level format enabled', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ + consoleOptions: { silent: true }, + transports: [transport], + omitPaths: ['service'], + redactPaths: ['authorization'], + mapAuditLevelForOtel: true, + flatten: true, + }) + + logger.error('everything on', { service: 'svc', authorization: 'Bearer abc', error: aborted() }) + + expect(transport.logs[0].service).toBeUndefined() + expect(transport.logs[0].authorization).toBe('') + expect(transport.logs[0].error).toContain('TimeoutError') + }) + + it('serialises one with a configured errorSerializer, wherever it sits in the record', () => { + const transport = new InMemoryTransport({}) + const logger = createLogger({ + consoleOptions: { silent: true }, + transports: [transport], + redactPaths: ['authorization'], + errorSerializer: (error) => ({ kind: error.name, detail: error.message }), + }) + + logger.error('nested', { error: aborted() }) + logger.error(aborted() as unknown as string) + + expect(transport.logs[0].error).toEqual({ kind: 'TimeoutError', detail: 'the operation timed out' }) + expect(transport.logs[1]).toMatchObject({ kind: 'TimeoutError', detail: 'the operation timed out' }) + }) +}) + class InMemoryTransport extends TransportStream { // eslint-disable-next-line @typescript-eslint/no-explicit-any logs: Record[] diff --git a/src/index.ts b/src/index.ts index b39f656..7b2df00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -177,9 +177,10 @@ export interface CreateLoggerOptions { mapAuditLevelForOtel?: boolean /** - * Custom serializer used whenever an `Error` instance is encountered, both by the logger-level - * `serializeErrorFormat` (walks the full info tree) and by the Console transport's - * `format.json` replacer (safety net for errors that slip through). Defaults to the library's + * Custom serializer used whenever an `Error` instance is encountered: by the logger-level + * `serializeErrorFormat` (walks the full info tree), by `redactFormat` (which substitutes a + * `DOMException` no deep clone can rebuild), and by the Console transport's `format.json` + * replacer (safety net for errors that slip through). Defaults to the library's * `serializeError`, which captures `name`/`message`/`stack`/`code`/`cause`/`errors` even when * non-enumerable, walks own enumerable properties, and is safe against circular references. */ @@ -270,7 +271,11 @@ export function createLogger(options: CreateLoggerOptions): any { const loggerFormats: Format[] = [serializeErrorFormat({ serializer: errorSerializer })] if (mapAuditForOtel) loggerFormats.push(mapAuditLevelForOtel()) if (omitPaths) loggerFormats.push(omitFormat({ paths: omitPaths })) - if (redactPaths) loggerFormats.push(redactFormat({ paths: redactPaths, redactedValue })) + // `errorSerializer` is passed for consistency rather than for effect: `serializeErrorFormat` runs + // first and leaves no live `Error` behind, so redaction has nothing left to substitute here. It + // matters when `redactFormat` is composed on its own, and it keeps the shapes aligned if a future + // path ever does let one through. + if (redactPaths) loggerFormats.push(redactFormat({ paths: redactPaths, redactedValue, errorSerializer })) if (options.loggerOptions?.format) loggerFormats.push(options.loggerOptions.format) // flatten is applied last so all prior transformations are captured in the stringified values if (flatten) loggerFormats.push(jsonStringifyValuesFormat({ replacer: flattenReplacer })) diff --git a/src/redact-format.spec.ts b/src/redact-format.spec.ts new file mode 100644 index 0000000..fa2885e --- /dev/null +++ b/src/redact-format.spec.ts @@ -0,0 +1,37 @@ +import { TransformableInfo } from 'logform' +import { LEVEL } from 'triple-beam' +import { describe, expect, it } from 'vitest' +import { redactFormat } from './redact-format' + +const run = (info: Record, opts: Record) => { + const input = { [LEVEL]: 'info', level: 'info', message: '', ...info } as TransformableInfo + const fmt = redactFormat(opts) + return fmt.transform(input, fmt.options) as unknown as Record +} + +describe('redactFormat', () => { + it('redacts the given paths', () => { + const result = run({ authorization: 'Bearer abc' }, { paths: ['authorization'] }) + expect(result.authorization).toBe('') + }) + + it('honours a custom redactedValue', () => { + const result = run({ token: 'abc' }, { paths: ['token'], redactedValue: '***' }) + expect(result.token).toBe('***') + }) + + // Composed on its own, without `serializeErrorFormat` ahead of it, this is the only thing + // standing between a `DOMException` and a deep clone that cannot rebuild one. + it('substitutes a DOMException with the supplied errorSerializer', () => { + const result = run( + { error: new DOMException('the operation timed out', 'TimeoutError') }, + { paths: ['authorization'], errorSerializer: (error: Error) => ({ kind: error.name }) }, + ) + expect(result.error).toEqual({ kind: 'TimeoutError' }) + }) + + it('falls back to the library serializer when none is supplied', () => { + const result = run({ error: new DOMException('the operation timed out', 'TimeoutError') }, { paths: ['authorization'] }) + expect(result.error).toMatchObject({ name: 'TimeoutError', message: 'the operation timed out' }) + }) +}) diff --git a/src/redact-format.ts b/src/redact-format.ts index 1f4421d..24ca216 100644 --- a/src/redact-format.ts +++ b/src/redact-format.ts @@ -1,8 +1,17 @@ import { TransformableInfo } from 'logform' import { format } from 'winston' import { redactValuesWith } from './redact-values' +import { type ErrorSerializer } from './serialize-error' export const redactFormat = format((info, opts) => { - const { paths, redactedValue = '' } = opts as { paths: string[]; redactedValue?: string } - return redactValuesWith(redactedValue)(info, ...paths) as TransformableInfo + const { + paths, + redactedValue = '', + errorSerializer, + } = opts as { + paths: string[] + redactedValue?: string + errorSerializer?: ErrorSerializer + } + return redactValuesWith(redactedValue, errorSerializer)(info, ...paths) as TransformableInfo }) diff --git a/src/redact-values.spec.ts b/src/redact-values.spec.ts new file mode 100644 index 0000000..9f5b33f --- /dev/null +++ b/src/redact-values.spec.ts @@ -0,0 +1,91 @@ +import { LEVEL } from 'triple-beam' +import { describe, expect, it } from 'vitest' +import { redactValues, redactValuesWith } from './redact-values' + +describe('redactValues', () => { + it('clones a DOMException instead of failing on its getter-only properties', () => { + const aborted = new DOMException('the operation timed out', 'TimeoutError') + + const result = redactValues({ error: aborted }, 'authorization') as { error: Record } + + expect(result.error).toMatchObject({ + name: 'TimeoutError', + message: 'the operation timed out', + stack: aborted.stack, + }) + }) + + it('redacts inside a DOMException', () => { + const aborted = Object.assign(new DOMException('the operation timed out', 'TimeoutError'), { + authorization: 'Bearer secret', + }) + + const result = redactValues({ error: aborted }, 'authorization') as { error: Record } + + expect(result.error.authorization).toBe('') + expect(aborted.authorization).toBe('Bearer secret') + }) + + it('keeps the symbol properties of a DOMException logged as the whole record', () => { + const info = Object.assign(new DOMException('the operation timed out', 'TimeoutError'), { + [LEVEL]: 'error', + level: 'error', + }) + + const result = redactValues(info, 'authorization') as Record + + expect(result).toMatchObject({ name: 'TimeoutError', message: 'the operation timed out', level: 'error' }) + expect(result[LEVEL]).toBe('error') + }) + it('substitutes a DOMException with the supplied serializer', () => { + const aborted = new DOMException('the operation timed out', 'TimeoutError') + const redact = redactValuesWith('', (error) => ({ kind: error.name, detail: error.message })) + + const result = redact({ error: aborted }, 'authorization') as { error: Record } + + expect(result.error).toEqual({ kind: 'TimeoutError', detail: 'the operation timed out' }) + }) + it('does not mutate the record a custom serializer returns', () => { + const record = Object.assign(new DOMException('the operation timed out', 'TimeoutError'), { + [LEVEL]: 'error', + level: 'error', + }) + const redact = redactValuesWith('', (error) => Object.freeze({ kind: error.name })) + + const result = redact(record, 'authorization') as Record + + expect(result).toMatchObject({ kind: 'TimeoutError' }) + expect(result[LEVEL]).toBe('error') + }) + it('does not write into the nested values a serializer returns', () => { + const cached = { context: { authorization: 'secret' } } + const redact = redactValuesWith('', () => cached) + + const result = redact({ error: new DOMException('t', 'TimeoutError') }, 'authorization') as { + error: { context: { authorization: string } } + } + + expect(result.error.context.authorization).toBe('') + expect(cached.context.authorization).toBe('secret') + }) + + it('substitutes a serializer that returns the error it was given', () => { + const aborted = new DOMException('the operation timed out', 'TimeoutError') + const redact = redactValuesWith('', (error) => ({ kind: error.name, original: error })) + + const result = redact({ error: aborted }, 'authorization') as { error: Record } + + expect(result.error.kind).toBe('TimeoutError') + expect(result.error.original).toBe(result.error) + }) + it('redacts a cyclic record instead of overflowing the stack', () => { + const cyclic: Record = { authorization: 'secret' } + cyclic.self = cyclic + + const result = redactValues({ a: cyclic }, 'authorization') as { a: Record } + + expect(result.a.authorization).toBe('') + expect(result.a.self).toBe(result.a) + expect(cyclic.authorization).toBe('secret') + }) +}) diff --git a/src/redact-values.ts b/src/redact-values.ts index b9b8dd9..6c2321b 100644 --- a/src/redact-values.ts +++ b/src/redact-values.ts @@ -1,4 +1,41 @@ -import { cloneDeep, forOwn, get, isNil, isObject, set } from 'es-toolkit/compat' +import { cloneDeepWith, forOwn, get, isNil, isObject, set } from 'es-toolkit/compat' +import { type ErrorSerializer, serializeError } from './serialize-error' + +// A `DOMException` — the reason an `AbortSignal` carries, so what a cancelled operation rejects +// with — cannot survive a deep clone. es-toolkit clones an `Error` with `structuredClone` and then +// re-assigns `message` and `name`, but `structuredClone` rebuilds a `DOMException` as a +// `DOMException`, whose `message` and `name` are getter-only prototype accessors, so the assignment +// throws a `TypeError`. Thrown from inside a format, that `TypeError` comes out of the +// `logger.error(...)` call itself: the caller loses the log line and everything it meant to do +// after it. Substitute the plain object the serializer builds, which holds the same facts and +// clones without complaint. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const cloneForRedaction = (obj: any, serializer: ErrorSerializer) => { + // Each `DOMException` maps to the object standing in for it, so a serializer that puts the error + // back into its own output resolves to that substitute rather than recursing forever. + const substituted = new Map>() + + const substitute = (value: unknown): unknown => { + if (!(value instanceof DOMException)) return undefined + const existing = substituted.get(value) + if (existing) return existing + + const plain: Record = {} + substituted.set(value, plain) + // Deep-cloned rather than spread. Redaction writes into what it is given, and a serializer may + // return a frozen record, a cached one shared between calls, or one holding a reference to the + // error's own properties — sharing any of those would corrupt the original. + Object.assign(plain, cloneDeepWith(serializer(value), substitute)) + // A serializer walks string keys only. Carry own symbols across so a `DOMException` given to + // the logger as the whole record keeps winston's `LEVEL` and `SPLAT` routing symbols. + for (const symbol of Object.getOwnPropertySymbols(value)) { + plain[symbol] = cloneDeepWith((value as unknown as Record)[symbol], substitute) + } + return plain + } + + return cloneDeepWith(obj, substitute) +} // Expands a single path against the current node, supporting `[*]` to iterate every element of an // array segment. Without `[*]` it falls back to lodash-style get/set on a dot path. @@ -30,13 +67,25 @@ const applyPath = (current: unknown, path: string, redactedValue: string) => { * - a dot-separated path (`user.email`) — uses es-toolkit/compat's get/set * - a path with `[*]` wildcards (`files[*].name`, `users[*].addresses[*].zip`, `tags[*]`) — iterates each element of the array at that segment * Key checks are applied at every level of the object via recursion. + * @param errorSerializer Used to substitute a `DOMException`, which no deep clone can rebuild. + * Defaults to the library's {@link serializeError}. `createLogger` passes whatever `errorSerializer` + * it was given, so a `DOMException` reaches the transports in the same shape as every other error. * @returns A new object with the specified keys redacted */ export const redactValuesWith = - (redactedValue: string) => + (redactedValue: string, errorSerializer: ErrorSerializer = serializeError) => // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, ...keys: string[]) => { + // A clone of a cyclic record is cyclic too, and the walk below would never return — a + // RangeError out of the log call, which is the one thing redaction must not cause. Visiting a + // node once is enough whether it was reached through a cycle or shared by two branches: + // redaction writes the same value either way. + const seen = new WeakSet() return (function redact(current) { + if (isObject(current)) { + if (seen.has(current)) return current + seen.add(current) + } for (const k of keys) { applyPath(current, k, redactedValue) } @@ -45,7 +94,7 @@ export const redactValuesWith = if (isObject(value)) redact(value) }) return current - })(cloneDeep(obj)) + })(cloneForRedaction(obj, errorSerializer)) } export const redactValues = redactValuesWith('') diff --git a/src/serialize-error-format.spec.ts b/src/serialize-error-format.spec.ts index b4f46ba..ac544e1 100644 --- a/src/serialize-error-format.spec.ts +++ b/src/serialize-error-format.spec.ts @@ -1,5 +1,5 @@ import { TransformableInfo } from 'logform' -import { LEVEL } from 'triple-beam' +import { LEVEL, SPLAT } from 'triple-beam' import { describe, expect, it } from 'vitest' import { serializeErrorFormat, SerializeErrorFormatOptions } from './serialize-error-format' @@ -73,4 +73,189 @@ describe('serializeErrorFormat', () => { expect(result.a).toEqual({ value: 42 }) expect(result.b).toEqual({ value: 42 }) }) + + it('serialises errors held under the SPLAT symbol', () => { + const error = new Error('boom') + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [{ error }] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + + expect(result[SPLAT][0].error).not.toBeInstanceOf(Error) + expect(result[SPLAT][0].error.message).toBe('boom') + }) + + it('leaves a SPLAT holding no errors alone', () => { + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: ['a', 1] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + + expect(result[SPLAT]).toEqual(['a', 1]) + }) + it('leaves non-error splat arguments as they are, so format.splat() can interpolate them', () => { + const date = new Date('2020-01-02T03:04:05Z') + const nested = { when: date, n: 1 } + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [date, nested] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + + // Same references: rebuilt as plain records these hold no own enumerable keys, and `%j` would + // render `{}` in place of the date. + expect(result[SPLAT][0]).toBe(date) + expect(result[SPLAT][1]).toBe(nested) + }) + + it('rebuilds only the splat branch that holds an error', () => { + const untouched = { n: 1 } + const input = { + [LEVEL]: 'info', + level: 'info', + message: '', + [SPLAT]: [untouched, { error: new Error('boom'), when: new Date('2020-01-02T03:04:05Z') }], + } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record[]> + + expect(result[SPLAT][0]).toBe(untouched) + expect(result[SPLAT][1].error).toMatchObject({ message: 'boom' }) + expect(result[SPLAT][1].when).toBeInstanceOf(Date) + }) + it('replaces a record that is itself an error, keeping level and the routing symbols', () => { + const error = Object.assign(new TypeError('boom'), { [LEVEL]: 'error', level: 'error' }) + const fmt = serializeErrorFormat() + + const result = fmt.transform(error as unknown as TransformableInfo, fmt.options) as unknown as Record + + expect(result).not.toBeInstanceOf(Error) + expect(result).toMatchObject({ name: 'TypeError', message: 'boom', level: 'error' }) + expect(result.stack).toBeDefined() + expect(result[LEVEL]).toBe('error') + }) + + it('serialises a record that is itself an error with the configured serializer', () => { + const error = Object.assign(new TypeError('boom'), { [LEVEL]: 'error', level: 'error' }) + const fmt = serializeErrorFormat({ serializer: (e: Error) => ({ kind: e.name }) }) + + const result = fmt.transform(error as unknown as TransformableInfo, fmt.options) as unknown as Record + + expect(result).toMatchObject({ kind: 'TypeError', level: 'error' }) + expect(result[LEVEL]).toBe('error') + }) + + it('leaves a plain info object that merely looks like a log record alone', () => { + const result = run({ message: 'not an error', stack: 'a string' }) + expect(result).toMatchObject({ message: 'not an error', stack: 'a string' }) + }) + it('rebuilds a cyclic splat branch so no live error is reachable through the cycle', () => { + const branch: Record = { error: new Error('boom') } + branch.self = branch + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [branch] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record[]> + const walked = result[SPLAT][0] as unknown as { error: unknown; self: { error: unknown } } + + expect(walked.error).not.toBeInstanceOf(Error) + expect(walked.self).toBe(walked) + expect(walked.self.error).not.toBeInstanceOf(Error) + }) + + it('keeps a shared splat reference shared', () => { + const shared = { error: new Error('boom') } + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [{ a: shared, b: shared }] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + + expect(result[SPLAT][0].a).toBe(result[SPLAT][0].b) + }) + + it('does not mutate the record a custom serializer returns', () => { + const error = Object.assign(new TypeError('boom'), { [LEVEL]: 'error', level: 'error' }) + const fmt = serializeErrorFormat({ serializer: (e: Error) => Object.freeze({ kind: e.name }) }) + + const result = fmt.transform(error as unknown as TransformableInfo, fmt.options) as unknown as Record + + expect(result).toMatchObject({ kind: 'TypeError', level: 'error' }) + expect(result[LEVEL]).toBe('error') + }) + it('leaves a cyclic splat argument holding no error completely alone', () => { + const marker = Symbol('marker') + const cyclic: Record = { n: 1, [marker]: 'kept' } + cyclic.self = cyclic + Object.defineProperty(cyclic, 'hidden', { value: 'kept', enumerable: false }) + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [cyclic] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + const walked = result[SPLAT][0] as Record + + expect(walked).toBe(cyclic) + expect(walked[marker]).toBe('kept') + expect(walked.hidden).toBe('kept') + }) + + it('replaces only the argument that holds an error', () => { + const untouched: Record = { n: 1 } + untouched.self = untouched + const input = { + [LEVEL]: 'info', + level: 'info', + message: '', + [SPLAT]: [untouched, { error: new Error('boom') }], + } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record[]> + + expect(result[SPLAT][0]).toBe(untouched) + expect(result[SPLAT][1].error).toMatchObject({ message: 'boom' }) + }) + it('keeps the symbols and non-enumerable properties of a rebuilt splat argument', () => { + const marker = Symbol('marker') + const argument: Record = { error: new Error('boom'), [marker]: 'kept' } + Object.defineProperty(argument, 'hidden', { value: 'kept', enumerable: false }) + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [argument] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + const walked = result[SPLAT][0] as Record + + expect(walked).not.toBe(argument) + expect(walked.error).not.toBeInstanceOf(Error) + expect(walked[marker]).toBe('kept') + expect(walked.hidden).toBe('kept') + expect(Object.getOwnPropertyDescriptor(walked, 'hidden')?.enumerable).toBe(false) + }) + + it('keeps the prototype of a rebuilt null-prototype splat argument', () => { + const argument = Object.assign(Object.create(null), { error: new Error('boom') }) as object + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [argument] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + + expect(Object.getPrototypeOf(result[SPLAT][0])).toBeNull() + }) + + it('keeps the extra properties of a rebuilt splat array', () => { + const marker = Symbol('marker') + const argument = [new Error('boom')] as unknown[] & Record + argument.extra = 'kept' + argument[marker] = 'kept' + const input = { [LEVEL]: 'info', level: 'info', message: '', [SPLAT]: [argument] } as TransformableInfo + const fmt = serializeErrorFormat() + + const result = fmt.transform(input, fmt.options) as unknown as Record + const walked = result[SPLAT][0] as unknown[] & Record + + expect(Array.isArray(walked)).toBe(true) + expect(walked.length).toBe(1) + expect(walked[0]).not.toBeInstanceOf(Error) + expect(walked.extra).toBe('kept') + expect(walked[marker]).toBe('kept') + }) }) diff --git a/src/serialize-error-format.ts b/src/serialize-error-format.ts index df7cdee..126d56b 100644 --- a/src/serialize-error-format.ts +++ b/src/serialize-error-format.ts @@ -1,4 +1,5 @@ import { TransformableInfo } from 'logform' +import { SPLAT } from 'triple-beam' import { format } from 'winston' import { ErrorSerializer, serializeError } from './serialize-error' @@ -10,14 +11,146 @@ export interface SerializeErrorFormatOptions { serializer?: ErrorSerializer } +const isPlainObject = (value: object) => { + const prototype = Object.getPrototypeOf(value) as unknown + return prototype === Object.prototype || prototype === null +} + +/** + * Substitutes serialized errors inside the raw splat arguments, and changes nothing else. + * + * The main walk rebuilds every object it visits as a plain record, which is what metadata bound for + * a transport needs. `SPLAT` is not metadata: `format.splat()` interpolates it into the message, so + * a `Date` or a `Map` has to arrive as itself — rebuilt as a plain record it holds no own + * enumerable keys, and `%j` would render `{}` in place of its value. So this recurses through + * arrays and plain objects only, and rebuilds one only when it really holds an error. Anything + * untouched comes back as the same reference, and a container that must be rebuilt keeps its + * prototype and every own key the rebuild did not replace. + * + * `rebuilt` maps each source container to its replacement, so a back-edge resolves to the + * replacement rather than to the unprocessed source — otherwise a rebuilt branch would link back to + * the original and leave a live error reachable through the cycle. A container reached twice + * without a cycle resolves to the same replacement both times, which keeps shared references + * shared. + */ +/** + * Whether an error is reachable from a splat argument, so {@link replaceErrors} runs only where it + * has something to do. + * + * A container is marked `false` while it is being examined, which cuts cycles: going back round one + * reaches nothing the walk is not already looking at. That makes an in-progress entry pessimistic + * for a sibling asked later, so this is only ever asked about a whole argument — the node holding + * the error always finishes examining its own keys, and the answer for the argument is right. + */ +const holdsError = (value: unknown, seen: Map): boolean => { + if (value instanceof Error) return true + if (!value || typeof value !== 'object') return false + const known = seen.get(value) + if (known !== undefined) return known + if (!Array.isArray(value) && !isPlainObject(value)) return false + + seen.set(value, false) + const source = value as Record + const found = Array.isArray(value) + ? value.some((entry) => holdsError(entry, seen)) + : Object.keys(source).some((key) => holdsError(source[key], seen)) + seen.set(value, found) + return found +} + +/** + * Carries across every own key the rebuild did not visit — symbols, non-enumerable properties, an + * array's non-index properties — so a container rebuilt to replace an error inside it keeps + * everything the replacement did not touch. `length` is skipped: an array manages its own. + */ +const carryOverUnvisited = (out: T, source: object, visited: Set): T => { + for (const key of Reflect.ownKeys(source)) { + if (visited.has(key)) continue + if (Array.isArray(source) && key === 'length') continue + Object.defineProperty(out, key, Object.getOwnPropertyDescriptor(source, key) as PropertyDescriptor) + } + return out +} + +const replaceErrors = (value: unknown, serializer: ErrorSerializer, rebuilt: Map): unknown => { + if (value instanceof Error) return serializer(value) + if (!value || typeof value !== 'object') return value + if (rebuilt.has(value)) return rebuilt.get(value) + if (!Array.isArray(value) && !isPlainObject(value)) return value + + const visited = new Set() + + if (Array.isArray(value)) { + const out: unknown[] = [] + rebuilt.set(value, out) + let replaced = false + for (let index = 0; index < value.length; index++) { + out[index] = replaceErrors(value[index], serializer, rebuilt) + visited.add(String(index)) + if (out[index] !== value[index]) replaced = true + } + // `replaced` is true whenever a back-edge was taken, because the replacement it resolved to is + // not the source. So an unreplaced container never has one pointing at its discarded copy. + if (!replaced) rebuilt.set(value, value) + return replaced ? carryOverUnvisited(out, value, visited) : value + } + + const source = value as Record + // Built on the source's own prototype, so a null-prototype argument stays one. + const out = Object.create(Object.getPrototypeOf(value) as object | null) as Record + rebuilt.set(value, out) + let replaced = false + for (const key of Object.keys(source)) { + out[key] = replaceErrors(source[key], serializer, rebuilt) + visited.add(key) + if (out[key] !== source[key]) replaced = true + } + if (!replaced) rebuilt.set(value, value) + return replaced ? carryOverUnvisited(out, value, visited) : value +} + +/** + * Replaces the record itself when it is an `Error`. + * + * `logger.error(err)` takes a winston branch of its own: it assigns `level` and the routing symbols + * onto the error and writes the error as the record. `message`, `stack` and `name` are not own + * enumerable properties of an `Error`, so every transport that spreads or enumerates the record + * loses them — `{ ...info }` yields neither a message nor a stack. Serializing lifts them onto a + * plain object. + * + * `level` and every own symbol are re-applied afterwards: they are winston's routing, not error + * data, and a custom serializer has no reason to return them. `message` is left to the serializer, + * so one that drops it produces a record without one, exactly as it already does for a nested + * error. + */ +const serializeRecord = (error: Error, serializer: ErrorSerializer): Record => { + // Copied, never mutated in place: a serializer is free to return a frozen record, or a cached one + // shared between calls, and stamping routing onto either would throw or leak. + const record = { ...serializer(error) } as Record + // Guarded because `serializeErrorFormat` is also usable outside `createLogger`, on a record + // winston has not stamped a level onto. + if ('level' in error) record.level = (error as unknown as { level: unknown }).level + for (const symbol of Object.getOwnPropertySymbols(error)) { + record[symbol] = (error as unknown as Record)[symbol] + } + return record +} + /** * Walks the log info object, replacing any `Error` instances (including nested ones) * with the plain-object result of the configured serializer so downstream formats and * transports see JSON-serializable errors with `message` and `stack` intact. * - * Only the top-level `info` object is mutated (to preserve winston's Symbol-keyed + * A record that is itself an `Error` is replaced outright: see {@link serializeRecord}. Otherwise + * only the top-level `info` object is mutated (to preserve winston's Symbol-keyed * routing props); nested objects and arrays are rebuilt, so caller-supplied metadata * references are never mutated. + * + * Errors under the `SPLAT` symbol are replaced too. Winston keeps the raw metadata argument there + * in addition to merging its properties onto `info`, so an `Error` logged as + * `logger.error(msg, { error })` is reachable twice, and serializing only the string keys leaves + * the live `Error` under `SPLAT` for every later format to trip over. `SPLAT` is treated more + * gently than the rest of the record: see {@link replaceErrors}. */ export const serializeErrorFormat = format((info, opts) => { const serializer = (opts as SerializeErrorFormatOptions | undefined)?.serializer ?? serializeError @@ -36,8 +169,17 @@ export const serializeErrorFormat = format((info, opts) => { seen.delete(value) } } - const record = info as unknown as Record + const record = (info instanceof Error ? serializeRecord(info, serializer) : info) as unknown as Record const seen = new WeakSet([record]) for (const key of Object.keys(record)) record[key] = walk(record[key], seen) - return info as TransformableInfo + const splat = record[SPLAT] + if (Array.isArray(splat)) { + // Gated per argument. A cyclic container always takes a back-edge to its replacement, which + // counts as a change, so without this an argument holding no error at all would be rebuilt — + // losing its identity and any symbol or non-enumerable property with it. + const rebuilt = new Map() + const next = splat.map((value) => (holdsError(value, new Map()) ? replaceErrors(value, serializer, rebuilt) : value)) + if (next.some((value, index) => value !== splat[index])) record[SPLAT] = next + } + return record as unknown as TransformableInfo })