From d98a1023ac349ea547b6e2effa15a157e19199f0 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 14:59:21 -0600 Subject: [PATCH 01/51] logger: replace Winston with in-house logger impl --- packages/logger/CHANGELOG.md | 8 + packages/logger/README.md | 67 ++++--- packages/logger/package-lock.json | 315 +----------------------------- packages/logger/package.json | 7 +- packages/logger/src/config.ts | 95 +++------ packages/logger/src/format.ts | 199 +++++++------------ packages/logger/src/index.ts | 23 +-- packages/logger/src/logger.ts | 282 +++++++++++++++----------- packages/logger/src/plugins.ts | 14 +- 9 files changed, 334 insertions(+), 676 deletions(-) diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 45a7b1793e..519cc5b6c2 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.0.0] + +### BREAKING CHANGES + +- Removed Winston and its transport/format APIs. +- Loggers now emit structured JSON directly and expose the package-owned `Logger` type. +- The supported sinks are stdout and custom writable Node streams. + ## [3.0.0] - 2026-05-27 ### BREAKING CHANGES diff --git a/packages/logger/README.md b/packages/logger/README.md index 37799a6aa4..38dd36129c 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -1,48 +1,63 @@

@saasquatch/logger

-

Thin wrapper around Winston to provide unified logging across SaaSquatch NodeJS services

-

- NPM version - NPM downloads -

+

Small structured JSON logger for SaaSquatch NodeJS services

-The SaaSquatch logging package facilitates unified structured logging across all -NodeJS-based SaaSquatch services. It wraps Winston and provides standard log formats for -general logging, debugging, and HTTP services. +The package provides a lightweight structured logger that writes newline-delimited JSON to stdout by default. It supports syslog-style levels, named loggers, custom writable stream sinks, and Express HTTP logging middleware. -## Getting Started - -Install the package from NPM: +## Getting started ```bash npm install @saasquatch/logger ``` -Basic usage: - ```typescript -import { initializeLogger, getLogger } from "@saasquatch/logger"; +import { initializeLogger } from "@saasquatch/logger"; + +const logger = initializeLogger(); +logger.info("Hello", { service: "example" }); +logger.error(new Error("Something failed")); + +// Child fields are included in every record emitted by the child. +const requestLogger = logger.child({ requestId: "req-123" }); +requestLogger.info("Request started"); + +// Collect records for a response, while still writing them to the sink. +requestLogger.startLogCollection({ maxEntries: 100 }); +requestLogger.info("Request finished"); +const logs = requestLogger.getCollectedLogs(); +requestLogger.stopLogCollection(); +requestLogger.clearCollectedLogs(); +``` -function main() { - const logger = initializeLogger(); +Collection retains the newest 1,000 records by default. `maxEntries` can be set when +starting collection; it must be a positive integer. Collection is independent for +each logger and child logger. - logger.info("Hello"); - otherFunction(); -} +Each enabled call writes one JSON record. The default minimum level is `info`; it can also be configured with `SSQT_LOG_LEVEL`. -function otherFunction() { - getLogger()!.info("World!"); -} +### Custom stream sinks -main(); +Use a writable Node stream in addition to, or instead of, stdout: + +```typescript +import { createWriteStream } from "node:fs"; +import { initializeLogger } from "@saasquatch/logger"; + +const logger = initializeLogger({ + transports: [{ type: "stream", stream: createWriteStream("service.log") }], +}); ``` -Express HTTP logging middleware: +The supported transport types are `console` and `stream`. File, HTTP, and Winston transport configuration are not part of this API. + +### Express HTTP logging ```typescript import express from "express"; import { httpLogMiddleware, initializeLogger } from "@saasquatch/logger"; -const server = express(); +const app = express(); const logger = initializeLogger(); -server.use(httpLogMiddleware(logger)); +app.use(httpLogMiddleware(logger)); ``` + +HTTP records include normalized method, URL, status, response time, and request ID fields. Sensitive URL query parameters are removed by the middleware. diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index 495a36fd51..ffc1c7ce58 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.0", + "version": "4.0.0", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", @@ -17,8 +17,7 @@ "node": "^24 || ^26" }, "peerDependencies": { - "express": "^4", - "winston": "^3" + "express": "^4" }, "peerDependenciesMeta": { "express": { @@ -26,39 +25,6 @@ } } }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "peer": true, - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -177,236 +143,6 @@ "@types/node": "*" } }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT", - "peer": true - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT", - "peer": true - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT", - "peer": true - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT", - "peer": true - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT", - "peer": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT", - "peer": true - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT", - "peer": true - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -427,51 +163,6 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "peer": true - }, - "node_modules/winston": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", - "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "peer": true, - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } } } } diff --git a/packages/logger/package.json b/packages/logger/package.json index 2c249d7606..2864f07bb4 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,8 +1,8 @@ { "name": "@saasquatch/logger", - "version": "3.0.0", + "version": "4.0.0", "type": "module", - "description": "Thin wrapper around Winston to provide unified logging across SaaSquatch NodeJS services", + "description": "Small structured JSON logger for SaaSquatch NodeJS services", "private": false, "publishConfig": { "access": "public", @@ -39,8 +39,7 @@ "typescript": "^6.0.3" }, "peerDependencies": { - "express": "^4", - "winston": "^3" + "express": "^4" }, "peerDependenciesMeta": { "express": { diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 83db277baa..44526ee39d 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -1,21 +1,6 @@ import type { Writable } from "node:stream"; -import winston from "winston"; -/** - * Numerical Code | Severity - * 0 | Emergency: system is unusable - * 1 | Alert: action must be taken immediately - * 2 | Critical: critical conditions - * 3 | Error: error conditions - * 4 | Warning: warning conditions - * 5 | Notice: normal but significant condition - * 6 | Informational: informational messages - * 7 | Debug: debug-level messages - * - * Table 2. Syslog Message Severities - * RFC 5424 - * https://www.rfc-editor.org/rfc/rfc5424#section-6.2.1 - */ +/** Syslog-compatible logging levels, from most to least severe. */ export const LOG_LEVELS = [ "emerg", "alert", @@ -30,82 +15,48 @@ export const LOG_LEVELS = [ export type LogLevel = (typeof LOG_LEVELS)[number]; -export type ConsoleTransport = { - type: "console"; - options?: winston.transports.ConsoleTransportOptions; -}; - -export type FileTransport = { - type: "file"; - options?: winston.transports.FileTransportOptions; +export const LOG_LEVEL_VALUES: Record = { + emerg: 0, + alert: 1, + crit: 2, + error: 3, + warning: 4, + warn: 4, + notice: 5, + info: 6, + debug: 7, }; -export type HttpTransport = { - type: "http"; - options?: winston.transports.HttpTransportOptions; +export type ConsoleTransport = { + type: "console"; }; export type StreamTransport = { type: "stream"; stream: Writable; - options?: Omit; }; -export type Transport = - | ConsoleTransport - | FileTransport - | HttpTransport - | StreamTransport; +export type Transport = ConsoleTransport | StreamTransport; export type LoggerConfig = { - /** - * The log level. Defaults to "info" - */ + /** Minimum severity to emit. Defaults to `info`. */ logLevel: LogLevel; - - /** - * The NodeJS environment. Defaults to `process.env.NODE_ENV` - */ + /** Retained for compatibility and future output policies. */ environment: string; - - /** - * The list of transports to log to. Defaults to a single Console - * transport. - */ + /** Outputs for log records. Defaults to stdout. */ transports: Transport[]; }; -/** - * Retrieve the default configuration based on sane defaults - * and optional environment variables - * - * @return {LoggerConfig} The default logging configuration - */ export function defaultConfig(): LoggerConfig { - let transports: Transport[] = [{ type: "console" }]; - - if (process.env["SSQT_LOG_TRANSPORTS"] !== undefined) { - try { - transports = JSON.parse(process.env["SSQT_LOG_TRANSPORTS"]); - } catch (e) { - if (e instanceof Error) { - console.error(e.message); - } - - throw new Error( - "Failed to parse log transports from environment variable", - ); - } - } + const transports: Transport[] = [{ type: "console" }]; + const configuredLevel = process.env["SSQT_LOG_LEVEL"]; let logLevel: LogLevel = "info"; - const logEnv = process.env["SSQT_LOG_LEVEL"]; - if (logEnv !== undefined) { - if (LOG_LEVELS.includes(logEnv as LogLevel)) { - logLevel = logEnv as LogLevel; - } else { - throw new Error(`Invalid log level "${logEnv}"`); + if (configuredLevel !== undefined) { + if (!LOG_LEVELS.includes(configuredLevel as LogLevel)) { + throw new Error(`Invalid log level "${configuredLevel}"`); } + logLevel = configuredLevel as LogLevel; } return { diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 30941543bb..fc86ba7282 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -1,143 +1,92 @@ -import winston from "winston"; +import type { LogLevel } from "./config.ts"; import { DEFAULT_LOGGER_NAME, LOG_TYPE_MARKER } from "./logger.ts"; type HTTPMessage = { method: string; - status: string; - time: BigInt | string; + status: string | number; + time: bigint | string | number; url: string; requestId?: string; }; -/** - * JSON format logs to be consumed by upstream log processors - */ -export function jsonFormat(name: string): winston.Logform.Format { - return winston.format.combine( - winston.format.timestamp(), - winston.format.splat(), - addLoggerName(name)(), - prefixTenantAlias(), - formatHttpLog(), - cleanLogTypeMarker(), - winston.format.uncolorize(), - dataDogStatus(), - winston.format.json(), - ); -} - -/** - * Human-readable log format for consoles - */ -export function prettyFormat(name: string): winston.Logform.Format { - return winston.format.combine( - winston.format.timestamp(), - winston.format.splat(), - addLoggerName(name)(), - prefixTenantAlias(), - formatHttpLog(), - cleanLogTypeMarker(), - winston.format.colorize(), - dataDogStatus(), - winston.format.simple(), - prettyDevFormat, - ); -} - -/** - * Human-readable HTTP information to be placed in the - * `message` field of the log - */ -const friendlyHttpFormat = (message: HTTPMessage): string => { - return [ - message.status, - message.method, - message.time.toString().padStart(6, " "), - message.url, - ].join(" "); +export type LogRecord = Record & { + level: LogLevel; + timestamp: string; }; -/** - * Place the HTTP information in the correct - * fields for Datadog to consume - */ -const formatHttpLog = winston.format((info) => { - const newInfo = { ...info }; - if (newInfo[LOG_TYPE_MARKER] === "HTTP") { - // NOTE: it would of course be safer to validate this using a real - // schema validator like zod, but for a logging library we just - // don't want that kind of overhead. The HTTP log messages are generated - // using middleware from our own package so it's unlikely to be invalid - const message = newInfo.message as any as HTTPMessage; - - const micros = Number(message.time); - if (micros < 1000) { - message.time = `${micros} μs`; - } else { - message.time = `${Math.round(micros / 1000)} ms`; - } - - newInfo.message = friendlyHttpFormat(message); - newInfo["http.url"] = message.url; - newInfo["http.method"] = message.method; - newInfo["http.status_code"] = message.status; - newInfo["http.response_time"] = micros; - - if (message.requestId) { - newInfo["http.request_id"] = message.requestId; +/** Build the JSON record emitted by the logger. */ +export function formatRecord( + name: string, + level: LogLevel, + message: unknown, + fields: Record = {}, +): LogRecord { + const record: Record = { + ...fields, + level, + timestamp: new Date().toISOString(), + message, + }; + + if (name !== DEFAULT_LOGGER_NAME) { + record["logger.name"] = name; + } + if (record["tenantAlias"] && typeof record["message"] === "string") { + const alias = String(record["tenantAlias"]); + if (!(record["message"] as string).startsWith(`[${alias}]`)) { + record["message"] = `[${alias}] ${record["message"]}`; } } - return newInfo; -}); - -/** - * The `status` field of the JSON is reserved in Datadog for the severity - * level of the log. - */ -const dataDogStatus = winston.format((info) => { - return { ...info, status: info.level }; -}); - -/** - * Simple human-readable log format - */ -const prettyDevFormat = winston.format.printf((info) => { - const newInfo = { ...info }; - const message = newInfo.message; - if (typeof message === "object") { - newInfo.message = JSON.stringify(newInfo.message); + if (record[LOG_TYPE_MARKER] === "HTTP") { + formatHttpRecord(record); } + delete record[LOG_TYPE_MARKER]; + record["status"] = level; + return record as LogRecord; +} - return `[${newInfo.level}] ${newInfo.message}`; -}); - -/** - * If the logger has a custom non-default name, populate the - * `logger.name` field - */ -const addLoggerName = (name: string) => - name !== DEFAULT_LOGGER_NAME - ? winston.format((info) => ({ ...info, "logger.name": name })) - : winston.format((info) => info); - -const prefixTenantAlias = winston.format((info) => { - if ( - info["tenantAlias"] && - typeof info.message === "string" && - !info.message.startsWith(`[${info["tenantAlias"]}]`) - ) { - return { ...info, message: `[${info["tenantAlias"]}] ${info.message}` }; +function formatHttpRecord(record: Record): void { + const value = record["message"]; + if (value === null || typeof value !== "object") { + return; } + const message = value as unknown as HTTPMessage; + const micros = Number(message.time); + const displayTime = + micros < 1000 ? `${micros} μs` : `${Math.round(micros / 1000)} ms`; - return info; -}); + record["message"] = [ + message.status, + message.method, + displayTime.padStart(6, " "), + message.url, + ].join(" "); + record["http.url"] = message.url; + record["http.method"] = message.method; + record["http.status_code"] = message.status; + record["http.response_time"] = micros; + if (message.requestId) { + record["http.request_id"] = message.requestId; + } +} -/** - * Clean up the log type marker which is only used for some of the other - * formatting middleware - */ -const cleanLogTypeMarker = winston.format((info) => { - delete info[LOG_TYPE_MARKER]; - return info; -}); +/** Safely serialize records, including Error and BigInt values. */ +export function serializeRecord(record: LogRecord): string { + const seen = new WeakSet(); + return JSON.stringify(record, (_key, value: unknown) => { + if (typeof value === "bigint") { + return value.toString(); + } + if (value instanceof Error) { + return { name: value.name, message: value.message, stack: value.stack }; + } + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return "[Circular]"; + } + seen.add(value); + } + return value; + }); +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index dec3c827c4..ab397c8376 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,48 +1,41 @@ import { type ConsoleTransport, defaultConfig, - type FileTransport, - type HttpTransport, LOG_LEVELS, type LoggerConfig, type LogLevel, type StreamTransport, type Transport, } from "./config.ts"; -import { jsonFormat, prettyFormat } from "./format.ts"; +import type { LogRecord } from "./format.ts"; import { - error, + DEFAULT_LOG_COLLECTION_LIMIT, getLogger, - info, initializeLogger, isLoggerInitialized, LOG_TYPE_MARKER, + type LogCollectionOptions, + type Logger, SYSLOG_LOG_LEVELS, - transportConfigToRealTransport, - warn, } from "./logger.ts"; import { httpLogMiddleware } from "./plugins.ts"; export { + DEFAULT_LOG_COLLECTION_LIMIT, defaultConfig, - error, getLogger, httpLogMiddleware, - info, initializeLogger, isLoggerInitialized, - jsonFormat, LOG_LEVELS, LOG_TYPE_MARKER, - prettyFormat, SYSLOG_LOG_LEVELS, - transportConfigToRealTransport, - warn, type ConsoleTransport, - type FileTransport, - type HttpTransport, + type LogCollectionOptions, + type Logger, type LoggerConfig, type LogLevel, + type LogRecord, type StreamTransport, type Transport, }; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index a4a7cc76f9..a49904ad84 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -1,145 +1,197 @@ -import winston from "winston"; -import { defaultConfig, type LoggerConfig, type Transport } from "./config.ts"; -import { jsonFormat, prettyFormat } from "./format.ts"; +import type { Writable } from "node:stream"; +import { + defaultConfig, + LOG_LEVEL_VALUES, + type LoggerConfig, + type LogLevel, + type Transport, +} from "./config.ts"; +import { formatRecord, serializeRecord, type LogRecord } from "./format.ts"; -let _loggers: Record = {}; +export type LogCollectionOptions = { + /** Maximum number of records retained. The newest records are kept. */ + maxEntries?: number; +}; + +export const DEFAULT_LOG_COLLECTION_LIMIT = 1000; export const LOG_TYPE_MARKER = "__ssqt_log_type"; export const DEFAULT_LOGGER_NAME = "_ssqt_default_logger"; -export const SYSLOG_LOG_LEVELS = { - emerg: 0, - alert: 1, - crit: 2, - error: 3, - warning: 4, - warn: 4, - notice: 5, - info: 6, - debug: 7, -}; +/** Numeric values retained for callers that used the old syslog mapping. */ +export const SYSLOG_LOG_LEVELS = LOG_LEVEL_VALUES; -/** - * Return the initialized logger. If the logger has not - * yet been initialized, it will be initialized with the - * default configuration - * - * @param {string | undefined} logger - The label of the logger to get - * @return {winston.Logger} The logger - */ -export function getLogger(logger?: string): winston.Logger { +export interface Logger { + readonly name: string; + level: LogLevel; + log( + level: LogLevel, + message: unknown, + fields?: Record, + ): void; + emerg(message: unknown, fields?: Record): void; + alert(message: unknown, fields?: Record): void; + crit(message: unknown, fields?: Record): void; + error(message: unknown, fields?: Record): void; + warning(message: unknown, fields?: Record): void; + warn(message: unknown, fields?: Record): void; + notice(message: unknown, fields?: Record): void; + info(message: unknown, fields?: Record): void; + debug(message: unknown, fields?: Record): void; + child(record: Record): Logger; + startLogCollection(options?: LogCollectionOptions): void; + stopLogCollection(): void; + getCollectedLogs(): LogRecord[]; + clearCollectedLogs(): void; +} + +type Sink = (serializedRecord: string) => void; +const _loggers = new Map(); + +export function getLogger(logger?: string): Logger { const name = logger ?? DEFAULT_LOGGER_NAME; - if (_loggers[name] === undefined) { + if (!_loggers.has(name)) { initializeLogger(name); } - return _loggers[name]; + return _loggers.get(name)!; } -/** - * Check if the given logger has been initialized yet - * - * @param {string | undefined} logger - The name of the logger - * @return {boolean} Whether the logger has been initialized - */ export function isLoggerInitialized(logger?: string): boolean { - const name = logger ?? DEFAULT_LOGGER_NAME; - return _loggers[name] !== undefined; + return _loggers.has(logger ?? DEFAULT_LOGGER_NAME); } -/** - * Convenience shorthand for getLogger().info - */ -export const info: winston.LeveledLogMethod = (...args): winston.Logger => { - return getLogger().info(...args); -}; - -/** - * Convenience shorthand for getLogger().warn - */ -export const warn: winston.LeveledLogMethod = (...args): winston.Logger => { - return getLogger().warn(...args); -}; - -/** - * Convenience shorthand for getLogger().error - */ -export const error: winston.LeveledLogMethod = (...args): winston.Logger => { - return getLogger().error(...args); -}; - -/** - * Initialize the logger, optionally with a custom configuration. Calling - * this function when the logger has already been initialized will throw an error. - * - * @param {LoggerConfig | string | undefined} nameOrConfig - The logger config - * to use, or the label of the logger to initialize - * @param {LoggerConfig} config - The logger config to use, if the first parameter was the logger label - * @return {winston.Logger} The initialized logger - */ export function initializeLogger( nameOrConfig?: Partial | string, config?: Partial, -): winston.Logger { +): Logger { const name = typeof nameOrConfig === "string" ? nameOrConfig : DEFAULT_LOGGER_NAME; - - if (_loggers[name] !== undefined) { + if (_loggers.has(name)) { throw new Error("Logger has already been initialized"); } - let finalConfig: Partial = defaultConfig(); - if (config !== undefined) { - finalConfig = config; - } else if (nameOrConfig !== undefined && typeof nameOrConfig !== "string") { - finalConfig = nameOrConfig; - } + const supplied = + config ?? (typeof nameOrConfig === "string" ? {} : (nameOrConfig ?? {})); + const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; + + const sinks = conf.transports.map(transportToSink); + + const level = { value: conf.logLevel }; + const logger = createLogger(name, sinks, level, {}); - const conf: LoggerConfig = { - ...defaultConfig(), - ...finalConfig, + _loggers.set(name, logger); + return logger; +} + +function createLogger( + name: string, + sinks: Sink[], + level: { value: LogLevel }, + baseFields: Record, +): Logger { + const collection: { + enabled: boolean; + maxEntries: number; + records: LogRecord[]; + } = { + enabled: false, + maxEntries: DEFAULT_LOG_COLLECTION_LIMIT, + records: [], }; - _loggers[name] = winston.createLogger({ - level: conf.logLevel, - levels: SYSLOG_LOG_LEVELS, - format: - conf.environment === "production" ? jsonFormat(name) : prettyFormat(name), - transports: conf.transports - // we need to put the console transports last because the colorize - // formatter actually mutates the info field... - .sort((a, b) => { - if (a.type === "console" && b.type !== "console") { - return 1; + const logger: Logger = { + name, + get level() { + return level.value; + }, + set level(value: LogLevel) { + level.value = value; + }, + log(messageLevel, message, fields) { + if (LOG_LEVEL_VALUES[messageLevel] > LOG_LEVEL_VALUES[level.value]) { + return; + } + + let actualMessage = message; + let messageFields = fields ?? {}; + if (fields === undefined && isRecord(message)) { + messageFields = message; + actualMessage = message["message"]; + } + + // Per-message fields take precedence over inherited child fields. + const record = formatRecord(name, messageLevel, actualMessage, { + ...baseFields, + ...messageFields, + }); + if (collection.enabled) { + collection.records.push(record); + if (collection.records.length > collection.maxEntries) { + collection.records.splice( + 0, + collection.records.length - collection.maxEntries, + ); + } + } + // Do not serialize when there are no active sinks (for example, when + // this logger is only being used for collection). Serialization is also + // shared across all sinks. + if (sinks.length > 0) { + const serializedRecord = `${serializeRecord(record)}\n`; + for (const sink of sinks) { + sink(serializedRecord); } - return 0; - }) - .map(transportConfigToRealTransport(name)), - }); + } + }, + child(record) { + return createLogger(name, sinks, level, { ...baseFields, ...record }); + }, + startLogCollection(options) { + const maxEntries = options?.maxEntries ?? DEFAULT_LOG_COLLECTION_LIMIT; + if (!Number.isInteger(maxEntries) || maxEntries < 1) { + throw new Error("Log collection maxEntries must be a positive integer"); + } + collection.maxEntries = maxEntries; + collection.enabled = true; + if (collection.records.length > maxEntries) { + collection.records.splice(0, collection.records.length - maxEntries); + } + }, + stopLogCollection() { + collection.enabled = false; + }, + getCollectedLogs() { + return [...collection.records]; + }, + clearCollectedLogs() { + collection.records.length = 0; + }, + emerg(message, fields) { this.log("emerg", message, fields); }, + alert(message, fields) { this.log("alert", message, fields); }, + crit(message, fields) { this.log("crit", message, fields); }, + error(message, fields) { this.log("error", message, fields); }, + warning(message, fields) { this.log("warning", message, fields); }, + warn(message, fields) { this.log("warn", message, fields); }, + notice(message, fields) { this.log("notice", message, fields); }, + info(message, fields) { this.log("info", message, fields); }, + debug(message, fields) { this.log("debug", message, fields); }, + }; + return logger; +} - return _loggers[name]; +function isRecord(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + !(value instanceof Error) + ); } -export const transportConfigToRealTransport = (name: string) => { - return (transport: Transport) => { - switch (transport.type) { - case "console": - return new winston.transports.Console(transport.options); - case "file": - return new winston.transports.File({ - format: jsonFormat(name), - ...transport.options, - }); - case "http": - return new winston.transports.Http({ - format: jsonFormat(name), - ...transport.options, - }); - case "stream": - return new winston.transports.Stream({ - format: jsonFormat(name), - stream: transport.stream, - ...transport.options, - }); - } +function transportToSink(transport: Transport): Sink { + const stream: Writable = + transport.type === "console" ? process.stdout : transport.stream; + return (serializedRecord) => { + stream.write(serializedRecord); }; -}; +} diff --git a/packages/logger/src/plugins.ts b/packages/logger/src/plugins.ts index 4227b91df8..79d37c8193 100644 --- a/packages/logger/src/plugins.ts +++ b/packages/logger/src/plugins.ts @@ -1,7 +1,7 @@ import type { Request, Response } from "express"; import { URL } from "node:url"; -import winston from "winston"; import type { LogLevel } from "./config.ts"; +import type { Logger } from "./logger.ts"; import { LOG_TYPE_MARKER } from "./logger.ts"; export type HttpLogMiddlewareOptions = { @@ -31,10 +31,10 @@ const STRIP_PARAMS = [ * A simple Express.js middleware which logs the URL, method, response code, * and response time of all HTTP requests. * - * @param {winston.Logger} logger - The logger to use + * @param {Logger} logger - The logger to use */ export function httpLogMiddleware( - logger: winston.Logger, + logger: Logger, opts?: HttpLogMiddlewareOptions, ) { return ( @@ -78,10 +78,10 @@ export function httpLogMiddleware( status >= 500 ? "error" : status >= 400 - ? "warn" - : isHealthcheck - ? "debug" - : opts?.nonErrorLogLevel ?? "info"; + ? "warn" + : isHealthcheck + ? "debug" + : (opts?.nonErrorLogLevel ?? "info"); const message = { method, status, time, url: cleanUrl, requestId }; logger.log(level, { [LOG_TYPE_MARKER]: "HTTP", message, extraData }); From 007c7dda64bbdb1d950a0364bae1c85c9eac92b8 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 15:12:26 -0600 Subject: [PATCH 02/51] logger: fix review comments --- packages/logger/src/config.ts | 1 + packages/logger/src/format.ts | 24 ++++++++++++--- packages/logger/src/logger.ts | 58 ++++++++++++++++++++++++++++------- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 44526ee39d..769a62d971 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -52,6 +52,7 @@ export function defaultConfig(): LoggerConfig { const configuredLevel = process.env["SSQT_LOG_LEVEL"]; let logLevel: LogLevel = "info"; + if (configuredLevel !== undefined) { if (!LOG_LEVELS.includes(configuredLevel as LogLevel)) { throw new Error(`Invalid log level "${configuredLevel}"`); diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index fc86ba7282..574add5287 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -31,6 +31,7 @@ export function formatRecord( if (name !== DEFAULT_LOGGER_NAME) { record["logger.name"] = name; } + if (record["tenantAlias"] && typeof record["message"] === "string") { const alias = String(record["tenantAlias"]); if (!(record["message"] as string).startsWith(`[${alias}]`)) { @@ -41,8 +42,10 @@ export function formatRecord( if (record[LOG_TYPE_MARKER] === "HTTP") { formatHttpRecord(record); } + delete record[LOG_TYPE_MARKER]; record["status"] = level; + return record as LogRecord; } @@ -51,6 +54,7 @@ function formatHttpRecord(record: Record): void { if (value === null || typeof value !== "object") { return; } + const message = value as unknown as HTTPMessage; const micros = Number(message.time); const displayTime = @@ -62,10 +66,12 @@ function formatHttpRecord(record: Record): void { displayTime.padStart(6, " "), message.url, ].join(" "); + record["http.url"] = message.url; record["http.method"] = message.method; record["http.status_code"] = message.status; record["http.response_time"] = micros; + if (message.requestId) { record["http.request_id"] = message.requestId; } @@ -73,19 +79,29 @@ function formatHttpRecord(record: Record): void { /** Safely serialize records, including Error and BigInt values. */ export function serializeRecord(record: LogRecord): string { - const seen = new WeakSet(); - return JSON.stringify(record, (_key, value: unknown) => { + // Keep only the current ancestor chain. A WeakSet of every object seen in + // the record would incorrectly classify valid shared sibling references as + // circular. + const ancestors: object[] = []; + return JSON.stringify(record, function (_key, value: unknown) { if (typeof value === "bigint") { return value.toString(); } + if (value instanceof Error) { return { name: value.name, message: value.message, stack: value.stack }; } + if (typeof value === "object" && value !== null) { - if (seen.has(value)) { + while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) { + ancestors.pop(); + } + + if (ancestors.includes(value)) { return "[Circular]"; } - seen.add(value); + + ancestors.push(value); } return value; }); diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index a49904ad84..388216b7a9 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -66,16 +66,16 @@ export function initializeLogger( ): Logger { const name = typeof nameOrConfig === "string" ? nameOrConfig : DEFAULT_LOGGER_NAME; + if (_loggers.has(name)) { throw new Error("Logger has already been initialized"); } const supplied = config ?? (typeof nameOrConfig === "string" ? {} : (nameOrConfig ?? {})); - const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; + const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; const sinks = conf.transports.map(transportToSink); - const level = { value: conf.logLevel }; const logger = createLogger(name, sinks, level, {}); @@ -101,12 +101,15 @@ function createLogger( const logger: Logger = { name, + get level() { return level.value; }, + set level(value: LogLevel) { level.value = value; }, + log(messageLevel, message, fields) { if (LOG_LEVEL_VALUES[messageLevel] > LOG_LEVEL_VALUES[level.value]) { return; @@ -143,9 +146,11 @@ function createLogger( } } }, + child(record) { return createLogger(name, sinks, level, { ...baseFields, ...record }); }, + startLogCollection(options) { const maxEntries = options?.maxEntries ?? DEFAULT_LOG_COLLECTION_LIMIT; if (!Number.isInteger(maxEntries) || maxEntries < 1) { @@ -157,24 +162,54 @@ function createLogger( collection.records.splice(0, collection.records.length - maxEntries); } }, + stopLogCollection() { collection.enabled = false; }, + getCollectedLogs() { return [...collection.records]; }, + clearCollectedLogs() { collection.records.length = 0; }, - emerg(message, fields) { this.log("emerg", message, fields); }, - alert(message, fields) { this.log("alert", message, fields); }, - crit(message, fields) { this.log("crit", message, fields); }, - error(message, fields) { this.log("error", message, fields); }, - warning(message, fields) { this.log("warning", message, fields); }, - warn(message, fields) { this.log("warn", message, fields); }, - notice(message, fields) { this.log("notice", message, fields); }, - info(message, fields) { this.log("info", message, fields); }, - debug(message, fields) { this.log("debug", message, fields); }, + + emerg(message, fields) { + this.log("emerg", message, fields); + }, + + alert(message, fields) { + this.log("alert", message, fields); + }, + + crit(message, fields) { + this.log("crit", message, fields); + }, + + error(message, fields) { + this.log("error", message, fields); + }, + + warning(message, fields) { + this.log("warning", message, fields); + }, + + warn(message, fields) { + this.log("warn", message, fields); + }, + + notice(message, fields) { + this.log("notice", message, fields); + }, + + info(message, fields) { + this.log("info", message, fields); + }, + + debug(message, fields) { + this.log("debug", message, fields); + }, }; return logger; } @@ -191,6 +226,7 @@ function isRecord(value: unknown): value is Record { function transportToSink(transport: Transport): Sink { const stream: Writable = transport.type === "console" ? process.stdout : transport.stream; + return (serializedRecord) => { stream.write(serializedRecord); }; From 1d40940b859cf11950aadb0ec1422beb361dbe53 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:19:37 +0000 Subject: [PATCH 03/51] publish: @saasquatch/logger v4.0.1-0 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index ffc1c7ce58..6ad9ce3a32 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "4.0.0", + "version": "4.0.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "4.0.0", + "version": "4.0.1-0", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index 2864f07bb4..07e42191e9 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "4.0.0", + "version": "4.0.1-0", "type": "module", "description": "Small structured JSON logger for SaaSquatch NodeJS services", "private": false, From 42c24927a2ad181ebb62fa3a6a1af5d12178da4b Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 15:31:29 -0600 Subject: [PATCH 04/51] logger: option to return collected logs serialized --- packages/logger/src/logger.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 388216b7a9..c5b55b2dec 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -13,6 +13,10 @@ export type LogCollectionOptions = { maxEntries?: number; }; +export type GetCollectedLogsOptions = { + serialized?: boolean; +}; + export const DEFAULT_LOG_COLLECTION_LIMIT = 1000; export const LOG_TYPE_MARKER = "__ssqt_log_type"; @@ -41,7 +45,9 @@ export interface Logger { child(record: Record): Logger; startLogCollection(options?: LogCollectionOptions): void; stopLogCollection(): void; - getCollectedLogs(): LogRecord[]; + getCollectedLogs( + opts?: GetCollectedLogsOptions & { serialized?: T }, + ): T extends true ? string : LogRecord[]; clearCollectedLogs(): void; } @@ -127,6 +133,7 @@ function createLogger( ...baseFields, ...messageFields, }); + if (collection.enabled) { collection.records.push(record); if (collection.records.length > collection.maxEntries) { @@ -136,6 +143,7 @@ function createLogger( ); } } + // Do not serialize when there are no active sinks (for example, when // this logger is only being used for collection). Serialization is also // shared across all sinks. @@ -167,8 +175,16 @@ function createLogger( collection.enabled = false; }, - getCollectedLogs() { - return [...collection.records]; + getCollectedLogs( + opts?: GetCollectedLogsOptions & { serialized?: T }, + ): T extends true ? string : LogRecord[] { + if (opts?.serialized) { + return collection.records + .map((record) => `${serializeRecord(record)}\n`) + .join("") as T extends true ? string : LogRecord[]; + } + + return [...collection.records] as T extends true ? string : LogRecord[]; }, clearCollectedLogs() { From 30a4dbb8b208554496de065f33a2502fb9eb05fb Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 15:32:39 -0600 Subject: [PATCH 05/51] logger: revert package version in package(-lock) Wasn't supposed to be updated manually --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index 6ad9ce3a32..d26b06aafd 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "4.0.1-0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "4.0.1-0", + "version": "3.0.0", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index 07e42191e9..0f3a7649a5 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "4.0.1-0", + "version": "3.0.0", "type": "module", "description": "Small structured JSON logger for SaaSquatch NodeJS services", "private": false, From 9cdd2d7d57ce933a2c62fa7fea50025d398acaa1 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 15:41:31 -0600 Subject: [PATCH 06/51] express-boilerplate: update logger dependency --- .../express-boilerplate/package-lock.json | 297 +----------------- packages/express-boilerplate/package.json | 5 +- .../express-boilerplate/src/async-wrapper.ts | 2 +- .../express-boilerplate/src/healthcheck.ts | 2 +- .../express-boilerplate/src/middleware.ts | 2 +- packages/express-boilerplate/src/shutdown.ts | 2 +- .../src/tests/middleware.test.ts | 3 +- 7 files changed, 13 insertions(+), 300 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 7f324c7bc6..09991490a3 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@saasquatch/logger": "^3.0.0", + "@saasquatch/logger": "^4.0.1-0", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -28,30 +28,7 @@ "node": "^24 || ^26" }, "peerDependencies": { - "express": "4", - "winston": "3" - } - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "express": "4" } }, "node_modules/@eslint-community/eslint-utils": { @@ -355,17 +332,16 @@ } }, "node_modules/@saasquatch/logger": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.0.tgz", - "integrity": "sha512-uDzhgrGfP+nYZV0o7iohPoGGTmwaPTBpLkPtiKUH9wsGRwL6tEeQ8PxvWLwGmZPgfDFPWZ2sE9EorJNii2cEfQ==", + "version": "4.0.1-0", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-4.0.1-0.tgz", + "integrity": "sha512-dg6IsixLjzrQArHIUnNYTuNeqFj/xGb0TanmUsauqBHcWZ9EP6J5v8OHZkSdhZ5Or6/mtsjebXszn7syCCf/5g==", "dev": true, "license": "MIT", "engines": { "node": "^24 || ^26" }, "peerDependencies": { - "express": "^4", - "winston": "^3" + "express": "^4" }, "peerDependenciesMeta": { "express": { @@ -373,17 +349,6 @@ } } }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "peer": true, - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -561,13 +526,6 @@ "@types/superagent": "^8.1.0" } }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT", - "peer": true - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.4", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", @@ -876,13 +834,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT", - "peer": true - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1004,66 +955,6 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.20" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1244,13 +1135,6 @@ "license": "MIT", "peer": true }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT", - "peer": true - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1635,13 +1519,6 @@ } } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT", - "peer": true - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1729,13 +1606,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT", - "peer": true - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -2026,19 +1896,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2090,13 +1947,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT", - "peer": true - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2127,24 +1977,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2309,16 +2141,6 @@ "wrappy": "1" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fn.name": "1.x.x" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2507,21 +2329,6 @@ "node": ">= 0.8" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2553,16 +2360,6 @@ "license": "MIT", "peer": true }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2743,16 +2540,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -2763,16 +2550,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2845,13 +2622,6 @@ "node": ">=6.6.0" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT", - "peer": true - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -2879,16 +2649,6 @@ "node": ">=0.6" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -2970,13 +2730,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "peer": true - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3013,44 +2766,6 @@ "node": ">= 8" } }, - "node_modules/winston": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", - "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.4.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "peer": true, - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 4f15038508..a941c95da3 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@saasquatch/logger": "^3.0.0", + "@saasquatch/logger": "^4.0.1-0", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -51,8 +51,7 @@ "typescript": "^6.0.3" }, "peerDependencies": { - "express": "4", - "winston": "3" + "express": "4" }, "prettier": {} } diff --git a/packages/express-boilerplate/src/async-wrapper.ts b/packages/express-boilerplate/src/async-wrapper.ts index aa6eb293d0..cacadd9464 100644 --- a/packages/express-boilerplate/src/async-wrapper.ts +++ b/packages/express-boilerplate/src/async-wrapper.ts @@ -1,5 +1,5 @@ import type { Request, Response } from "express"; -import { Logger } from "winston"; +import type { Logger } from "@saasquatch/logger"; import { formatGenericError } from "./error.ts"; import { nanoid } from "./nanoid.ts"; diff --git a/packages/express-boilerplate/src/healthcheck.ts b/packages/express-boilerplate/src/healthcheck.ts index 02f2646f5d..9d02a2e4c5 100644 --- a/packages/express-boilerplate/src/healthcheck.ts +++ b/packages/express-boilerplate/src/healthcheck.ts @@ -1,5 +1,5 @@ +import type { Logger } from "@saasquatch/logger"; import type { Application, Request, Response } from "express"; -import { Logger } from "winston"; import { formatGenericError } from "./error.ts"; import { TERMINATION_APP_LOCAL_KEY } from "./shutdown.ts"; diff --git a/packages/express-boilerplate/src/middleware.ts b/packages/express-boilerplate/src/middleware.ts index 89512f6ab8..92f7496db7 100644 --- a/packages/express-boilerplate/src/middleware.ts +++ b/packages/express-boilerplate/src/middleware.ts @@ -1,5 +1,5 @@ +import type { Logger } from "@saasquatch/logger"; import type { NextFunction, Request, Response } from "express"; -import { Logger } from "winston"; import { nanoid } from "./nanoid.ts"; /** diff --git a/packages/express-boilerplate/src/shutdown.ts b/packages/express-boilerplate/src/shutdown.ts index 1da6540a41..3e60f29680 100644 --- a/packages/express-boilerplate/src/shutdown.ts +++ b/packages/express-boilerplate/src/shutdown.ts @@ -1,6 +1,6 @@ +import type { Logger } from "@saasquatch/logger"; import type { Application } from "express"; import { Server, createServer } from "http"; -import { Logger } from "winston"; import { formatGenericError } from "./error.ts"; const handleHookError = (hook: string, logger: Logger) => (err: unknown) => { diff --git a/packages/express-boilerplate/src/tests/middleware.test.ts b/packages/express-boilerplate/src/tests/middleware.test.ts index d736e576b2..a778c00eab 100644 --- a/packages/express-boilerplate/src/tests/middleware.test.ts +++ b/packages/express-boilerplate/src/tests/middleware.test.ts @@ -2,7 +2,6 @@ import express from "express"; import * as assert from "node:assert"; import { test } from "node:test"; import request from "supertest"; -import { Logger } from "winston"; import { requestIdAndLogger } from "../middleware.ts"; import { jestLogger } from "./util.ts"; @@ -15,7 +14,7 @@ test("requestIdAndLogger adds requestId and logger", async () => { app.use(requestIdAndLogger(logger)); app.use((_req, res, next) => { assert.strictEqual(typeof res.locals["requestId"], "string"); - assert.ok(res.locals["logger"] instanceof Logger); + assert.ok(res.locals["logger"]); next(); }); From 525bf73ed3d958e654241cf0fccd387e54fd4131 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 15:54:50 -0600 Subject: [PATCH 07/51] express-boilerplate: fix PR review comments --- packages/express-boilerplate/package-lock.json | 1 + packages/express-boilerplate/package.json | 1 + packages/express-boilerplate/src/tests/middleware.test.ts | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 09991490a3..dc6bd90a8f 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -28,6 +28,7 @@ "node": "^24 || ^26" }, "peerDependencies": { + "@saasquatch/logger": "^4.0.1-0", "express": "4" } }, diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index a941c95da3..3777dca7d3 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -51,6 +51,7 @@ "typescript": "^6.0.3" }, "peerDependencies": { + "@saasquatch/logger": "^4.0.1-0", "express": "4" }, "prettier": {} diff --git a/packages/express-boilerplate/src/tests/middleware.test.ts b/packages/express-boilerplate/src/tests/middleware.test.ts index a778c00eab..70bc42fa02 100644 --- a/packages/express-boilerplate/src/tests/middleware.test.ts +++ b/packages/express-boilerplate/src/tests/middleware.test.ts @@ -14,7 +14,10 @@ test("requestIdAndLogger adds requestId and logger", async () => { app.use(requestIdAndLogger(logger)); app.use((_req, res, next) => { assert.strictEqual(typeof res.locals["requestId"], "string"); - assert.ok(res.locals["logger"]); + const requestLogger = res.locals["logger"] as typeof logger; + assert.notStrictEqual(requestLogger, logger); + assert.strictEqual(requestLogger.name, logger.name); + assert.strictEqual(typeof requestLogger.info, "function"); next(); }); From 3cc8963661d73bd962e92d1728d401162e6255ae Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 15:55:43 -0600 Subject: [PATCH 08/51] logger: fix PR review comments --- packages/logger/src/index.ts | 2 ++ packages/logger/src/logger.ts | 64 +++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index ab397c8376..29a200bb3b 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -10,6 +10,7 @@ import { import type { LogRecord } from "./format.ts"; import { DEFAULT_LOG_COLLECTION_LIMIT, + type GetCollectedLogsOptions, getLogger, initializeLogger, isLoggerInitialized, @@ -31,6 +32,7 @@ export { LOG_TYPE_MARKER, SYSLOG_LOG_LEVELS, type ConsoleTransport, + type GetCollectedLogsOptions, type LogCollectionOptions, type Logger, type LoggerConfig, diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index c5b55b2dec..7a9c237c9b 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -99,10 +99,14 @@ function createLogger( enabled: boolean; maxEntries: number; records: LogRecord[]; + start: number; + size: number; } = { enabled: false, maxEntries: DEFAULT_LOG_COLLECTION_LIMIT, records: [], + start: 0, + size: 0, }; const logger: Logger = { @@ -135,12 +139,14 @@ function createLogger( }); if (collection.enabled) { - collection.records.push(record); - if (collection.records.length > collection.maxEntries) { - collection.records.splice( - 0, - collection.records.length - collection.maxEntries, - ); + const index = + (collection.start + collection.size) % collection.maxEntries; + + collection.records[index] = record; + if (collection.size < collection.maxEntries) { + collection.size += 1; + } else { + collection.start = (collection.start + 1) % collection.maxEntries; } } @@ -161,14 +167,21 @@ function createLogger( startLogCollection(options) { const maxEntries = options?.maxEntries ?? DEFAULT_LOG_COLLECTION_LIMIT; + if (!Number.isInteger(maxEntries) || maxEntries < 1) { throw new Error("Log collection maxEntries must be a positive integer"); } + + if (collection.maxEntries !== maxEntries) { + const retained = getCollectedRecords(collection); + const records = retained.slice(-maxEntries); + collection.records = records; + collection.start = 0; + collection.size = records.length; + } + collection.maxEntries = maxEntries; collection.enabled = true; - if (collection.records.length > maxEntries) { - collection.records.splice(0, collection.records.length - maxEntries); - } }, stopLogCollection() { @@ -178,17 +191,20 @@ function createLogger( getCollectedLogs( opts?: GetCollectedLogsOptions & { serialized?: T }, ): T extends true ? string : LogRecord[] { + const records = getCollectedRecords(collection); if (opts?.serialized) { - return collection.records + return records .map((record) => `${serializeRecord(record)}\n`) .join("") as T extends true ? string : LogRecord[]; } - return [...collection.records] as T extends true ? string : LogRecord[]; + return records as T extends true ? string : LogRecord[]; }, clearCollectedLogs() { collection.records.length = 0; + collection.start = 0; + collection.size = 0; }, emerg(message, fields) { @@ -230,13 +246,27 @@ function createLogger( return logger; } +function getCollectedRecords(collection: { + records: LogRecord[]; + start: number; + size: number; + maxEntries: number; +}): LogRecord[] { + const records = new Array(collection.size); + for (let index = 0; index < collection.size; index += 1) { + records[index] = + collection.records[(collection.start + index) % collection.maxEntries]; + } + return records; +} + function isRecord(value: unknown): value is Record { - return ( - typeof value === "object" && - value !== null && - !Array.isArray(value) && - !(value instanceof Error) - ); + if (typeof value !== "object" || value === null) { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; } function transportToSink(transport: Transport): Sink { From 6b45fd02c1ee2b692e9704d61192498b5a2911fd Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 16:02:59 -0600 Subject: [PATCH 09/51] express-boilerplate: fix peer deps again --- packages/express-boilerplate/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 3777dca7d3..50adcda66a 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -51,8 +51,8 @@ "typescript": "^6.0.3" }, "peerDependencies": { - "@saasquatch/logger": "^4.0.1-0", - "express": "4" + "@saasquatch/logger": "^4.0.0", + "express": "^4.0.0" }, "prettier": {} } From 39b87e610a60688adc8df30b02600eb810bb81b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:03:58 +0000 Subject: [PATCH 10/51] publish: @saasquatch/logger v3.0.1-0 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index d26b06aafd..6611514a31 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.0", + "version": "3.0.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.0", + "version": "3.0.1-0", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index 0f3a7649a5..715078d4ca 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.0", + "version": "3.0.1-0", "type": "module", "description": "Small structured JSON logger for SaaSquatch NodeJS services", "private": false, From 9ffc53df202a37bf2d9a422689e5c690e0f2fb1f Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Tue, 15 Sep 2026 16:09:23 -0600 Subject: [PATCH 11/51] express-boilerplate: bump logger package --- packages/express-boilerplate/package-lock.json | 12 ++++++------ packages/express-boilerplate/package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index dc6bd90a8f..915e5399fd 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@saasquatch/logger": "^4.0.1-0", + "@saasquatch/logger": "^3.0.1-0", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -28,8 +28,8 @@ "node": "^24 || ^26" }, "peerDependencies": { - "@saasquatch/logger": "^4.0.1-0", - "express": "4" + "@saasquatch/logger": "^3.0.0", + "express": "^4.0.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -333,9 +333,9 @@ } }, "node_modules/@saasquatch/logger": { - "version": "4.0.1-0", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-4.0.1-0.tgz", - "integrity": "sha512-dg6IsixLjzrQArHIUnNYTuNeqFj/xGb0TanmUsauqBHcWZ9EP6J5v8OHZkSdhZ5Or6/mtsjebXszn7syCCf/5g==", + "version": "3.0.1-0", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-0.tgz", + "integrity": "sha512-+J5/YElPQhr8e6KWu06QoQYbTMBRkKXq/12b8ep8Dkr1pGt0EiPv7h+231t93l1nvST9EULLXtaEz+O3pikbIg==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 50adcda66a..57b16b848f 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@saasquatch/logger": "^4.0.1-0", + "@saasquatch/logger": "^3.0.1-0", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -51,7 +51,7 @@ "typescript": "^6.0.3" }, "peerDependencies": { - "@saasquatch/logger": "^4.0.0", + "@saasquatch/logger": "^3.0.0", "express": "^4.0.0" }, "prettier": {} From d83ae5a544b9f394a46b8049b1989819dfa821a5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:10:25 +0000 Subject: [PATCH 12/51] publish: @saasquatch/express-boilerplate v2.0.1-0 This commit was generated by GitHub Actions CI --- packages/express-boilerplate/package-lock.json | 4 ++-- packages/express-boilerplate/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 915e5399fd..6fb7abf672 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.0", + "version": "2.0.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/express-boilerplate", - "version": "2.0.0", + "version": "2.0.1-0", "license": "MIT", "dependencies": { "nanoid": "^3.3.7" diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 57b16b848f..f3ef46c833 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.0", + "version": "2.0.1-0", "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", "main": "dist/index.js", "type": "module", From 0344957c34bc60bf77e38fae800e3ab067adb86b Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Wed, 16 Sep 2026 12:05:16 -0600 Subject: [PATCH 13/51] logger: use modern dev tools --- packages/logger/.oxfmtrc.json | 8 + packages/logger/.oxlintrc.json | 11 + packages/logger/package-lock.json | 1182 ++++++++++++++++++++++++++++- packages/logger/package.json | 42 +- packages/logger/src/logger.ts | 17 +- packages/logger/src/plugins.ts | 2 +- 6 files changed, 1225 insertions(+), 37 deletions(-) create mode 100644 packages/logger/.oxfmtrc.json create mode 100644 packages/logger/.oxlintrc.json diff --git a/packages/logger/.oxfmtrc.json b/packages/logger/.oxfmtrc.json new file mode 100644 index 0000000000..e1ceec9ec9 --- /dev/null +++ b/packages/logger/.oxfmtrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [], + "printWidth": 80, + "sortImports": { + "newlinesBetween": false + } +} diff --git a/packages/logger/.oxlintrc.json b/packages/logger/.oxlintrc.json new file mode 100644 index 0000000000..31e054fe5d --- /dev/null +++ b/packages/logger/.oxlintrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc"], + "categories": { + "correctness": "error" + }, + "rules": {}, + "env": { + "builtin": true + } +} diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index 6611514a31..3a976d1eaa 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -11,7 +11,9 @@ "devDependencies": { "@types/express": "^4.17.25", "@types/node": "^24.12.4", - "typescript": "^6.0.3" + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", + "typescript": "^7.0.2" }, "engines": { "node": "^24 || ^26" @@ -25,6 +27,700 @@ } } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.67.0.tgz", + "integrity": "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.67.0.tgz", + "integrity": "sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.67.0.tgz", + "integrity": "sha512-MfONZx/O2o9M5v2jDFol556G9+A+P9xCuJ4DZ+qhE+RnaCdoscy6Eu5nq1dbuNxhwdJyZ6kLI7fnG9mwEeOeGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.67.0.tgz", + "integrity": "sha512-CYnIx5LvFVJnyJcCqwH2jxMKjFjqo5678MPjdmNFoSGMhlOvZ/xRZqvhDcolKrXc8fezW3AKh+C4wyoFuWOSSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.67.0.tgz", + "integrity": "sha512-7/iF1orvIS9mxhKUqnmtMgm+OrSQ5acPwuvdQrm6ECgqbwPmC+Pw9cdke3sNfVN6pT2hbJ58+jP8BCThl5HXOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.67.0.tgz", + "integrity": "sha512-yy+OGys07IZOpOmYPZoObKyUQLkfxeQqeCypk+1jaZd8HGo77hzvU1Jg8X3+W75o+9lszOjBfg0nkGtlwYywXw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.67.0.tgz", + "integrity": "sha512-wPIeeigXgJpwNw3wydYRt3U9iN9Y/ejpOZuYL9IA7igxWs7LIQMOkhKxTumRvy6dIv0iXKk3RTw3Vmjg0i+2sg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.67.0.tgz", + "integrity": "sha512-0+XNxcdbkTfxdcD4qW6Ci9n+mBNJ8xTBumnxKvKBmRFOdx0Wf8/KiHjCJayooXmYkqRpRVd98Q5egvzx5BLSgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.67.0.tgz", + "integrity": "sha512-I75LKPJyNOYUzkqAiAMIE31+Ye7xtQXZdoty1IXn4B+bw5Zpmez5wfG19ejGpNnS/BzQ7LFS+7jxuTPb+vHiZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.67.0.tgz", + "integrity": "sha512-c2M5iRpe1QMZSRE/UvZoPdXBWb5Ic/ycvOyNiKCqPwQ/OyOKIMiJs02ynlNnjb7ZZJnRXYLmGcohoINOcwDK3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.67.0.tgz", + "integrity": "sha512-dQzzYlV24Udhfm5ECuSdgqRvFJU/CGHzcYYEO3dLM6W6+CHiBFrq9OjIllkdCcPhsoSQ8o223Dja84MOSzed9A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.67.0.tgz", + "integrity": "sha512-rFNq1CgX4qMJANOq42LkAs90JE80GpiaEohAV2qn/gT2hGjQTW1zBO5zQBxArI4926pM1OSzo3CN0tBszGBIaA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.67.0.tgz", + "integrity": "sha512-Sky6rEdz2o5IGq01lPhS12yEvDdChVEcaYrcLHkveh4Fx0qPjljE/Iul6SX/bRMl6lNc8J7J/mDQdzgBdA++Pg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.67.0.tgz", + "integrity": "sha512-vPXmlNORV8AZq2Ocxh07pxwMjfENUWCV/eZArnao0qC3NO/hDeTVkQvee7SJJUbIiF5PZbBa4kYmaXnu7Rk58w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.67.0.tgz", + "integrity": "sha512-x/WAtFqYtVr3vZ9ni8nr4kn9whSitg8fOljq/pZzBpxopRdY1BMLZCZkrbIbaBcYkm46qGbqVea2FCWmtQ2P9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.67.0.tgz", + "integrity": "sha512-eRw9Neh4/aA6i+q/R3WU1gGQINhVM0J4fXIm6t27caOamkr/37uAkp1IdBx4zlJH97hmXR63z/q9n5c5dN7MzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.67.0.tgz", + "integrity": "sha512-YIMvb+sGNYN2uc6+QK2HLPeEKM2vl7QZ5onQzpAJRb6pnf0DwUFP5R8tdS9R0l8hdUil2gu4Uxd0Yxrop0iT4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.67.0.tgz", + "integrity": "sha512-LzmU9MyACPzwNDIK0ItMedHPz735Ug7ELWguxo4/kuy6zWuDoeglOAEFCY8jLg0PzRpFO3hDyLFe2Gu2eFDeGA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.67.0.tgz", + "integrity": "sha512-sbQOIDNLUEeVZcAJcSL5VURn7kfjvilPviody4Yl5n8lQCDtUm+C9oHTTwZS/m4d/Z6Vv3jNEiAofH932NPPCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.82.0.tgz", + "integrity": "sha512-a3LB+C5Dsj5b/qtmG/mv5WrzuiXEpg1KF5nXWcEvaoN5TYAqkIvxPOwTPp3Jy/FoGpRo8zsTFhMElMXfeoOEzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.82.0.tgz", + "integrity": "sha512-OBlhRgNqFblGpGenno/aqOfJLOkQ2B8Ig3iDAalfn0H8hJGZKXPeexCRTDm6uwv6YUjSA9Xnwt1y/Bgj5ZH8uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.82.0.tgz", + "integrity": "sha512-dsopxqtY5ZdyT9uLHyGt1SyiLop6hi7hWI3PKpePodkRQOkLaCm+OE4fR9CAz9qdfjiFO8531tX/QDyP/psjFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.82.0.tgz", + "integrity": "sha512-94Lu0SgTClKColU66g1VDuigV3HkcbkJBnTtZjGYfE8UPugaWDgKrm2icjC6HJVUYler2OXaHP/X0TBy8+CowQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.82.0.tgz", + "integrity": "sha512-hne/V06ewhh1i0w8+l7GDNROAGCGPmyFuOwiP7YTRu0JycyStJ4785dmF8xU5p0uUwt2emvIF9vc7Xjis+cJ0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.82.0.tgz", + "integrity": "sha512-aWY2xtbZf1LneW9Qsv/n2Sp8gOu74JrlQzEtj4coHX2SHFrCfhmAumaU+sI/A5nr+yoTRTSmI/pL2s6ADlNSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.82.0.tgz", + "integrity": "sha512-Fe+TtXCXMh/5f7kWlZ2VAwsMumZWtraFlKVk1NJlL52/beGwfDE7ov+/8gVirHzWokzGu7X65hSPq0ucPDskWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.82.0.tgz", + "integrity": "sha512-6azCZ6OJudlvipNttXCCQcyeFfcJ/NvUZdSN1z8elo73kCHtyQC7WTiUcSjWYvJ1jaq9KDUyMAoAS/vNzhBomA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.82.0.tgz", + "integrity": "sha512-PLEaSD8IAIIlwW4dwOd9YaxuxeOpwiXL4J24rcnE4iNtyM5j9Q9/3+gti08oXpx0u2ygNjRDx9xjWWpQonuJEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.82.0.tgz", + "integrity": "sha512-D94em/BwknNTn4vqxjHh5wb2oL566eFhArabqKIr0cNZMHOJuiraFp1A8tXpH05bbE5tqwEfLXTI0MWEGtn3Dw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.82.0.tgz", + "integrity": "sha512-MOprxBaoYU2D4VgxXCl3ghydThWtx7Um1lL51kGYNeQ5Al7WzsH7/tqGdNtbLrIWnjq3bsm13+nz/gRIxjrOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.82.0.tgz", + "integrity": "sha512-5h55QsfJ/luDXZzC20k6SNOY1Az+dCP9WvntKtcUWh2JhckAdwApY2ZusaBTwLENnReXU+A2fJtSrYvZJNKNPg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.82.0.tgz", + "integrity": "sha512-IE8NJNLlHr0CaXyGJPGVn0eTkUyoj1I2UfA8x7I4PSOYKsQ/6btVC7Pywrj5onk0cMH25r6Z38SoN3AvE5Zuog==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.82.0.tgz", + "integrity": "sha512-XUUUxaBo9XKl+J1B9EmP1cTGQPddzeURvoGkfwh/94PGnbW+hBprDljneoI2M1jzC1bzrIV3ihc7iM9UXl8+tg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.82.0.tgz", + "integrity": "sha512-SWLSFulX9TDuH6yvbPYp4+VNn6jkkIvvI+KiujDM5rWBRHEfkesCC/pCneIIUr6ovkxZ5fRtpi2v5Cz5FrMJZg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.82.0.tgz", + "integrity": "sha512-BQy35f6ZUdNr9a6c7B7orxQTcLjByGT2z3WAgmRovpRwmPYAaJ+NTplmMzhdjdJ4qSchfMNZy/Ukg+qRg6zseQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.82.0.tgz", + "integrity": "sha512-V4QhSTg5gctZue8RJjsGi7NpQPThr/p1/HfmiMC5kfe1KFEup9SQRVub4A6kijQjdHfxj7bLL1KO3QO7/5bwMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.82.0.tgz", + "integrity": "sha512-TUSCLaKB2yktpFAJ/r3HAUYsaV/3DT7JS4iNKyoh3a9YNwD0UG7Ezh4D8m23654vQcU6P/RQrCAjRPKe4peP/A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.82.0.tgz", + "integrity": "sha512-VTVoRIWJTb+wvUX8EYoPArfFH02whuR10goFXE/LHRRX33ajRrFgqbcONXZMiF4C5rnattfkm87HqYn8jb8hmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -143,18 +839,490 @@ "@types/node": "*" } }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/oxfmt": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.67.0.tgz", + "integrity": "sha512-vV7sSiPsaO0mSxdoUdayipVDFPzW/UQ+hrezEHa20+Tx1dnMdZLSRHMT0PdS67FFbhd74M1n08asW21aLGeCrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.2" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.67.0", + "@oxfmt/binding-android-arm64": "0.67.0", + "@oxfmt/binding-darwin-arm64": "0.67.0", + "@oxfmt/binding-darwin-x64": "0.67.0", + "@oxfmt/binding-freebsd-x64": "0.67.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.67.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.67.0", + "@oxfmt/binding-linux-arm64-gnu": "0.67.0", + "@oxfmt/binding-linux-arm64-musl": "0.67.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.67.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.67.0", + "@oxfmt/binding-linux-riscv64-musl": "0.67.0", + "@oxfmt/binding-linux-s390x-gnu": "0.67.0", + "@oxfmt/binding-linux-x64-gnu": "0.67.0", + "@oxfmt/binding-linux-x64-musl": "0.67.0", + "@oxfmt/binding-openharmony-arm64": "0.67.0", + "@oxfmt/binding-win32-arm64-msvc": "0.67.0", + "@oxfmt/binding-win32-ia32-msvc": "0.67.0", + "@oxfmt/binding-win32-x64-msvc": "0.67.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.82.0.tgz", + "integrity": "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.82.0", + "@oxlint/binding-android-arm64": "1.82.0", + "@oxlint/binding-darwin-arm64": "1.82.0", + "@oxlint/binding-darwin-x64": "1.82.0", + "@oxlint/binding-freebsd-x64": "1.82.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.82.0", + "@oxlint/binding-linux-arm-musleabihf": "1.82.0", + "@oxlint/binding-linux-arm64-gnu": "1.82.0", + "@oxlint/binding-linux-arm64-musl": "1.82.0", + "@oxlint/binding-linux-ppc64-gnu": "1.82.0", + "@oxlint/binding-linux-riscv64-gnu": "1.82.0", + "@oxlint/binding-linux-riscv64-musl": "1.82.0", + "@oxlint/binding-linux-s390x-gnu": "1.82.0", + "@oxlint/binding-linux-x64-gnu": "1.82.0", + "@oxlint/binding-linux-x64-musl": "1.82.0", + "@oxlint/binding-openharmony-arm64": "1.82.0", + "@oxlint/binding-win32-arm64-msvc": "1.82.0", + "@oxlint/binding-win32-ia32-msvc": "1.82.0", + "@oxlint/binding-win32-x64-msvc": "1.82.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/tinypool": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.2.tgz", + "integrity": "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/undici-types": { diff --git a/packages/logger/package.json b/packages/logger/package.json index 715078d4ca..b4e68ac488 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,42 +1,41 @@ { "name": "@saasquatch/logger", "version": "3.0.1-0", - "type": "module", - "description": "Small structured JSON logger for SaaSquatch NodeJS services", "private": false, - "publishConfig": { - "access": "public", - "provenance": true + "description": "Small structured JSON logger for SaaSquatch NodeJS services", + "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", + "bugs": { + "url": "https://github.com/saasquatch/program-tools/issues" }, - "prettier": {}, - "author": "ReferralSaaSquatch.com, Inc.", "license": "MIT", - "engines": { - "node": "^24 || ^26" - }, - "files": [ - "dist" - ], + "author": "ReferralSaaSquatch.com, Inc.", "repository": { "type": "git", "url": "git+https://github.com/saasquatch/program-tools.git" }, - "bugs": { - "url": "https://github.com/saasquatch/program-tools/issues" - }, - "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", + "files": [ + "dist" + ], + "type": "module", "main": "dist/index.js", + "publishConfig": { + "access": "public", + "provenance": true + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout test/*.test.ts", "build": "tsc -b", - "check": "tsc --noEmit", + "check": "tsc --noEmit && oxlint", + "format": "oxfmt", "prepublishOnly": "if [ \"$CI\" != \"true\" ]; then echo Error: Publishing manually is not supported. Please use the GitHub action. && exit 1; fi", "prepack": "npm run build" }, "devDependencies": { "@types/express": "^4.17.25", "@types/node": "^24.12.4", - "typescript": "^6.0.3" + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", + "typescript": "^7.0.2" }, "peerDependencies": { "express": "^4" @@ -45,5 +44,8 @@ "express": { "optional": true } + }, + "engines": { + "node": "^24 || ^26" } } diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 7a9c237c9b..4bdd5bcbab 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -193,9 +193,9 @@ function createLogger( ): T extends true ? string : LogRecord[] { const records = getCollectedRecords(collection); if (opts?.serialized) { - return records - .map((record) => `${serializeRecord(record)}\n`) - .join("") as T extends true ? string : LogRecord[]; + return records.map(serializeRecord).join("\n") as T extends true + ? string + : LogRecord[]; } return records as T extends true ? string : LogRecord[]; @@ -252,12 +252,11 @@ function getCollectedRecords(collection: { size: number; maxEntries: number; }): LogRecord[] { - const records = new Array(collection.size); - for (let index = 0; index < collection.size; index += 1) { - records[index] = - collection.records[(collection.start + index) % collection.maxEntries]; - } - return records; + return Array.from({ length: collection.size }, (_unused, index) => { + return collection.records[ + (collection.start + index) % collection.maxEntries + ]; + }); } function isRecord(value: unknown): value is Record { diff --git a/packages/logger/src/plugins.ts b/packages/logger/src/plugins.ts index 79d37c8193..374fc455a2 100644 --- a/packages/logger/src/plugins.ts +++ b/packages/logger/src/plugins.ts @@ -1,5 +1,5 @@ -import type { Request, Response } from "express"; import { URL } from "node:url"; +import type { Request, Response } from "express"; import type { LogLevel } from "./config.ts"; import type { Logger } from "./logger.ts"; import { LOG_TYPE_MARKER } from "./logger.ts"; From b9e0fb18899a109833166f533f0e8e1ff70f27ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:06:08 +0000 Subject: [PATCH 14/51] publish: @saasquatch/logger v3.0.1-1 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index 3a976d1eaa..ee9966b893 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-0", + "version": "3.0.1-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-0", + "version": "3.0.1-1", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index b4e68ac488..662df88ffa 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-0", + "version": "3.0.1-1", "private": false, "description": "Small structured JSON logger for SaaSquatch NodeJS services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From 328e63bc8cb3ebba37c4c7a3bb6bcf881017da00 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Wed, 16 Sep 2026 12:42:03 -0600 Subject: [PATCH 15/51] logger: type-aware linting --- packages/logger/.oxlintrc.json | 7 +- packages/logger/package-lock.json | 103 ++++++++++++++++++++++++++++++ packages/logger/package.json | 1 + packages/logger/src/config.ts | 2 + packages/logger/src/format.ts | 18 ++++-- packages/logger/src/logger.ts | 14 ++-- packages/logger/src/plugins.ts | 1 + 7 files changed, 132 insertions(+), 14 deletions(-) diff --git a/packages/logger/.oxlintrc.json b/packages/logger/.oxlintrc.json index 31e054fe5d..7977bd9fa1 100644 --- a/packages/logger/.oxlintrc.json +++ b/packages/logger/.oxlintrc.json @@ -2,7 +2,12 @@ "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["typescript", "unicorn", "oxc"], "categories": { - "correctness": "error" + "correctness": "error", + "suspicious": "error" + }, + "options": { + "typeAware": true, + "typeCheck": true }, "rules": {}, "env": { diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index ee9966b893..a42420b670 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "^24.12.4", "oxfmt": "^0.67.0", "oxlint": "^1.82.0", + "oxlint-tsgolint": "^7.0.2001", "typescript": "^7.0.2" }, "engines": { @@ -374,6 +375,90 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz", + "integrity": "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2001.tgz", + "integrity": "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2001.tgz", + "integrity": "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2001.tgz", + "integrity": "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2001.tgz", + "integrity": "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz", + "integrity": "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@oxlint/binding-android-arm-eabi": { "version": "1.82.0", "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.82.0.tgz", @@ -1280,6 +1365,24 @@ } } }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2001.tgz", + "integrity": "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2001", + "@oxlint-tsgolint/darwin-x64": "7.0.2001", + "@oxlint-tsgolint/linux-arm64": "7.0.2001", + "@oxlint-tsgolint/linux-x64": "7.0.2001", + "@oxlint-tsgolint/win32-arm64": "7.0.2001", + "@oxlint-tsgolint/win32-x64": "7.0.2001" + } + }, "node_modules/tinypool": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.2.tgz", diff --git a/packages/logger/package.json b/packages/logger/package.json index 662df88ffa..d46518532c 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -35,6 +35,7 @@ "@types/node": "^24.12.4", "oxfmt": "^0.67.0", "oxlint": "^1.82.0", + "oxlint-tsgolint": "^7.0.2001", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 769a62d971..1319f287da 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -54,9 +54,11 @@ export function defaultConfig(): LoggerConfig { let logLevel: LogLevel = "info"; if (configuredLevel !== undefined) { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion if (!LOG_LEVELS.includes(configuredLevel as LogLevel)) { throw new Error(`Invalid log level "${configuredLevel}"`); } + // oxlint-disable-next-line typescript/no-unsafe-type-assertion logLevel = configuredLevel as LogLevel; } diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 574add5287..7448ec5bca 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -21,7 +21,7 @@ export function formatRecord( message: unknown, fields: Record = {}, ): LogRecord { - const record: Record = { + const record: LogRecord = { ...fields, level, timestamp: new Date().toISOString(), @@ -32,10 +32,13 @@ export function formatRecord( record["logger.name"] = name; } - if (record["tenantAlias"] && typeof record["message"] === "string") { - const alias = String(record["tenantAlias"]); - if (!(record["message"] as string).startsWith(`[${alias}]`)) { - record["message"] = `[${alias}] ${record["message"]}`; + if ( + typeof record["tenantAlias"] === "string" && + typeof record["message"] === "string" + ) { + const tenantAliasTag = `[${record["tenantAlias"]}]`; + if (!record["message"].startsWith(tenantAliasTag)) { + record["message"] = `${tenantAliasTag} ${record["message"]}`; } } @@ -46,7 +49,7 @@ export function formatRecord( delete record[LOG_TYPE_MARKER]; record["status"] = level; - return record as LogRecord; + return record; } function formatHttpRecord(record: Record): void { @@ -55,7 +58,8 @@ function formatHttpRecord(record: Record): void { return; } - const message = value as unknown as HTTPMessage; + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const message = value as HTTPMessage; const micros = Number(message.time); const displayTime = micros < 1000 ? `${micros} μs` : `${Math.round(micros / 1000)} ms`; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 4bdd5bcbab..bda0ae6a4f 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -52,18 +52,18 @@ export interface Logger { } type Sink = (serializedRecord: string) => void; -const _loggers = new Map(); +const loggers = new Map(); export function getLogger(logger?: string): Logger { const name = logger ?? DEFAULT_LOGGER_NAME; - if (!_loggers.has(name)) { + if (!loggers.has(name)) { initializeLogger(name); } - return _loggers.get(name)!; + return loggers.get(name)!; } export function isLoggerInitialized(logger?: string): boolean { - return _loggers.has(logger ?? DEFAULT_LOGGER_NAME); + return loggers.has(logger ?? DEFAULT_LOGGER_NAME); } export function initializeLogger( @@ -73,7 +73,7 @@ export function initializeLogger( const name = typeof nameOrConfig === "string" ? nameOrConfig : DEFAULT_LOGGER_NAME; - if (_loggers.has(name)) { + if (loggers.has(name)) { throw new Error("Logger has already been initialized"); } @@ -85,7 +85,7 @@ export function initializeLogger( const level = { value: conf.logLevel }; const logger = createLogger(name, sinks, level, {}); - _loggers.set(name, logger); + loggers.set(name, logger); return logger; } @@ -193,11 +193,13 @@ function createLogger( ): T extends true ? string : LogRecord[] { const records = getCollectedRecords(collection); if (opts?.serialized) { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion return records.map(serializeRecord).join("\n") as T extends true ? string : LogRecord[]; } + // oxlint-disable-next-line typescript/no-unsafe-type-assertion return records as T extends true ? string : LogRecord[]; }, diff --git a/packages/logger/src/plugins.ts b/packages/logger/src/plugins.ts index 374fc455a2..401b5e341c 100644 --- a/packages/logger/src/plugins.ts +++ b/packages/logger/src/plugins.ts @@ -70,6 +70,7 @@ export function httpLogMiddleware( const method = req.method; const requestId = res.locals?.["requestId"]; + // oxlint-disable-next-line typescript/no-unsafe-type-assertion const extraData = res.locals?.["extraData"] as | Record | undefined; From 93f1e8a390134e8c7e973a66e44f1faf13635413 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:43:22 +0000 Subject: [PATCH 16/51] publish: @saasquatch/logger v3.0.1-2 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index a42420b670..2b66fda8b7 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-1", + "version": "3.0.1-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-1", + "version": "3.0.1-2", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index d46518532c..293c81146e 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-1", + "version": "3.0.1-2", "private": false, "description": "Small structured JSON logger for SaaSquatch NodeJS services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From 686ff2878046b2e5055bb59b888d9e78e9d93e18 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Wed, 16 Sep 2026 12:28:41 -0600 Subject: [PATCH 17/51] express-boilerplate: use modern dev tools Prettier -> oxfmt ESLint -> oxlint TypeScript 6 -> TypeScript 7 --- packages/express-boilerplate/.oxfmtrc.json | 8 + packages/express-boilerplate/.oxlintrc.json | 16 + .../express-boilerplate/eslint.config.cjs | 42 - .../express-boilerplate/package-lock.json | 2470 ++++++++--------- packages/express-boilerplate/package.json | 53 +- .../express-boilerplate/src/async-wrapper.ts | 8 +- packages/express-boilerplate/src/error.ts | 2 +- .../express-boilerplate/src/healthcheck.ts | 7 +- packages/express-boilerplate/src/shutdown.ts | 30 +- .../src/tests/async-wrapper.test.ts | 20 +- .../src/tests/error.test.ts | 2 +- .../src/tests/healthcheck.test.ts | 5 +- .../src/tests/middleware.test.ts | 6 +- .../src/tests/nanoid.test.ts | 2 +- .../src/tests/shutdown.test.ts | 2 +- 15 files changed, 1272 insertions(+), 1401 deletions(-) create mode 100644 packages/express-boilerplate/.oxfmtrc.json create mode 100644 packages/express-boilerplate/.oxlintrc.json delete mode 100644 packages/express-boilerplate/eslint.config.cjs diff --git a/packages/express-boilerplate/.oxfmtrc.json b/packages/express-boilerplate/.oxfmtrc.json new file mode 100644 index 0000000000..e1ceec9ec9 --- /dev/null +++ b/packages/express-boilerplate/.oxfmtrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [], + "printWidth": 80, + "sortImports": { + "newlinesBetween": false + } +} diff --git a/packages/express-boilerplate/.oxlintrc.json b/packages/express-boilerplate/.oxlintrc.json new file mode 100644 index 0000000000..7977bd9fa1 --- /dev/null +++ b/packages/express-boilerplate/.oxlintrc.json @@ -0,0 +1,16 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc"], + "categories": { + "correctness": "error", + "suspicious": "error" + }, + "options": { + "typeAware": true, + "typeCheck": true + }, + "rules": {}, + "env": { + "builtin": true + } +} diff --git a/packages/express-boilerplate/eslint.config.cjs b/packages/express-boilerplate/eslint.config.cjs deleted file mode 100644 index 3ff7a7568f..0000000000 --- a/packages/express-boilerplate/eslint.config.cjs +++ /dev/null @@ -1,42 +0,0 @@ -const { defineConfig } = require("eslint/config"); - -const tsParser = require("@typescript-eslint/parser"); -const typescriptEslint = require("@typescript-eslint/eslint-plugin"); -const js = require("@eslint/js"); - -const { FlatCompat } = require("@eslint/eslintrc"); - -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, -}); - -module.exports = defineConfig([ - { - extends: compat.extends( - "eslint:recommended", - "plugin:@typescript-eslint/strict-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - ), - - languageOptions: { - parserOptions: { - project: true, - tsconfigRootDir: __dirname, - }, - - parser: tsParser, - }, - - plugins: { - "@typescript-eslint": typescriptEslint, - }, - - rules: { - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/consistent-type-definitions": ["error", "type"], - }, - }, -]); diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 6fb7abf672..91ad99a8a3 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -9,20 +9,18 @@ "version": "2.0.1-0", "license": "MIT", "dependencies": { - "nanoid": "^3.3.7" + "nanoid": "^6.0.1" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "^10.0.1", - "@saasquatch/logger": "^3.0.1-0", + "@saasquatch/logger": "^3.0.1-2", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", - "@typescript-eslint/eslint-plugin": "^8.59.4", - "@typescript-eslint/parser": "^8.59.4", - "eslint": "^10.4.0", + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", + "oxlint-tsgolint": "^7.0.2001", "supertest": "^7.2.2", - "typescript": "^6.0.3" + "typescript": "^7.0.2" }, "engines": { "node": "^24 || ^26" @@ -32,294 +30,795 @@ "express": "^4.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^14.21.3 || >=16" }, "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.67.0.tgz", + "integrity": "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.67.0.tgz", + "integrity": "sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.67.0.tgz", + "integrity": "sha512-MfONZx/O2o9M5v2jDFol556G9+A+P9xCuJ4DZ+qhE+RnaCdoscy6Eu5nq1dbuNxhwdJyZ6kLI7fnG9mwEeOeGg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.67.0.tgz", + "integrity": "sha512-CYnIx5LvFVJnyJcCqwH2jxMKjFjqo5678MPjdmNFoSGMhlOvZ/xRZqvhDcolKrXc8fezW3AKh+C4wyoFuWOSSg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.67.0.tgz", + "integrity": "sha512-7/iF1orvIS9mxhKUqnmtMgm+OrSQ5acPwuvdQrm6ECgqbwPmC+Pw9cdke3sNfVN6pT2hbJ58+jP8BCThl5HXOg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.67.0.tgz", + "integrity": "sha512-yy+OGys07IZOpOmYPZoObKyUQLkfxeQqeCypk+1jaZd8HGo77hzvU1Jg8X3+W75o+9lszOjBfg0nkGtlwYywXw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@eslint/eslintrc/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==", + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.67.0.tgz", + "integrity": "sha512-wPIeeigXgJpwNw3wydYRt3U9iN9Y/ejpOZuYL9IA7igxWs7LIQMOkhKxTumRvy6dIv0iXKk3RTw3Vmjg0i+2sg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.67.0.tgz", + "integrity": "sha512-0+XNxcdbkTfxdcD4qW6Ci9n+mBNJ8xTBumnxKvKBmRFOdx0Wf8/KiHjCJayooXmYkqRpRVd98Q5egvzx5BLSgQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.67.0.tgz", + "integrity": "sha512-I75LKPJyNOYUzkqAiAMIE31+Ye7xtQXZdoty1IXn4B+bw5Zpmez5wfG19ejGpNnS/BzQ7LFS+7jxuTPb+vHiZw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.67.0.tgz", + "integrity": "sha512-c2M5iRpe1QMZSRE/UvZoPdXBWb5Ic/ycvOyNiKCqPwQ/OyOKIMiJs02ynlNnjb7ZZJnRXYLmGcohoINOcwDK3w==", + "cpu": [ + "ppc64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.67.0.tgz", + "integrity": "sha512-dQzzYlV24Udhfm5ECuSdgqRvFJU/CGHzcYYEO3dLM6W6+CHiBFrq9OjIllkdCcPhsoSQ8o223Dja84MOSzed9A==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "*" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.67.0.tgz", + "integrity": "sha512-rFNq1CgX4qMJANOq42LkAs90JE80GpiaEohAV2qn/gT2hGjQTW1zBO5zQBxArI4926pM1OSzo3CN0tBszGBIaA==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.67.0.tgz", + "integrity": "sha512-Sky6rEdz2o5IGq01lPhS12yEvDdChVEcaYrcLHkveh4Fx0qPjljE/Iul6SX/bRMl6lNc8J7J/mDQdzgBdA++Pg==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "Apache-2.0", + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.67.0.tgz", + "integrity": "sha512-vPXmlNORV8AZq2Ocxh07pxwMjfENUWCV/eZArnao0qC3NO/hDeTVkQvee7SJJUbIiF5PZbBa4kYmaXnu7Rk58w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.67.0.tgz", + "integrity": "sha512-x/WAtFqYtVr3vZ9ni8nr4kn9whSitg8fOljq/pZzBpxopRdY1BMLZCZkrbIbaBcYkm46qGbqVea2FCWmtQ2P9w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.67.0.tgz", + "integrity": "sha512-eRw9Neh4/aA6i+q/R3WU1gGQINhVM0J4fXIm6t27caOamkr/37uAkp1IdBx4zlJH97hmXR63z/q9n5c5dN7MzA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=18.18.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.67.0.tgz", + "integrity": "sha512-YIMvb+sGNYN2uc6+QK2HLPeEKM2vl7QZ5onQzpAJRb6pnf0DwUFP5R8tdS9R0l8hdUil2gu4Uxd0Yxrop0iT4w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.67.0.tgz", + "integrity": "sha512-LzmU9MyACPzwNDIK0ItMedHPz735Ug7ELWguxo4/kuy6zWuDoeglOAEFCY8jLg0PzRpFO3hDyLFe2Gu2eFDeGA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.67.0.tgz", + "integrity": "sha512-sbQOIDNLUEeVZcAJcSL5VURn7kfjvilPviody4Yl5n8lQCDtUm+C9oHTTwZS/m4d/Z6Vv3jNEiAofH932NPPCg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz", + "integrity": "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2001.tgz", + "integrity": "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2001.tgz", + "integrity": "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2001.tgz", + "integrity": "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2001.tgz", + "integrity": "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz", + "integrity": "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.82.0.tgz", + "integrity": "sha512-a3LB+C5Dsj5b/qtmG/mv5WrzuiXEpg1KF5nXWcEvaoN5TYAqkIvxPOwTPp3Jy/FoGpRo8zsTFhMElMXfeoOEzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.82.0.tgz", + "integrity": "sha512-OBlhRgNqFblGpGenno/aqOfJLOkQ2B8Ig3iDAalfn0H8hJGZKXPeexCRTDm6uwv6YUjSA9Xnwt1y/Bgj5ZH8uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.82.0.tgz", + "integrity": "sha512-dsopxqtY5ZdyT9uLHyGt1SyiLop6hi7hWI3PKpePodkRQOkLaCm+OE4fR9CAz9qdfjiFO8531tX/QDyP/psjFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.82.0.tgz", + "integrity": "sha512-94Lu0SgTClKColU66g1VDuigV3HkcbkJBnTtZjGYfE8UPugaWDgKrm2icjC6HJVUYler2OXaHP/X0TBy8+CowQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.82.0.tgz", + "integrity": "sha512-hne/V06ewhh1i0w8+l7GDNROAGCGPmyFuOwiP7YTRu0JycyStJ4785dmF8xU5p0uUwt2emvIF9vc7Xjis+cJ0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.82.0.tgz", + "integrity": "sha512-aWY2xtbZf1LneW9Qsv/n2Sp8gOu74JrlQzEtj4coHX2SHFrCfhmAumaU+sI/A5nr+yoTRTSmI/pL2s6ADlNSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.82.0.tgz", + "integrity": "sha512-Fe+TtXCXMh/5f7kWlZ2VAwsMumZWtraFlKVk1NJlL52/beGwfDE7ov+/8gVirHzWokzGu7X65hSPq0ucPDskWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.82.0.tgz", + "integrity": "sha512-6azCZ6OJudlvipNttXCCQcyeFfcJ/NvUZdSN1z8elo73kCHtyQC7WTiUcSjWYvJ1jaq9KDUyMAoAS/vNzhBomA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.82.0.tgz", + "integrity": "sha512-PLEaSD8IAIIlwW4dwOd9YaxuxeOpwiXL4J24rcnE4iNtyM5j9Q9/3+gti08oXpx0u2ygNjRDx9xjWWpQonuJEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.82.0.tgz", + "integrity": "sha512-D94em/BwknNTn4vqxjHh5wb2oL566eFhArabqKIr0cNZMHOJuiraFp1A8tXpH05bbE5tqwEfLXTI0MWEGtn3Dw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.82.0.tgz", + "integrity": "sha512-MOprxBaoYU2D4VgxXCl3ghydThWtx7Um1lL51kGYNeQ5Al7WzsH7/tqGdNtbLrIWnjq3bsm13+nz/gRIxjrOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.82.0.tgz", + "integrity": "sha512-5h55QsfJ/luDXZzC20k6SNOY1Az+dCP9WvntKtcUWh2JhckAdwApY2ZusaBTwLENnReXU+A2fJtSrYvZJNKNPg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.82.0.tgz", + "integrity": "sha512-IE8NJNLlHr0CaXyGJPGVn0eTkUyoj1I2UfA8x7I4PSOYKsQ/6btVC7Pywrj5onk0cMH25r6Z38SoN3AvE5Zuog==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.82.0.tgz", + "integrity": "sha512-XUUUxaBo9XKl+J1B9EmP1cTGQPddzeURvoGkfwh/94PGnbW+hBprDljneoI2M1jzC1bzrIV3ihc7iM9UXl8+tg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.82.0.tgz", + "integrity": "sha512-SWLSFulX9TDuH6yvbPYp4+VNn6jkkIvvI+KiujDM5rWBRHEfkesCC/pCneIIUr6ovkxZ5fRtpi2v5Cz5FrMJZg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.82.0.tgz", + "integrity": "sha512-BQy35f6ZUdNr9a6c7B7orxQTcLjByGT2z3WAgmRovpRwmPYAaJ+NTplmMzhdjdJ4qSchfMNZy/Ukg+qRg6zseQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.82.0.tgz", + "integrity": "sha512-V4QhSTg5gctZue8RJjsGi7NpQPThr/p1/HfmiMC5kfe1KFEup9SQRVub4A6kijQjdHfxj7bLL1KO3QO7/5bwMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.82.0.tgz", + "integrity": "sha512-TUSCLaKB2yktpFAJ/r3HAUYsaV/3DT7JS4iNKyoh3a9YNwD0UG7Ezh4D8m23654vQcU6P/RQrCAjRPKe4peP/A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.82.0.tgz", + "integrity": "sha512-VTVoRIWJTb+wvUX8EYoPArfFH02whuR10goFXE/LHRRX33ajRrFgqbcONXZMiF4C5rnattfkm87HqYn8jb8hmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@paralleldrive/cuid2": { @@ -333,9 +832,9 @@ } }, "node_modules/@saasquatch/logger": { - "version": "3.0.1-0", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-0.tgz", - "integrity": "sha512-+J5/YElPQhr8e6KWu06QoQYbTMBRkKXq/12b8ep8Dkr1pGt0EiPv7h+231t93l1nvST9EULLXtaEz+O3pikbIg==", + "version": "3.0.1-2", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-2.tgz", + "integrity": "sha512-2Ib3PrJBHoe9gAxAqH+kYy6txJ6Mrcv2bQy2YkXR3hJaIHEeLO8u/aJQr7z2olZRvwHS/vPCTVoRVa7N2ZKiAQ==", "dev": true, "license": "MIT", "engines": { @@ -378,20 +877,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -425,13 +910,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -527,237 +1005,344 @@ "@types/superagent": "^8.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", - "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/type-utils": "8.59.4", - "@typescript-eslint/utils": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.4", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", - "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", - "debug": "^4.4.3" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", - "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.4", - "@typescript-eslint/types": "^8.59.4", - "debug": "^4.4.3" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", - "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", - "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", - "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4", - "@typescript-eslint/utils": "8.59.4", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", - "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", - "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.4", - "@typescript-eslint/tsconfig-utils": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", - "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", - "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.4", - "eslint-visitor-keys": "^5.0.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], "dev": true, "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=16.20.0" } }, "node_modules/accepts": { @@ -774,53 +1359,6 @@ "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -842,20 +1380,10 @@ "dev": true, "license": "MIT" }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", "license": "MIT", "peer": true, "dependencies": { @@ -867,7 +1395,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.15.1", + "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -894,17 +1422,21 @@ "license": "MIT", "peer": true }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", + "node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "peer": true, "dependencies": { - "balanced-match": "^4.0.2" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/bytes": { @@ -946,16 +1478,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -979,13 +1501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1033,21 +1548,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1066,13 +1566,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1199,207 +1692,6 @@ "license": "MIT", "peer": true }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", - "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1474,27 +1766,6 @@ "license": "MIT", "peer": true }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1502,37 +1773,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -1569,56 +1809,18 @@ "license": "MIT", "peer": true }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -1695,43 +1897,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, "node_modules/gopd": { @@ -1775,9 +1951,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1820,43 +1996,6 @@ "node": ">=0.10.0" } }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "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/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1874,110 +2013,6 @@ "node": ">= 0.10" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2050,22 +2085,6 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2073,9 +2092,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.1.tgz", + "integrity": "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==", "funding": [ { "type": "github", @@ -2084,19 +2103,12 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^22 || ^24 || >=26" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2142,67 +2154,123 @@ "wrappy": "1" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/oxfmt": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.67.0.tgz", + "integrity": "sha512-vV7sSiPsaO0mSxdoUdayipVDFPzW/UQ+hrezEHa20+Tx1dnMdZLSRHMT0PdS67FFbhd74M1n08asW21aLGeCrA==", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "tinypool": "2.1.2" }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" + "bin": { + "oxfmt": "bin/oxfmt" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.67.0", + "@oxfmt/binding-android-arm64": "0.67.0", + "@oxfmt/binding-darwin-arm64": "0.67.0", + "@oxfmt/binding-darwin-x64": "0.67.0", + "@oxfmt/binding-freebsd-x64": "0.67.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.67.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.67.0", + "@oxfmt/binding-linux-arm64-gnu": "0.67.0", + "@oxfmt/binding-linux-arm64-musl": "0.67.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.67.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.67.0", + "@oxfmt/binding-linux-riscv64-musl": "0.67.0", + "@oxfmt/binding-linux-s390x-gnu": "0.67.0", + "@oxfmt/binding-linux-x64-gnu": "0.67.0", + "@oxfmt/binding-linux-x64-musl": "0.67.0", + "@oxfmt/binding-openharmony-arm64": "0.67.0", + "@oxfmt/binding-win32-arm64-msvc": "0.67.0", + "@oxfmt/binding-win32-ia32-msvc": "0.67.0", + "@oxfmt/binding-win32-x64-msvc": "0.67.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/oxlint": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.82.0.tgz", + "integrity": "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ==", "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" + "bin": { + "oxlint": "bin/oxlint" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.82.0", + "@oxlint/binding-android-arm64": "1.82.0", + "@oxlint/binding-darwin-arm64": "1.82.0", + "@oxlint/binding-darwin-x64": "1.82.0", + "@oxlint/binding-freebsd-x64": "1.82.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.82.0", + "@oxlint/binding-linux-arm-musleabihf": "1.82.0", + "@oxlint/binding-linux-arm64-gnu": "1.82.0", + "@oxlint/binding-linux-arm64-musl": "1.82.0", + "@oxlint/binding-linux-ppc64-gnu": "1.82.0", + "@oxlint/binding-linux-riscv64-gnu": "1.82.0", + "@oxlint/binding-linux-riscv64-musl": "1.82.0", + "@oxlint/binding-linux-s390x-gnu": "1.82.0", + "@oxlint/binding-linux-x64-gnu": "1.82.0", + "@oxlint/binding-linux-x64-musl": "1.82.0", + "@oxlint/binding-openharmony-arm64": "1.82.0", + "@oxlint/binding-win32-arm64-msvc": "1.82.0", + "@oxlint/binding-win32-ia32-msvc": "1.82.0", + "@oxlint/binding-win32-x64-msvc": "1.82.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, - "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==", + "node_modules/oxlint-tsgolint": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2001.tgz", + "integrity": "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==", "dev": true, "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" + "bin": { + "tsgolint": "bin/tsgolint.js" }, - "engines": { - "node": ">=6" + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2001", + "@oxlint-tsgolint/darwin-x64": "7.0.2001", + "@oxlint-tsgolint/linux-arm64": "7.0.2001", + "@oxlint-tsgolint/linux-x64": "7.0.2001", + "@oxlint-tsgolint/win32-arm64": "7.0.2001", + "@oxlint-tsgolint/win32-x64": "7.0.2001" } }, "node_modules/parseurl": { @@ -2215,26 +2283,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -2242,29 +2290,6 @@ "license": "MIT", "peer": true }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2279,23 +2304,14 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -2330,16 +2346,6 @@ "node": ">= 0.8" } }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2368,19 +2374,6 @@ "license": "MIT", "peer": true }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -2446,38 +2439,15 @@ "license": "ISC", "peer": true }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -2551,19 +2521,6 @@ "node": ">= 0.8" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -2623,21 +2580,14 @@ "node": ">=6.6.0" } }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "node_modules/tinypool": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.2.tgz", + "integrity": "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==", "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": "^20.0.0 || >=22.0.0" } }, "node_modules/toidentifier": { @@ -2650,32 +2600,6 @@ "node": ">=0.6" } }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2691,17 +2615,38 @@ } }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/undici-types": { @@ -2721,16 +2666,6 @@ "node": ">= 0.8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2751,51 +2686,12 @@ "node": ">= 0.8" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index f3ef46c833..7e65c28038 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -1,58 +1,55 @@ { "name": "@saasquatch/express-boilerplate", "version": "2.0.1-0", - "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", - "main": "dist/index.js", - "type": "module", "private": false, - "publishConfig": { - "provenance": true, - "access": "public" + "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", + "homepage": "https://github.com/saasquatch/program-tools/packages/express-boilerplate#readme", + "bugs": { + "url": "https://github.com/saasquatch/program-tools/issues" }, - "author": "ReferralSaaSquatch.com, Inc.", "license": "MIT", - "engines": { - "node": "^24 || ^26" + "author": "ReferralSaaSquatch.com, Inc.", + "repository": { + "type": "git", + "url": "git+https://github.com/saasquatch/program-tools.git" }, "files": [ "dist/*.js", "dist/*.d.ts" ], - "repository": { - "type": "git", - "url": "git+https://github.com/saasquatch/program-tools.git" - }, - "bugs": { - "url": "https://github.com/saasquatch/program-tools/issues" + "type": "module", + "main": "dist/index.js", + "publishConfig": { + "access": "public", + "provenance": true }, - "homepage": "https://github.com/saasquatch/program-tools/packages/express-boilerplate#readme", "scripts": { "test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout src/tests/**/*.ts", "build": "tsc -b", - "check": "tsc --noEmit && npm run lint", + "check": "tsc --noEmit && oxlint", + "format": "oxfmt", "prepublishOnly": "if [ \"$CI\" != \"true\" ]; then echo Error: Publishing manually is not supported. Please use the GitHub action. && exit 1; fi", - "prepack": "npm run build", - "lint": "eslint ./src --report-unused-disable-directives" + "prepack": "npm run build" }, "dependencies": { - "nanoid": "^3.3.7" + "nanoid": "^6.0.1" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "^10.0.1", - "@saasquatch/logger": "^3.0.1-0", + "@saasquatch/logger": "^3.0.1-2", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", - "@typescript-eslint/eslint-plugin": "^8.59.4", - "@typescript-eslint/parser": "^8.59.4", - "eslint": "^10.4.0", + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", + "oxlint-tsgolint": "^7.0.2001", "supertest": "^7.2.2", - "typescript": "^6.0.3" + "typescript": "^7.0.2" }, "peerDependencies": { "@saasquatch/logger": "^3.0.0", "express": "^4.0.0" }, - "prettier": {} + "engines": { + "node": "^24 || ^26" + } } diff --git a/packages/express-boilerplate/src/async-wrapper.ts b/packages/express-boilerplate/src/async-wrapper.ts index cacadd9464..6295444486 100644 --- a/packages/express-boilerplate/src/async-wrapper.ts +++ b/packages/express-boilerplate/src/async-wrapper.ts @@ -1,5 +1,5 @@ -import type { Request, Response } from "express"; import type { Logger } from "@saasquatch/logger"; +import type { Request, Response } from "express"; import { formatGenericError } from "./error.ts"; import { nanoid } from "./nanoid.ts"; @@ -13,7 +13,9 @@ export function asyncHandlerWrapper( ): (req: Request, res: Response) => void { return (req, res) => { handler(req, res).catch((e: unknown) => { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion const logger = res.locals["logger"] as Logger | undefined; + // oxlint-disable-next-line typescript/no-unsafe-type-assertion const requestId = res.locals["requestId"] as string | undefined; const debugId = nanoid(8); @@ -45,10 +47,10 @@ export function asyncHandlerWrapper( .then((html) => { res.status(status).contentType("text/html;charset=utf8").send(html); }) - .catch((e: unknown) => { + .catch((e2: unknown) => { logger?.error({ message: "Error occurred while rendering error page!", - ...formatGenericError(e), + ...formatGenericError(e2), }); res diff --git a/packages/express-boilerplate/src/error.ts b/packages/express-boilerplate/src/error.ts index aec4b8ea09..11110fc8c7 100644 --- a/packages/express-boilerplate/src/error.ts +++ b/packages/express-boilerplate/src/error.ts @@ -9,7 +9,7 @@ export function formatGenericError(e: unknown): GenericError { e, // we don't care that the value of e might be weird in the string template, the // whole point is to just convert it to a string no matter the type. - // eslint-disable-next-line -- @typescript-eslint/restrict-template-expressions + // oxlint-disable-next-line typescript/restrict-template-expressions eStr: `${e}`, eJson: JSON.stringify(e), }; diff --git a/packages/express-boilerplate/src/healthcheck.ts b/packages/express-boilerplate/src/healthcheck.ts index 9d02a2e4c5..2e6dfb9988 100644 --- a/packages/express-boilerplate/src/healthcheck.ts +++ b/packages/express-boilerplate/src/healthcheck.ts @@ -25,11 +25,11 @@ export function healthCheck( customCheck?: () => Promise>, ): (req: Request, res: Response) => void { return (_req, res) => { - // eslint-disable-next-line -- @typescript-eslint/no-unsafe-assignment const terminating = app.locals[TERMINATION_APP_LOCAL_KEY]; if (typeof terminating === "boolean" && terminating) { logger.info("App is in TERMINATING state, sending health check failure"); - return res.status(503).json({ status: "TERMINATING" }); + res.status(503).json({ status: "TERMINATING" }); + return; } if (customCheck) { @@ -51,7 +51,8 @@ export function healthCheck( }); return; } else { - return res.status(200).json({ status: "OK" }); + res.status(200).json({ status: "OK" }); + return; } }; } diff --git a/packages/express-boilerplate/src/shutdown.ts b/packages/express-boilerplate/src/shutdown.ts index 3e60f29680..3626f8771b 100644 --- a/packages/express-boilerplate/src/shutdown.ts +++ b/packages/express-boilerplate/src/shutdown.ts @@ -1,6 +1,6 @@ +import { Server, createServer } from "http"; import type { Logger } from "@saasquatch/logger"; import type { Application } from "express"; -import { Server, createServer } from "http"; import { formatGenericError } from "./error.ts"; const handleHookError = (hook: string, logger: Logger) => (err: unknown) => { @@ -23,22 +23,22 @@ export type ShutdownManagerConfig = { afterShutdown?: () => Promise; }; +const optionalInt = (key: string, defaultVal: number): number => { + const env = process.env[key]; + if (!env) { + return defaultVal; + } + + const parsedEnv = parseInt(env, 10); + if (Number.isNaN(parsedEnv)) { + throw new Error(`Environment variable "${key}" is not an integer`); + } + return parsedEnv; +}; + export function shutdownManagerConfigFromEnv( defaults?: Partial, ): ShutdownManagerConfig { - const optionalInt = (key: string, defaultVal: number): number => { - const env = process.env[key]; - if (!env) { - return defaultVal; - } - - const parsedEnv = parseInt(env, 10); - if (Number.isNaN(parsedEnv)) { - throw new Error(`Environment variable "${key}" is not an integer`); - } - return parsedEnv; - }; - const keepAliveTimeoutSeconds = optionalInt( "SSQT_HTTP_KEEP_ALIVE_SECONDS", defaults?.keepAliveTimeoutSeconds ?? 60, @@ -67,7 +67,6 @@ export function installShutdownManager( app.disable("x-powered-by"); const server = createServer(app); - // eslint-disable-next-line -- @typescript-eslint/no-unsafe-assignment const shutdownManagerInstalled = app.locals[INSTALLATION_APP_LOCAL_KEY]; if ( @@ -85,7 +84,6 @@ export function installShutdownManager( server.headersTimeout = (config.keepAliveTimeoutSeconds + 1) * 1000; const gracefulShutdown = (signal: string) => () => { - // eslint-disable-next-line -- @typescript-eslint/no-unsafe-assignment const isTerminating = app.locals[TERMINATION_APP_LOCAL_KEY]; if (typeof isTerminating === "boolean" && isTerminating) { diff --git a/packages/express-boilerplate/src/tests/async-wrapper.test.ts b/packages/express-boilerplate/src/tests/async-wrapper.test.ts index 6afc44a7d4..416065d75c 100644 --- a/packages/express-boilerplate/src/tests/async-wrapper.test.ts +++ b/packages/express-boilerplate/src/tests/async-wrapper.test.ts @@ -1,13 +1,12 @@ -import express from "express"; +import * as assert from "node:assert"; import { test } from "node:test"; +import express from "express"; import request from "supertest"; import { asyncHandlerWrapper } from "../async-wrapper.ts"; import { requestIdAndLogger } from "../middleware.ts"; import { jestLogger } from "./util.ts"; -import * as assert from "node:assert"; -/* eslint-disable @typescript-eslint/no-floating-promises */ -/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +// oxlint-disable typescript/no-floating-promises test("wrapper with no rejected promise", async () => { const app = express(); @@ -85,9 +84,7 @@ test("default error message", async () => { app.get( "/", asyncHandlerWrapper(async () => { - // eslint-disable-next-line -- @typescript-eslint/no-throw-literal throw { random: "object" }; - return Promise.resolve(); }), ); @@ -150,12 +147,15 @@ test("rejection after headers sent", async () => { }); }); +const renderErrorPage = () => Promise.resolve(""); +const rejectErrorPage = (): Promise => + Promise.reject(new Error("failed to render html error page")); + test("custom html error page", async () => { const app = express(); const logger = jestLogger(); app.use(requestIdAndLogger(logger)); - const renderErrorPage = () => Promise.resolve(""); app.get( "/", @@ -185,7 +185,6 @@ test("custom html error page, no HTML accept header", async () => { const logger = jestLogger(); app.use(requestIdAndLogger(logger)); - const renderErrorPage = () => Promise.resolve(""); app.get( "/", @@ -216,14 +215,11 @@ test("error while rendering custom error page", async () => { app.use(requestIdAndLogger(logger)); - const renderErrorPage = (): Promise => - Promise.reject(new Error("failed to render html error page")); - app.get( "/", asyncHandlerWrapper(async () => { return Promise.reject(new Error("error message from the handler")); - }, renderErrorPage), + }, rejectErrorPage), ); await new Promise((resolve, reject) => { diff --git a/packages/express-boilerplate/src/tests/error.test.ts b/packages/express-boilerplate/src/tests/error.test.ts index 746cb08bc2..b9f978b5c8 100644 --- a/packages/express-boilerplate/src/tests/error.test.ts +++ b/packages/express-boilerplate/src/tests/error.test.ts @@ -2,7 +2,7 @@ import * as assert from "node:assert"; import { test } from "node:test"; import { formatGenericError } from "../error.ts"; -/* eslint-disable @typescript-eslint/no-floating-promises */ +// oxlint-disable typescript/no-floating-promises test("formatGenericError", () => { const error = new Error("Error message"); diff --git a/packages/express-boilerplate/src/tests/healthcheck.test.ts b/packages/express-boilerplate/src/tests/healthcheck.test.ts index d79056b87e..4b8327ed77 100644 --- a/packages/express-boilerplate/src/tests/healthcheck.test.ts +++ b/packages/express-boilerplate/src/tests/healthcheck.test.ts @@ -1,13 +1,12 @@ -import express from "express"; import * as assert from "node:assert"; import { test } from "node:test"; +import express from "express"; import request from "supertest"; import { healthCheck } from "../healthcheck.ts"; import { TERMINATION_APP_LOCAL_KEY } from "../shutdown.ts"; import { jestLogger } from "./util.ts"; -/* eslint-disable @typescript-eslint/no-floating-promises */ -/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +// oxlint-disable typescript/no-floating-promises test("returns 200 when not terminating", async () => { const app = express(); diff --git a/packages/express-boilerplate/src/tests/middleware.test.ts b/packages/express-boilerplate/src/tests/middleware.test.ts index 70bc42fa02..66bef51212 100644 --- a/packages/express-boilerplate/src/tests/middleware.test.ts +++ b/packages/express-boilerplate/src/tests/middleware.test.ts @@ -1,12 +1,11 @@ -import express from "express"; import * as assert from "node:assert"; import { test } from "node:test"; +import express from "express"; import request from "supertest"; import { requestIdAndLogger } from "../middleware.ts"; import { jestLogger } from "./util.ts"; -/* eslint-disable @typescript-eslint/no-floating-promises */ -/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +// oxlint-disable typescript/no-floating-promises test("requestIdAndLogger adds requestId and logger", async () => { const app = express(); @@ -14,6 +13,7 @@ test("requestIdAndLogger adds requestId and logger", async () => { app.use(requestIdAndLogger(logger)); app.use((_req, res, next) => { assert.strictEqual(typeof res.locals["requestId"], "string"); + // oxlint-disable-next-line typescript/no-unsafe-type-assertion const requestLogger = res.locals["logger"] as typeof logger; assert.notStrictEqual(requestLogger, logger); assert.strictEqual(requestLogger.name, logger.name); diff --git a/packages/express-boilerplate/src/tests/nanoid.test.ts b/packages/express-boilerplate/src/tests/nanoid.test.ts index 448aaf2127..39edee7d8a 100644 --- a/packages/express-boilerplate/src/tests/nanoid.test.ts +++ b/packages/express-boilerplate/src/tests/nanoid.test.ts @@ -2,7 +2,7 @@ import * as assert from "node:assert"; import { test } from "node:test"; import { nanoid } from "../nanoid.ts"; -/* eslint-disable @typescript-eslint/no-floating-promises */ +// oxlint-disable typescript/no-floating-promises test("nanoid", () => { assert.strictEqual(nanoid().length, 32); diff --git a/packages/express-boilerplate/src/tests/shutdown.test.ts b/packages/express-boilerplate/src/tests/shutdown.test.ts index 81eb115104..30bda462a8 100644 --- a/packages/express-boilerplate/src/tests/shutdown.test.ts +++ b/packages/express-boilerplate/src/tests/shutdown.test.ts @@ -2,7 +2,7 @@ import * as assert from "node:assert"; import { test } from "node:test"; import { shutdownManagerConfigFromEnv } from "../shutdown.ts"; -/* eslint-disable @typescript-eslint/no-floating-promises */ +// oxlint-disable typescript/no-floating-promises test("env parser with env vars", () => { process.env["SSQT_HTTP_KEEP_ALIVE_SECONDS"] = "275"; From d36c336948bac54c1794810f10762add17958602 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:08:08 +0000 Subject: [PATCH 18/51] logger: publish v3.0.1-3 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index 2b66fda8b7..fd9aeb74f5 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-2", + "version": "3.0.1-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-2", + "version": "3.0.1-3", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index 293c81146e..26eed0fb19 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-2", + "version": "3.0.1-3", "private": false, "description": "Small structured JSON logger for SaaSquatch NodeJS services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From 554f3a8cda097530c623e653c2009ba6f6b047da Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 10:39:49 -0600 Subject: [PATCH 19/51] logger: make Logger a class + more cleanup --- packages/logger/README.md | 6 +- packages/logger/src/config.ts | 26 +-- packages/logger/src/format.ts | 15 +- packages/logger/src/index.ts | 6 +- packages/logger/src/logger.ts | 339 ++++++++++++++++----------------- packages/logger/src/plugins.ts | 2 - 6 files changed, 191 insertions(+), 203 deletions(-) diff --git a/packages/logger/README.md b/packages/logger/README.md index 38dd36129c..c5d162f370 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -28,11 +28,11 @@ requestLogger.stopLogCollection(); requestLogger.clearCollectedLogs(); ``` -Collection retains the newest 1,000 records by default. `maxEntries` can be set when +Collection retains the newest 500 records by default. `maxEntries` can be set when starting collection; it must be a positive integer. Collection is independent for each logger and child logger. -Each enabled call writes one JSON record. The default minimum level is `info`; it can also be configured with `SSQT_LOG_LEVEL`. +Each log call writes one JSON record to each enabled sink. The default minimum level is `info`; it can also be configured with `SSQT_LOG_LEVEL`. ### Custom stream sinks @@ -47,7 +47,7 @@ const logger = initializeLogger({ }); ``` -The supported transport types are `console` and `stream`. File, HTTP, and Winston transport configuration are not part of this API. +The supported transport types are `console` and `stream`. ### Express HTTP logging diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 1319f287da..ccd2bff047 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -1,6 +1,8 @@ import type { Writable } from "node:stream"; -/** Syslog-compatible logging levels, from most to least severe. */ +/** + * Syslog-compatible logging levels, from most to least severe. + */ export const LOG_LEVELS = [ "emerg", "alert", @@ -39,11 +41,14 @@ export type StreamTransport = { export type Transport = ConsoleTransport | StreamTransport; export type LoggerConfig = { - /** Minimum severity to emit. Defaults to `info`. */ + /** + * Minimum severity to emit. Defaults to `info` + */ logLevel: LogLevel; - /** Retained for compatibility and future output policies. */ - environment: string; - /** Outputs for log records. Defaults to stdout. */ + + /** + * Outputs for log records. Defaults to stdout + */ transports: Transport[]; }; @@ -54,17 +59,14 @@ export function defaultConfig(): LoggerConfig { let logLevel: LogLevel = "info"; if (configuredLevel !== undefined) { - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - if (!LOG_LEVELS.includes(configuredLevel as LogLevel)) { + if (!(LOG_LEVELS as readonly string[]).includes(configuredLevel)) { throw new Error(`Invalid log level "${configuredLevel}"`); } + + // this is safe since we just checked it against the allowed log levels // oxlint-disable-next-line typescript/no-unsafe-type-assertion logLevel = configuredLevel as LogLevel; } - return { - logLevel, - environment: process.env["NODE_ENV"] ?? "production", - transports, - }; + return { logLevel, transports }; } diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 7448ec5bca..d87d17377f 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -14,7 +14,6 @@ export type LogRecord = Record & { timestamp: string; }; -/** Build the JSON record emitted by the logger. */ export function formatRecord( name: string, level: LogLevel, @@ -28,10 +27,12 @@ export function formatRecord( message, }; + // add the logger name field if it's non-default if (name !== DEFAULT_LOGGER_NAME) { record["logger.name"] = name; } + // Prepend a [] tag to the message if ( typeof record["tenantAlias"] === "string" && typeof record["message"] === "string" @@ -47,6 +48,9 @@ export function formatRecord( } delete record[LOG_TYPE_MARKER]; + + // for Datadog + // https://docs.datadoghq.com/standard-attributes record["status"] = level; return record; @@ -71,6 +75,8 @@ function formatHttpRecord(record: Record): void { message.url, ].join(" "); + // for Datadog + // https://docs.datadoghq.com/standard-attributes?search=HTTP record["http.url"] = message.url; record["http.method"] = message.method; record["http.status_code"] = message.status; @@ -81,11 +87,10 @@ function formatHttpRecord(record: Record): void { } } -/** Safely serialize records, including Error and BigInt values. */ +/** + * Safely serialize records, including Error and BigInt values. + */ export function serializeRecord(record: LogRecord): string { - // Keep only the current ancestor chain. A WeakSet of every object seen in - // the record would incorrectly classify valid shared sibling references as - // circular. const ancestors: object[] = []; return JSON.stringify(record, function (_key, value: unknown) { if (typeof value === "bigint") { diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 29a200bb3b..5e5e802708 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -16,8 +16,7 @@ import { isLoggerInitialized, LOG_TYPE_MARKER, type LogCollectionOptions, - type Logger, - SYSLOG_LOG_LEVELS, + Logger, } from "./logger.ts"; import { httpLogMiddleware } from "./plugins.ts"; @@ -30,11 +29,10 @@ export { isLoggerInitialized, LOG_LEVELS, LOG_TYPE_MARKER, - SYSLOG_LOG_LEVELS, type ConsoleTransport, type GetCollectedLogsOptions, type LogCollectionOptions, - type Logger, + Logger, type LoggerConfig, type LogLevel, type LogRecord, diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index bda0ae6a4f..f787207d8f 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -9,7 +9,9 @@ import { import { formatRecord, serializeRecord, type LogRecord } from "./format.ts"; export type LogCollectionOptions = { - /** Maximum number of records retained. The newest records are kept. */ + /** + * Maximum number of records retained. The newest records are kept. + */ maxEntries?: number; }; @@ -17,48 +19,21 @@ export type GetCollectedLogsOptions = { serialized?: boolean; }; -export const DEFAULT_LOG_COLLECTION_LIMIT = 1000; +export const DEFAULT_LOG_COLLECTION_LIMIT = 500; export const LOG_TYPE_MARKER = "__ssqt_log_type"; export const DEFAULT_LOGGER_NAME = "_ssqt_default_logger"; -/** Numeric values retained for callers that used the old syslog mapping. */ -export const SYSLOG_LOG_LEVELS = LOG_LEVEL_VALUES; - -export interface Logger { - readonly name: string; - level: LogLevel; - log( - level: LogLevel, - message: unknown, - fields?: Record, - ): void; - emerg(message: unknown, fields?: Record): void; - alert(message: unknown, fields?: Record): void; - crit(message: unknown, fields?: Record): void; - error(message: unknown, fields?: Record): void; - warning(message: unknown, fields?: Record): void; - warn(message: unknown, fields?: Record): void; - notice(message: unknown, fields?: Record): void; - info(message: unknown, fields?: Record): void; - debug(message: unknown, fields?: Record): void; - child(record: Record): Logger; - startLogCollection(options?: LogCollectionOptions): void; - stopLogCollection(): void; - getCollectedLogs( - opts?: GetCollectedLogsOptions & { serialized?: T }, - ): T extends true ? string : LogRecord[]; - clearCollectedLogs(): void; -} - type Sink = (serializedRecord: string) => void; const loggers = new Map(); export function getLogger(logger?: string): Logger { const name = logger ?? DEFAULT_LOGGER_NAME; + if (!loggers.has(name)) { initializeLogger(name); } + return loggers.get(name)!; } @@ -74,7 +49,7 @@ export function initializeLogger( typeof nameOrConfig === "string" ? nameOrConfig : DEFAULT_LOGGER_NAME; if (loggers.has(name)) { - throw new Error("Logger has already been initialized"); + throw new Error(`Logger "${name}" has already been initialized`); } const supplied = @@ -82,20 +57,19 @@ export function initializeLogger( const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; const sinks = conf.transports.map(transportToSink); - const level = { value: conf.logLevel }; - const logger = createLogger(name, sinks, level, {}); + const logger = new Logger(name, sinks, conf.logLevel, {}); loggers.set(name, logger); return logger; } -function createLogger( - name: string, - sinks: Sink[], - level: { value: LogLevel }, - baseFields: Record, -): Logger { - const collection: { +export class Logger { + private name: string; + private sinks: Sink[]; + private level: LogLevel; + private baseFields: Record; + + private collection: { enabled: boolean; maxEntries: number; records: LogRecord[]; @@ -109,156 +83,167 @@ function createLogger( size: 0, }; - const logger: Logger = { - name, - - get level() { - return level.value; - }, + constructor( + name: string, + sinks: Sink[], + level: LogLevel, + baseFields: Record, + ) { + this.name = name; + this.sinks = sinks; + this.level = level; + this.baseFields = baseFields; + } - set level(value: LogLevel) { - level.value = value; - }, + public setLevel(level: LogLevel): void { + this.level = level; + } - log(messageLevel, message, fields) { - if (LOG_LEVEL_VALUES[messageLevel] > LOG_LEVEL_VALUES[level.value]) { - return; - } + public setName(name: string): void { + this.name = name; + } - let actualMessage = message; - let messageFields = fields ?? {}; - if (fields === undefined && isRecord(message)) { - messageFields = message; - actualMessage = message["message"]; + public log( + messageLevel: LogLevel, + message: unknown, + fields?: Record, + ) { + if (LOG_LEVEL_VALUES[messageLevel] > LOG_LEVEL_VALUES[this.level]) { + return; + } + + let actualMessage = message; + let messageFields = fields ?? {}; + if (fields === undefined && isRecord(message)) { + messageFields = message; + actualMessage = message["message"]; + } + + // Per-message fields take precedence over inherited child fields. + const record = formatRecord(this.name, messageLevel, actualMessage, { + ...this.baseFields, + ...messageFields, + }); + + if (this.collection.enabled) { + const index = + (this.collection.start + this.collection.size) % + this.collection.maxEntries; + + this.collection.records[index] = record; + if (this.collection.size < this.collection.maxEntries) { + this.collection.size += 1; + } else { + this.collection.start = + (this.collection.start + 1) % this.collection.maxEntries; } - - // Per-message fields take precedence over inherited child fields. - const record = formatRecord(name, messageLevel, actualMessage, { - ...baseFields, - ...messageFields, - }); - - if (collection.enabled) { - const index = - (collection.start + collection.size) % collection.maxEntries; - - collection.records[index] = record; - if (collection.size < collection.maxEntries) { - collection.size += 1; - } else { - collection.start = (collection.start + 1) % collection.maxEntries; - } + } + + // Do not serialize when there are no active sinks (for example, when + // this logger is only being used for collection). Serialization is also + // shared across all sinks. + if (this.sinks.length > 0) { + const serializedRecord = `${serializeRecord(record)}\n`; + for (const sink of this.sinks) { + sink(serializedRecord); } + } + } - // Do not serialize when there are no active sinks (for example, when - // this logger is only being used for collection). Serialization is also - // shared across all sinks. - if (sinks.length > 0) { - const serializedRecord = `${serializeRecord(record)}\n`; - for (const sink of sinks) { - sink(serializedRecord); - } - } - }, + public child(record: Record) { + return new Logger(this.name, this.sinks, this.level, { + ...this.baseFields, + ...record, + }); + } - child(record) { - return createLogger(name, sinks, level, { ...baseFields, ...record }); - }, + public startLogCollection(options?: LogCollectionOptions) { + const maxEntries = options?.maxEntries ?? DEFAULT_LOG_COLLECTION_LIMIT; - startLogCollection(options) { - const maxEntries = options?.maxEntries ?? DEFAULT_LOG_COLLECTION_LIMIT; + if (!Number.isInteger(maxEntries) || maxEntries < 1) { + throw new Error("Log collection maxEntries must be a positive integer"); + } - if (!Number.isInteger(maxEntries) || maxEntries < 1) { - throw new Error("Log collection maxEntries must be a positive integer"); - } + if (this.collection.maxEntries !== maxEntries) { + const retained = this.getCollectedRecords(); + const records = retained.slice(-maxEntries); + this.collection.records = records; + this.collection.start = 0; + this.collection.size = records.length; + } - if (collection.maxEntries !== maxEntries) { - const retained = getCollectedRecords(collection); - const records = retained.slice(-maxEntries); - collection.records = records; - collection.start = 0; - collection.size = records.length; - } + this.collection.maxEntries = maxEntries; + this.collection.enabled = true; + } - collection.maxEntries = maxEntries; - collection.enabled = true; - }, - - stopLogCollection() { - collection.enabled = false; - }, - - getCollectedLogs( - opts?: GetCollectedLogsOptions & { serialized?: T }, - ): T extends true ? string : LogRecord[] { - const records = getCollectedRecords(collection); - if (opts?.serialized) { - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - return records.map(serializeRecord).join("\n") as T extends true - ? string - : LogRecord[]; - } + public stopLogCollection() { + this.collection.enabled = false; + } + public getCollectedLogs( + opts?: GetCollectedLogsOptions & { serialized?: T }, + ): T extends true ? string : LogRecord[] { + const records = this.getCollectedRecords(); + if (opts?.serialized) { // oxlint-disable-next-line typescript/no-unsafe-type-assertion - return records as T extends true ? string : LogRecord[]; - }, - - clearCollectedLogs() { - collection.records.length = 0; - collection.start = 0; - collection.size = 0; - }, - - emerg(message, fields) { - this.log("emerg", message, fields); - }, - - alert(message, fields) { - this.log("alert", message, fields); - }, - - crit(message, fields) { - this.log("crit", message, fields); - }, - - error(message, fields) { - this.log("error", message, fields); - }, - - warning(message, fields) { - this.log("warning", message, fields); - }, - - warn(message, fields) { - this.log("warn", message, fields); - }, - - notice(message, fields) { - this.log("notice", message, fields); - }, - - info(message, fields) { - this.log("info", message, fields); - }, - - debug(message, fields) { - this.log("debug", message, fields); - }, - }; - return logger; -} + return records.map(serializeRecord).join("\n") as T extends true + ? string + : LogRecord[]; + } + + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + return records as T extends true ? string : LogRecord[]; + } + + public clearCollectedLogs() { + this.collection.records.length = 0; + this.collection.start = 0; + this.collection.size = 0; + } + + public emerg(message: unknown, fields?: Record) { + this.log("emerg", message, fields); + } + + public alert(message: unknown, fields?: Record) { + this.log("alert", message, fields); + } + + public crit(message: unknown, fields?: Record) { + this.log("crit", message, fields); + } -function getCollectedRecords(collection: { - records: LogRecord[]; - start: number; - size: number; - maxEntries: number; -}): LogRecord[] { - return Array.from({ length: collection.size }, (_unused, index) => { - return collection.records[ - (collection.start + index) % collection.maxEntries - ]; - }); + public error(message: unknown, fields?: Record) { + this.log("error", message, fields); + } + + public warning(message: unknown, fields?: Record) { + this.log("warning", message, fields); + } + + public warn(message: unknown, fields?: Record) { + this.log("warn", message, fields); + } + + public notice(message: unknown, fields?: Record) { + this.log("notice", message, fields); + } + + public info(message: unknown, fields?: Record) { + this.log("info", message, fields); + } + + public debug(message: unknown, fields?: Record) { + this.log("debug", message, fields); + } + + private getCollectedRecords(): LogRecord[] { + return Array.from({ length: this.collection.size }, (_unused, index) => { + return this.collection.records[ + (this.collection.start + index) % this.collection.maxEntries + ]; + }); + } } function isRecord(value: unknown): value is Record { diff --git a/packages/logger/src/plugins.ts b/packages/logger/src/plugins.ts index 401b5e341c..56e443c4b4 100644 --- a/packages/logger/src/plugins.ts +++ b/packages/logger/src/plugins.ts @@ -30,8 +30,6 @@ const STRIP_PARAMS = [ /** * A simple Express.js middleware which logs the URL, method, response code, * and response time of all HTTP requests. - * - * @param {Logger} logger - The logger to use */ export function httpLogMiddleware( logger: Logger, From 340fdf47245ad6bf638116c460e6d356d1f67c2e Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 10:40:44 -0600 Subject: [PATCH 20/51] logger: more breaking change warnings in changelog --- packages/logger/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 519cc5b6c2..398cf039a4 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed Winston and its transport/format APIs. - Loggers now emit structured JSON directly and expose the package-owned `Logger` type. - The supported sinks are stdout and custom writable Node streams. +- Removed the top-level `info`, `warn`, and `error` exports; call methods on a `Logger` instance instead. +- Removed `SSQT_LOG_TRANSPORTS`; configure `console` or writable `stream` transports through `initializeLogger`. ## [3.0.0] - 2026-05-27 From 44036fe5ffe9ed930d2da02409c1ebd076023864 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:42:04 +0000 Subject: [PATCH 21/51] logger: publish v3.0.1-4 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index fd9aeb74f5..c6088f407a 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-3", + "version": "3.0.1-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-3", + "version": "3.0.1-4", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index 26eed0fb19..ac4bdcd0a2 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-3", + "version": "3.0.1-4", "private": false, "description": "Small structured JSON logger for SaaSquatch NodeJS services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From 593c6c83083bac908ae18a2b45999e4876ccdcc6 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 10:44:04 -0600 Subject: [PATCH 22/51] logger: make name and level public fields --- packages/logger/src/logger.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index f787207d8f..19fdd8133e 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -64,9 +64,10 @@ export function initializeLogger( } export class Logger { - private name: string; + public name: string; + public level: LogLevel; + private sinks: Sink[]; - private level: LogLevel; private baseFields: Record; private collection: { @@ -95,14 +96,6 @@ export class Logger { this.baseFields = baseFields; } - public setLevel(level: LogLevel): void { - this.level = level; - } - - public setName(name: string): void { - this.name = name; - } - public log( messageLevel: LogLevel, message: unknown, From 8713af211eec02a3e3c94be72c98531f54b96a7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:46:54 +0000 Subject: [PATCH 23/51] logger: publish v3.0.1-5 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index c6088f407a..948b606c5e 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-4", + "version": "3.0.1-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-4", + "version": "3.0.1-5", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index ac4bdcd0a2..44356d8541 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-4", + "version": "3.0.1-5", "private": false, "description": "Small structured JSON logger for SaaSquatch NodeJS services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From f9aaa700ae9479d54cf06993fe8581fa728f97b5 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 10:52:01 -0600 Subject: [PATCH 24/51] express-boilerplate: bump logger to 3.0.1-5 --- packages/express-boilerplate/package-lock.json | 9 ++++----- packages/express-boilerplate/package.json | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 91ad99a8a3..ce1bb01d6d 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -12,7 +12,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-2", + "@saasquatch/logger": "^3.0.1-5", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -26,7 +26,6 @@ "node": "^24 || ^26" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.0", "express": "^4.0.0" } }, @@ -832,9 +831,9 @@ } }, "node_modules/@saasquatch/logger": { - "version": "3.0.1-2", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-2.tgz", - "integrity": "sha512-2Ib3PrJBHoe9gAxAqH+kYy6txJ6Mrcv2bQy2YkXR3hJaIHEeLO8u/aJQr7z2olZRvwHS/vPCTVoRVa7N2ZKiAQ==", + "version": "3.0.1-5", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-5.tgz", + "integrity": "sha512-utIYTIBJ2r4QfBTtnkSEZmzz5YfqxuaJ0NaHFfgJOd4jqzuqKht874bnRYAOwQ2VQ4/xtKsmnfhaAjFolp5xWg==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 7e65c28038..28c81529d4 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -35,7 +35,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-2", + "@saasquatch/logger": "^3.0.1-5", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -46,7 +46,6 @@ "typescript": "^7.0.2" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.0", "express": "^4.0.0" }, "engines": { From 8b41f86f037979fff11e0599ce1ca20fdc340ea9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:53:04 +0000 Subject: [PATCH 25/51] express-boilerplate: publish v2.0.1-1 This commit was generated by GitHub Actions CI --- packages/express-boilerplate/package-lock.json | 4 ++-- packages/express-boilerplate/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index ce1bb01d6d..855f1c02db 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-0", + "version": "2.0.1-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-0", + "version": "2.0.1-1", "license": "MIT", "dependencies": { "nanoid": "^6.0.1" diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 28c81529d4..deec77ae49 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-0", + "version": "2.0.1-1", "private": false, "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", "homepage": "https://github.com/saasquatch/program-tools/packages/express-boilerplate#readme", From efcd9a6a16e60ce661b9a3f6cabf4d921942e4a3 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 11:23:18 -0600 Subject: [PATCH 26/51] logger: add unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: gpt-5.6-sol • medium via pi.dev --- packages/logger/test/config-format.test.ts | 153 +++++++++++++ .../test/default-initialization.test.ts | 17 ++ packages/logger/test/logger.test.ts | 204 ++++++++++++++++++ packages/logger/test/plugins.test.ts | 97 +++++++++ 4 files changed, 471 insertions(+) create mode 100644 packages/logger/test/config-format.test.ts create mode 100644 packages/logger/test/default-initialization.test.ts create mode 100644 packages/logger/test/logger.test.ts create mode 100644 packages/logger/test/plugins.test.ts diff --git a/packages/logger/test/config-format.test.ts b/packages/logger/test/config-format.test.ts new file mode 100644 index 0000000000..15d717d6ef --- /dev/null +++ b/packages/logger/test/config-format.test.ts @@ -0,0 +1,153 @@ +import assert from "node:assert/strict"; +import { afterEach, describe, test } from "node:test"; +import { defaultConfig, LOG_LEVELS, LOG_LEVEL_VALUES } from "../src/config.ts"; +import { formatRecord, serializeRecord } from "../src/format.ts"; +import { DEFAULT_LOGGER_NAME, LOG_TYPE_MARKER } from "../src/logger.ts"; + +void describe("configuration", () => { + const originalLevel = process.env.SSQT_LOG_LEVEL; + + afterEach(() => { + if (originalLevel === undefined) { + delete process.env.SSQT_LOG_LEVEL; + } else { + process.env.SSQT_LOG_LEVEL = originalLevel; + } + }); + + void test("uses stdout and info by default", () => { + delete process.env.SSQT_LOG_LEVEL; + assert.deepEqual(defaultConfig(), { + logLevel: "info", + transports: [{ type: "console" }], + }); + assert.deepEqual(LOG_LEVELS, [ + "emerg", + "alert", + "crit", + "error", + "warning", + "warn", + "notice", + "info", + "debug", + ]); + assert.equal(LOG_LEVEL_VALUES.warning, LOG_LEVEL_VALUES.warn); + }); + + void test("accepts supported environment levels and rejects invalid ones", () => { + process.env.SSQT_LOG_LEVEL = "debug"; + assert.equal(defaultConfig().logLevel, "debug"); + process.env.SSQT_LOG_LEVEL = "verbose"; + assert.throws(() => defaultConfig(), /Invalid log level "verbose"/); + }); +}); + +void describe("record formatting", () => { + void test("formats ordinary, named, and tenant records without allowing fields to replace core fields", () => { + const ordinary = formatRecord(DEFAULT_LOGGER_NAME, "info", "hello", { + level: "debug", + timestamp: "bad", + status: "bad", + }); + assert.equal(ordinary.level, "info"); + assert.equal(ordinary.message, "hello"); + assert.equal(ordinary.status, "info"); + assert.equal("logger.name" in ordinary, false); + assert.doesNotThrow(() => new Date(ordinary.timestamp).toISOString()); + + const tagged = formatRecord("api", "warning", "started", { + tenantAlias: "acme", + }); + assert.equal(tagged.message, "[acme] started"); + assert.equal(tagged["logger.name"], "api"); + assert.equal( + formatRecord("api", "info", "[acme] done", { tenantAlias: "acme" }) + .message, + "[acme] done", + ); + assert.equal( + formatRecord("api", "info", 12, { tenantAlias: "acme" }).message, + 12, + ); + assert.equal( + formatRecord("api", "info", "text", { tenantAlias: 12 }).message, + "text", + ); + }); + + void test("normalizes HTTP records in microseconds and milliseconds", () => { + const micro = formatRecord( + "web", + "info", + { + status: 201, + method: "POST", + time: 999n, + url: "/items", + requestId: "r1", + }, + { [LOG_TYPE_MARKER]: "HTTP" }, + ); + assert.equal(micro.message, "201 POST 999 μs /items"); + assert.equal(micro["http.url"], "/items"); + assert.equal(micro["http.method"], "POST"); + assert.equal(micro["http.status_code"], 201); + assert.equal(micro["http.response_time"], 999); + assert.equal(micro["http.request_id"], "r1"); + assert.equal(LOG_TYPE_MARKER in micro, false); + + const milli = formatRecord( + DEFAULT_LOGGER_NAME, + "error", + { + status: 500, + method: "GET", + time: "1501", + url: "/fail", + }, + { [LOG_TYPE_MARKER]: "HTTP" }, + ); + assert.equal(milli.message, "500 GET 2 ms /fail"); + assert.equal("http.request_id" in milli, false); + }); + + void test("leaves malformed HTTP messages alone and always removes the marker", () => { + for (const message of [null, "not an object"]) { + const record = formatRecord(DEFAULT_LOGGER_NAME, "info", message, { + [LOG_TYPE_MARKER]: "HTTP", + }); + assert.equal(record.message, message); + assert.equal(LOG_TYPE_MARKER in record, false); + } + + assert.equal( + formatRecord(DEFAULT_LOGGER_NAME, "info", "normal", { + [LOG_TYPE_MARKER]: "OTHER", + }).message, + "normal", + ); + }); +}); + +void describe("safe serialization", () => { + void test("serializes bigint, errors, circular references, and repeated non-circular references", () => { + const error = new TypeError("bad input"); + const circular: Record = { id: 1 }; + circular.self = circular; + const shared = { value: 2n }; + const record = formatRecord(DEFAULT_LOGGER_NAME, "error", "failed", { + error, + circular, + first: shared, + second: shared, + }); + const parsed = JSON.parse(serializeRecord(record)); + assert.equal(parsed.circular.self, "[Circular]"); + assert.deepEqual(parsed.first, { value: "2" }); + assert.deepEqual(parsed.second, { value: "2" }); + assert.equal(parsed.error.name, "TypeError"); + assert.equal(parsed.error.message, "bad input"); + assert.match(parsed.error.stack, /TypeError: bad input/); + }); +}); diff --git a/packages/logger/test/default-initialization.test.ts b/packages/logger/test/default-initialization.test.ts new file mode 100644 index 0000000000..119308df0e --- /dev/null +++ b/packages/logger/test/default-initialization.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { + DEFAULT_LOGGER_NAME, + getLogger, + initializeLogger, + isLoggerInitialized, +} from "../src/logger.ts"; + +void test("initializes the default logger when called without arguments", () => { + assert.equal(isLoggerInitialized(), false); + const logger = initializeLogger(); + assert.equal(logger.name, DEFAULT_LOGGER_NAME); + assert.equal(logger.level, "info"); + assert.equal(isLoggerInitialized(), true); + assert.equal(getLogger(), logger); +}); diff --git a/packages/logger/test/logger.test.ts b/packages/logger/test/logger.test.ts new file mode 100644 index 0000000000..a953f98259 --- /dev/null +++ b/packages/logger/test/logger.test.ts @@ -0,0 +1,204 @@ +import assert from "node:assert/strict"; +import { Writable } from "node:stream"; +import { describe, test } from "node:test"; +import { + DEFAULT_LOG_COLLECTION_LIMIT, + DEFAULT_LOGGER_NAME, + getLogger, + initializeLogger, + isLoggerInitialized, + Logger, +} from "../src/logger.ts"; + +class CaptureStream extends Writable { + chunks: string[] = []; + override _write( + chunk: Buffer | string, + _encoding: BufferEncoding, + done: (error?: Error | null) => void, + ) { + this.chunks.push(chunk.toString()); + done(); + } +} + +let sequence = 0; +const unique = (prefix: string) => `${prefix}-${++sequence}`; + +void describe("logger initialization", () => { + void test("accepts object-form configuration and reuses the default logger", () => { + assert.equal(isLoggerInitialized(), false); + const logger = initializeLogger({ transports: [], logLevel: "debug" }); + assert.equal(logger.name, DEFAULT_LOGGER_NAME); + assert.equal(logger.level, "debug"); + assert.equal(isLoggerInitialized(), true); + assert.equal(getLogger(), logger); + assert.throws(() => initializeLogger(), /already been initialized/); + }); + + void test("supports named loggers and all initialization argument forms", () => { + const named = unique("named"); + assert.equal(isLoggerInitialized(named), false); + const auto = getLogger(named); + assert.equal(isLoggerInitialized(named), true); + assert.equal(auto.name, named); + + const configured = initializeLogger(unique("configured"), { + transports: [], + logLevel: "debug", + }); + assert.equal(configured.level, "debug"); + assert.throws( + () => initializeLogger({ transports: [], logLevel: "warning" }), + /already been initialized/, + ); + }); +}); + +void describe("logging", () => { + void test("writes JSON lines, applies level filtering, merges child fields, and shares serialization", () => { + const one = new CaptureStream(); + const two = new CaptureStream(); + const logger = initializeLogger(unique("output"), { + logLevel: "info", + transports: [ + { type: "stream", stream: one }, + { type: "stream", stream: two }, + ], + }); + + logger.debug("hidden"); + logger.info("visible", { requestId: "message" }); + logger + .child({ requestId: "child", service: "api" }) + .warning({ message: "oops", requestId: "override" }); + + assert.equal(one.chunks.length, 2); + assert.deepEqual(one.chunks, two.chunks); + assert.ok(one.chunks.every((line) => line.endsWith("\n"))); + const first = JSON.parse(one.chunks[0]); + const second = JSON.parse(one.chunks[1]); + assert.equal(first.message, "visible"); + assert.equal(first.requestId, "message"); + assert.equal(second.message, "oops"); + assert.equal(second.requestId, "override"); + assert.equal(second.service, "api"); + }); + + void test("supports every convenience level and non-record messages", () => { + const stream = new CaptureStream(); + const logger = new Logger( + "levels", + [(line) => stream.write(line)], + "debug", + {}, + ); + logger.emerg("emerg"); + logger.alert(null); + logger.crit(["array"]); + logger.error(new Error("error")); + logger.warning("warning"); + logger.warn("warn"); + logger.notice("notice"); + logger.info("info"); + logger.debug("debug"); + assert.deepEqual( + stream.chunks.map((line) => JSON.parse(line).level), + [ + "emerg", + "alert", + "crit", + "error", + "warning", + "warn", + "notice", + "info", + "debug", + ], + ); + }); + + void test("uses the console transport", () => { + const writes: string[] = []; + const originalWrite = process.stdout.write.bind(process.stdout); + process.stdout.write = (chunk: string | Uint8Array) => { + writes.push(chunk.toString()); + return true; + }; + try { + initializeLogger(unique("console"), { + logLevel: "info", + transports: [{ type: "console" }], + }).info("console message"); + } finally { + process.stdout.write = originalWrite; + } + assert.equal(JSON.parse(writes[0]).message, "console message"); + }); +}); + +void describe("log collection", () => { + void test("collects without sinks, returns records or serialized NDJSON, and can stop and clear", () => { + const logger = new Logger("collector", [], "debug", {}); + logger.startLogCollection(); + assert.equal(DEFAULT_LOG_COLLECTION_LIMIT, 500); + logger.info("one"); + logger.info("two"); + assert.deepEqual( + logger.getCollectedLogs().map((record) => record.message), + ["one", "two"], + ); + const serialized = logger.getCollectedLogs({ serialized: true }); + assert.deepEqual( + serialized.split("\n").map((line) => JSON.parse(line).message), + ["one", "two"], + ); + logger.stopLogCollection(); + logger.info("not collected"); + assert.equal(logger.getCollectedLogs().length, 2); + logger.clearCollectedLogs(); + assert.deepEqual(logger.getCollectedLogs(), []); + }); + + void test("keeps newest entries in ring order and retains newest records when resized", () => { + const logger = new Logger("ring", [], "debug", {}); + logger.startLogCollection({ maxEntries: 3 }); + for (const message of ["one", "two", "three", "four"]) { + logger.info(message); + } + + assert.deepEqual( + logger.getCollectedLogs().map((record) => record.message), + ["two", "three", "four"], + ); + + logger.startLogCollection({ maxEntries: 2 }); + assert.deepEqual( + logger.getCollectedLogs().map((record) => record.message), + ["three", "four"], + ); + logger.startLogCollection({ maxEntries: 2 }); + logger.info("five"); + assert.deepEqual( + logger.getCollectedLogs().map((record) => record.message), + ["four", "five"], + ); + + logger.startLogCollection({ maxEntries: 4 }); + logger.info("six"); + assert.deepEqual( + logger.getCollectedLogs().map((record) => record.message), + ["four", "five", "six"], + ); + }); + + void test("rejects invalid collection limits", () => { + const logger = new Logger("invalid", [], "info", {}); + for (const maxEntries of [0, -1, 1.5, Number.NaN]) { + assert.throws( + () => logger.startLogCollection({ maxEntries }), + /positive integer/, + ); + } + }); +}); diff --git a/packages/logger/test/plugins.test.ts b/packages/logger/test/plugins.test.ts new file mode 100644 index 0000000000..364f4d509b --- /dev/null +++ b/packages/logger/test/plugins.test.ts @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import { EventEmitter } from "node:events"; +import { describe, test } from "node:test"; +import { Logger } from "../src/logger.ts"; +import { httpLogMiddleware } from "../src/plugins.ts"; + +class FakeResponse extends EventEmitter { + statusCode: number; + locals?: Record; + + constructor(statusCode: number, locals?: Record) { + super(); + this.statusCode = statusCode; + this.locals = locals; + } +} + +function performRequest( + logger: Logger, + statusCode: number, + url: string, + options?: Parameters[1], + locals?: Record, + method = "GET", +) { + const response = new FakeResponse(statusCode, locals); + let nextCalls = 0; + const middleware = httpLogMiddleware(logger, options); + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- only the request properties read by the middleware are needed + const request = { method, originalUrl: url } as never; + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- FakeResponse implements the response properties used by the middleware + const expressResponse = response as never; + middleware(request, expressResponse, () => nextCalls++); + assert.equal(nextCalls, 1); + response.emit("finish"); +} + +function collector() { + const logger = new Logger("http", [], "debug", {}); + logger.startLogCollection({ maxEntries: 20 }); + return logger; +} + +void describe("HTTP logging middleware", () => { + void test("logs successful responses and strips every sensitive query parameter", () => { + const logger = collector(); + performRequest( + logger, + 200, + "/items?keep=yes&itoken=1&token=2&jwt=3&auth=4&password=5&bearer=6&key=7", + undefined, + { requestId: "request-1", extraData: { region: "ca" } }, + "POST", + ); + const [record] = logger.getCollectedLogs(); + assert.equal(record?.level, "info"); + assert.equal(record?.message instanceof Object, false); + assert.match(String(record?.message), /^200 POST .* \/items\?keep=yes$/); + assert.equal(record?.["http.url"], "/items?keep=yes"); + assert.equal(record?.["http.request_id"], "request-1"); + assert.deepEqual(record?.extraData, { region: "ca" }); + }); + + void test("uses configured success, client error, and server error levels", () => { + const logger = collector(); + performRequest(logger, 204, "/ok", { nonErrorLogLevel: "notice" }); + performRequest(logger, 404, "/missing", { logNonErrorResponses: false }); + performRequest(logger, 500, "/broken"); + assert.deepEqual( + logger.getCollectedLogs().map((record) => record.level), + ["notice", "warn", "error"], + ); + }); + + void test("can suppress non-error responses", () => { + const logger = collector(); + performRequest(logger, 200, "/quiet", { logNonErrorResponses: false }); + assert.deepEqual(logger.getCollectedLogs(), []); + }); + + void test("handles health checks specially", () => { + const logger = collector(); + performRequest(logger, 200, "/healthz", { logHealthchecks: false }); + performRequest(logger, 503, "/readyz"); + performRequest(logger, 200, "/livez"); + assert.equal(logger.getCollectedLogs().length, 1); + assert.equal(logger.getCollectedLogs()[0]?.level, "debug"); + }); + + void test("works when response locals are absent", () => { + const logger = collector(); + performRequest(logger, 201, "/created"); + const record = logger.getCollectedLogs()[0]; + assert.equal("http.request_id" in record, false); + assert.equal(record.extraData, undefined); + }); +}); From 26fef7f8fd3361d3a2315461978b207a20aa1b47 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 11:37:50 -0600 Subject: [PATCH 27/51] logger: safer JSON.stringify --- packages/logger/src/format.ts | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index d87d17377f..9d0b105033 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -92,26 +92,32 @@ function formatHttpRecord(record: Record): void { */ export function serializeRecord(record: LogRecord): string { const ancestors: object[] = []; - return JSON.stringify(record, function (_key, value: unknown) { - if (typeof value === "bigint") { - return value.toString(); - } - - if (value instanceof Error) { - return { name: value.name, message: value.message, stack: value.stack }; - } - - if (typeof value === "object" && value !== null) { - while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) { - ancestors.pop(); + return JSON.stringify( + { ...record, toJSON: undefined }, + function (_key, value: unknown) { + if (typeof value === "bigint") { + return value.toString(); } - if (ancestors.includes(value)) { - return "[Circular]"; + if (value instanceof Error) { + return { name: value.name, message: value.message, stack: value.stack }; } - ancestors.push(value); - } - return value; - }); + if (typeof value === "object" && value !== null) { + while ( + ancestors.length > 0 && + ancestors[ancestors.length - 1] !== this + ) { + ancestors.pop(); + } + + if (ancestors.includes(value)) { + return "[Circular]"; + } + + ancestors.push(value); + } + return value; + }, + ); } From d584f7f3c7a55ad8a9ca3e3bf137fd2b94dc409f Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 11:37:58 -0600 Subject: [PATCH 28/51] express-boilerplate: fix peer deps --- packages/express-boilerplate/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index deec77ae49..9e253f7329 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -46,6 +46,7 @@ "typescript": "^7.0.2" }, "peerDependencies": { + "@saasquatch/logger": "^3.0.1-5", "express": "^4.0.0" }, "engines": { From 8f66c1eb31d5182c27aaea8ac4f8703b6fbc14a9 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 11:48:30 -0600 Subject: [PATCH 29/51] logger: don't prepend empty tenant aliases --- packages/logger/src/format.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 9d0b105033..bf2fadee4f 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -34,6 +34,7 @@ export function formatRecord( // Prepend a [] tag to the message if ( + record["tenantAlias"] && typeof record["tenantAlias"] === "string" && typeof record["message"] === "string" ) { From 075eb746c60d3a1c5abfca48d71280bbe42e74f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:53:46 +0000 Subject: [PATCH 30/51] logger: publish v3.0.1-6 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index 948b606c5e..bca960f823 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-5", + "version": "3.0.1-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-5", + "version": "3.0.1-6", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index 44356d8541..6f6bf5baab 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-5", + "version": "3.0.1-6", "private": false, "description": "Small structured JSON logger for SaaSquatch NodeJS services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From fe0f586ee62558b0528ca438650c79f4d100a5f3 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 11:55:25 -0600 Subject: [PATCH 31/51] express-boilerplate: bump logger to 3.0.1-6 --- packages/express-boilerplate/package-lock.json | 9 +++++---- packages/express-boilerplate/package.json | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 855f1c02db..c4fd6c7873 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -12,7 +12,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-5", + "@saasquatch/logger": "^3.0.1-6", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -26,6 +26,7 @@ "node": "^24 || ^26" }, "peerDependencies": { + "@saasquatch/logger": "^3.0.1-5", "express": "^4.0.0" } }, @@ -831,9 +832,9 @@ } }, "node_modules/@saasquatch/logger": { - "version": "3.0.1-5", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-5.tgz", - "integrity": "sha512-utIYTIBJ2r4QfBTtnkSEZmzz5YfqxuaJ0NaHFfgJOd4jqzuqKht874bnRYAOwQ2VQ4/xtKsmnfhaAjFolp5xWg==", + "version": "3.0.1-6", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-6.tgz", + "integrity": "sha512-xLgEmaYWhWz0rvz9Lt+c/r0rfcBBlEpfUTSLqurjUnrSjkB1HADQZkGlMRZWs/v9GUyejsPRkLdUAEYGiyvB1A==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 9e253f7329..5f7a8b133b 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -35,7 +35,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-5", + "@saasquatch/logger": "^3.0.1-6", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -46,7 +46,7 @@ "typescript": "^7.0.2" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.1-5", + "@saasquatch/logger": "^3.0.1-6", "express": "^4.0.0" }, "engines": { From 0a299ea3d7dce9a3193b47aaf3fbd2a52047dc3f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:56:28 +0000 Subject: [PATCH 32/51] express-boilerplate: publish v2.0.1-2 This commit was generated by GitHub Actions CI --- packages/express-boilerplate/package-lock.json | 4 ++-- packages/express-boilerplate/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index c4fd6c7873..b9c5f0728a 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-1", + "version": "2.0.1-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-1", + "version": "2.0.1-2", "license": "MIT", "dependencies": { "nanoid": "^6.0.1" diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 5f7a8b133b..0eb1d6983a 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-1", + "version": "2.0.1-2", "private": false, "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", "homepage": "https://github.com/saasquatch/program-tools/packages/express-boilerplate#readme", From f9c275a6488b68a11f42a56d0cf605d7f0bfc2ae Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 11:59:26 -0600 Subject: [PATCH 33/51] ci: run logger tests in CI --- .github/workflows/tests.yml | 9 +++++---- packages/express-boilerplate/package-lock.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d50d632d55..140f867b8c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,12 +17,13 @@ jobs: fail-fast: false matrix: package: - - program-boilerplate - component-boilerplate - - stencil-grapes-plugin - - mint-components - - integration-boilerplate-node - express-boilerplate + - integration-boilerplate-node + - logger + - mint-components + - program-boilerplate + - stencil-grapes-plugin steps: - name: Checkout diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index b9c5f0728a..06a68d133e 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -26,7 +26,7 @@ "node": "^24 || ^26" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.1-5", + "@saasquatch/logger": "^3.0.1-6", "express": "^4.0.0" } }, From e5c8b5fb0b510db5b7347ad52280029f1ad20c96 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Thu, 17 Sep 2026 15:29:19 -0600 Subject: [PATCH 34/51] logger: more cleanup --- packages/logger/CHANGELOG.md | 2 +- packages/logger/README.md | 21 +++++++++++++-------- packages/logger/src/config.ts | 24 +++++++++++++++++++----- packages/logger/src/format.ts | 6 +++++- packages/logger/src/index.ts | 20 ++++++++++---------- packages/logger/src/logger.ts | 9 +++++---- packages/logger/src/plugins.ts | 6 +----- packages/logger/test/plugins.test.ts | 6 ++++-- 8 files changed, 58 insertions(+), 36 deletions(-) diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 398cf039a4..2449abacdb 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed Winston and its transport/format APIs. - Loggers now emit structured JSON directly and expose the package-owned `Logger` type. - The supported sinks are stdout and custom writable Node streams. -- Removed the top-level `info`, `warn`, and `error` exports; call methods on a `Logger` instance instead. +- Removed the top-level `info`, `warn`, and `error` exports. - Removed `SSQT_LOG_TRANSPORTS`; configure `console` or writable `stream` transports through `initializeLogger`. ## [3.0.0] - 2026-05-27 diff --git a/packages/logger/README.md b/packages/logger/README.md index c5d162f370..a53c697951 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -1,7 +1,10 @@

@saasquatch/logger

Small structured JSON logger for SaaSquatch NodeJS services

-The package provides a lightweight structured logger that writes newline-delimited JSON to stdout by default. It supports syslog-style levels, named loggers, custom writable stream sinks, and Express HTTP logging middleware. +This package provides a lightweight structured logger that writes newline-delimited JSON +to stdout by default. It supports syslog-style levels, named loggers, custom writable +stream sinks, temporary log collection to an internal buffer, and Express HTTP logging +middleware. ## Getting started @@ -16,11 +19,11 @@ const logger = initializeLogger(); logger.info("Hello", { service: "example" }); logger.error(new Error("Something failed")); -// Child fields are included in every record emitted by the child. +// child fields are included in every record emitted by the child const requestLogger = logger.child({ requestId: "req-123" }); requestLogger.info("Request started"); -// Collect records for a response, while still writing them to the sink. +// collect records for a response, while still writing them to the sink requestLogger.startLogCollection({ maxEntries: 100 }); requestLogger.info("Request finished"); const logs = requestLogger.getCollectedLogs(); @@ -28,11 +31,12 @@ requestLogger.stopLogCollection(); requestLogger.clearCollectedLogs(); ``` -Collection retains the newest 500 records by default. `maxEntries` can be set when -starting collection; it must be a positive integer. Collection is independent for -each logger and child logger. +Collection mode retains the newest 500 records by default. `maxEntries` can be set when +enabling collection; it must be a positive integer. Collection is independent for each +logger and child logger. -Each log call writes one JSON record to each enabled sink. The default minimum level is `info`; it can also be configured with `SSQT_LOG_LEVEL`. +Each log call writes one JSON record to each enabled sink. The default minimum level is +`info`; it can also be configured with `SSQT_LOG_LEVEL`. ### Custom stream sinks @@ -60,4 +64,5 @@ const logger = initializeLogger(); app.use(httpLogMiddleware(logger)); ``` -HTTP records include normalized method, URL, status, response time, and request ID fields. Sensitive URL query parameters are removed by the middleware. +HTTP records include normalized method, URL, status, response time, and request ID +fields. Sensitive URL query parameters are removed by the middleware. diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index ccd2bff047..d9f6ec2209 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -1,7 +1,19 @@ import type { Writable } from "node:stream"; /** - * Syslog-compatible logging levels, from most to least severe. + * Numerical Code | Severity + * 0 | Emergency: system is unusable + * 1 | Alert: action must be taken immediately + * 2 | Critical: critical conditions + * 3 | Error: error conditions + * 4 | Warning: warning conditions + * 5 | Notice: normal but significant condition + * 6 | Informational: informational messages + * 7 | Debug: debug-level messages + * + * Table 2. Syslog Message Severities + * RFC 5424 + * https://www.rfc-editor.org/rfc/rfc5424#section-6.2.1 */ export const LOG_LEVELS = [ "emerg", @@ -59,13 +71,15 @@ export function defaultConfig(): LoggerConfig { let logLevel: LogLevel = "info"; if (configuredLevel !== undefined) { - if (!(LOG_LEVELS as readonly string[]).includes(configuredLevel)) { + const levelValid = ((l: string): l is LogLevel => { + return (LOG_LEVELS as readonly string[]).includes(l); + })(configuredLevel); + + if (!levelValid) { throw new Error(`Invalid log level "${configuredLevel}"`); } - // this is safe since we just checked it against the allowed log levels - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - logLevel = configuredLevel as LogLevel; + logLevel = configuredLevel; } return { logLevel, transports }; diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index bf2fadee4f..9941921d4b 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -32,7 +32,7 @@ export function formatRecord( record["logger.name"] = name; } - // Prepend a [] tag to the message + // prepend a [] tag to the message if ( record["tenantAlias"] && typeof record["tenantAlias"] === "string" && @@ -63,6 +63,10 @@ function formatHttpRecord(record: Record): void { return; } + // NOTE: it would of course be safer to validate this using a real + // schema validator like zod, but for a logging library we just + // don't want that kind of overhead. The HTTP log messages are generated + // using middleware from our own package so it's unlikely to be invalid // oxlint-disable-next-line typescript/no-unsafe-type-assertion const message = value as HTTPMessage; const micros = Number(message.time); diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 5e5e802708..534b720073 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,41 +1,41 @@ import { - type ConsoleTransport, - defaultConfig, LOG_LEVELS, - type LoggerConfig, + defaultConfig, + type ConsoleTransport, type LogLevel, + type LoggerConfig, type StreamTransport, type Transport, } from "./config.ts"; import type { LogRecord } from "./format.ts"; import { DEFAULT_LOG_COLLECTION_LIMIT, - type GetCollectedLogsOptions, + LOG_TYPE_MARKER, + Logger, getLogger, initializeLogger, isLoggerInitialized, - LOG_TYPE_MARKER, + type GetCollectedLogsOptions, type LogCollectionOptions, - Logger, } from "./logger.ts"; import { httpLogMiddleware } from "./plugins.ts"; export { DEFAULT_LOG_COLLECTION_LIMIT, + LOG_LEVELS, + LOG_TYPE_MARKER, + Logger, defaultConfig, getLogger, httpLogMiddleware, initializeLogger, isLoggerInitialized, - LOG_LEVELS, - LOG_TYPE_MARKER, type ConsoleTransport, type GetCollectedLogsOptions, type LogCollectionOptions, - Logger, - type LoggerConfig, type LogLevel, type LogRecord, + type LoggerConfig, type StreamTransport, type Transport, }; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 19fdd8133e..a139866ada 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -112,7 +112,7 @@ export class Logger { actualMessage = message["message"]; } - // Per-message fields take precedence over inherited child fields. + // per-message fields take precedence over inherited child fields const record = formatRecord(this.name, messageLevel, actualMessage, { ...this.baseFields, ...messageFields, @@ -132,9 +132,6 @@ export class Logger { } } - // Do not serialize when there are no active sinks (for example, when - // this logger is only being used for collection). Serialization is also - // shared across all sinks. if (this.sinks.length > 0) { const serializedRecord = `${serializeRecord(record)}\n`; for (const sink of this.sinks) { @@ -178,12 +175,16 @@ export class Logger { ): T extends true ? string : LogRecord[] { const records = this.getCollectedRecords(); if (opts?.serialized) { + // this is just some type mucking to get the return type to change + // depending on whether `serialized` is set or not // oxlint-disable-next-line typescript/no-unsafe-type-assertion return records.map(serializeRecord).join("\n") as T extends true ? string : LogRecord[]; } + // this is just some type mucking to get the return type to change + // depending on whether `serialized` is set or not // oxlint-disable-next-line typescript/no-unsafe-type-assertion return records as T extends true ? string : LogRecord[]; } diff --git a/packages/logger/src/plugins.ts b/packages/logger/src/plugins.ts index 56e443c4b4..949ec326b5 100644 --- a/packages/logger/src/plugins.ts +++ b/packages/logger/src/plugins.ts @@ -67,11 +67,7 @@ export function httpLogMiddleware( const time = (endTimeNs - startTimeNs) / BigInt(1000); const method = req.method; const requestId = res.locals?.["requestId"]; - - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - const extraData = res.locals?.["extraData"] as - | Record - | undefined; + const extraData = res.locals?.["extraData"]; const level = status >= 500 diff --git a/packages/logger/test/plugins.test.ts b/packages/logger/test/plugins.test.ts index 364f4d509b..f63c433ef9 100644 --- a/packages/logger/test/plugins.test.ts +++ b/packages/logger/test/plugins.test.ts @@ -26,9 +26,11 @@ function performRequest( const response = new FakeResponse(statusCode, locals); let nextCalls = 0; const middleware = httpLogMiddleware(logger, options); - // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- only the request properties read by the middleware are needed + // only the request properties read by the middleware are needed + // oxlint-disable-next-line typescript/no-unsafe-type-assertion const request = { method, originalUrl: url } as never; - // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- FakeResponse implements the response properties used by the middleware + // FakeResponse implements the response properties used by the middleware + // oxlint-disable-next-line typescript/no-unsafe-type-assertion const expressResponse = response as never; middleware(request, expressResponse, () => nextCalls++); assert.equal(nextCalls, 1); From 4aa6b64775bacbda50d125a3c0dff042b82ddba0 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 10:54:33 -0600 Subject: [PATCH 35/51] express-boilerplate: remove unneeded lint ignores --- .../src/tests/async-wrapper.test.ts | 16 +++++++--------- .../express-boilerplate/src/tests/error.test.ts | 4 +--- .../src/tests/healthcheck.test.ts | 6 ++---- .../src/tests/middleware.test.ts | 7 ++----- .../express-boilerplate/src/tests/nanoid.test.ts | 4 +--- .../src/tests/shutdown.test.ts | 8 +++----- 6 files changed, 16 insertions(+), 29 deletions(-) diff --git a/packages/express-boilerplate/src/tests/async-wrapper.test.ts b/packages/express-boilerplate/src/tests/async-wrapper.test.ts index 416065d75c..faa3a4f383 100644 --- a/packages/express-boilerplate/src/tests/async-wrapper.test.ts +++ b/packages/express-boilerplate/src/tests/async-wrapper.test.ts @@ -6,9 +6,7 @@ import { asyncHandlerWrapper } from "../async-wrapper.ts"; import { requestIdAndLogger } from "../middleware.ts"; import { jestLogger } from "./util.ts"; -// oxlint-disable typescript/no-floating-promises - -test("wrapper with no rejected promise", async () => { +void test("wrapper with no rejected promise", async () => { const app = express(); const logger = jestLogger(); @@ -37,7 +35,7 @@ test("wrapper with no rejected promise", async () => { }); }); -test("wrapper with rejected promise", async () => { +void test("wrapper with rejected promise", async () => { const app = express(); const logger = jestLogger(); @@ -75,7 +73,7 @@ test("wrapper with rejected promise", async () => { }); }); -test("default error message", async () => { +void test("default error message", async () => { const app = express(); const logger = jestLogger(); @@ -113,7 +111,7 @@ test("default error message", async () => { }); }); -test("rejection after headers sent", async () => { +void test("rejection after headers sent", async () => { const app = express(); const logger = jestLogger(); @@ -151,7 +149,7 @@ const renderErrorPage = () => Promise.resolve(""); const rejectErrorPage = (): Promise => Promise.reject(new Error("failed to render html error page")); -test("custom html error page", async () => { +void test("custom html error page", async () => { const app = express(); const logger = jestLogger(); @@ -180,7 +178,7 @@ test("custom html error page", async () => { }); }); -test("custom html error page, no HTML accept header", async () => { +void test("custom html error page, no HTML accept header", async () => { const app = express(); const logger = jestLogger(); @@ -209,7 +207,7 @@ test("custom html error page, no HTML accept header", async () => { }); }); -test("error while rendering custom error page", async () => { +void test("error while rendering custom error page", async () => { const app = express(); const logger = jestLogger(); diff --git a/packages/express-boilerplate/src/tests/error.test.ts b/packages/express-boilerplate/src/tests/error.test.ts index b9f978b5c8..2f761df8a8 100644 --- a/packages/express-boilerplate/src/tests/error.test.ts +++ b/packages/express-boilerplate/src/tests/error.test.ts @@ -2,9 +2,7 @@ import * as assert from "node:assert"; import { test } from "node:test"; import { formatGenericError } from "../error.ts"; -// oxlint-disable typescript/no-floating-promises - -test("formatGenericError", () => { +void test("formatGenericError", () => { const error = new Error("Error message"); const formatted = formatGenericError(error); diff --git a/packages/express-boilerplate/src/tests/healthcheck.test.ts b/packages/express-boilerplate/src/tests/healthcheck.test.ts index 4b8327ed77..1d0038e43a 100644 --- a/packages/express-boilerplate/src/tests/healthcheck.test.ts +++ b/packages/express-boilerplate/src/tests/healthcheck.test.ts @@ -6,9 +6,7 @@ import { healthCheck } from "../healthcheck.ts"; import { TERMINATION_APP_LOCAL_KEY } from "../shutdown.ts"; import { jestLogger } from "./util.ts"; -// oxlint-disable typescript/no-floating-promises - -test("returns 200 when not terminating", async () => { +void test("returns 200 when not terminating", async () => { const app = express(); const logger = jestLogger(); app.get("/healthz", healthCheck(app, logger)); @@ -32,7 +30,7 @@ test("returns 200 when not terminating", async () => { }); }); -test("returns 503 when terminating", async () => { +void test("returns 503 when terminating", async () => { const app = express(); const logger = jestLogger(); app.get("/healthz", healthCheck(app, logger)); diff --git a/packages/express-boilerplate/src/tests/middleware.test.ts b/packages/express-boilerplate/src/tests/middleware.test.ts index 66bef51212..5a68269445 100644 --- a/packages/express-boilerplate/src/tests/middleware.test.ts +++ b/packages/express-boilerplate/src/tests/middleware.test.ts @@ -5,16 +5,13 @@ import request from "supertest"; import { requestIdAndLogger } from "../middleware.ts"; import { jestLogger } from "./util.ts"; -// oxlint-disable typescript/no-floating-promises - -test("requestIdAndLogger adds requestId and logger", async () => { +void test("requestIdAndLogger adds requestId and logger", async () => { const app = express(); const logger = jestLogger(); app.use(requestIdAndLogger(logger)); app.use((_req, res, next) => { assert.strictEqual(typeof res.locals["requestId"], "string"); - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - const requestLogger = res.locals["logger"] as typeof logger; + const requestLogger = res.locals["logger"]; assert.notStrictEqual(requestLogger, logger); assert.strictEqual(requestLogger.name, logger.name); assert.strictEqual(typeof requestLogger.info, "function"); diff --git a/packages/express-boilerplate/src/tests/nanoid.test.ts b/packages/express-boilerplate/src/tests/nanoid.test.ts index 39edee7d8a..32e24db514 100644 --- a/packages/express-boilerplate/src/tests/nanoid.test.ts +++ b/packages/express-boilerplate/src/tests/nanoid.test.ts @@ -2,9 +2,7 @@ import * as assert from "node:assert"; import { test } from "node:test"; import { nanoid } from "../nanoid.ts"; -// oxlint-disable typescript/no-floating-promises - -test("nanoid", () => { +void test("nanoid", () => { assert.strictEqual(nanoid().length, 32); assert.strictEqual(nanoid(12).length, 12); assert.strictEqual(nanoid(64).length, 64); diff --git a/packages/express-boilerplate/src/tests/shutdown.test.ts b/packages/express-boilerplate/src/tests/shutdown.test.ts index 30bda462a8..b5f45cc37e 100644 --- a/packages/express-boilerplate/src/tests/shutdown.test.ts +++ b/packages/express-boilerplate/src/tests/shutdown.test.ts @@ -2,9 +2,7 @@ import * as assert from "node:assert"; import { test } from "node:test"; import { shutdownManagerConfigFromEnv } from "../shutdown.ts"; -// oxlint-disable typescript/no-floating-promises - -test("env parser with env vars", () => { +void test("env parser with env vars", () => { process.env["SSQT_HTTP_KEEP_ALIVE_SECONDS"] = "275"; process.env["SSQT_TERMINATION_DELAY_SECONDS"] = "18"; @@ -17,7 +15,7 @@ test("env parser with env vars", () => { assert.strictEqual(opts.terminationDelaySeconds, 18); }); -test("env parser with env vars and defaults", () => { +void test("env parser with env vars and defaults", () => { process.env["SSQT_HTTP_KEEP_ALIVE_SECONDS"] = ""; process.env["SSQT_TERMINATION_DELAY_SECONDS"] = "18"; @@ -34,7 +32,7 @@ test("env parser with env vars and defaults", () => { assert.strictEqual(opts.terminationDelaySeconds, 18); }); -test("env parser with no env vars and no defaults", () => { +void test("env parser with no env vars and no defaults", () => { process.env["SSQT_HTTP_KEEP_ALIVE_SECONDS"] = ""; process.env["SSQT_TERMINATION_DELAY_SECONDS"] = ""; From c997b80332d335bcf8b5e3a97b1a9a3d7fba4f92 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 11:29:32 -0600 Subject: [PATCH 36/51] logger: rename Transport -> Sink in public API --- packages/logger/src/config.ts | 12 +++++----- packages/logger/src/index.ts | 12 +++++----- packages/logger/src/logger.ts | 26 +++++++++++----------- packages/logger/test/config-format.test.ts | 2 +- packages/logger/test/logger.test.ts | 12 +++++----- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index d9f6ec2209..5ecf8d1213 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -41,16 +41,16 @@ export const LOG_LEVEL_VALUES: Record = { debug: 7, }; -export type ConsoleTransport = { +export type ConsoleSink = { type: "console"; }; -export type StreamTransport = { +export type StreamSink = { type: "stream"; stream: Writable; }; -export type Transport = ConsoleTransport | StreamTransport; +export type Sink = ConsoleSink | StreamSink; export type LoggerConfig = { /** @@ -61,11 +61,11 @@ export type LoggerConfig = { /** * Outputs for log records. Defaults to stdout */ - transports: Transport[]; + sinks: Sink[]; }; export function defaultConfig(): LoggerConfig { - const transports: Transport[] = [{ type: "console" }]; + const sinks: Sink[] = [{ type: "console" }]; const configuredLevel = process.env["SSQT_LOG_LEVEL"]; let logLevel: LogLevel = "info"; @@ -82,5 +82,5 @@ export function defaultConfig(): LoggerConfig { logLevel = configuredLevel; } - return { logLevel, transports }; + return { logLevel, sinks: sinks }; } diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 534b720073..50a9c431e7 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,11 +1,11 @@ import { LOG_LEVELS, defaultConfig, - type ConsoleTransport, + type ConsoleSink, type LogLevel, type LoggerConfig, - type StreamTransport, - type Transport, + type Sink, + type StreamSink, } from "./config.ts"; import type { LogRecord } from "./format.ts"; import { @@ -30,12 +30,12 @@ export { httpLogMiddleware, initializeLogger, isLoggerInitialized, - type ConsoleTransport, + type ConsoleSink, type GetCollectedLogsOptions, type LogCollectionOptions, type LogLevel, type LogRecord, type LoggerConfig, - type StreamTransport, - type Transport, + type Sink, + type StreamSink, }; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index a139866ada..c56d47456d 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -4,7 +4,7 @@ import { LOG_LEVEL_VALUES, type LoggerConfig, type LogLevel, - type Transport, + type Sink, } from "./config.ts"; import { formatRecord, serializeRecord, type LogRecord } from "./format.ts"; @@ -24,7 +24,7 @@ export const DEFAULT_LOG_COLLECTION_LIMIT = 500; export const LOG_TYPE_MARKER = "__ssqt_log_type"; export const DEFAULT_LOGGER_NAME = "_ssqt_default_logger"; -type Sink = (serializedRecord: string) => void; +type SinkFn = (serializedRecord: string) => void; const loggers = new Map(); export function getLogger(logger?: string): Logger { @@ -56,7 +56,7 @@ export function initializeLogger( config ?? (typeof nameOrConfig === "string" ? {} : (nameOrConfig ?? {})); const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; - const sinks = conf.transports.map(transportToSink); + const sinks = conf.sinks.map(getSinkFn); const logger = new Logger(name, sinks, conf.logLevel, {}); loggers.set(name, logger); @@ -67,7 +67,7 @@ export class Logger { public name: string; public level: LogLevel; - private sinks: Sink[]; + private sinks: SinkFn[]; private baseFields: Record; private collection: { @@ -77,16 +77,16 @@ export class Logger { start: number; size: number; } = { - enabled: false, - maxEntries: DEFAULT_LOG_COLLECTION_LIMIT, - records: [], - start: 0, - size: 0, - }; + enabled: false, + maxEntries: DEFAULT_LOG_COLLECTION_LIMIT, + records: [], + start: 0, + size: 0, + }; constructor( name: string, - sinks: Sink[], + sinks: SinkFn[], level: LogLevel, baseFields: Record, ) { @@ -249,9 +249,9 @@ function isRecord(value: unknown): value is Record { return prototype === Object.prototype || prototype === null; } -function transportToSink(transport: Transport): Sink { +function getSinkFn(sink: Sink): SinkFn { const stream: Writable = - transport.type === "console" ? process.stdout : transport.stream; + sink.type === "console" ? process.stdout : sink.stream; return (serializedRecord) => { stream.write(serializedRecord); diff --git a/packages/logger/test/config-format.test.ts b/packages/logger/test/config-format.test.ts index 15d717d6ef..03b97284c7 100644 --- a/packages/logger/test/config-format.test.ts +++ b/packages/logger/test/config-format.test.ts @@ -19,7 +19,7 @@ void describe("configuration", () => { delete process.env.SSQT_LOG_LEVEL; assert.deepEqual(defaultConfig(), { logLevel: "info", - transports: [{ type: "console" }], + sinks: [{ type: "console" }], }); assert.deepEqual(LOG_LEVELS, [ "emerg", diff --git a/packages/logger/test/logger.test.ts b/packages/logger/test/logger.test.ts index a953f98259..15bd55f4e6 100644 --- a/packages/logger/test/logger.test.ts +++ b/packages/logger/test/logger.test.ts @@ -28,7 +28,7 @@ const unique = (prefix: string) => `${prefix}-${++sequence}`; void describe("logger initialization", () => { void test("accepts object-form configuration and reuses the default logger", () => { assert.equal(isLoggerInitialized(), false); - const logger = initializeLogger({ transports: [], logLevel: "debug" }); + const logger = initializeLogger({ sinks: [], logLevel: "debug" }); assert.equal(logger.name, DEFAULT_LOGGER_NAME); assert.equal(logger.level, "debug"); assert.equal(isLoggerInitialized(), true); @@ -44,12 +44,12 @@ void describe("logger initialization", () => { assert.equal(auto.name, named); const configured = initializeLogger(unique("configured"), { - transports: [], + sinks: [], logLevel: "debug", }); assert.equal(configured.level, "debug"); assert.throws( - () => initializeLogger({ transports: [], logLevel: "warning" }), + () => initializeLogger({ sinks: [], logLevel: "warning" }), /already been initialized/, ); }); @@ -61,7 +61,7 @@ void describe("logging", () => { const two = new CaptureStream(); const logger = initializeLogger(unique("output"), { logLevel: "info", - transports: [ + sinks: [ { type: "stream", stream: one }, { type: "stream", stream: two }, ], @@ -118,7 +118,7 @@ void describe("logging", () => { ); }); - void test("uses the console transport", () => { + void test("uses the console sink", () => { const writes: string[] = []; const originalWrite = process.stdout.write.bind(process.stdout); process.stdout.write = (chunk: string | Uint8Array) => { @@ -128,7 +128,7 @@ void describe("logging", () => { try { initializeLogger(unique("console"), { logLevel: "info", - transports: [{ type: "console" }], + sinks: [{ type: "console" }], }).info("console message"); } finally { process.stdout.write = originalWrite; From ecaea6222c355ffe45f36aac11b71c266f5b1b30 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 11:29:41 -0600 Subject: [PATCH 37/51] logger: documentation touch ups --- packages/logger/CHANGELOG.md | 2 +- packages/logger/README.md | 70 +++++++++++++++++++++++------------- packages/logger/package.json | 2 +- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 2449abacdb..a700882171 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Loggers now emit structured JSON directly and expose the package-owned `Logger` type. - The supported sinks are stdout and custom writable Node streams. - Removed the top-level `info`, `warn`, and `error` exports. -- Removed `SSQT_LOG_TRANSPORTS`; configure `console` or writable `stream` transports through `initializeLogger`. +- Removed `SSQT_LOG_TRANSPORTS`; configure `console` or writable `stream` sink through `initializeLogger`. ## [3.0.0] - 2026-05-27 diff --git a/packages/logger/README.md b/packages/logger/README.md index a53c697951..65708897e5 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -1,5 +1,5 @@

@saasquatch/logger

-

Small structured JSON logger for SaaSquatch NodeJS services

+

Small structured JSON logger for SaaSquatch Node.js services

This package provides a lightweight structured logger that writes newline-delimited JSON to stdout by default. It supports syslog-style levels, named loggers, custom writable @@ -19,41 +19,63 @@ const logger = initializeLogger(); logger.info("Hello", { service: "example" }); logger.error(new Error("Something failed")); +const myValue = { field1: "string", field2: 1.2345 }; +logger.debug({ message: "Trying to debug something here", myValue }); + // child fields are included in every record emitted by the child const requestLogger = logger.child({ requestId: "req-123" }); -requestLogger.info("Request started"); - -// collect records for a response, while still writing them to the sink -requestLogger.startLogCollection({ maxEntries: 100 }); -requestLogger.info("Request finished"); -const logs = requestLogger.getCollectedLogs(); -requestLogger.stopLogCollection(); -requestLogger.clearCollectedLogs(); +requestLogger.info("This log will include the above requestId field"); ``` -Collection mode retains the newest 500 records by default. `maxEntries` can be set when -enabling collection; it must be a positive integer. Collection is independent for each -logger and child logger. +## Customization of log sinks -Each log call writes one JSON record to each enabled sink. The default minimum level is -`info`; it can also be configured with `SSQT_LOG_LEVEL`. +```typescript +import { createWriteStream } from "node:fs"; +import { initializeLogger } from "@saasquatch/logger"; + +const logger = initializeLogger("my-logger", { + logLevel: "info", + sinks: [ + // write to stdout (default) + { type: "console" }, + // write to log file + { type: "stream", stream: createWriteStream("./service.log") }, + ], +}); +``` -### Custom stream sinks +The supported sink types are `console` and `stream`. -Use a writable Node stream in addition to, or instead of, stdout: +## Temporary log collection ```typescript -import { createWriteStream } from "node:fs"; import { initializeLogger } from "@saasquatch/logger"; -const logger = initializeLogger({ - transports: [{ type: "stream", stream: createWriteStream("service.log") }], -}); +const logger = initializeLogger(); + +// collect records for a response, while still writing them to the sink +logger.startLogCollection({ maxEntries: 100 }); +logger.info("Request finished"); + +// logs: LogRecord[] +const logs = logger.getCollectedLogs(); + +// serializedLogs: string +const serializedLogs = logger.getCollectedLogs({ serialized: true }); + +logger.stopLogCollection(); +logger.clearCollectedLogs(); ``` -The supported transport types are `console` and `stream`. +Collection mode retains the newest 500 records by default. The size of the collection +buffer can be customized by passing `maxEntries` to the `startLogCollection` function. +Collection is independent for each logger and child logger. + +The collected logs can be retrieved either in their raw or serialized form. Passing +`{ serialized: true }` to the function options will use the logger's built-in +serialization machinery on each collected log, then join the results with newlines. -### Express HTTP logging +## Express HTTP logging ```typescript import express from "express"; @@ -64,5 +86,5 @@ const logger = initializeLogger(); app.use(httpLogMiddleware(logger)); ``` -HTTP records include normalized method, URL, status, response time, and request ID -fields. Sensitive URL query parameters are removed by the middleware. +HTTP records include method, URL, status, response time, and request ID (if present) +fields. Common sensitive URL query parameters are removed by the middleware. diff --git a/packages/logger/package.json b/packages/logger/package.json index 6f6bf5baab..effa158465 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -2,7 +2,7 @@ "name": "@saasquatch/logger", "version": "3.0.1-6", "private": false, - "description": "Small structured JSON logger for SaaSquatch NodeJS services", + "description": "Small structured JSON logger for SaaSquatch Node.js services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", "bugs": { "url": "https://github.com/saasquatch/program-tools/issues" From 0ff9f09af9591ff1ebb48110f8a7db4426193aa1 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 11:50:13 -0600 Subject: [PATCH 38/51] logger: remove LOG_TYPE_MARKER hack --- packages/logger/src/format.ts | 53 +-------------------- packages/logger/src/index.ts | 2 - packages/logger/src/logger.ts | 1 - packages/logger/src/plugins.ts | 23 +++++++-- packages/logger/test/config-format.test.ts | 55 +--------------------- 5 files changed, 23 insertions(+), 111 deletions(-) diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 9941921d4b..8cbbfdc85f 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -1,13 +1,5 @@ import type { LogLevel } from "./config.ts"; -import { DEFAULT_LOGGER_NAME, LOG_TYPE_MARKER } from "./logger.ts"; - -type HTTPMessage = { - method: string; - status: string | number; - time: bigint | string | number; - url: string; - requestId?: string; -}; +import { DEFAULT_LOGGER_NAME } from "./logger.ts"; export type LogRecord = Record & { level: LogLevel; @@ -44,12 +36,6 @@ export function formatRecord( } } - if (record[LOG_TYPE_MARKER] === "HTTP") { - formatHttpRecord(record); - } - - delete record[LOG_TYPE_MARKER]; - // for Datadog // https://docs.datadoghq.com/standard-attributes record["status"] = level; @@ -57,41 +43,6 @@ export function formatRecord( return record; } -function formatHttpRecord(record: Record): void { - const value = record["message"]; - if (value === null || typeof value !== "object") { - return; - } - - // NOTE: it would of course be safer to validate this using a real - // schema validator like zod, but for a logging library we just - // don't want that kind of overhead. The HTTP log messages are generated - // using middleware from our own package so it's unlikely to be invalid - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - const message = value as HTTPMessage; - const micros = Number(message.time); - const displayTime = - micros < 1000 ? `${micros} μs` : `${Math.round(micros / 1000)} ms`; - - record["message"] = [ - message.status, - message.method, - displayTime.padStart(6, " "), - message.url, - ].join(" "); - - // for Datadog - // https://docs.datadoghq.com/standard-attributes?search=HTTP - record["http.url"] = message.url; - record["http.method"] = message.method; - record["http.status_code"] = message.status; - record["http.response_time"] = micros; - - if (message.requestId) { - record["http.request_id"] = message.requestId; - } -} - /** * Safely serialize records, including Error and BigInt values. */ @@ -99,7 +50,7 @@ export function serializeRecord(record: LogRecord): string { const ancestors: object[] = []; return JSON.stringify( { ...record, toJSON: undefined }, - function (_key, value: unknown) { + function(_key, value: unknown) { if (typeof value === "bigint") { return value.toString(); } diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 50a9c431e7..da24015271 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -10,7 +10,6 @@ import { import type { LogRecord } from "./format.ts"; import { DEFAULT_LOG_COLLECTION_LIMIT, - LOG_TYPE_MARKER, Logger, getLogger, initializeLogger, @@ -23,7 +22,6 @@ import { httpLogMiddleware } from "./plugins.ts"; export { DEFAULT_LOG_COLLECTION_LIMIT, LOG_LEVELS, - LOG_TYPE_MARKER, Logger, defaultConfig, getLogger, diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index c56d47456d..81dde417db 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -21,7 +21,6 @@ export type GetCollectedLogsOptions = { export const DEFAULT_LOG_COLLECTION_LIMIT = 500; -export const LOG_TYPE_MARKER = "__ssqt_log_type"; export const DEFAULT_LOGGER_NAME = "_ssqt_default_logger"; type SinkFn = (serializedRecord: string) => void; diff --git a/packages/logger/src/plugins.ts b/packages/logger/src/plugins.ts index 949ec326b5..41a23c9d47 100644 --- a/packages/logger/src/plugins.ts +++ b/packages/logger/src/plugins.ts @@ -2,7 +2,6 @@ import { URL } from "node:url"; import type { Request, Response } from "express"; import type { LogLevel } from "./config.ts"; import type { Logger } from "./logger.ts"; -import { LOG_TYPE_MARKER } from "./logger.ts"; export type HttpLogMiddlewareOptions = { nonErrorLogLevel?: LogLevel; @@ -78,8 +77,26 @@ export function httpLogMiddleware( ? "debug" : (opts?.nonErrorLogLevel ?? "info"); - const message = { method, status, time, url: cleanUrl, requestId }; - logger.log(level, { [LOG_TYPE_MARKER]: "HTTP", message, extraData }); + const micros = Number(time); + const displayTime = + micros < 1000 ? `${micros} μs` : `${Math.round(micros / 1000)} ms`; + + logger.log(level, { + message: [status, method, displayTime.padStart(6, " "), cleanUrl].join( + " ", + ), + + // for Datadog + // https://docs.datadoghq.com/standard-attributes?search=HTTP + "http.url": cleanUrl, + "http.method": method, + "http.status_code": status, + "http.response_time": micros, + + ...(requestId ? { "http.request_id": requestId } : {}), + + extraData, + }); }); next(); diff --git a/packages/logger/test/config-format.test.ts b/packages/logger/test/config-format.test.ts index 03b97284c7..fb42bc3e10 100644 --- a/packages/logger/test/config-format.test.ts +++ b/packages/logger/test/config-format.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { afterEach, describe, test } from "node:test"; import { defaultConfig, LOG_LEVELS, LOG_LEVEL_VALUES } from "../src/config.ts"; import { formatRecord, serializeRecord } from "../src/format.ts"; -import { DEFAULT_LOGGER_NAME, LOG_TYPE_MARKER } from "../src/logger.ts"; +import { DEFAULT_LOGGER_NAME } from "../src/logger.ts"; void describe("configuration", () => { const originalLevel = process.env.SSQT_LOG_LEVEL; @@ -75,59 +75,6 @@ void describe("record formatting", () => { "text", ); }); - - void test("normalizes HTTP records in microseconds and milliseconds", () => { - const micro = formatRecord( - "web", - "info", - { - status: 201, - method: "POST", - time: 999n, - url: "/items", - requestId: "r1", - }, - { [LOG_TYPE_MARKER]: "HTTP" }, - ); - assert.equal(micro.message, "201 POST 999 μs /items"); - assert.equal(micro["http.url"], "/items"); - assert.equal(micro["http.method"], "POST"); - assert.equal(micro["http.status_code"], 201); - assert.equal(micro["http.response_time"], 999); - assert.equal(micro["http.request_id"], "r1"); - assert.equal(LOG_TYPE_MARKER in micro, false); - - const milli = formatRecord( - DEFAULT_LOGGER_NAME, - "error", - { - status: 500, - method: "GET", - time: "1501", - url: "/fail", - }, - { [LOG_TYPE_MARKER]: "HTTP" }, - ); - assert.equal(milli.message, "500 GET 2 ms /fail"); - assert.equal("http.request_id" in milli, false); - }); - - void test("leaves malformed HTTP messages alone and always removes the marker", () => { - for (const message of [null, "not an object"]) { - const record = formatRecord(DEFAULT_LOGGER_NAME, "info", message, { - [LOG_TYPE_MARKER]: "HTTP", - }); - assert.equal(record.message, message); - assert.equal(LOG_TYPE_MARKER in record, false); - } - - assert.equal( - formatRecord(DEFAULT_LOGGER_NAME, "info", "normal", { - [LOG_TYPE_MARKER]: "OTHER", - }).message, - "normal", - ); - }); }); void describe("safe serialization", () => { From 0aa7b435080387736e3d6d40fcf228abebf740c7 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 12:03:43 -0600 Subject: [PATCH 39/51] logger: test hygiene fix --- .../test/default-initialization.test.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/logger/test/default-initialization.test.ts b/packages/logger/test/default-initialization.test.ts index 119308df0e..208dc94261 100644 --- a/packages/logger/test/default-initialization.test.ts +++ b/packages/logger/test/default-initialization.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { test } from "node:test"; +import { afterEach, describe, test } from "node:test"; import { DEFAULT_LOGGER_NAME, getLogger, @@ -7,11 +7,24 @@ import { isLoggerInitialized, } from "../src/logger.ts"; -void test("initializes the default logger when called without arguments", () => { - assert.equal(isLoggerInitialized(), false); - const logger = initializeLogger(); - assert.equal(logger.name, DEFAULT_LOGGER_NAME); - assert.equal(logger.level, "info"); - assert.equal(isLoggerInitialized(), true); - assert.equal(getLogger(), logger); +void describe("configuration", () => { + const originalLevel = process.env.SSQT_LOG_LEVEL; + + afterEach(() => { + if (originalLevel === undefined) { + delete process.env.SSQT_LOG_LEVEL; + } else { + process.env.SSQT_LOG_LEVEL = originalLevel; + } + }); + + void test("initializes the default logger when called without arguments", () => { + delete process.env.SSQT_LOG_LEVEL; + assert.equal(isLoggerInitialized(), false); + const logger = initializeLogger(); + assert.equal(logger.name, DEFAULT_LOGGER_NAME); + assert.equal(logger.level, "info"); + assert.equal(isLoggerInitialized(), true); + assert.equal(getLogger(), logger); + }); }); From 6bda469150ca7c2745a8ca6692952dcb6b37fd1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:04:31 +0000 Subject: [PATCH 40/51] logger: publish v3.0.1-7 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index bca960f823..dfdd4ed937 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-6", + "version": "3.0.1-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-6", + "version": "3.0.1-7", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index effa158465..a51ddb65de 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-6", + "version": "3.0.1-7", "private": false, "description": "Small structured JSON logger for SaaSquatch Node.js services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From 5e8de8b0830d4c374ff3dfaed6b01af545078078 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 12:09:06 -0600 Subject: [PATCH 41/51] express-boilerplate: bump logger to 3.0.1-7 --- packages/express-boilerplate/package-lock.json | 10 +++++----- packages/express-boilerplate/package.json | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 06a68d133e..77efe491a5 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -12,7 +12,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-6", + "@saasquatch/logger": "^3.0.1-7", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -26,7 +26,7 @@ "node": "^24 || ^26" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.1-6", + "@saasquatch/logger": "^3.0.1-7", "express": "^4.0.0" } }, @@ -832,9 +832,9 @@ } }, "node_modules/@saasquatch/logger": { - "version": "3.0.1-6", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-6.tgz", - "integrity": "sha512-xLgEmaYWhWz0rvz9Lt+c/r0rfcBBlEpfUTSLqurjUnrSjkB1HADQZkGlMRZWs/v9GUyejsPRkLdUAEYGiyvB1A==", + "version": "3.0.1-7", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-7.tgz", + "integrity": "sha512-DSnq+L7V6qEQiBkWyO6hYTeMaVMAvJztdSP00pBLDlYs/4Wz2RvtafycfeRQgN7dkY7QN/Vc1vy1pHcvYIG04g==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 0eb1d6983a..1a86aaae13 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -35,7 +35,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-6", + "@saasquatch/logger": "^3.0.1-7", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -46,7 +46,7 @@ "typescript": "^7.0.2" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.1-6", + "@saasquatch/logger": "^3.0.1-7", "express": "^4.0.0" }, "engines": { From 586c42a3963c7bf3c0ef6187a8d72c3e021a768e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:10:37 +0000 Subject: [PATCH 42/51] express-boilerplate: publish v2.0.1-3 This commit was generated by GitHub Actions CI --- packages/express-boilerplate/package-lock.json | 4 ++-- packages/express-boilerplate/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 77efe491a5..6551f9ebdc 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-2", + "version": "2.0.1-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-2", + "version": "2.0.1-3", "license": "MIT", "dependencies": { "nanoid": "^6.0.1" diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index 1a86aaae13..a846600133 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-2", + "version": "2.0.1-3", "private": false, "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", "homepage": "https://github.com/saasquatch/program-tools/packages/express-boilerplate#readme", From 7a81d315861505d690021531657908a477028612 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 12:22:26 -0600 Subject: [PATCH 43/51] logger: formatting fixes --- packages/logger/src/format.ts | 2 +- packages/logger/src/logger.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 8cbbfdc85f..3a539c51af 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -50,7 +50,7 @@ export function serializeRecord(record: LogRecord): string { const ancestors: object[] = []; return JSON.stringify( { ...record, toJSON: undefined }, - function(_key, value: unknown) { + function (_key, value: unknown) { if (typeof value === "bigint") { return value.toString(); } diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 81dde417db..77583616e4 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -76,12 +76,12 @@ export class Logger { start: number; size: number; } = { - enabled: false, - maxEntries: DEFAULT_LOG_COLLECTION_LIMIT, - records: [], - start: 0, - size: 0, - }; + enabled: false, + maxEntries: DEFAULT_LOG_COLLECTION_LIMIT, + records: [], + start: 0, + size: 0, + }; constructor( name: string, From 98082e1943bb91b6d140b83a8da23e0bab5ae02e Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 15:01:36 -0600 Subject: [PATCH 44/51] logger: stream backpressure handling This is quite a bit of code to handle what will most likely be an extremely rare edge case, but I'd rather deal with this before it becomes a problem in production and causes a real headache --- packages/logger/CHANGELOG.md | 3 +- packages/logger/README.md | 57 ++++- packages/logger/src/config.ts | 21 +- packages/logger/src/index.ts | 6 +- packages/logger/src/logger.ts | 55 ++-- packages/logger/src/stream-sink.ts | 278 +++++++++++++++++++++ packages/logger/test/config-format.test.ts | 2 +- packages/logger/test/logger.test.ts | 7 +- packages/logger/test/stream-sink.test.ts | 198 +++++++++++++++ 9 files changed, 570 insertions(+), 57 deletions(-) create mode 100644 packages/logger/src/stream-sink.ts create mode 100644 packages/logger/test/stream-sink.test.ts diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index a700882171..c00abfceb9 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed Winston and its transport/format APIs. - Loggers now emit structured JSON directly and expose the package-owned `Logger` type. -- The supported sinks are stdout and custom writable Node streams. +- All sinks now use the Node.js `Writable` stream API. Use the `process.stdout` stream to + print to stdout. - Removed the top-level `info`, `warn`, and `error` exports. - Removed `SSQT_LOG_TRANSPORTS`; configure `console` or writable `stream` sink through `initializeLogger`. diff --git a/packages/logger/README.md b/packages/logger/README.md index 65708897e5..37ec1c8e88 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -3,8 +3,8 @@ This package provides a lightweight structured logger that writes newline-delimited JSON to stdout by default. It supports syslog-style levels, named loggers, custom writable -stream sinks, temporary log collection to an internal buffer, and Express HTTP logging -middleware. +stream sinks, temporary log collection to an internal buffer, Express HTTP logging +middleware, and correct backpressure handling. ## Getting started @@ -37,9 +37,11 @@ const logger = initializeLogger("my-logger", { logLevel: "info", sinks: [ // write to stdout (default) - { type: "console" }, + { stream: process.stdout }, + // write to stderr + { stream: process.stderr }, // write to log file - { type: "stream", stream: createWriteStream("./service.log") }, + { stream: createWriteStream("./service.log") }, ], }); ``` @@ -71,8 +73,8 @@ Collection mode retains the newest 500 records by default. The size of the colle buffer can be customized by passing `maxEntries` to the `startLogCollection` function. Collection is independent for each logger and child logger. -The collected logs can be retrieved either in their raw or serialized form. Passing -`{ serialized: true }` to the function options will use the logger's built-in +The collected logs can be retrieved either in their raw or serialized form. Specifying +`{ serialized: true }` in the function options will use the logger's built-in serialization machinery on each collected log, then join the results with newlines. ## Express HTTP logging @@ -86,5 +88,44 @@ const logger = initializeLogger(); app.use(httpLogMiddleware(logger)); ``` -HTTP records include method, URL, status, response time, and request ID (if present) -fields. Common sensitive URL query parameters are removed by the middleware. +HTTP records include the method, URL, status, response time, and request ID (if present). +Common sensitive URL query parameters are removed by the middleware. + +## Sink backpressure handling + +All log sinks are Node.js `Writable` streams, meaning they can exhibit backpressure +behavior as described in the [`Writable.write`][1] documentation. Backpressure is handled +by the logger to keep the log methods synchronous and prevent callers from having to +implement their own backpressure handling strategies. + +Backpressure is handled per-sink. When a sink's stream becomes blocked (`write()` returns +false), logs begin to collect in an internal FIFO queue. The size of the queue is bounded +to prevent the memory exhaustion scenario described in the [`Writable.write`][1] docs. +The logs in the queue will be written when the stream becomes available for writing +again. + +If the queue becomes full before the stream is available for writing, logs will be +dropped in order of ascending severity and receipt time (lowest severity logs are dropped +first, and if all logs in the queue have the same severity, the oldest will be dropped +first). + +The size of the queue defaults to 1,000 and is configurable: + +```typescript +import { createWriteStream } from "node:fs"; +import { initializeLogger } from "@saasquatch/logger"; + +const logger = initializeLogger({ + logLevel: "info", + sinks: [{ stream: createWriteStream("./service.log"), maxQueueSize: 500 }], +}); +``` + +Each sink maintains its own queue, so the sum of all `maxQueueSize`s will be the maximum +possible number of logs stored by the logger in the event that all streams are +simultaneously blocked. This number does not include the internal buffers of the streams +themselves, nor does it include any buffered writes that the Node.js runtime is holding +onto from other writers who may have failed to stop writing when the stream became +blocked. + +[1]: https://nodejs.org/api/stream.html#writablewritechunk-encoding-callback diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 5ecf8d1213..2d63001041 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -41,16 +41,21 @@ export const LOG_LEVEL_VALUES: Record = { debug: 7, }; -export type ConsoleSink = { - type: "console"; -}; +export const LOWEST_PRIO_LEVEL = Math.max(...Object.values(LOG_LEVEL_VALUES)); + +export const DEFAULT_SINK_QUEUE_SIZE = 1000; -export type StreamSink = { - type: "stream"; +export type Sink = { + /** + * The stream to write logs to + */ stream: Writable; -}; -export type Sink = ConsoleSink | StreamSink; + /** + * Maximum number of records retained while the stream applies backpressure + */ + maxQueueSize?: number; +}; export type LoggerConfig = { /** @@ -65,7 +70,7 @@ export type LoggerConfig = { }; export function defaultConfig(): LoggerConfig { - const sinks: Sink[] = [{ type: "console" }]; + const sinks: Sink[] = [{ stream: process.stdout }]; const configuredLevel = process.env["SSQT_LOG_LEVEL"]; let logLevel: LogLevel = "info"; diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index da24015271..671285ef7a 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,11 +1,10 @@ import { + DEFAULT_SINK_QUEUE_SIZE, LOG_LEVELS, defaultConfig, - type ConsoleSink, type LogLevel, type LoggerConfig, type Sink, - type StreamSink, } from "./config.ts"; import type { LogRecord } from "./format.ts"; import { @@ -21,6 +20,7 @@ import { httpLogMiddleware } from "./plugins.ts"; export { DEFAULT_LOG_COLLECTION_LIMIT, + DEFAULT_SINK_QUEUE_SIZE, LOG_LEVELS, Logger, defaultConfig, @@ -28,12 +28,10 @@ export { httpLogMiddleware, initializeLogger, isLoggerInitialized, - type ConsoleSink, type GetCollectedLogsOptions, type LogCollectionOptions, type LogLevel, type LogRecord, type LoggerConfig, type Sink, - type StreamSink, }; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 77583616e4..4bc66b4a42 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -1,12 +1,11 @@ -import type { Writable } from "node:stream"; import { defaultConfig, LOG_LEVEL_VALUES, type LoggerConfig, type LogLevel, - type Sink, } from "./config.ts"; import { formatRecord, serializeRecord, type LogRecord } from "./format.ts"; +import { createSinkWriter, type SinkWriter } from "./stream-sink.ts"; export type LogCollectionOptions = { /** @@ -23,7 +22,6 @@ export const DEFAULT_LOG_COLLECTION_LIMIT = 500; export const DEFAULT_LOGGER_NAME = "_ssqt_default_logger"; -type SinkFn = (serializedRecord: string) => void; const loggers = new Map(); export function getLogger(logger?: string): Logger { @@ -55,7 +53,7 @@ export function initializeLogger( config ?? (typeof nameOrConfig === "string" ? {} : (nameOrConfig ?? {})); const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; - const sinks = conf.sinks.map(getSinkFn); + const sinks = conf.sinks.map((sink) => createSinkWriter(name, sink)); const logger = new Logger(name, sinks, conf.logLevel, {}); loggers.set(name, logger); @@ -66,7 +64,7 @@ export class Logger { public name: string; public level: LogLevel; - private sinks: SinkFn[]; + private sinks: SinkWriter[]; private baseFields: Record; private collection: { @@ -85,7 +83,7 @@ export class Logger { constructor( name: string, - sinks: SinkFn[], + sinks: SinkWriter[], level: LogLevel, baseFields: Record, ) { @@ -99,7 +97,7 @@ export class Logger { messageLevel: LogLevel, message: unknown, fields?: Record, - ) { + ): void { if (LOG_LEVEL_VALUES[messageLevel] > LOG_LEVEL_VALUES[this.level]) { return; } @@ -134,19 +132,25 @@ export class Logger { if (this.sinks.length > 0) { const serializedRecord = `${serializeRecord(record)}\n`; for (const sink of this.sinks) { - sink(serializedRecord); + sink(serializedRecord, messageLevel); } } } - public child(record: Record) { + public async flush(): Promise { + await Promise.all( + this.sinks.map((sink) => sink.flush?.() ?? Promise.resolve()), + ); + } + + public child(record: Record): Logger { return new Logger(this.name, this.sinks, this.level, { ...this.baseFields, ...record, }); } - public startLogCollection(options?: LogCollectionOptions) { + public startLogCollection(options?: LogCollectionOptions): void { const maxEntries = options?.maxEntries ?? DEFAULT_LOG_COLLECTION_LIMIT; if (!Number.isInteger(maxEntries) || maxEntries < 1) { @@ -165,7 +169,7 @@ export class Logger { this.collection.enabled = true; } - public stopLogCollection() { + public stopLogCollection(): void { this.collection.enabled = false; } @@ -188,45 +192,45 @@ export class Logger { return records as T extends true ? string : LogRecord[]; } - public clearCollectedLogs() { + public clearCollectedLogs(): void { this.collection.records.length = 0; this.collection.start = 0; this.collection.size = 0; } - public emerg(message: unknown, fields?: Record) { + public emerg(message: unknown, fields?: Record): void { this.log("emerg", message, fields); } - public alert(message: unknown, fields?: Record) { + public alert(message: unknown, fields?: Record): void { this.log("alert", message, fields); } - public crit(message: unknown, fields?: Record) { + public crit(message: unknown, fields?: Record): void { this.log("crit", message, fields); } - public error(message: unknown, fields?: Record) { + public error(message: unknown, fields?: Record): void { this.log("error", message, fields); } - public warning(message: unknown, fields?: Record) { + public warning(message: unknown, fields?: Record): void { this.log("warning", message, fields); } - public warn(message: unknown, fields?: Record) { + public warn(message: unknown, fields?: Record): void { this.log("warn", message, fields); } - public notice(message: unknown, fields?: Record) { + public notice(message: unknown, fields?: Record): void { this.log("notice", message, fields); } - public info(message: unknown, fields?: Record) { + public info(message: unknown, fields?: Record): void { this.log("info", message, fields); } - public debug(message: unknown, fields?: Record) { + public debug(message: unknown, fields?: Record): void { this.log("debug", message, fields); } @@ -247,12 +251,3 @@ function isRecord(value: unknown): value is Record { const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } - -function getSinkFn(sink: Sink): SinkFn { - const stream: Writable = - sink.type === "console" ? process.stdout : sink.stream; - - return (serializedRecord) => { - stream.write(serializedRecord); - }; -} diff --git a/packages/logger/src/stream-sink.ts b/packages/logger/src/stream-sink.ts new file mode 100644 index 0000000000..83d6c99abf --- /dev/null +++ b/packages/logger/src/stream-sink.ts @@ -0,0 +1,278 @@ +import type { Writable } from "node:stream"; +import { + DEFAULT_SINK_QUEUE_SIZE, + LOG_LEVEL_VALUES, + LOWEST_PRIO_LEVEL, + type LogLevel, + type Sink, +} from "./config.ts"; +import { formatRecord, serializeRecord } from "./format.ts"; + +/* + * JC: all of the code in this file is necessary to ensure that the + * requirements of the `Writable.write` function are upheld: + * + * > If false is returned, further attempts to write data to the stream should + * > stop until the 'drain' event is emitted. + * + * > While calling write() on a stream that is not draining is allowed, Node.js + * > will buffer all written chunks until maximum memory usage occurs, at which + * > point it will abort unconditionally. + * + * https://nodejs.org/api/stream.html#writablewritechunk-encoding-callback + * + * we don't want to pass this backpressure up to the callers since that would + * require users of the library to implement their own backpressure handling at + * every log callsite, which is ridiculous. we will implement our own backpressure + * handling that uses a bounded queue to hold messages while the stream is blocked + * and drops "least unimportant" messages if the queue becomes full + * + * in Winston, which this library used to wrap, the `write` method is called + * without checking the return value: + * + * https://github.com/winstonjs/winston/blob/ff0b79de8562bb322c390fbc82fe71c11f373428/lib/winston/transports/stream.js#L47-L61 + * + * this means that consumers of the @saasquatch/logger package prior to the + * 4.0 update were vulnerable to this possible memory exhaustion issue + */ + +export type SinkWriter = (( + serializedRecord: string, + level: LogLevel, +) => void) & { + flush?: () => Promise; +}; + +type QueuedRecord = { + serializedRecord: string; + level: LogLevel; +}; + +type StreamMonitor = { + error?: Error; + failureHandlers: Set<(error: Error) => void>; +}; + +export function createSinkWriter(loggerName: string, sink: Sink): SinkWriter { + const maxQueueSize = sink.maxQueueSize ?? DEFAULT_SINK_QUEUE_SIZE; + + if (!Number.isInteger(maxQueueSize) || maxQueueSize < 1) { + throw new Error("Sink maxQueueSize must be a positive integer"); + } + + const queue: QueuedRecord[] = []; + + // keep track of callers who are waiting for the SinkWriter to flush + const flushWaiters: Array<{ + resolve: () => void; + reject: (error: Error) => void; + }> = []; + + let blocked = false; + let droppedRecords = 0; + let pendingWrites = 0; + let streamError: Error | undefined; + + const settleFlushWaiters = () => { + // if the stream is errored, all the flush waiters will be rejected + // instantly + if (streamError !== undefined) { + for (const waiter of flushWaiters.splice(0)) { + waiter.reject(streamError); + } + + return; + } + + if (!blocked && queue.length === 0 && pendingWrites === 0) { + for (const waiter of flushWaiters.splice(0)) { + waiter.resolve(); + } + } + }; + + const fail = (error: Error) => { + if (streamError !== undefined) { + return; + } + + streamError = error; + blocked = false; + queue.length = 0; + sink.stream.removeListener("drain", drainQueue); + settleFlushWaiters(); + }; + + const writeChunk = (serializedRecord: string): boolean => { + pendingWrites += 1; + + try { + return sink.stream.write(serializedRecord, (error) => { + pendingWrites -= 1; + if (error) { + fail(error); + return; + } + + settleFlushWaiters(); + }); + } catch (error) { + pendingWrites -= 1; + fail(error instanceof Error ? error : new Error(String(error))); + return false; + } + }; + + const waitForDrain = () => { + if (streamError !== undefined) { + return; + } + + blocked = true; + sink.stream.once("drain", drainQueue); + }; + + // called when the stream is drained and becomes available for writing again + function drainQueue() { + if (streamError !== undefined) { + return; + } + + blocked = false; + + // emit a warning if some logs were dropped while we were blocked by + // backpressure + if (droppedRecords > 0) { + const count = droppedRecords; + droppedRecords = 0; + const noun = count === 1 ? "record" : "records"; + const warning = formatRecord( + loggerName, + "warn", + `Dropped ${count} log ${noun} due to sink backpressure`, + { droppedLogs: count }, + ); + + if (!writeChunk(`${serializeRecord(warning)}\n`)) { + waitForDrain(); + return; + } + } + + // write the queued records until the queue is empty or we become blocked + // again + while (queue.length > 0) { + const record = queue.shift()!; + if (!writeChunk(record.serializedRecord)) { + waitForDrain(); + return; + } + } + + // if we managed to empty the queue then we can proceed to settle the flush + // waiters + settleFlushWaiters(); + } + + const dropLowestSeverityRecord = () => { + let dropIndex = 0; + let dropSeverity = 0; + + for ( + let i = 1; + // if we encounter a log with the lowest possible severity we can short + // circuit this loop + i < queue.length && dropSeverity !== LOWEST_PRIO_LEVEL; + i += 1 + ) { + const thisSeverity = LOG_LEVEL_VALUES[queue[i].level]; + if (thisSeverity > LOG_LEVEL_VALUES[queue[dropIndex].level]) { + dropIndex = i; + dropSeverity = thisSeverity; + } + } + + queue.splice(dropIndex, 1); + droppedRecords += 1; + }; + + const sinkWriter: SinkWriter = (serializedRecord, level) => { + if (streamError !== undefined) { + return; + } + + if (!blocked) { + if (!writeChunk(serializedRecord)) { + waitForDrain(); + } + + return; + } + + queue.push({ serializedRecord, level }); + if (queue.length > maxQueueSize) { + dropLowestSeverityRecord(); + } + }; + + sinkWriter.flush = () => { + if (streamError !== undefined) { + return Promise.reject(streamError); + } + + if (!blocked && queue.length === 0 && pendingWrites === 0) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + flushWaiters.push({ resolve, reject }); + }); + }; + + monitorStream(sink.stream, fail); + return sinkWriter; +} + +// we'll keep track of which streams have monitors attached to them in case the +// same stream is used in multiple loggers (happens when children are created) +// +// this way we won't add a ton of listeners to the streams like Winston does +// https://github.com/winstonjs/winston/blob/ff0b79de8562bb322c390fbc82fe71c11f373428/lib/winston/transports/stream.js#L33-L34 +const streamMonitors = new WeakMap(); + +function monitorStream( + stream: Writable, + failureHandler: (error: Error) => void, +) { + let monitor = streamMonitors.get(stream); + if (monitor === undefined) { + const newMonitor: StreamMonitor = { failureHandlers: new Set() }; + monitor = newMonitor; + streamMonitors.set(stream, newMonitor); + + const failStream = (error: Error) => { + if (newMonitor.error !== undefined) { + return; + } + + newMonitor.error = error; + for (const handler of newMonitor.failureHandlers) { + handler(error); + } + }; + + stream.on("error", failStream); + stream.on("close", () => { + failStream( + new Error("Log sink stream closed before all records were written"), + ); + }); + } + + if (monitor.error !== undefined) { + failureHandler(monitor.error); + return; + } + + monitor.failureHandlers.add(failureHandler); +} diff --git a/packages/logger/test/config-format.test.ts b/packages/logger/test/config-format.test.ts index fb42bc3e10..6dcd9f7578 100644 --- a/packages/logger/test/config-format.test.ts +++ b/packages/logger/test/config-format.test.ts @@ -19,7 +19,7 @@ void describe("configuration", () => { delete process.env.SSQT_LOG_LEVEL; assert.deepEqual(defaultConfig(), { logLevel: "info", - sinks: [{ type: "console" }], + sinks: [{ stream: process.stdout }], }); assert.deepEqual(LOG_LEVELS, [ "emerg", diff --git a/packages/logger/test/logger.test.ts b/packages/logger/test/logger.test.ts index 15bd55f4e6..a9ec55c6e3 100644 --- a/packages/logger/test/logger.test.ts +++ b/packages/logger/test/logger.test.ts @@ -61,10 +61,7 @@ void describe("logging", () => { const two = new CaptureStream(); const logger = initializeLogger(unique("output"), { logLevel: "info", - sinks: [ - { type: "stream", stream: one }, - { type: "stream", stream: two }, - ], + sinks: [{ stream: one }, { stream: two }], }); logger.debug("hidden"); @@ -128,7 +125,7 @@ void describe("logging", () => { try { initializeLogger(unique("console"), { logLevel: "info", - sinks: [{ type: "console" }], + sinks: [{ stream: process.stdout }], }).info("console message"); } finally { process.stdout.write = originalWrite; diff --git a/packages/logger/test/stream-sink.test.ts b/packages/logger/test/stream-sink.test.ts new file mode 100644 index 0000000000..73c534b2b8 --- /dev/null +++ b/packages/logger/test/stream-sink.test.ts @@ -0,0 +1,198 @@ +import assert from "node:assert/strict"; +import { Writable } from "node:stream"; +import { describe, test } from "node:test"; +import type { LogLevel } from "../src/config.ts"; +import { createSinkWriter, type SinkWriter } from "../src/stream-sink.ts"; + +class CaptureStream extends Writable { + chunks: string[] = []; + + override _write( + chunk: Buffer | string, + _encoding: BufferEncoding, + done: (error?: Error | null) => void, + ) { + this.chunks.push(chunk.toString()); + done(); + } +} + +class FailedWriteStream extends Writable { + override _write( + _chunk: Buffer | string, + _encoding: BufferEncoding, + done: (error?: Error | null) => void, + ) { + done(new Error("write callback failed")); + } +} + +class BackpressureStream extends Writable { + chunks: string[] = []; + private callbacks: Array<(error?: Error | null) => void> = []; + + constructor() { + super({ highWaterMark: 1 }); + } + + override _write( + chunk: Buffer | string, + _encoding: BufferEncoding, + done: (error?: Error | null) => void, + ) { + this.chunks.push(chunk.toString()); + this.callbacks.push(done); + } + + releaseNext() { + const callback = this.callbacks.shift(); + assert.ok(callback, "Expected a pending stream write"); + callback(); + } +} + +const waitForIO = () => new Promise((resolve) => setImmediate(resolve)); + +function flush(writer: SinkWriter): Promise { + const flushWriter = writer.flush; + assert.ok(flushWriter); + return flushWriter(); +} + +function writeRecord(writer: SinkWriter, message: string, level: LogLevel) { + writer(`${JSON.stringify({ message })}\n`, level); +} + +void describe("stream sinks", () => { + void test("bounds a blocked queue and drops the oldest record at the lowest severity", async () => { + const stream = new BackpressureStream(); + const writer = createSinkWriter("backpressure", { + stream, + maxQueueSize: 3, + }); + + writeRecord(writer, "blocking", "info"); + writeRecord(writer, "old info", "info"); + writeRecord(writer, "error", "error"); + writeRecord(writer, "new info", "info"); + writeRecord(writer, "debug", "debug"); + writeRecord(writer, "emergency", "emerg"); + + let didFlush = false; + const flushing = flush(writer).then(() => { + didFlush = true; + }); + await waitForIO(); + assert.equal(didFlush, false); + + stream.releaseNext(); + await waitForIO(); + + const warning = JSON.parse(stream.chunks[1]); + assert.equal(warning.level, "warn"); + assert.equal(warning.droppedLogs, 2); + assert.match(warning.message, /Dropped 2 log records/); + + for (let writes = 0; writes < 4; writes += 1) { + stream.releaseNext(); + await waitForIO(); + } + + await flushing; + assert.equal(didFlush, true); + assert.deepEqual( + stream.chunks.map((chunk) => JSON.parse(chunk).message), + [ + "blocking", + "Dropped 2 log records due to sink backpressure", + "error", + "new info", + "emergency", + ], + ); + }); + + void test("flushes accepted writes and resolves immediately when idle", async () => { + const stream = new CaptureStream(); + const writer = createSinkWriter("flush", { stream }); + + writeRecord(writer, "one", "info"); + await flush(writer); + await flush(writer); + + assert.deepEqual( + stream.chunks.map((chunk) => JSON.parse(chunk).message), + ["one"], + ); + }); + + void test("rejects pending and future flushes after a shared stream error", async () => { + const stream = new BackpressureStream(); + const one = createSinkWriter("one", { stream }); + const two = createSinkWriter("two", { stream }); + + writeRecord(one, "one", "info"); + writeRecord(two, "two", "info"); + const flushOne = flush(one); + const flushTwo = flush(two); + stream.destroy(new Error("stream failed")); + + await assert.rejects(flushOne, /stream failed/); + await assert.rejects(flushTwo, /stream failed/); + + writeRecord(one, "ignored", "emerg"); + await assert.rejects(flush(one), /stream failed/); + + const three = createSinkWriter("three", { stream }); + await assert.rejects(flush(three), /stream failed/); + }); + + void test("rejects flushes after the stream closes", async () => { + const stream = new CaptureStream(); + const writer = createSinkWriter("closed", { stream }); + const closed = new Promise((resolve) => { + stream.once("close", resolve); + }); + + stream.destroy(); + await closed; + + await assert.rejects(flush(writer), /stream closed/); + }); + + void test("captures stream write callback failures", async () => { + const stream = new FailedWriteStream(); + const writer = createSinkWriter("failed-callback", { + stream, + }); + + writeRecord(writer, "failure", "error"); + + await assert.rejects(flush(writer), /write callback failed/); + }); + + void test("captures synchronous stream write failures", async () => { + const stream = new CaptureStream(); + stream.write = (() => { + throw new Error("write failed"); + }) as typeof stream.write; + const writer = createSinkWriter("throwing", { stream }); + + writeRecord(writer, "failure", "error"); + + await assert.rejects(flush(writer), /write failed/); + }); + + void test("rejects invalid queue sizes", () => { + for (const maxQueueSize of [0, -1, 1.5, Number.NaN]) { + assert.throws( + () => + createSinkWriter("invalid", { + stream: new CaptureStream(), + maxQueueSize, + }), + /positive integer/, + ); + } + }); +}); From a12856b505bba264991a503b4a81bc9157bcd1f4 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 15:03:34 -0600 Subject: [PATCH 45/51] logger: rename logLevel -> level --- packages/logger/README.md | 4 ++-- packages/logger/src/config.ts | 8 ++++---- packages/logger/src/logger.ts | 2 +- packages/logger/test/config-format.test.ts | 4 ++-- packages/logger/test/logger.test.ts | 10 +++++----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/logger/README.md b/packages/logger/README.md index 37ec1c8e88..d6a7300189 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -34,7 +34,7 @@ import { createWriteStream } from "node:fs"; import { initializeLogger } from "@saasquatch/logger"; const logger = initializeLogger("my-logger", { - logLevel: "info", + level: "info", sinks: [ // write to stdout (default) { stream: process.stdout }, @@ -116,7 +116,7 @@ import { createWriteStream } from "node:fs"; import { initializeLogger } from "@saasquatch/logger"; const logger = initializeLogger({ - logLevel: "info", + level: "info", sinks: [{ stream: createWriteStream("./service.log"), maxQueueSize: 500 }], }); ``` diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 2d63001041..5c5ce0ab3a 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -61,7 +61,7 @@ export type LoggerConfig = { /** * Minimum severity to emit. Defaults to `info` */ - logLevel: LogLevel; + level: LogLevel; /** * Outputs for log records. Defaults to stdout @@ -73,7 +73,7 @@ export function defaultConfig(): LoggerConfig { const sinks: Sink[] = [{ stream: process.stdout }]; const configuredLevel = process.env["SSQT_LOG_LEVEL"]; - let logLevel: LogLevel = "info"; + let level: LogLevel = "info"; if (configuredLevel !== undefined) { const levelValid = ((l: string): l is LogLevel => { @@ -84,8 +84,8 @@ export function defaultConfig(): LoggerConfig { throw new Error(`Invalid log level "${configuredLevel}"`); } - logLevel = configuredLevel; + level = configuredLevel; } - return { logLevel, sinks: sinks }; + return { level, sinks }; } diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 4bc66b4a42..b5d2214afc 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -54,7 +54,7 @@ export function initializeLogger( const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; const sinks = conf.sinks.map((sink) => createSinkWriter(name, sink)); - const logger = new Logger(name, sinks, conf.logLevel, {}); + const logger = new Logger(name, sinks, conf.level, {}); loggers.set(name, logger); return logger; diff --git a/packages/logger/test/config-format.test.ts b/packages/logger/test/config-format.test.ts index 6dcd9f7578..8c2fe349a7 100644 --- a/packages/logger/test/config-format.test.ts +++ b/packages/logger/test/config-format.test.ts @@ -18,7 +18,7 @@ void describe("configuration", () => { void test("uses stdout and info by default", () => { delete process.env.SSQT_LOG_LEVEL; assert.deepEqual(defaultConfig(), { - logLevel: "info", + level: "info", sinks: [{ stream: process.stdout }], }); assert.deepEqual(LOG_LEVELS, [ @@ -37,7 +37,7 @@ void describe("configuration", () => { void test("accepts supported environment levels and rejects invalid ones", () => { process.env.SSQT_LOG_LEVEL = "debug"; - assert.equal(defaultConfig().logLevel, "debug"); + assert.equal(defaultConfig().level, "debug"); process.env.SSQT_LOG_LEVEL = "verbose"; assert.throws(() => defaultConfig(), /Invalid log level "verbose"/); }); diff --git a/packages/logger/test/logger.test.ts b/packages/logger/test/logger.test.ts index a9ec55c6e3..ac0491f626 100644 --- a/packages/logger/test/logger.test.ts +++ b/packages/logger/test/logger.test.ts @@ -28,7 +28,7 @@ const unique = (prefix: string) => `${prefix}-${++sequence}`; void describe("logger initialization", () => { void test("accepts object-form configuration and reuses the default logger", () => { assert.equal(isLoggerInitialized(), false); - const logger = initializeLogger({ sinks: [], logLevel: "debug" }); + const logger = initializeLogger({ sinks: [], level: "debug" }); assert.equal(logger.name, DEFAULT_LOGGER_NAME); assert.equal(logger.level, "debug"); assert.equal(isLoggerInitialized(), true); @@ -45,11 +45,11 @@ void describe("logger initialization", () => { const configured = initializeLogger(unique("configured"), { sinks: [], - logLevel: "debug", + level: "debug", }); assert.equal(configured.level, "debug"); assert.throws( - () => initializeLogger({ sinks: [], logLevel: "warning" }), + () => initializeLogger({ sinks: [], level: "warning" }), /already been initialized/, ); }); @@ -60,7 +60,7 @@ void describe("logging", () => { const one = new CaptureStream(); const two = new CaptureStream(); const logger = initializeLogger(unique("output"), { - logLevel: "info", + level: "info", sinks: [{ stream: one }, { stream: two }], }); @@ -124,7 +124,7 @@ void describe("logging", () => { }; try { initializeLogger(unique("console"), { - logLevel: "info", + level: "info", sinks: [{ stream: process.stdout }], }).info("console message"); } finally { From 5e91b54b26cf958ad5299337523fcbf7656e45d2 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 15:53:13 -0600 Subject: [PATCH 46/51] logger: remove heavy machinery for backpressure Not worth implementing considering Winston doesn't handle it correctly and we haven't experienced any issues in 5+ years of using it in production. --- packages/logger/README.md | 43 +--- packages/logger/src/config.ts | 9 - packages/logger/src/index.ts | 2 - packages/logger/src/logger.ts | 28 ++- packages/logger/src/stream-sink.ts | 278 ----------------------- packages/logger/test/stream-sink.test.ts | 198 ---------------- 6 files changed, 20 insertions(+), 538 deletions(-) delete mode 100644 packages/logger/src/stream-sink.ts delete mode 100644 packages/logger/test/stream-sink.test.ts diff --git a/packages/logger/README.md b/packages/logger/README.md index d6a7300189..842089173f 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -3,8 +3,8 @@ This package provides a lightweight structured logger that writes newline-delimited JSON to stdout by default. It supports syslog-style levels, named loggers, custom writable -stream sinks, temporary log collection to an internal buffer, Express HTTP logging -middleware, and correct backpressure handling. +stream sinks, temporary log collection to an internal buffer, and Express HTTP logging +middleware. ## Getting started @@ -90,42 +90,3 @@ app.use(httpLogMiddleware(logger)); HTTP records include the method, URL, status, response time, and request ID (if present). Common sensitive URL query parameters are removed by the middleware. - -## Sink backpressure handling - -All log sinks are Node.js `Writable` streams, meaning they can exhibit backpressure -behavior as described in the [`Writable.write`][1] documentation. Backpressure is handled -by the logger to keep the log methods synchronous and prevent callers from having to -implement their own backpressure handling strategies. - -Backpressure is handled per-sink. When a sink's stream becomes blocked (`write()` returns -false), logs begin to collect in an internal FIFO queue. The size of the queue is bounded -to prevent the memory exhaustion scenario described in the [`Writable.write`][1] docs. -The logs in the queue will be written when the stream becomes available for writing -again. - -If the queue becomes full before the stream is available for writing, logs will be -dropped in order of ascending severity and receipt time (lowest severity logs are dropped -first, and if all logs in the queue have the same severity, the oldest will be dropped -first). - -The size of the queue defaults to 1,000 and is configurable: - -```typescript -import { createWriteStream } from "node:fs"; -import { initializeLogger } from "@saasquatch/logger"; - -const logger = initializeLogger({ - level: "info", - sinks: [{ stream: createWriteStream("./service.log"), maxQueueSize: 500 }], -}); -``` - -Each sink maintains its own queue, so the sum of all `maxQueueSize`s will be the maximum -possible number of logs stored by the logger in the event that all streams are -simultaneously blocked. This number does not include the internal buffers of the streams -themselves, nor does it include any buffered writes that the Node.js runtime is holding -onto from other writers who may have failed to stop writing when the stream became -blocked. - -[1]: https://nodejs.org/api/stream.html#writablewritechunk-encoding-callback diff --git a/packages/logger/src/config.ts b/packages/logger/src/config.ts index 5c5ce0ab3a..66b7e9bbfa 100644 --- a/packages/logger/src/config.ts +++ b/packages/logger/src/config.ts @@ -41,20 +41,11 @@ export const LOG_LEVEL_VALUES: Record = { debug: 7, }; -export const LOWEST_PRIO_LEVEL = Math.max(...Object.values(LOG_LEVEL_VALUES)); - -export const DEFAULT_SINK_QUEUE_SIZE = 1000; - export type Sink = { /** * The stream to write logs to */ stream: Writable; - - /** - * Maximum number of records retained while the stream applies backpressure - */ - maxQueueSize?: number; }; export type LoggerConfig = { diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 671285ef7a..412c64e54a 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,5 +1,4 @@ import { - DEFAULT_SINK_QUEUE_SIZE, LOG_LEVELS, defaultConfig, type LogLevel, @@ -20,7 +19,6 @@ import { httpLogMiddleware } from "./plugins.ts"; export { DEFAULT_LOG_COLLECTION_LIMIT, - DEFAULT_SINK_QUEUE_SIZE, LOG_LEVELS, Logger, defaultConfig, diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index b5d2214afc..f3bd91bf80 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -3,9 +3,9 @@ import { LOG_LEVEL_VALUES, type LoggerConfig, type LogLevel, + type Sink, } from "./config.ts"; import { formatRecord, serializeRecord, type LogRecord } from "./format.ts"; -import { createSinkWriter, type SinkWriter } from "./stream-sink.ts"; export type LogCollectionOptions = { /** @@ -19,9 +19,9 @@ export type GetCollectedLogsOptions = { }; export const DEFAULT_LOG_COLLECTION_LIMIT = 500; - export const DEFAULT_LOGGER_NAME = "_ssqt_default_logger"; +type SinkWriter = (serializedRecord: string) => void; const loggers = new Map(); export function getLogger(logger?: string): Logger { @@ -53,7 +53,7 @@ export function initializeLogger( config ?? (typeof nameOrConfig === "string" ? {} : (nameOrConfig ?? {})); const conf: LoggerConfig = { ...defaultConfig(), ...supplied }; - const sinks = conf.sinks.map((sink) => createSinkWriter(name, sink)); + const sinks = conf.sinks.map(getSinkFn); const logger = new Logger(name, sinks, conf.level, {}); loggers.set(name, logger); @@ -132,17 +132,11 @@ export class Logger { if (this.sinks.length > 0) { const serializedRecord = `${serializeRecord(record)}\n`; for (const sink of this.sinks) { - sink(serializedRecord, messageLevel); + sink(serializedRecord); } } } - public async flush(): Promise { - await Promise.all( - this.sinks.map((sink) => sink.flush?.() ?? Promise.resolve()), - ); - } - public child(record: Record): Logger { return new Logger(this.name, this.sinks, this.level, { ...this.baseFields, @@ -251,3 +245,17 @@ function isRecord(value: unknown): value is Record { const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } + +function getSinkFn(sink: Sink): SinkWriter { + return (serializedRecord) => { + // FIXME: writing without checking whether the stream is applying + // backpressure is problematic, but the machinery needed to handle this + // case correctly is quite large and not really worth implementing. + // previous versions of the logger wrapped Winston, which does not handle + // backpressure correctly for any transport except the File transport. this + // means we aren't introducing any regression here and it's unlikely we'll + // encounter any issues in production considering we haven't seen any in + // the past 5+ years we've been using Winston + sink.stream.write(serializedRecord); + }; +} diff --git a/packages/logger/src/stream-sink.ts b/packages/logger/src/stream-sink.ts deleted file mode 100644 index 83d6c99abf..0000000000 --- a/packages/logger/src/stream-sink.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { Writable } from "node:stream"; -import { - DEFAULT_SINK_QUEUE_SIZE, - LOG_LEVEL_VALUES, - LOWEST_PRIO_LEVEL, - type LogLevel, - type Sink, -} from "./config.ts"; -import { formatRecord, serializeRecord } from "./format.ts"; - -/* - * JC: all of the code in this file is necessary to ensure that the - * requirements of the `Writable.write` function are upheld: - * - * > If false is returned, further attempts to write data to the stream should - * > stop until the 'drain' event is emitted. - * - * > While calling write() on a stream that is not draining is allowed, Node.js - * > will buffer all written chunks until maximum memory usage occurs, at which - * > point it will abort unconditionally. - * - * https://nodejs.org/api/stream.html#writablewritechunk-encoding-callback - * - * we don't want to pass this backpressure up to the callers since that would - * require users of the library to implement their own backpressure handling at - * every log callsite, which is ridiculous. we will implement our own backpressure - * handling that uses a bounded queue to hold messages while the stream is blocked - * and drops "least unimportant" messages if the queue becomes full - * - * in Winston, which this library used to wrap, the `write` method is called - * without checking the return value: - * - * https://github.com/winstonjs/winston/blob/ff0b79de8562bb322c390fbc82fe71c11f373428/lib/winston/transports/stream.js#L47-L61 - * - * this means that consumers of the @saasquatch/logger package prior to the - * 4.0 update were vulnerable to this possible memory exhaustion issue - */ - -export type SinkWriter = (( - serializedRecord: string, - level: LogLevel, -) => void) & { - flush?: () => Promise; -}; - -type QueuedRecord = { - serializedRecord: string; - level: LogLevel; -}; - -type StreamMonitor = { - error?: Error; - failureHandlers: Set<(error: Error) => void>; -}; - -export function createSinkWriter(loggerName: string, sink: Sink): SinkWriter { - const maxQueueSize = sink.maxQueueSize ?? DEFAULT_SINK_QUEUE_SIZE; - - if (!Number.isInteger(maxQueueSize) || maxQueueSize < 1) { - throw new Error("Sink maxQueueSize must be a positive integer"); - } - - const queue: QueuedRecord[] = []; - - // keep track of callers who are waiting for the SinkWriter to flush - const flushWaiters: Array<{ - resolve: () => void; - reject: (error: Error) => void; - }> = []; - - let blocked = false; - let droppedRecords = 0; - let pendingWrites = 0; - let streamError: Error | undefined; - - const settleFlushWaiters = () => { - // if the stream is errored, all the flush waiters will be rejected - // instantly - if (streamError !== undefined) { - for (const waiter of flushWaiters.splice(0)) { - waiter.reject(streamError); - } - - return; - } - - if (!blocked && queue.length === 0 && pendingWrites === 0) { - for (const waiter of flushWaiters.splice(0)) { - waiter.resolve(); - } - } - }; - - const fail = (error: Error) => { - if (streamError !== undefined) { - return; - } - - streamError = error; - blocked = false; - queue.length = 0; - sink.stream.removeListener("drain", drainQueue); - settleFlushWaiters(); - }; - - const writeChunk = (serializedRecord: string): boolean => { - pendingWrites += 1; - - try { - return sink.stream.write(serializedRecord, (error) => { - pendingWrites -= 1; - if (error) { - fail(error); - return; - } - - settleFlushWaiters(); - }); - } catch (error) { - pendingWrites -= 1; - fail(error instanceof Error ? error : new Error(String(error))); - return false; - } - }; - - const waitForDrain = () => { - if (streamError !== undefined) { - return; - } - - blocked = true; - sink.stream.once("drain", drainQueue); - }; - - // called when the stream is drained and becomes available for writing again - function drainQueue() { - if (streamError !== undefined) { - return; - } - - blocked = false; - - // emit a warning if some logs were dropped while we were blocked by - // backpressure - if (droppedRecords > 0) { - const count = droppedRecords; - droppedRecords = 0; - const noun = count === 1 ? "record" : "records"; - const warning = formatRecord( - loggerName, - "warn", - `Dropped ${count} log ${noun} due to sink backpressure`, - { droppedLogs: count }, - ); - - if (!writeChunk(`${serializeRecord(warning)}\n`)) { - waitForDrain(); - return; - } - } - - // write the queued records until the queue is empty or we become blocked - // again - while (queue.length > 0) { - const record = queue.shift()!; - if (!writeChunk(record.serializedRecord)) { - waitForDrain(); - return; - } - } - - // if we managed to empty the queue then we can proceed to settle the flush - // waiters - settleFlushWaiters(); - } - - const dropLowestSeverityRecord = () => { - let dropIndex = 0; - let dropSeverity = 0; - - for ( - let i = 1; - // if we encounter a log with the lowest possible severity we can short - // circuit this loop - i < queue.length && dropSeverity !== LOWEST_PRIO_LEVEL; - i += 1 - ) { - const thisSeverity = LOG_LEVEL_VALUES[queue[i].level]; - if (thisSeverity > LOG_LEVEL_VALUES[queue[dropIndex].level]) { - dropIndex = i; - dropSeverity = thisSeverity; - } - } - - queue.splice(dropIndex, 1); - droppedRecords += 1; - }; - - const sinkWriter: SinkWriter = (serializedRecord, level) => { - if (streamError !== undefined) { - return; - } - - if (!blocked) { - if (!writeChunk(serializedRecord)) { - waitForDrain(); - } - - return; - } - - queue.push({ serializedRecord, level }); - if (queue.length > maxQueueSize) { - dropLowestSeverityRecord(); - } - }; - - sinkWriter.flush = () => { - if (streamError !== undefined) { - return Promise.reject(streamError); - } - - if (!blocked && queue.length === 0 && pendingWrites === 0) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - flushWaiters.push({ resolve, reject }); - }); - }; - - monitorStream(sink.stream, fail); - return sinkWriter; -} - -// we'll keep track of which streams have monitors attached to them in case the -// same stream is used in multiple loggers (happens when children are created) -// -// this way we won't add a ton of listeners to the streams like Winston does -// https://github.com/winstonjs/winston/blob/ff0b79de8562bb322c390fbc82fe71c11f373428/lib/winston/transports/stream.js#L33-L34 -const streamMonitors = new WeakMap(); - -function monitorStream( - stream: Writable, - failureHandler: (error: Error) => void, -) { - let monitor = streamMonitors.get(stream); - if (monitor === undefined) { - const newMonitor: StreamMonitor = { failureHandlers: new Set() }; - monitor = newMonitor; - streamMonitors.set(stream, newMonitor); - - const failStream = (error: Error) => { - if (newMonitor.error !== undefined) { - return; - } - - newMonitor.error = error; - for (const handler of newMonitor.failureHandlers) { - handler(error); - } - }; - - stream.on("error", failStream); - stream.on("close", () => { - failStream( - new Error("Log sink stream closed before all records were written"), - ); - }); - } - - if (monitor.error !== undefined) { - failureHandler(monitor.error); - return; - } - - monitor.failureHandlers.add(failureHandler); -} diff --git a/packages/logger/test/stream-sink.test.ts b/packages/logger/test/stream-sink.test.ts deleted file mode 100644 index 73c534b2b8..0000000000 --- a/packages/logger/test/stream-sink.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import assert from "node:assert/strict"; -import { Writable } from "node:stream"; -import { describe, test } from "node:test"; -import type { LogLevel } from "../src/config.ts"; -import { createSinkWriter, type SinkWriter } from "../src/stream-sink.ts"; - -class CaptureStream extends Writable { - chunks: string[] = []; - - override _write( - chunk: Buffer | string, - _encoding: BufferEncoding, - done: (error?: Error | null) => void, - ) { - this.chunks.push(chunk.toString()); - done(); - } -} - -class FailedWriteStream extends Writable { - override _write( - _chunk: Buffer | string, - _encoding: BufferEncoding, - done: (error?: Error | null) => void, - ) { - done(new Error("write callback failed")); - } -} - -class BackpressureStream extends Writable { - chunks: string[] = []; - private callbacks: Array<(error?: Error | null) => void> = []; - - constructor() { - super({ highWaterMark: 1 }); - } - - override _write( - chunk: Buffer | string, - _encoding: BufferEncoding, - done: (error?: Error | null) => void, - ) { - this.chunks.push(chunk.toString()); - this.callbacks.push(done); - } - - releaseNext() { - const callback = this.callbacks.shift(); - assert.ok(callback, "Expected a pending stream write"); - callback(); - } -} - -const waitForIO = () => new Promise((resolve) => setImmediate(resolve)); - -function flush(writer: SinkWriter): Promise { - const flushWriter = writer.flush; - assert.ok(flushWriter); - return flushWriter(); -} - -function writeRecord(writer: SinkWriter, message: string, level: LogLevel) { - writer(`${JSON.stringify({ message })}\n`, level); -} - -void describe("stream sinks", () => { - void test("bounds a blocked queue and drops the oldest record at the lowest severity", async () => { - const stream = new BackpressureStream(); - const writer = createSinkWriter("backpressure", { - stream, - maxQueueSize: 3, - }); - - writeRecord(writer, "blocking", "info"); - writeRecord(writer, "old info", "info"); - writeRecord(writer, "error", "error"); - writeRecord(writer, "new info", "info"); - writeRecord(writer, "debug", "debug"); - writeRecord(writer, "emergency", "emerg"); - - let didFlush = false; - const flushing = flush(writer).then(() => { - didFlush = true; - }); - await waitForIO(); - assert.equal(didFlush, false); - - stream.releaseNext(); - await waitForIO(); - - const warning = JSON.parse(stream.chunks[1]); - assert.equal(warning.level, "warn"); - assert.equal(warning.droppedLogs, 2); - assert.match(warning.message, /Dropped 2 log records/); - - for (let writes = 0; writes < 4; writes += 1) { - stream.releaseNext(); - await waitForIO(); - } - - await flushing; - assert.equal(didFlush, true); - assert.deepEqual( - stream.chunks.map((chunk) => JSON.parse(chunk).message), - [ - "blocking", - "Dropped 2 log records due to sink backpressure", - "error", - "new info", - "emergency", - ], - ); - }); - - void test("flushes accepted writes and resolves immediately when idle", async () => { - const stream = new CaptureStream(); - const writer = createSinkWriter("flush", { stream }); - - writeRecord(writer, "one", "info"); - await flush(writer); - await flush(writer); - - assert.deepEqual( - stream.chunks.map((chunk) => JSON.parse(chunk).message), - ["one"], - ); - }); - - void test("rejects pending and future flushes after a shared stream error", async () => { - const stream = new BackpressureStream(); - const one = createSinkWriter("one", { stream }); - const two = createSinkWriter("two", { stream }); - - writeRecord(one, "one", "info"); - writeRecord(two, "two", "info"); - const flushOne = flush(one); - const flushTwo = flush(two); - stream.destroy(new Error("stream failed")); - - await assert.rejects(flushOne, /stream failed/); - await assert.rejects(flushTwo, /stream failed/); - - writeRecord(one, "ignored", "emerg"); - await assert.rejects(flush(one), /stream failed/); - - const three = createSinkWriter("three", { stream }); - await assert.rejects(flush(three), /stream failed/); - }); - - void test("rejects flushes after the stream closes", async () => { - const stream = new CaptureStream(); - const writer = createSinkWriter("closed", { stream }); - const closed = new Promise((resolve) => { - stream.once("close", resolve); - }); - - stream.destroy(); - await closed; - - await assert.rejects(flush(writer), /stream closed/); - }); - - void test("captures stream write callback failures", async () => { - const stream = new FailedWriteStream(); - const writer = createSinkWriter("failed-callback", { - stream, - }); - - writeRecord(writer, "failure", "error"); - - await assert.rejects(flush(writer), /write callback failed/); - }); - - void test("captures synchronous stream write failures", async () => { - const stream = new CaptureStream(); - stream.write = (() => { - throw new Error("write failed"); - }) as typeof stream.write; - const writer = createSinkWriter("throwing", { stream }); - - writeRecord(writer, "failure", "error"); - - await assert.rejects(flush(writer), /write failed/); - }); - - void test("rejects invalid queue sizes", () => { - for (const maxQueueSize of [0, -1, 1.5, Number.NaN]) { - assert.throws( - () => - createSinkWriter("invalid", { - stream: new CaptureStream(), - maxQueueSize, - }), - /positive integer/, - ); - } - }); -}); From e0690d757498381e62d41b08c2de0bd433a4b9bf Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 16:20:46 -0600 Subject: [PATCH 47/51] logger: serialize BigInt as number if it fits --- packages/logger/src/format.ts | 4 +++- packages/logger/test/config-format.test.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 3a539c51af..9701cfd08d 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -52,7 +52,9 @@ export function serializeRecord(record: LogRecord): string { { ...record, toJSON: undefined }, function (_key, value: unknown) { if (typeof value === "bigint") { - return value.toString(); + return value > BigInt(Number.MAX_SAFE_INTEGER) + ? value.toString() + : Number(value); } if (value instanceof Error) { diff --git a/packages/logger/test/config-format.test.ts b/packages/logger/test/config-format.test.ts index 8c2fe349a7..4bbee113a2 100644 --- a/packages/logger/test/config-format.test.ts +++ b/packages/logger/test/config-format.test.ts @@ -83,16 +83,19 @@ void describe("safe serialization", () => { const circular: Record = { id: 1 }; circular.self = circular; const shared = { value: 2n }; + const tooBig = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(500); const record = formatRecord(DEFAULT_LOGGER_NAME, "error", "failed", { error, circular, first: shared, second: shared, + tooBig, }); const parsed = JSON.parse(serializeRecord(record)); assert.equal(parsed.circular.self, "[Circular]"); - assert.deepEqual(parsed.first, { value: "2" }); - assert.deepEqual(parsed.second, { value: "2" }); + assert.deepEqual(parsed.first, { value: 2 }); + assert.deepEqual(parsed.second, { value: 2 }); + assert.deepEqual(parsed.tooBig, "9007199254741491"); assert.equal(parsed.error.name, "TypeError"); assert.equal(parsed.error.message, "bad input"); assert.match(parsed.error.stack, /TypeError: bad input/); From d3c0aeab0e10224b335b10924311ec9556e0778a Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 16:54:48 -0600 Subject: [PATCH 48/51] logger: handle negative bigints --- packages/logger/src/format.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/logger/src/format.ts b/packages/logger/src/format.ts index 9701cfd08d..554cea065b 100644 --- a/packages/logger/src/format.ts +++ b/packages/logger/src/format.ts @@ -52,7 +52,8 @@ export function serializeRecord(record: LogRecord): string { { ...record, toJSON: undefined }, function (_key, value: unknown) { if (typeof value === "bigint") { - return value > BigInt(Number.MAX_SAFE_INTEGER) + const maxSafeInteger = BigInt(Number.MAX_SAFE_INTEGER); + return value > maxSafeInteger || value < -maxSafeInteger ? value.toString() : Number(value); } From de41234056daa6a7b8edad4ae7c0e91ed44d9a77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:55:38 +0000 Subject: [PATCH 49/51] logger: publish v3.0.1-8 This commit was generated by GitHub Actions CI --- packages/logger/package-lock.json | 4 ++-- packages/logger/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/logger/package-lock.json b/packages/logger/package-lock.json index dfdd4ed937..5456d52af9 100644 --- a/packages/logger/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-7", + "version": "3.0.1-8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/logger", - "version": "3.0.1-7", + "version": "3.0.1-8", "license": "MIT", "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/logger/package.json b/packages/logger/package.json index a51ddb65de..ab8d47c830 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/logger", - "version": "3.0.1-7", + "version": "3.0.1-8", "private": false, "description": "Small structured JSON logger for SaaSquatch Node.js services", "homepage": "https://github.com/saasquatch/program-tools/packages/logger#readme", From 50aecec60a3a84596e0a3ccd415bfb64326bae71 Mon Sep 17 00:00:00 2001 From: Jayden Chan Date: Fri, 18 Sep 2026 17:00:20 -0600 Subject: [PATCH 50/51] express-boilerplate: bump logger to 3.0.1-8 --- packages/express-boilerplate/package-lock.json | 10 +++++----- packages/express-boilerplate/package.json | 4 ++-- packages/express-boilerplate/src/tests/util.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index 6551f9ebdc..b20955f419 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -12,7 +12,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-7", + "@saasquatch/logger": "^3.0.1-8", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -26,7 +26,7 @@ "node": "^24 || ^26" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.1-7", + "@saasquatch/logger": "^3.0.1-8", "express": "^4.0.0" } }, @@ -832,9 +832,9 @@ } }, "node_modules/@saasquatch/logger": { - "version": "3.0.1-7", - "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-7.tgz", - "integrity": "sha512-DSnq+L7V6qEQiBkWyO6hYTeMaVMAvJztdSP00pBLDlYs/4Wz2RvtafycfeRQgN7dkY7QN/Vc1vy1pHcvYIG04g==", + "version": "3.0.1-8", + "resolved": "https://registry.npmjs.org/@saasquatch/logger/-/logger-3.0.1-8.tgz", + "integrity": "sha512-fGfEzE6lCrC3SCTHPqzQl904xUCL6KMEHzv0DM3HW6HlRJUvjHz6K47CGtHPjHtyCanuQtRogfEZ0jrVd7oM6g==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index a846600133..ad9da4ab08 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -35,7 +35,7 @@ "nanoid": "^6.0.1" }, "devDependencies": { - "@saasquatch/logger": "^3.0.1-7", + "@saasquatch/logger": "^3.0.1-8", "@types/express": "4", "@types/node": "^24.12.4", "@types/supertest": "^7.2.0", @@ -46,7 +46,7 @@ "typescript": "^7.0.2" }, "peerDependencies": { - "@saasquatch/logger": "^3.0.1-7", + "@saasquatch/logger": "^3.0.1-8", "express": "^4.0.0" }, "engines": { diff --git a/packages/express-boilerplate/src/tests/util.ts b/packages/express-boilerplate/src/tests/util.ts index 3fe5f67de2..6030fc003a 100644 --- a/packages/express-boilerplate/src/tests/util.ts +++ b/packages/express-boilerplate/src/tests/util.ts @@ -10,6 +10,6 @@ export const jestLogger = () => { } return initializeLogger("jest-express-boilerplate", { - logLevel: "crit", + level: "crit", }); }; From c998cde6c0fec5ddb2bc20527da4d1c0f89b9f2a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 23:01:14 +0000 Subject: [PATCH 51/51] express-boilerplate: publish v2.0.1-4 This commit was generated by GitHub Actions CI --- packages/express-boilerplate/package-lock.json | 4 ++-- packages/express-boilerplate/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/express-boilerplate/package-lock.json b/packages/express-boilerplate/package-lock.json index b20955f419..abd10238e8 100644 --- a/packages/express-boilerplate/package-lock.json +++ b/packages/express-boilerplate/package-lock.json @@ -1,12 +1,12 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-3", + "version": "2.0.1-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-3", + "version": "2.0.1-4", "license": "MIT", "dependencies": { "nanoid": "^6.0.1" diff --git a/packages/express-boilerplate/package.json b/packages/express-boilerplate/package.json index ad9da4ab08..5ace273097 100644 --- a/packages/express-boilerplate/package.json +++ b/packages/express-boilerplate/package.json @@ -1,6 +1,6 @@ { "name": "@saasquatch/express-boilerplate", - "version": "2.0.1-3", + "version": "2.0.1-4", "private": false, "description": "Boilerplate middleware and functions for building Express apps on Kubernetes", "homepage": "https://github.com/saasquatch/program-tools/packages/express-boilerplate#readme",