From 4e6bdf07eff793ba1753aea28e81c818f6adb362 Mon Sep 17 00:00:00 2001 From: Karl Pietrzak Date: Wed, 22 Apr 2026 16:31:53 -0400 Subject: [PATCH 01/55] Update Medplum logo in README (#3659) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc052cccf..4ca28ce5d 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ If you or your company are benefiting from node-postgres and would like to help Special thanks to [medplum](https://medplum.com) for their generous and thoughtful support of node-postgres! -![medplum](https://raw.githubusercontent.com/medplum/medplum-logo/refs/heads/main/medplum-logo.png) +Medplum logo ## Contributing From 77cb77150d6c0f07b0aa2134c6b3ef0cb80335c1 Mon Sep 17 00:00:00 2001 From: Karl Pietrzak Date: Wed, 22 Apr 2026 17:11:39 -0400 Subject: [PATCH 02/55] remove deprecated 'version' attribute from docker-compose.ymla (#3660) Removes the warning: ``` WARN[0000] .../node-postgres/.devcontainer/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion ``` The `version` attribute has been removed as of v2 of the docker compose plugin (https://github.com/compose-spec/compose-spec/blob/main/spec.md#version-top-level-element-obsolete). --- .devcontainer/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index d0ab0e8dd..83e302207 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -3,7 +3,6 @@ # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- -version: '3.9' services: web: # Uncomment the next line to use a non-root user for all processes. You can also From 341cb60b0f4579382c7f65be97815c3fe4621064 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Thu, 30 Apr 2026 10:31:26 +0000 Subject: [PATCH 03/55] =?UTF-8?q?docs:=20Fix=20changelog=20entry=E2=80=99s?= =?UTF-8?q?=20spelling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a7136c0..8d167ceef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ We do not include break-fix version release in this file. ## pg@8.14.0 -- Add support from SCRAM-SAH-256-PLUS i.e. [channel binding](https://github.com/brianc/node-postgres/pull/3356). +- Add support for SCRAM-SHA-256-PLUS, i.e. [channel binding](https://github.com/brianc/node-postgres/pull/3356). ## pg@8.13.0 From 02367b8325d6f378419242b07ec3b206309e049f Mon Sep 17 00:00:00 2001 From: Chow Loong Jin Date: Sat, 9 May 2026 22:29:50 +0800 Subject: [PATCH 04/55] Upgrade eslint and typescript (#3662) * Upgrade eslint and typescript * eslint: Port config to new flat config format * Fix preserve-caught-error eslint warning * Drop unused eslint-disable-line * pg-cloudflare: Fix typescript errors - rootDir defaults have changed, so we need to specify it manually now - baseUrl is no longer supported - types no longer loads everything in @types by default, so we have to specify that we want node types - Pin @types/node to 16.* because we support node16 and above * pg-cloudflare: Workaround typescript bug regarding Buffer.from Fixes the following error: % yarn build yarn run v1.22.19 $ tsc --build packages/pg-cloudflare/src/index.ts:156:29 - error TS2769: No overload matches this call. The last overload gave the following error. Argument of type 'ArrayBuffer | Uint8Array' is not assignable to parameter of type 'WithImplicitCoercion | { [Symbol.toPrimitive](hint: "string"): string; }'. Type 'ArrayBuffer' is not assignable to type 'WithImplicitCoercion | { [Symbol.toPrimitive](hint: "string"): string; }'. 156 const hex = Buffer.from(data).toString('hex') ~~~~ node_modules/@types/node/buffer.buffer.d.ts:83:13 83 from( ~~~~~ 84 str: ~~~~~~~~~~~~~~~~~~~~ ... 89 encoding?: BufferEncoding, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 90 ): Buffer; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The last overload is declared here. Found 1 error. See https://github.com/microsoft/TypeScript/issues/63447 for more info * Fix tsconfig for pg-protocol and pg-query-stream * Standardize @types/node on ^16 Fixes the following typescript error: node_modules/typescript/lib/lib.esnext.intl.d.ts:26:135 - error TS2552: Cannot find name 'DateTimeRangeFormatPart'. Did you mean 'DateTimeFormatPart'? 26 formatRangeToParts(startDate: FormattableTemporalObject | Date | number, endDate: FormattableTemporalObject | Date | number): DateTimeRangeFormatPart[]; * pg-protocol: Narrow type of BufferReader.encoding `BufferReader.encoding` to `BufferEncoding` from `string` to match the new signature of `Buffer.toString`. * pg-query-stream: Bump eslint-plugin-promise to fix unmet peer dependency * Run eslint on its own config --- .eslintignore | 1 - .eslintrc | 35 - eslint.config.mjs | 76 ++ package.json | 11 +- packages/pg-cloudflare/package.json | 2 +- packages/pg-cloudflare/src/index.ts | 7 +- packages/pg-cloudflare/tsconfig.json | 9 +- packages/pg-connection-string/package.json | 2 +- packages/pg-protocol/package.json | 4 +- packages/pg-protocol/src/buffer-reader.ts | 2 +- packages/pg-protocol/tsconfig.json | 12 +- packages/pg-query-stream/package.json | 6 +- packages/pg-query-stream/tsconfig.json | 2 +- packages/pg/package.json | 2 +- .../client/async-stack-trace-tests.js | 4 +- yarn.lock | 787 ++++++++---------- 16 files changed, 446 insertions(+), 516 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 eslint.config.mjs diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 050c39538..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -/packages/*/dist/ diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index b1999b544..000000000 --- a/.eslintrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "plugins": ["@typescript-eslint", "prettier"], - "parser": "@typescript-eslint/parser", - "extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"], - "ignorePatterns": ["node_modules", "coverage", "packages/pg-protocol/dist/**/*", "packages/pg-query-stream/dist/**/*"], - "parserOptions": { - "ecmaVersion": 2017, - "sourceType": "module" - }, - "env": { - "node": true, - "es6": true, - "mocha": true - }, - "rules": { - "@typescript-eslint/no-unused-vars": ["error", { - "args": "none", - "varsIgnorePattern": "^_$" - }], - "no-unused-vars": ["error", { - "args": "none", - "varsIgnorePattern": "^_$" - }], - "no-var": "error", - "prefer-const": "error" - }, - "overrides": [ - { - "files": ["*.ts", "*.mts", "*.cts", "*.tsx"], - "rules": { - "no-undef": "off" - } - } - ] -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..3f95083a0 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,76 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import prettier from 'eslint-plugin-prettier' +import globals from 'globals' +import tsParser from '@typescript-eslint/parser' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default defineConfig([ + globalIgnores([ + '**/node_modules', + '**/coverage', + 'packages/*/dist', + 'packages/pg-protocol/dist/**/*', + 'packages/pg-query-stream/dist/**/*', + ]), + { + extends: compat.extends('eslint:recommended', 'plugin:prettier/recommended', 'prettier'), + + plugins: { + '@typescript-eslint': typescriptEslint, + prettier, + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.mocha, + }, + + parser: tsParser, + ecmaVersion: 2017, + sourceType: 'module', + }, + + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'none', + caughtErrors: 'none', + varsIgnorePattern: '^_$', + }, + ], + + // handled by @typescript-eslint/no-unused-vars + 'no-unused-vars': 'off', + + 'no-var': 'error', + 'prefer-const': 'error', + 'no-constant-condition': [ + 'error', + { + checkLoops: 'all', + }, + ], + }, + }, + { + files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.tsx'], + + rules: { + 'no-undef': 'off', + }, + }, +]) diff --git a/package.json b/package.json index 1f40662b9..9285ad142 100644 --- a/package.json +++ b/package.json @@ -20,15 +20,18 @@ "lint": "eslint --cache 'packages/**/*.{js,ts,tsx}'" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^6.17.0", - "eslint": "^8.56.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^10.2.1", "eslint-config-prettier": "^10.1.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.2", "lerna": "^3.19.0", "prettier": "3.0.3", - "typescript": "^4.0.3" + "typescript": "^6.0.3", + "@types/node": "^16" }, "prettier": { "semi": false, diff --git a/packages/pg-cloudflare/package.json b/packages/pg-cloudflare/package.json index c1584fc09..ac68bb22e 100644 --- a/packages/pg-cloudflare/package.json +++ b/packages/pg-cloudflare/package.json @@ -7,7 +7,7 @@ "license": "MIT", "devDependencies": { "ts-node": "^8.5.4", - "typescript": "^4.0.3" + "typescript": "^6.0.3" }, "exports": { ".": { diff --git a/packages/pg-cloudflare/src/index.ts b/packages/pg-cloudflare/src/index.ts index 1e55c4165..9b1e517ba 100644 --- a/packages/pg-cloudflare/src/index.ts +++ b/packages/pg-cloudflare/src/index.ts @@ -1,4 +1,4 @@ -import { SocketOptions, Socket, TlsOptions } from 'cloudflare:sockets' // eslint-disable-line +import { SocketOptions, Socket, TlsOptions } from 'cloudflare:sockets' import { EventEmitter } from 'events' /** @@ -153,7 +153,10 @@ const debug = false function dump(data: unknown) { if (data instanceof Uint8Array || data instanceof ArrayBuffer) { - const hex = Buffer.from(data).toString('hex') + // workaround https://github.com/microsoft/TypeScript/issues/63447 + const buf = data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(data) + + const hex = buf.toString('hex') const str = new TextDecoder().decode(data) return `\n>>> STR: "${str.replace(/\n/g, '\\n')}"\n>>> HEX: ${hex}\n` } else { diff --git a/packages/pg-cloudflare/tsconfig.json b/packages/pg-cloudflare/tsconfig.json index 31d494681..840b52aff 100644 --- a/packages/pg-cloudflare/tsconfig.json +++ b/packages/pg-cloudflare/tsconfig.json @@ -9,15 +9,16 @@ "moduleResolution": "node16", "sourceMap": true, "outDir": "dist", + "rootDir": "./src", "incremental": true, - "baseUrl": ".", "declaration": true, "paths": { "*": [ - "node_modules/*", - "src/types/*" + "./node_modules/*", + "./src/types/*" ] - } + }, + "types": ["node"] }, "include": [ "src/**/*" diff --git a/packages/pg-connection-string/package.json b/packages/pg-connection-string/package.json index a60131456..d02588a6c 100644 --- a/packages/pg-connection-string/package.json +++ b/packages/pg-connection-string/package.json @@ -41,7 +41,7 @@ "mocha": "^11.7.5", "nyc": "^15", "tsx": "^4.19.4", - "typescript": "^4.0.3" + "typescript": "^6.0.3" }, "files": [ "index.js", diff --git a/packages/pg-protocol/package.json b/packages/pg-protocol/package.json index 896c21e69..d3326bdbc 100644 --- a/packages/pg-protocol/package.json +++ b/packages/pg-protocol/package.json @@ -17,12 +17,12 @@ "devDependencies": { "@types/chai": "^4.2.7", "@types/mocha": "^10.0.10", - "@types/node": "^12.12.21", + "@types/node": "^16", "chai": "^4.2.0", "chunky": "^0.0.0", "mocha": "^11.7.5", "ts-node": "^8.5.4", - "typescript": "^4.0.3" + "typescript": "^6.0.3" }, "scripts": { "test": "mocha dist/**/*.test.js", diff --git a/packages/pg-protocol/src/buffer-reader.ts b/packages/pg-protocol/src/buffer-reader.ts index b89aceb89..c9d9c2b66 100644 --- a/packages/pg-protocol/src/buffer-reader.ts +++ b/packages/pg-protocol/src/buffer-reader.ts @@ -2,7 +2,7 @@ export class BufferReader { private buffer: Buffer = Buffer.allocUnsafe(0) // TODO(bmc): support non-utf8 encoding? - private encoding: string = 'utf-8' + private encoding: BufferEncoding = 'utf-8' constructor(private offset: number = 0) {} diff --git a/packages/pg-protocol/tsconfig.json b/packages/pg-protocol/tsconfig.json index 0ae32c8dc..e09c03cd7 100644 --- a/packages/pg-protocol/tsconfig.json +++ b/packages/pg-protocol/tsconfig.json @@ -9,15 +9,19 @@ "moduleResolution": "node16", "sourceMap": true, "outDir": "dist", + "rootDir": "./src", "incremental": true, - "baseUrl": ".", "declaration": true, "paths": { "*": [ - "node_modules/*", - "src/types/*" + "./node_modules/*", + "./src/types/*" ] - } + }, + "types": [ + "node", + "mocha" + ] }, "include": [ "src/**/*" diff --git a/packages/pg-query-stream/package.json b/packages/pg-query-stream/package.json index 5369a3c4c..42ef8f268 100644 --- a/packages/pg-query-stream/package.json +++ b/packages/pg-query-stream/package.json @@ -39,16 +39,16 @@ "devDependencies": { "@types/chai": "^4.2.13", "@types/mocha": "^10.0.10", - "@types/node": "^14.0.0", + "@types/node": "^16.0.0", "@types/pg": "^7.14.5", "JSONStream": "~1.3.5", "concat-stream": "~1.0.1", - "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-promise": "^7.3.0", "mocha": "^11.7.5", "pg": "^8.20.0", "stream-spec": "~0.3.5", "ts-node": "^8.5.4", - "typescript": "^4.0.3" + "typescript": "^6.0.3" }, "peerDependencies": { "pg": "^8" diff --git a/packages/pg-query-stream/tsconfig.json b/packages/pg-query-stream/tsconfig.json index 56eec5083..e9c97b335 100644 --- a/packages/pg-query-stream/tsconfig.json +++ b/packages/pg-query-stream/tsconfig.json @@ -10,8 +10,8 @@ "sourceMap": true, "pretty": true, "outDir": "dist", + "rootDir": "./src", "incremental": true, - "baseUrl": ".", "declaration": true, "types": [ "node", diff --git a/packages/pg/package.json b/packages/pg/package.json index 6be526ee8..d14e448d6 100644 --- a/packages/pg/package.json +++ b/packages/pg/package.json @@ -45,7 +45,7 @@ "bluebird": "3.7.2", "co": "4.6.0", "pg-copy-streams": "0.3.0", - "typescript": "^4.0.3", + "typescript": "^6.0.3", "vitest": "~3.0.9", "wrangler": "^3.x" }, diff --git a/packages/pg/test/integration/client/async-stack-trace-tests.js b/packages/pg/test/integration/client/async-stack-trace-tests.js index 92ca3e4d2..8f289f5ad 100644 --- a/packages/pg/test/integration/client/async-stack-trace-tests.js +++ b/packages/pg/test/integration/client/async-stack-trace-tests.js @@ -23,7 +23,7 @@ if (NODE_MAJOR_VERSION >= 16) { } catch (e) { const stack = e.stack if (!e.stack.includes('innerFunction') || !e.stack.includes('outerFunction')) { - throw Error('async stack trace does not contain wanted values: ' + stack) + throw Error('async stack trace does not contain wanted values: ' + stack, { cause: e }) } } }) @@ -44,7 +44,7 @@ if (NODE_MAJOR_VERSION >= 16) { } catch (e) { const stack = e.stack if (!e.stack.includes('innerFunction') || !e.stack.includes('outerFunction')) { - throw Error('async stack trace does not contain wanted values: ' + stack) + throw Error('async stack trace does not contain wanted values: ' + stack, { cause: e }) } } }) diff --git a/yarn.lock b/yarn.lock index dd6662852..b221bd37a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -654,37 +654,80 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== +"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== +"@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/config-array@^0.23.5": + version "0.23.5" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.5.tgz#56e86d243049195d8acc0c06a1b3dfdc3fa3de95" + integrity sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA== + dependencies: + "@eslint/object-schema" "^3.0.5" + debug "^4.3.1" + minimatch "^10.2.4" + +"@eslint/config-helpers@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.5.tgz#ae16134e4792ac5fbdc533548a24ac1ea9f7f3ae" + integrity sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w== + dependencies: + "@eslint/core" "^1.2.1" + +"@eslint/core@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.2.1.tgz#c1da7cd1b82fa8787f98b5629fb811848a1b63ce" + integrity sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ== dependencies: - ajv "^6.12.4" + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60" + integrity sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg== + dependencies: + ajv "^6.14.0" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" + js-yaml "^4.1.1" + minimatch "^3.1.5" strip-json-comments "^3.1.1" -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-10.0.1.tgz#1e8a876f50117af8ab67e47d5ad94d38d6622583" + integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA== + +"@eslint/object-schema@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091" + integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw== + +"@eslint/plugin-kit@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz#c4125fd015eceeb09b793109fdbcd4dd0a02d346" + integrity sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ== + dependencies: + "@eslint/core" "^1.2.1" + levn "^0.4.1" "@evocateur/libnpmaccess@^3.1.2": version "3.1.2" @@ -765,24 +808,36 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanfs/core@^0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.2.tgz#a8272ca03b2acf492670222b2320b6c421bfde60" + integrity sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" + "@humanfs/types" "^0.15.0" + +"@humanfs/node@^0.16.6": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.8.tgz#8f800cccc13f4f8cd3116e2d9c0a94939da3e3ed" + integrity sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ== + dependencies: + "@humanfs/core" "^0.19.2" + "@humanfs/types" "^0.15.0" + "@humanwhocodes/retry" "^0.4.0" + +"@humanfs/types@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@humanfs/types/-/types-0.15.0.tgz#f2a09f62012390b2bff3fc6fb248ddec8c09a090" + integrity sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q== "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== "@img/sharp-darwin-arm64@0.33.5": version "0.33.5" @@ -1678,37 +1733,11 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@npmcli/agent@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-3.0.0.tgz#1685b1fbd4a1b7bb4f930cbb68ce801edfe7aa44" @@ -2010,12 +2039,17 @@ "@types/estree" "*" "@types/json-schema" "*" +"@types/esrecurse@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" + integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== + "@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.7" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== -"@types/estree@1.0.8": +"@types/estree@1.0.8", "@types/estree@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -2028,7 +2062,7 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2053,15 +2087,10 @@ resolved "https://registry.npmjs.org/@types/node/-/node-12.12.21.tgz" integrity sha512-8sRGhbpU+ck1n0PGAUgVrWrWdjSW2aqNeyC15W88GRsMpSwzv6RJGlLhE7s2RhVSOdyDmxbqlWSeThq4/7xqlA== -"@types/node@^12.12.21": - version "12.12.67" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.67.tgz" - integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== - -"@types/node@^14.0.0": - version "14.11.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz" - integrity sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw== +"@types/node@^16", "@types/node@^16.0.0": + version "16.18.126" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.126.tgz#27875faa2926c0f475b39a8bb1e546c0176f8d4b" + integrity sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -2095,136 +2124,101 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== -"@types/semver@^7.5.0": - version "7.5.6" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" - integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== - -"@typescript-eslint/eslint-plugin@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.0.tgz#62cda0d35bbf601683c6e58cf5d04f0275caca4e" - integrity sha512-M72SJ0DkcQVmmsbqlzc6EJgb/3Oz2Wdm6AyESB4YkGgCxP8u5jt5jn4/OBMPK3HLOxcttZq5xbBBU7e2By4SZQ== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.0" - "@typescript-eslint/type-utils" "7.0.0" - "@typescript-eslint/utils" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" - debug "^4.3.4" - graphemer "^1.4.0" - ignore "^5.2.4" +"@typescript-eslint/eslint-plugin@^8.58.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz#fcbe76b693ce2412410cf4d48aefd617d345f2d9" + integrity sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.59.0" + "@typescript-eslint/type-utils" "8.59.0" + "@typescript-eslint/utils" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" + ignore "^7.0.5" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/parser@^6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.17.0.tgz#8cd7a0599888ca6056082225b2fdf9a635bf32a1" - integrity sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A== - dependencies: - "@typescript-eslint/scope-manager" "6.17.0" - "@typescript-eslint/types" "6.17.0" - "@typescript-eslint/typescript-estree" "6.17.0" - "@typescript-eslint/visitor-keys" "6.17.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz#70e6c1334d0d76562dfa61aed9009c140a7601b4" - integrity sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA== - dependencies: - "@typescript-eslint/types" "6.17.0" - "@typescript-eslint/visitor-keys" "6.17.0" - -"@typescript-eslint/scope-manager@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.0.tgz#15ea9abad2b56fc8f5c0b516775f41c86c5c8685" - integrity sha512-IxTStwhNDPO07CCrYuAqjuJ3Xf5MrMaNgbAZPxFXAUpAtwqFxiuItxUaVtP/SJQeCdJjwDGh9/lMOluAndkKeg== - dependencies: - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" - -"@typescript-eslint/type-utils@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.0.tgz#a4c7ae114414e09dbbd3c823b5924793f7483252" - integrity sha512-FIM8HPxj1P2G7qfrpiXvbHeHypgo2mFpFGoh5I73ZlqmJOsloSa1x0ZyXCer43++P1doxCgNqIOLqmZR6SOT8g== - dependencies: - "@typescript-eslint/typescript-estree" "7.0.0" - "@typescript-eslint/utils" "7.0.0" - debug "^4.3.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/types@6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.17.0.tgz#844a92eb7c527110bf9a7d177e3f22bd5a2f40cb" - integrity sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A== - -"@typescript-eslint/types@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.0.tgz#2e5889c7fe3c873fc6dc6420aa77775f17cd5dc6" - integrity sha512-9ZIJDqagK1TTs4W9IyeB2sH/s1fFhN9958ycW8NRTg1vXGzzH5PQNzq6KbsbVGMT+oyyfa17DfchHDidcmf5cg== + ts-api-utils "^2.5.0" + +"@typescript-eslint/parser@^8.58.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.0.tgz#57a138280b3ceaf07904fbd62c433d5cc1ee1573" + integrity sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg== + dependencies: + "@typescript-eslint/scope-manager" "8.59.0" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" + debug "^4.4.3" + +"@typescript-eslint/project-service@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.0.tgz#914bf62069d870faa0389ffd725774a200f511bf" + integrity sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.59.0" + "@typescript-eslint/types" "^8.59.0" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz#f71be268bd31da1c160815c689e4dde7c9bc9e8e" + integrity sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg== + dependencies: + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" + +"@typescript-eslint/tsconfig-utils@8.59.0", "@typescript-eslint/tsconfig-utils@^8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz#1276077f5ad77e384446ea28a2474e8f8be1af41" + integrity sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg== + +"@typescript-eslint/type-utils@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz#2834ea3b179cedfc9244dcd4f74105a27751a439" + integrity sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg== + dependencies: + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" + "@typescript-eslint/utils" "8.59.0" + debug "^4.4.3" + ts-api-utils "^2.5.0" + +"@typescript-eslint/types@8.59.0", "@typescript-eslint/types@^8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.0.tgz#cfcc643c6e879016479775850d86d84c14492738" + integrity sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A== + +"@typescript-eslint/typescript-estree@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz#feba58a70ab6ea7ac53a2f3ae900db28ce3454c2" + integrity sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw== + dependencies: + "@typescript-eslint/project-service" "8.59.0" + "@typescript-eslint/tsconfig-utils" "8.59.0" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" + debug "^4.4.3" + minimatch "^10.2.2" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.5.0" -"@typescript-eslint/typescript-estree@6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz#b913d19886c52d8dc3db856903a36c6c64fd62aa" - integrity sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg== +"@typescript-eslint/utils@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.0.tgz#f50df9bd6967881ef64fba62230111153179ead5" + integrity sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g== dependencies: - "@typescript-eslint/types" "6.17.0" - "@typescript-eslint/visitor-keys" "6.17.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.59.0" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" -"@typescript-eslint/typescript-estree@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.0.tgz#7ce66f2ce068517f034f73fba9029300302fdae9" - integrity sha512-JzsOzhJJm74aQ3c9um/aDryHgSHfaX8SHFIu9x4Gpik/+qxLvxUylhTsO9abcNu39JIdhY2LgYrFxTii3IajLA== +"@typescript-eslint/visitor-keys@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz#2e80de30e7e944ed4bd47d751e37dcb04db03795" + integrity sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q== dependencies: - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/utils@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.0.tgz#e43710af746c6ae08484f7afc68abc0212782c7e" - integrity sha512-kuPZcPAdGcDBAyqDn/JVeJVhySvpkxzfXjJq1X1BFSTYo1TTuo4iyb937u457q4K0In84p6u2VHQGaFnv7VYqg== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.0" - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/typescript-estree" "7.0.0" - semver "^7.5.4" - -"@typescript-eslint/visitor-keys@6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz#3ed043709c39b43ec1e58694f329e0b0430c26b6" - integrity sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg== - dependencies: - "@typescript-eslint/types" "6.17.0" - eslint-visitor-keys "^3.4.1" - -"@typescript-eslint/visitor-keys@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.0.tgz#83cdadd193ee735fe9ea541f6a2b4d76dfe62081" - integrity sha512-JZP0uw59PRHp7sHQl3aF/lFgwOW2rgNVnXUksj1d932PMita9wFBd3621vHQRDvHwPsSY9FMAAHVc8gTvLYY4w== - dependencies: - "@typescript-eslint/types" "7.0.0" - eslint-visitor-keys "^3.4.1" - -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@typescript-eslint/types" "8.59.0" + eslint-visitor-keys "^5.0.0" "@vitest/expect@3.0.9": version "3.0.9" @@ -2490,10 +2484,10 @@ acorn@^8.14.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== -acorn@^8.9.0: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.15.0, acorn@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== agent-base@4, agent-base@^4.3.0: version "4.3.0" @@ -2545,7 +2539,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.12.3: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2555,6 +2549,16 @@ ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.14.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492" + integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw== + 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" + ajv@^8.0.0, ajv@^8.9.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" @@ -2708,11 +2712,6 @@ array-union@^1.0.2: dependencies: array-uniq "^1.0.1" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz" @@ -2901,6 +2900,13 @@ brace-expansion@^5.0.2: dependencies: balanced-match "^4.0.2" +brace-expansion@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" + integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + dependencies: + balanced-match "^4.0.2" + braces@^2.3.1: version "2.3.2" resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz" @@ -2917,13 +2923,6 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" @@ -3059,7 +3058,7 @@ callsites@^2.0.0: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase-keys@^2.0.0: @@ -3151,14 +3150,6 @@ chalk@^2.0.0, chalk@^2.3.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -3590,7 +3581,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.0: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3599,7 +3590,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^7.0.3: +cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -3679,7 +3670,7 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.4.0: dependencies: ms "^2.1.3" -debug@^4.3.5: +debug@^4.3.5, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -3844,20 +3835,6 @@ dir-glob@^2.2.2: dependencies: path-type "^3.0.0" -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dot-prop@^4.2.0: version "4.2.1" resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz" @@ -4200,10 +4177,10 @@ eslint-plugin-prettier@^5.1.2: prettier-linter-helpers "^1.0.0" synckit "^0.11.7" -eslint-plugin-promise@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz#a0652195700aea40b926dc3c74b38e373377bfb0" - integrity sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA== +eslint-plugin-promise@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz#7c61e117f5db8d7a300bd5143c15d1d828e4c124" + integrity sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" @@ -4215,11 +4192,13 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802" + integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ== dependencies: + "@types/esrecurse" "^4.3.1" + "@types/estree" "^1.0.8" esrecurse "^4.3.0" estraverse "^5.2.0" @@ -4235,63 +4214,74 @@ eslint-visitor-keys@^1.1.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.56.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" + integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== + +eslint@^10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.2.1.tgz#224b2a6caeb34473eddcf918762363e2e063222a" + integrity sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.2" + "@eslint/config-array" "^0.23.5" + "@eslint/config-helpers" "^0.5.5" + "@eslint/core" "^1.2.1" + "@eslint/plugin-kit" "^0.7.1" + "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.14.0" + cross-spawn "^7.0.6" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" + 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 "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" + minimatch "^10.2.4" natural-compare "^1.4.0" optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== +espree@^10.0.1: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== dependencies: - acorn "^8.9.0" + acorn "^8.15.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.2.1" + +espree@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5" + integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw== + dependencies: + acorn "^8.16.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^5.0.1" esprima@2.7.x, esprima@^2.7.1: version "2.7.3" @@ -4303,10 +4293,10 @@ esprima@^4.0.0: resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== +esquery@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== dependencies: estraverse "^5.1.0" @@ -4490,17 +4480,6 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" @@ -4521,13 +4500,6 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== -fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== - dependencies: - reusify "^1.0.4" - fdir@^6.2.0, fdir@^6.4.3, fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" @@ -4545,12 +4517,12 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" file-uri-to-path@1.0.0: version "1.0.0" @@ -4567,13 +4539,6 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - find-cache-dir@^3.2.0: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" @@ -4621,14 +4586,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + keyv "^4.5.4" flat@^5.0.2: version "5.0.2" @@ -4917,7 +4881,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.2: +glob-parent@^5.0.0: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -4992,24 +4956,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globby@^9.2.0: version "9.2.0" @@ -5030,11 +4980,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - handlebars@^4.0.1, handlebars@^4.7.6: version "4.7.7" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" @@ -5247,11 +5192,16 @@ ignore@^4.0.3: resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.1.1, ignore@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz" @@ -5261,9 +5211,9 @@ import-fresh@^2.0.0: resolve-from "^3.0.0" import-fresh@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -5540,11 +5490,6 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - is-obj@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" @@ -5797,6 +5742,13 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + jsbn@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -5879,7 +5831,7 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -keyv@^4.5.3: +keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -6060,11 +6012,6 @@ lodash.ismatch@^4.4.0: resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz" @@ -6317,7 +6264,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.2.3: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -6341,14 +6288,6 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz" @@ -6422,19 +6361,26 @@ miniflare@4.20250428.0: youch "3.3.4" zod "3.22.3" -"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^10.2.2, minimatch@^10.2.4: + version "10.2.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== dependencies: - brace-expansion "^2.0.1" + brace-expansion "^5.0.5" + +minimatch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" minimatch@^9.0.4: version "9.0.4" @@ -7220,7 +7166,7 @@ parallel-transform@^1.1.0: parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" @@ -7349,11 +7295,6 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - pathe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" @@ -7425,11 +7366,6 @@ picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" @@ -8005,11 +7941,6 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" @@ -8017,7 +7948,7 @@ rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -8083,11 +8014,6 @@ run-async@^2.2.0: resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz" @@ -8144,7 +8070,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2: +semver@^7.3.5, semver@^7.5.3, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3: version "7.7.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== @@ -8258,11 +8184,6 @@ slash@^2.0.0: resolved "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - sliced@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" @@ -8566,7 +8487,7 @@ stream-spec@~0.3.5: dependencies: macgyver "~1.10" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8601,15 +8522,6 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -8649,7 +8561,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8677,13 +8589,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -8868,11 +8773,6 @@ text-extensions@^1.0.0: resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz" integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" @@ -8963,13 +8863,6 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz" @@ -9025,10 +8918,10 @@ trim-off-newlines@^1.0.0: resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz" integrity sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg== -ts-api-utils@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" - integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== +ts-api-utils@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" + integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== ts-node@^8.5.4: version "8.10.2" @@ -9097,11 +8990,6 @@ type-fest@^0.13.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^0.3.0: version "0.3.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz" @@ -9129,10 +9017,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.0.3: - version "4.8.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +typescript@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" + integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== ufo@^1.5.4: version "1.6.1" @@ -9618,7 +9506,7 @@ wrangler@^3.x: fsevents "~2.3.2" sharp "^0.33.5" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9645,15 +9533,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 1025d12b24f277f9b7cdba2d5488103745939d6b Mon Sep 17 00:00:00 2001 From: francesco Date: Mon, 11 May 2026 18:39:54 +0200 Subject: [PATCH 05/55] Node JS 26 (#3667) * chore: update libpq to 1.11.0 * chore: add node 26 --- .github/workflows/ci.yml | 2 +- packages/pg-native/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d90e474c..1aae36233 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - '20' - '22' - '24' - - '25' + - '26' os: - ubuntu-latest name: Node.js ${{ matrix.node }} diff --git a/packages/pg-native/package.json b/packages/pg-native/package.json index 4c8148ac1..b75b2ffdb 100644 --- a/packages/pg-native/package.json +++ b/packages/pg-native/package.json @@ -34,7 +34,7 @@ }, "homepage": "https://github.com/brianc/node-postgres/tree/master/packages/pg-native", "dependencies": { - "libpq": "^1.8.15", + "libpq": "^1.11.0", "pg-types": "2.2.0" }, "devDependencies": { From 7674d8c27fe8c10f981f4db22c89d1ae566c9380 Mon Sep 17 00:00:00 2001 From: "Herman J. Radtke III" Date: Mon, 11 May 2026 15:20:28 -0400 Subject: [PATCH 06/55] Fix pg prototype pollution via server supplied column names (#3656) * fix(pg-connection-string): prototype pollution via query strings * fix(pg): prototype pollution via server-supplied column names Fixes #3654 --- packages/pg-connection-string/index.js | 6 +- .../pg-connection-string/test/clientConfig.ts | 14 +-- packages/pg-connection-string/test/parse.ts | 34 ++++++ packages/pg/lib/result.js | 2 +- packages/pg/test/unit/result-tests.js | 111 ++++++++++++++++++ 5 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 packages/pg/test/unit/result-tests.js diff --git a/packages/pg-connection-string/index.js b/packages/pg-connection-string/index.js index 29ffeafd7..4b8d7afb9 100644 --- a/packages/pg-connection-string/index.js +++ b/packages/pg-connection-string/index.js @@ -14,7 +14,7 @@ function parse(str, options = {}) { // Check for empty host in URL - const config = {} + const config = Object.create(null) let result let dummyHost = false if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) { @@ -164,7 +164,7 @@ function toConnectionOptions(sslConfig) { } return c - }, {}) + }, Object.create(null)) return connectionOptions } @@ -200,7 +200,7 @@ function toClientConfig(config) { } return c - }, {}) + }, Object.create(null)) return poolConfig } diff --git a/packages/pg-connection-string/test/clientConfig.ts b/packages/pg-connection-string/test/clientConfig.ts index 14759570f..c4aeec6a7 100644 --- a/packages/pg-connection-string/test/clientConfig.ts +++ b/packages/pg-connection-string/test/clientConfig.ts @@ -46,7 +46,7 @@ describe('toClientConfig', function () { const config = parse('pg:///?sslmode=no-verify') const clientConfig = toClientConfig(config) - clientConfig.ssl?.should.deep.equal({ + expect(clientConfig.ssl).to.deep.equal({ rejectUnauthorized: false, }) }) @@ -55,14 +55,14 @@ describe('toClientConfig', function () { const config = parse('pg:///?sslmode=verify-ca') const clientConfig = toClientConfig(config) - clientConfig.ssl?.should.deep.equal({}) + expect(clientConfig.ssl).to.deep.equal({}) }) it('converts other sslmode options', function () { const config = parse('pg:///?sslmode=verify-ca') const clientConfig = toClientConfig(config) - clientConfig.ssl?.should.deep.equal({}) + expect(clientConfig.ssl).to.deep.equal({}) }) it('converts ssl cert options', function () { @@ -77,7 +77,7 @@ describe('toClientConfig', function () { const config = parse(connectionString) const clientConfig = toClientConfig(config) - clientConfig.ssl?.should.deep.equal({ + expect(clientConfig.ssl).to.deep.equal({ ca: 'example ca\n', cert: 'example cert\n', key: 'example key\n', @@ -106,9 +106,9 @@ describe('toClientConfig', function () { const clientConfig = toClientConfig(config) - clientConfig.host?.should.equal('boom') - clientConfig.database?.should.equal('lala') - clientConfig.ssl?.should.deep.equal({}) + expect(clientConfig.host).to.equal('boom') + expect(clientConfig.database).to.equal('lala') + expect(clientConfig.ssl).to.deep.equal({}) }) }) diff --git a/packages/pg-connection-string/test/parse.ts b/packages/pg-connection-string/test/parse.ts index a58edbe9c..814e49c58 100644 --- a/packages/pg-connection-string/test/parse.ts +++ b/packages/pg-connection-string/test/parse.ts @@ -467,4 +467,38 @@ describe('parse', function () { const subject = parse(connectionString) subject.port?.should.equal('1234') }) + + describe('prototype pollution protection', function () { + it('returns object with null prototype', function () { + const subject = parse('postgres://localhost/db') + expect(Object.getPrototypeOf(subject)).to.equal(null) + }) + + it('__proto__ query parameter is stored as regular property', function () { + const subject = parse('postgres://localhost/db?__proto__=malicious') + expect(Object.getPrototypeOf(subject)).to.equal(null) + expect(subject['__proto__']).to.equal('malicious') + // global Object.prototype should not be affected + expect(({} as any).malicious).to.equal(undefined) + }) + + it('constructor query parameter is stored as regular property', function () { + const subject = parse('postgres://localhost/db?constructor=evil') + expect(subject.constructor).to.equal('evil') + }) + + it('prototype query parameter is stored as regular property', function () { + const subject = parse('postgres://localhost/db?prototype=evil') + expect(subject['prototype']).to.equal('evil') + }) + + it('multiple dangerous query parameters are handled safely', function () { + const subject = parse('postgres://localhost/db?__proto__=a&constructor=b&prototype=c&toString=d') + expect(Object.getPrototypeOf(subject)).to.equal(null) + expect(subject['__proto__']).to.equal('a') + expect(subject.constructor).to.equal('b') + expect(subject['prototype']).to.equal('c') + expect(subject['toString']).to.equal('d') + }) + }) }) diff --git a/packages/pg/lib/result.js b/packages/pg/lib/result.js index 0ab7bb80c..329fbf9fc 100644 --- a/packages/pg/lib/result.js +++ b/packages/pg/lib/result.js @@ -89,7 +89,7 @@ class Result { this._parsers = new Array(fieldDescriptions.length) } - const row = {} + const row = Object.create(null) for (let i = 0; i < fieldDescriptions.length; i++) { const desc = fieldDescriptions[i] diff --git a/packages/pg/test/unit/result-tests.js b/packages/pg/test/unit/result-tests.js new file mode 100644 index 000000000..5135723ed --- /dev/null +++ b/packages/pg/test/unit/result-tests.js @@ -0,0 +1,111 @@ +'use strict' +const helper = require('./test-helper') +const assert = require('assert') +const suite = new helper.Suite() +const test = suite.test.bind(suite) + +const Result = require('../../lib/result') + +test('__proto__ column name does not pollute prototype', function () { + const result = new Result() + result.addFields([ + { name: '__proto__', dataTypeID: 25, format: 'text' }, + { name: 'id', dataTypeID: 23, format: 'text' }, + ]) + const row = result.parseRow(['malicious', '1']) + + // __proto__ should be a regular property, not affect prototype chain + assert.strictEqual(row['__proto__'], 'malicious') + assert.strictEqual(row.id, 1) + + // global Object.prototype should not be affected + assert.strictEqual({}.malicious, undefined) + assert.strictEqual(Object.prototype.malicious, undefined) +}) + +test('__proto__ column with object value does not inject prototype', function () { + // custom type parser that returns objects (like JSON) + const customTypes = { + getTypeParser: () => (val) => JSON.parse(val), + } + const result = new Result('object', customTypes) + result.addFields([ + { name: '__proto__', dataTypeID: 114, format: 'text' }, + { name: 'id', dataTypeID: 23, format: 'text' }, + ]) + + const maliciousPayload = JSON.stringify({ isAdmin: true, role: 'admin' }) + const row = result.parseRow([maliciousPayload, '1']) + + // __proto__ should be stored as a regular property + assert.deepStrictEqual(row['__proto__'], { isAdmin: true, role: 'admin' }) + + // the row should NOT inherit from the malicious payload + assert.strictEqual('isAdmin' in row, false) + assert.strictEqual('role' in row, false) +}) + +test('constructor column name is safely stored as property', function () { + const result = new Result() + result.addFields([ + { name: 'constructor', dataTypeID: 25, format: 'text' }, + { name: 'id', dataTypeID: 23, format: 'text' }, + ]) + const row = result.parseRow(['malicious', '1']) + + assert.strictEqual(row.constructor, 'malicious') + assert.strictEqual(row.id, 1) +}) + +test('hasOwnProperty column name is safely stored as property', function () { + const result = new Result() + result.addFields([ + { name: 'hasOwnProperty', dataTypeID: 25, format: 'text' }, + { name: 'data', dataTypeID: 25, format: 'text' }, + ]) + const row = result.parseRow(['not_a_function', 'value']) + + assert.strictEqual(row.hasOwnProperty, 'not_a_function') + assert.strictEqual(row.data, 'value') + + // can still check properties using Object.prototype.hasOwnProperty.call + assert.strictEqual(Object.prototype.hasOwnProperty.call(row, 'data'), true) +}) + +test('toString column name is safely stored as property', function () { + const result = new Result() + result.addFields([{ name: 'toString', dataTypeID: 25, format: 'text' }]) + const row = result.parseRow(['not_a_function']) + + assert.strictEqual(row.toString, 'not_a_function') +}) + +test('prototype column name is safely stored as property', function () { + const result = new Result() + result.addFields([ + { name: 'prototype', dataTypeID: 25, format: 'text' }, + { name: 'id', dataTypeID: 23, format: 'text' }, + ]) + const row = result.parseRow(['value', '1']) + + assert.strictEqual(row.prototype, 'value') + assert.strictEqual(row.id, 1) +}) + +test('multiple dangerous column names handled safely', function () { + const result = new Result() + result.addFields([ + { name: '__proto__', dataTypeID: 25, format: 'text' }, + { name: 'constructor', dataTypeID: 25, format: 'text' }, + { name: 'prototype', dataTypeID: 25, format: 'text' }, + { name: '__defineGetter__', dataTypeID: 25, format: 'text' }, + { name: 'id', dataTypeID: 23, format: 'text' }, + ]) + const row = result.parseRow(['a', 'b', 'c', 'd', '1']) + + assert.strictEqual(row['__proto__'], 'a') + assert.strictEqual(row.constructor, 'b') + assert.strictEqual(row.prototype, 'c') + assert.strictEqual(row['__defineGetter__'], 'd') + assert.strictEqual(row.id, 1) +}) From effc3f6c2e785f8295a14c53a0c6848c8a66910c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 14:40:03 -0500 Subject: [PATCH 07/55] build(deps-dev): bump eslint-plugin-prettier from 5.5.1 to 5.5.5 (#3648) Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.5.1 to 5.5.5. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.1...v5.5.5) --- updated-dependencies: - dependency-name: eslint-plugin-prettier dependency-version: 5.5.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index b221bd37a..3379c62b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1870,10 +1870,10 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pkgr/core@^0.2.4": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058" - integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== +"@pkgr/core@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" + integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== "@rollup/plugin-commonjs@^28.0.3": version "28.0.3" @@ -4170,12 +4170,12 @@ eslint-plugin-node@^11.1.0: semver "^6.1.0" eslint-plugin-prettier@^5.1.2: - version "5.5.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15" - integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw== + version "5.5.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz#9eae11593faa108859c26f9a9c367d619a0769c0" + integrity sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw== dependencies: - prettier-linter-helpers "^1.0.0" - synckit "^0.11.7" + prettier-linter-helpers "^1.0.1" + synckit "^0.11.12" eslint-plugin-promise@^7.3.0: version "7.3.0" @@ -7485,10 +7485,10 @@ prelude-ls@~1.1.2: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== +prettier-linter-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz#6a31f88a4bad6c7adda253de12ba4edaea80ebcd" + integrity sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg== dependencies: fast-diff "^1.1.2" @@ -8684,12 +8684,12 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@^0.11.7: - version "0.11.8" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457" - integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== +synckit@^0.11.12: + version "0.11.12" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.12.tgz#abe74124264fbc00a48011b0d98bdc1cffb64a7b" + integrity sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ== dependencies: - "@pkgr/core" "^0.2.4" + "@pkgr/core" "^0.2.9" tapable@^2.1.1, tapable@^2.2.0: version "2.2.2" From 939725e02c392a6f863cd57970aa3202fb500912 Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Mon, 11 May 2026 16:55:10 -0300 Subject: [PATCH 08/55] feat: add new client.getTransactionStatus() method (#3645) * feat: add new client.getTransactionStatus() method Adds a new public method to retrieve the current transaction status of the client connection. Returns 'I' (idle), 'T' (in transaction), 'E' (error/aborted), or null (initial state/native client). The transaction status is tracked from PostgreSQL's ReadyForQuery message after each query completes. Native client returns null as it does not support this feature yet. * feat: add native client support for getTransactionStatus() - Add getTransactionStatus() to pg-native using libpq's PQtransactionStatus() with status mapping (0->I, 2->T, 3->E) - Update pg native client wrapper to delegate to pg-native - Remove native guard from txstatus tests (now runs in both modes) - Bump libpq to ^1.10.0 for transactionStatus() binding support * docs * Tests * fix: docs * clear docs --------- Co-authored-by: Brian C --- docs/pages/apis/client.mdx | 55 +++++++++++++ packages/pg-native/index.js | 8 ++ packages/pg/lib/client.js | 6 ++ packages/pg/lib/native/client.js | 4 + .../test/integration/client/txstatus-tests.js | 82 +++++++++++++++++++ yarn.lock | 15 ++-- 6 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 packages/pg/test/integration/client/txstatus-tests.js diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index 5867ad5a6..ecfd67fca 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -175,6 +175,61 @@ await client.end() console.log('client has disconnected') ``` +## client.getTransactionStatus + +`client.getTransactionStatus() => string | null` + +Returns the current transaction status of the client connection. This can be useful for debugging transaction state issues or implementing custom transaction management logic. + +**Return values:** + +- `'I'` - Idle (not in a transaction) +- `'T'` - Transaction active (BEGIN has been issued) +- `'E'` - Error (transaction aborted, requires ROLLBACK) +- `null` - Initial state (before first query) + +The transaction status is updated after each query completes based on the PostgreSQL backend's `ReadyForQuery` message. + +**Example: Checking transaction state** + +```js +import { Client } from 'pg' +const client = new Client() +await client.connect() + +await client.query('BEGIN') +console.log(client.getTransactionStatus()) // 'T' - in transaction + +await client.query('SELECT * FROM users') +console.log(client.getTransactionStatus()) // 'T' - still in transaction + +await client.query('COMMIT') +console.log(client.getTransactionStatus()) // 'I' - idle + +await client.end() +``` + +**Example: Handling transaction errors** + +```js +import { Client } from 'pg' +const client = new Client() +await client.connect() + +await client.query('BEGIN') +try { + await client.query('INVALID SQL') +} catch (err) { + console.log(client.getTransactionStatus()) // 'E' - error state + + // Must rollback to recover + await client.query('ROLLBACK') + console.log(client.getTransactionStatus()) // 'I' - idle again +} + +await client.end() +``` + ## events ### error diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js index 8c83406bb..1c18241db 100644 --- a/packages/pg-native/index.js +++ b/packages/pg-native/index.js @@ -6,6 +6,10 @@ const types = require('pg-types') const buildResult = require('./lib/build-result') const CopyStream = require('./lib/copy-stream') +// https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQTRANSACTIONSTATUS +// 0=IDLE, 1=ACTIVE, 2=INTRANS, 3=INERROR +const statusMap = { 0: 'I', 2: 'T', 3: 'E' } + const Client = (module.exports = function (config) { if (!(this instanceof Client)) { return new Client(config) @@ -145,6 +149,10 @@ Client.prototype.escapeIdentifier = function (value) { return this.pq.escapeIdentifier(value) } +Client.prototype.getTransactionStatus = function () { + return statusMap[this.pq.transactionStatus()] ?? null +} + // export the version number so we can check it in node-postgres module.exports.version = require('./package.json').version diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 9200dded6..48d3a595b 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -71,6 +71,7 @@ class Client extends EventEmitter { this._connectionError = false this._queryable = true this._activeQuery = null + this._txStatus = null this.enableChannelBinding = Boolean(c.enableChannelBinding) // set true to use SCRAM-SHA-256-PLUS when offered this.connection = @@ -359,6 +360,7 @@ class Client extends EventEmitter { } const activeQuery = this._getActiveQuery() this._activeQuery = null + this._txStatus = msg?.status ?? null this.readyForQuery = true if (activeQuery) { activeQuery.handleReadyForQuery(this.connection) @@ -703,6 +705,10 @@ class Client extends EventEmitter { this.connection.unref() } + getTransactionStatus() { + return this._txStatus + } + end(cb) { this._ending = true diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index d8bb4dce5..6df471b83 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -321,3 +321,7 @@ Client.prototype.getTypeParser = function (oid, format) { Client.prototype.isConnected = function () { return this._connected } + +Client.prototype.getTransactionStatus = function () { + return this.native.getTransactionStatus() +} diff --git a/packages/pg/test/integration/client/txstatus-tests.js b/packages/pg/test/integration/client/txstatus-tests.js new file mode 100644 index 000000000..cb8b740f8 --- /dev/null +++ b/packages/pg/test/integration/client/txstatus-tests.js @@ -0,0 +1,82 @@ +'use strict' +const helper = require('./test-helper') +const suite = new helper.Suite() +const pg = helper.pg +const assert = require('assert') + +suite.test('txStatus tracking', function (done) { + const client = new pg.Client() + client.connect( + assert.success(function () { + // Run a simple query to initialize txStatus + client.query( + 'SELECT 1', + assert.success(function () { + // Test 1: Initial state after query (should be idle) + assert.equal(client.getTransactionStatus(), 'I', 'should start in idle state') + + // Test 2: BEGIN transaction + client.query( + 'BEGIN', + assert.success(function () { + assert.equal(client.getTransactionStatus(), 'T', 'should be in transaction state') + + // Test 3: COMMIT + client.query( + 'COMMIT', + assert.success(function () { + assert.equal(client.getTransactionStatus(), 'I', 'should return to idle after commit') + + client.end(done) + }) + ) + }) + ) + }) + ) + }) + ) +}) + +suite.test('txStatus error state', function (done) { + const client = new pg.Client() + client.connect( + assert.success(function () { + // Run a simple query to initialize txStatus + client.query( + 'SELECT 1', + assert.success(function () { + client.query( + 'BEGIN', + assert.success(function () { + // Execute invalid SQL to trigger error state + client.query('INVALID SQL SYNTAX', function (err) { + assert(err, 'should receive error from invalid query') + + // Issue a sync query to ensure ReadyForQuery has been processed + // This guarantees transaction status has been updated + client.query('SELECT 1', function () { + // This callback fires after ReadyForQuery is processed + assert.equal(client.getTransactionStatus(), 'E', 'should be in error state') + + // Rollback to recover + client.query( + 'ROLLBACK', + assert.success(function () { + assert.equal( + client.getTransactionStatus(), + 'I', + 'should return to idle after rollback from error' + ) + client.end(done) + }) + ) + }) + }) + }) + ) + }) + ) + }) + ) +}) diff --git a/yarn.lock b/yarn.lock index 3379c62b4..6e45ace78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5907,13 +5907,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libpq@^1.8.15: - version "1.8.15" - resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.8.15.tgz#bf9cea8e59e1a4a911d06df01d408213a09925ad" - integrity sha512-4lSWmly2Nsj3LaTxxtFmJWuP3Kx+0hYHEd+aNrcXEWT0nKWaPd9/QZPiMkkC680zeALFGHQdQWjBvnilL+vgWA== +libpq@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.10.0.tgz#238d01d416abca8768aab09bc82d81af9c7ffa23" + integrity sha512-PHY+JGD3+9X5b2emXLh+WJEnz1jhczO1xs25ZH0xbMWvQi+Hd9X/mTZOrGA99Rcw/DvNjsBRlegroqigpNfaJA== dependencies: bindings "1.5.0" - nan "~2.22.2" + nan "~2.23.1" lines-and-columns@^1.1.6: version "1.1.6" @@ -6632,6 +6632,11 @@ nan@~2.22.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== +nan@~2.23.1: + version "2.23.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.23.1.tgz#6f86a31dd87e3d1eb77512bf4b9e14c8aded3975" + integrity sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw== + nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" From 7ba4efe25d40359c0fb2dec0cd674a3abc5d1026 Mon Sep 17 00:00:00 2001 From: felipe stival <14948182+v0idpwn@users.noreply.github.com> Date: Mon, 11 May 2026 17:42:35 -0300 Subject: [PATCH 09/55] Handle SASL SCRAM server error responses (#3521) Add proper error handling for SCRAM-SERVER-FINAL-MESSAGE error attribute. The SCRAM specification allows servers to return error messages via the 'e' attribute in the server final message. Currently, these errors are ignored and authentication fails later during signature verification. Postgres typically doesn't return this error (see [here](https://github.com/postgres/postgres/blob/2047ad068139f0b8c6da73d0b845ca9ba30fb33d/src/backend/libpq/auth-scram.c#L423) on why), but poolers, or other applications using the postgres protocol might, and it's part of the SCRAM spec, so it probably makes sense for node-postgres to handle it. Aligns behaviour with psql, postgrex, and somewhat with pgJDBC (pgJDBC in particular is stricter with scram errors). For reference: - libpq handling it: https://github.com/postgres/postgres/blob/2047ad068139f0b8c6da73d0b845ca9ba30fb33d/src/interfaces/libpq/fe-auth-scram.c#L708 --- packages/pg/lib/crypto/sasl.js | 6 ++++++ .../pg/test/unit/client/sasl-scram-tests.js | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/pg/lib/crypto/sasl.js b/packages/pg/lib/crypto/sasl.js index 47b77610c..a782ae48a 100644 --- a/packages/pg/lib/crypto/sasl.js +++ b/packages/pg/lib/crypto/sasl.js @@ -178,7 +178,13 @@ function parseServerFirstMessage(data) { function parseServerFinalMessage(serverData) { const attrPairs = parseAttributePairs(serverData) + const error = attrPairs.get('e') const serverSignature = attrPairs.get('v') + + if (error) { + throw new Error(`SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "${error}"`) + } + if (!serverSignature) { throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missing') } else if (!isBase64(serverSignature)) { diff --git a/packages/pg/test/unit/client/sasl-scram-tests.js b/packages/pg/test/unit/client/sasl-scram-tests.js index 2df0f1860..7554a9814 100644 --- a/packages/pg/test/unit/client/sasl-scram-tests.js +++ b/packages/pg/test/unit/client/sasl-scram-tests.js @@ -284,6 +284,23 @@ suite.test('sasl/scram', function () { ) }) + suite.test('fails when server returns an error', function () { + assert.throws( + function () { + sasl.finalizeSession( + { + message: 'SASLResponse', + serverSignature: 'abcd', + }, + 'e=no-resources' + ) + }, + { + message: 'SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "no-resources"', + } + ) + }) + suite.test('fails when server signature does not match', function () { assert.throws( function () { From 3bb9fbaa5f1b25078cd4ba12d501d4bb05677d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?sebastian=20kr=C3=A4mer?= Date: Mon, 11 May 2026 22:43:46 +0200 Subject: [PATCH 10/55] Add error handling for non-function callback (#3561) * Add error handling for non-function callback catch callback not a function earlier to get a proper callstack. later when executing the callback the stack may be wrong/insufficient. * fix: lint * fix: lint * fix: test * feat: add test for new error --- packages/pg-pool/index.js | 2 +- packages/pg/lib/client.js | 4 ++++ packages/pg/test/unit/client/simple-query-tests.js | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index 2fbdb78d5..ab514fa88 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -438,7 +438,7 @@ class Pool extends EventEmitter { return response.result } - // allow plain text query without values + // allow plain text query without values, but callback if (typeof values === 'function') { cb = values values = undefined diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 48d3a595b..33a8e24f0 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -633,6 +633,10 @@ class Client extends EventEmitter { Error.captureStackTrace(err) throw err }) + } else { + if (!(typeof query.callback === 'function')) { + throw new Error('callback is not a function') + } } } diff --git a/packages/pg/test/unit/client/simple-query-tests.js b/packages/pg/test/unit/client/simple-query-tests.js index d7d938992..6c20c576b 100644 --- a/packages/pg/test/unit/client/simple-query-tests.js +++ b/packages/pg/test/unit/client/simple-query-tests.js @@ -140,5 +140,17 @@ test('executing query', function () { ) } }) + + test('throws an error when callback is not a function', function () { + try { + client.query('SELECT $1', [1], 'notafunction') + } catch (error) { + assert.equal( + error.message, + 'callback is not a function', + 'Should have thrown an Error for non function callback' + ) + } + }) }) }) From 0f56b76d09c9596940a78bec4a438712d1823fb5 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Mon, 11 May 2026 15:14:33 -0700 Subject: [PATCH 11/55] Throw `TypeError` instead of base `Error` when query callback is not a function & style fix. Follow-up to #3561. --- packages/pg/lib/client.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 33a8e24f0..bb5e6d5f0 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -633,10 +633,8 @@ class Client extends EventEmitter { Error.captureStackTrace(err) throw err }) - } else { - if (!(typeof query.callback === 'function')) { - throw new Error('callback is not a function') - } + } else if (typeof query.callback !== 'function') { + throw new TypeError('callback is not a function') } } From c73a645779838a6c0cf7ae7400e71d94243f8cb2 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Tue, 12 May 2026 14:10:58 -0700 Subject: [PATCH 12/55] =?UTF-8?q?test:=20Ensure=20failure=20to=20throw=20a?= =?UTF-8?q?t=20all=20doesn=E2=80=99t=20pass=20(#3671)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pg/test/unit/client/simple-query-tests.js | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/pg/test/unit/client/simple-query-tests.js b/packages/pg/test/unit/client/simple-query-tests.js index 6c20c576b..8cc550830 100644 --- a/packages/pg/test/unit/client/simple-query-tests.js +++ b/packages/pg/test/unit/client/simple-query-tests.js @@ -118,39 +118,36 @@ test('executing query', function () { const client = helper.client() test('throws an error when config is null', function () { - try { - client.query(null, undefined) - } catch (error) { - assert.equal( - error.message, - 'Client was passed a null or undefined query', - 'Should have thrown an Error for null queries' - ) - } + assert.throws( + () => { + client.query(null, undefined) + }, + { + message: 'Client was passed a null or undefined query', + } + ) }) test('throws an error when config is undefined', function () { - try { - client.query() - } catch (error) { - assert.equal( - error.message, - 'Client was passed a null or undefined query', - 'Should have thrown an Error for null queries' - ) - } + assert.throws( + () => { + client.query() + }, + { + message: 'Client was passed a null or undefined query', + } + ) }) test('throws an error when callback is not a function', function () { - try { - client.query('SELECT $1', [1], 'notafunction') - } catch (error) { - assert.equal( - error.message, - 'callback is not a function', - 'Should have thrown an Error for non function callback' - ) - } + assert.throws( + () => { + client.query('SELECT $1', [1], 'notafunction') + }, + { + message: 'callback is not a function', + } + ) }) }) }) From be880d45552269f0b847a3e568014bde6536eae3 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Tue, 12 May 2026 22:54:13 -0700 Subject: [PATCH 13/55] Assorted test fixes and cleanup (#3672) * cleanup: Remove duplicate test * cleanup: Remove nonsense test * cleanup: Simplify promise rejection test * test: Fix and tighten assertion that would always pass because of the `SELECTR` typo. * cleanup: Add missing `await`s when using `assert.rejects` in tests; remove unneeded function wrappers --- .../integration/client/promise-api-tests.js | 29 +--- .../test/integration/gh-issues/3174-tests.js | 8 +- .../pg/test/unit/client/sasl-scram-tests.js | 139 ++++++++---------- 3 files changed, 69 insertions(+), 107 deletions(-) diff --git a/packages/pg/test/integration/client/promise-api-tests.js b/packages/pg/test/integration/client/promise-api-tests.js index 9e2ffec0c..8c3cd076b 100644 --- a/packages/pg/test/integration/client/promise-api-tests.js +++ b/packages/pg/test/integration/client/promise-api-tests.js @@ -13,13 +13,6 @@ suite.test('valid connection completes promise', () => { }) }) -suite.test('valid connection completes promise', () => { - const client = new pg.Client() - return client.connect().then(() => { - return client.end().then(() => {}) - }) -}) - suite.test('valid connection returns the client in a promise', () => { const client = new pg.Client() return client.connect().then((clientInside) => { @@ -28,25 +21,7 @@ suite.test('valid connection returns the client in a promise', () => { }) }) -suite.test('invalid connection rejects promise', (done) => { +suite.test('invalid connection rejects promise', async () => { const client = new pg.Client({ host: 'alksdjflaskdfj', port: 1234 }) - return client.connect().catch((e) => { - assert(e instanceof Error) - done() - }) -}) - -suite.test('connected client does not reject promise after connection', (done) => { - const client = new pg.Client() - return client.connect().then(() => { - setTimeout(() => { - client.on('error', (e) => { - assert(e instanceof Error) - client.end() - done() - }) - // manually kill the connection - client.emit('error', new Error('something bad happened...but not really')) - }, 50) - }) + await assert.rejects(client.connect(), Error) }) diff --git a/packages/pg/test/integration/gh-issues/3174-tests.js b/packages/pg/test/integration/gh-issues/3174-tests.js index 99044df0e..cd920346a 100644 --- a/packages/pg/test/integration/gh-issues/3174-tests.js +++ b/packages/pg/test/integration/gh-issues/3174-tests.js @@ -104,7 +104,9 @@ const testErrorBuffer = (bufferName, errorBuffer) => { if (!cli.native) { assert(errorHit) // further queries on the client should fail since its in an invalid state - await assert.rejects(() => client.query('SELECTR NOW()'), 'Further queries on the client should reject') + await assert.rejects(client.query('SELECT NOW()'), { + message: 'Client has encountered a connection error and is not queryable', + }) } await closeServer() @@ -129,7 +131,9 @@ const testErrorBuffer = (bufferName, errorBuffer) => { if (!cli.native) { assert(errorHit) // further queries on the client should fail since its in an invalid state - await assert.rejects(() => client.query('SELECTR NOW()'), 'Further queries on the client should reject') + await assert.rejects(client.query('SELECT NOW()'), { + message: 'Client has encountered a connection error and is not queryable', + }) } await client.end() diff --git a/packages/pg/test/unit/client/sasl-scram-tests.js b/packages/pg/test/unit/client/sasl-scram-tests.js index 7554a9814..8b0376d67 100644 --- a/packages/pg/test/unit/client/sasl-scram-tests.js +++ b/packages/pg/test/unit/client/sasl-scram-tests.js @@ -58,64 +58,53 @@ suite.test('sasl/scram', function () { }) suite.test('continueSession', function () { - suite.test('fails when last session message was not SASLInitialResponse', async function () { - assert.rejects( - function () { - return sasl.continueSession({}, '', '') - }, - { - message: 'SASL: Last message was not SASLInitialResponse', - } - ) + suite.test('fails when last session message was not SASLInitialResponse', async () => { + await assert.rejects(sasl.continueSession({}, '', ''), { + message: 'SASL: Last message was not SASLInitialResponse', + }) }) - suite.test('fails when nonce is missing in server message', function () { - assert.rejects( - function () { - return sasl.continueSession( - { - message: 'SASLInitialResponse', - }, - 'bad-password', - 's=1,i=1' - ) - }, + suite.test('fails when nonce is missing in server message', async () => { + await assert.rejects( + sasl.continueSession( + { + message: 'SASLInitialResponse', + }, + 'bad-password', + 's=1,i=1' + ), { message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing', } ) }) - suite.test('fails when salt is missing in server message', function () { - assert.rejects( - function () { - return sasl.continueSession( - { - message: 'SASLInitialResponse', - }, - 'bad-password', - 'r=1,i=1' - ) - }, + suite.test('fails when salt is missing in server message', async () => { + await assert.rejects( + sasl.continueSession( + { + message: 'SASLInitialResponse', + }, + 'bad-password', + 'r=1,i=1' + ), { message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing', } ) }) - suite.test('fails when client password is not a string', function () { + suite.test('fails when client password is not a string', async () => { for (const badPasswordValue of [null, undefined, 123, new Date(), {}]) { - assert.rejects( - function () { - return sasl.continueSession( - { - message: 'SASLInitialResponse', - clientNonce: 'a', - }, - badPasswordValue, - 'r=1,i=1' - ) - }, + await assert.rejects( + sasl.continueSession( + { + message: 'SASLInitialResponse', + clientNonce: 'a', + }, + badPasswordValue, + 'r=1,i=1' + ), { message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string', } @@ -123,53 +112,47 @@ suite.test('sasl/scram', function () { } }) - suite.test('fails when client password is an empty string', function () { - assert.rejects( - function () { - return sasl.continueSession( - { - message: 'SASLInitialResponse', - clientNonce: 'a', - }, - '', - 'r=1,i=1' - ) - }, + suite.test('fails when client password is an empty string', async () => { + await assert.rejects( + sasl.continueSession( + { + message: 'SASLInitialResponse', + clientNonce: 'a', + }, + '', + 'r=1,i=1' + ), { message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a non-empty string', } ) }) - suite.test('fails when iteration is missing in server message', function () { - assert.rejects( - function () { - return sasl.continueSession( - { - message: 'SASLInitialResponse', - }, - 'bad-password', - 'r=1,s=abcd' - ) - }, + suite.test('fails when iteration is missing in server message', async () => { + await assert.rejects( + sasl.continueSession( + { + message: 'SASLInitialResponse', + }, + 'bad-password', + 'r=1,s=abcd' + ), { message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing', } ) }) - suite.test('fails when server nonce does not start with client nonce', function () { - assert.rejects( - function () { - return sasl.continueSession( - { - message: 'SASLInitialResponse', - clientNonce: '2', - }, - 'bad-password', - 'r=1,s=abcd,i=1' - ) - }, + suite.test('fails when server nonce does not start with client nonce', async () => { + await assert.rejects( + sasl.continueSession( + { + message: 'SASLInitialResponse', + clientNonce: '2', + }, + 'bad-password', + 'r=1,s=abcd,i=1' + ), { message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce', } From 63c921bbc7dfba684a75f3da6bc10e4f2cdc27fd Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Tue, 12 May 2026 22:54:41 -0700 Subject: [PATCH 14/55] ci: Node 26 followup (#3670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert unneeded pg-native→libpq dependency range adjustment This reverts part of commit 1025d12b24f277f9b7cdba2d5488103745939d6b. * dev: Upgrade libpq/nan in lockfile for Node 26 compatibility --- packages/pg-native/package.json | 2 +- yarn.lock | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/pg-native/package.json b/packages/pg-native/package.json index b75b2ffdb..4c8148ac1 100644 --- a/packages/pg-native/package.json +++ b/packages/pg-native/package.json @@ -34,7 +34,7 @@ }, "homepage": "https://github.com/brianc/node-postgres/tree/master/packages/pg-native", "dependencies": { - "libpq": "^1.11.0", + "libpq": "^1.8.15", "pg-types": "2.2.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 6e45ace78..92028ecc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5907,13 +5907,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libpq@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.10.0.tgz#238d01d416abca8768aab09bc82d81af9c7ffa23" - integrity sha512-PHY+JGD3+9X5b2emXLh+WJEnz1jhczO1xs25ZH0xbMWvQi+Hd9X/mTZOrGA99Rcw/DvNjsBRlegroqigpNfaJA== +libpq@^1.8.15: + version "1.11.0" + resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.11.0.tgz#1baf0920eb51ebe1399de942414e012142dcead8" + integrity sha512-mHoPlvMwYDMJV36bS2w3eSdFD4eDSm7P9FsvruUldQxzE23/W6qitT9VU/yD1+g2vpgpDktnk2iEYJyhy1RR5g== dependencies: bindings "1.5.0" - nan "~2.23.1" + nan "~2.26.2" lines-and-columns@^1.1.6: version "1.1.6" @@ -6627,15 +6627,10 @@ mz@^2.5.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@~2.22.2: - version "2.22.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" - integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== - -nan@~2.23.1: - version "2.23.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.23.1.tgz#6f86a31dd87e3d1eb77512bf4b9e14c8aded3975" - integrity sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw== +nan@~2.26.2: + version "2.26.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.26.2.tgz#2e5e25764224c737b9897790b57c3294d4dcee9c" + integrity sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw== nanoid@^3.3.11: version "3.3.11" From 0ac3eddef6481f4e4f9359c65d3c0cfd7d2124e1 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 13 May 2026 07:55:52 +0200 Subject: [PATCH 15/55] fix: apply SASLprep (RFC 4013) to passwords before SCRAM-SHA-256 PBKDF2 (#3669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: apply SASLprep (RFC 4013) to passwords before SCRAM-SHA-256 PBKDF2 `pg`'s SCRAM-SHA-256 client passes the raw password into PBKDF2 with no normalization, while PostgreSQL's server (and libpq) apply SASLprep (B.1 mapping -> NFKC -> prohibition + bidi check) when computing the stored verifier. Passwords whose NFKC form differs from themselves (e.g. containing U+00A8 dieresis, U+2011 non-breaking hyphen, U+00BC vulgar one quarter, NBSP, soft hyphen) authenticate with psql/libpq but fail against pg with `28P01`. Wire `@mongodb-js/saslprep` (the maintained fork used by mongodb's official Node driver) into `continueSession` before `crypto.deriveKey`, with a try/catch fallback to the raw password on prohibited / bidi violations to match `libpq`'s `pg_saslprep` behavior. Also adds: - Unit tests covering the soft-hyphen B.1 mapping equivalence, the Roman-numeral-IX NFKC asymmetry, the prohibited-char fallback, and a deterministic snapshot for the original bug-report password. - A gated integration test block (SCRAM_TEST_PGUSER_UNICODE / SCRAM_TEST_PGPASSWORD_UNICODE) covering raw + NFKC-equivalent + wrong password. - A `scram_unicode_test` role (password `U&'IX-\2168'`) provisioned in CI plus matching env vars so the new integration tests run on every Node version. - A Cloudflare Workers regression guard that exercises `sasl.continueSession` to ensure `@mongodb-js/saslprep` resolves cleanly under workerd. - A `pg@8.21.0` CHANGELOG entry. * fix: inline SASLprep, drop @mongodb-js/saslprep dependency Per review feedback on #3669: ship the SASLprep step as a small in-tree function instead of pulling a runtime dep with an unpinned transitive. The function performs only the three byte-changing steps from RFC 4013 (Table C.1.2 -> SPACE, Table B.1 -> empty, NFKC) and skips the prohibition (RFC 4013 section 2.3) and bidi (RFC 3454 section 6) checks, since libpq is forgiving on those paths and Postgres's own SASLprep is similarly lenient. Removes the try/catch fallback (no code path throws). The deterministic snapshot tests stay byte-for-byte valid because none of them touch U+200B, the only edge case where the inline impl diverges from `@mongodb-js/saslprep`. RFC 3454 places U+200B in Table B.1 (mapped to nothing); the dep maps it to SPACE. PostgreSQL's saslprep.c follows the RFC, so the inline impl matches libpq more closely on that codepoint. The B.1 unit-test rename ("passes ASCII control characters through normalization unchanged") keeps the same snapshot bytes since BEL is unchanged by all three steps. Co-authored-by: charmander * Revert unrelated no-op changes to yarn.lock now that the associated dependency isn’t being added. * cleanup: Allow Prettier to format some lines * cleanup: Remove changelog entry for unreleased pg version normally added as part of the release process * refactor: Simplify comments in sasl.js and remove unused test cases Updated comments in sasl.js to clarify the password normalization process and removed redundant test cases from vitest-cf.test.ts, streamlining the codebase. * Remove redundant NFKC-only SASLprep test Confirmed in pull request comments that the “macOS/iOS” thing was an AI inventing an unneeded justification, and NFKC is already covered by another test. * fix: SASLprep zero-width space the same way PostgreSQL does As mentioned in the test comment, RFC 3454 defines appendix B for mapping tables and appendix C for prohibition tables. RFC 4013 SASLprep is probably misusing that list of non-ASCII spaces, and says nothing about the overlap. (At least it’s obsoleted.) * cleanup: Simplify regex character classes with ranges --------- Co-authored-by: charmander Co-authored-by: Charmander <~@charmander.me> --- .github/workflows/ci.yml | 10 ++- packages/pg/lib/crypto/sasl.js | 30 ++++++- .../integration/client/sasl-scram-tests.js | 79 +++++++++++++++++++ .../pg/test/unit/client/sasl-scram-tests.js | 58 ++++++++++++++ 4 files changed, 175 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1aae36233..1a266291d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,13 @@ jobs: PGTESTNOSSL: 'true' SCRAM_TEST_PGUSER: scram_test SCRAM_TEST_PGPASSWORD: test4scram + SCRAM_TEST_PGUSER_UNICODE: scram_unicode_test + # Raw form of a password whose NFKC normalization differs from itself. + # U+2168 (ROMAN NUMERAL IX) decomposes to ASCII "IX" under NFKC; the + # server stores the verifier from the SASLprep-normalized form, so the + # client must apply SASLprep too. This is the regression check for the + # RFC 4013 fix in packages/pg/lib/crypto/sasl.js. + SCRAM_TEST_PGPASSWORD_UNICODE: "IX-\u2168" steps: - name: Show OS run: | @@ -63,7 +70,8 @@ jobs: - run: | psql \ -c "SET password_encryption = 'scram-sha-256'" \ - -c "CREATE ROLE scram_test LOGIN PASSWORD 'test4scram'" + -c "CREATE ROLE scram_test LOGIN PASSWORD 'test4scram'" \ + -c "CREATE ROLE scram_unicode_test LOGIN PASSWORD U&'IX-\2168'" - uses: actions/checkout@v4 with: persist-credentials: false diff --git a/packages/pg/lib/crypto/sasl.js b/packages/pg/lib/crypto/sasl.js index a782ae48a..39af4e4cf 100644 --- a/packages/pg/lib/crypto/sasl.js +++ b/packages/pg/lib/crypto/sasl.js @@ -2,6 +2,34 @@ const crypto = require('./utils') const { signatureAlgorithmHashFromCertificate } = require('./cert-signatures') +// SASLprep (RFC 4013) — minimal in-tree implementation. +// +// Per RFC 5802 §2.2, the SCRAM-SHA-256 client must normalize the password via +// SASLprep before feeding it into PBKDF2. PostgreSQL's server applies the same +// SASLprep when computing the stored verifier, and libpq does the same client +// side, so passwords whose NFKC form differs from the raw form +// would otherwise authenticate against psql/libpq but fail against pg with `28P01`. +// +// We deliberately implement only the three steps that change the byte content: +// 1. RFC 3454 Table C.1.2 (non-ASCII space) → U+0020 SPACE. +// 2. RFC 3454 Table B.1 (commonly mapped to nothing) → empty. +// 3. NFKC normalization. +// We skip the prohibition (RFC 4013 §2.3) and bidi (RFC 3454 §6) checks. +// libpq is forgiving on those paths and Postgres's own SASLprep matches that +// leniency for legacy roles, so omitting the rejection logic keeps existing +// roles working without adding complexity. +function saslprep(password) { + // RFC 3454 Table C.1.2 — non-ASCII space characters, mapped to U+0020. + const nonAsciiSpace = /[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]/g + // RFC 3454 Table B.1 — "commonly mapped to nothing". The set intentionally + // contains zero-width joiners and variation selectors — the very characters + // ESLint's no-misleading-character-class warns about — because they combine + // with their neighbors and the RFC strips them for that reason. + // eslint-disable-next-line no-misleading-character-class + const mappedToNothing = /[\u00AD\u034F\u1806\u180B\u180C\u180D\u200C\u200D\u2060\uFE00-\uFE0F\uFEFF]/g + return password.replace(nonAsciiSpace, ' ').replace(mappedToNothing, '').normalize('NFKC') +} + function startSession(mechanisms, stream) { const candidates = ['SCRAM-SHA-256'] if (stream) candidates.unshift('SCRAM-SHA-256-PLUS') // higher-priority, so placed first @@ -70,7 +98,7 @@ async function continueSession(session, password, serverData, stream) { const authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof const saltBytes = Buffer.from(sv.salt, 'base64') - const saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration) + const saltedPassword = await crypto.deriveKey(saslprep(password), saltBytes, sv.iteration) const clientKey = await crypto.hmacSha256(saltedPassword, 'Client Key') const storedKey = await crypto.sha256(clientKey) const clientSignature = await crypto.hmacSha256(storedKey, authMessage) diff --git a/packages/pg/test/integration/client/sasl-scram-tests.js b/packages/pg/test/integration/client/sasl-scram-tests.js index 85bf2cd34..bf1dfcb0d 100644 --- a/packages/pg/test/integration/client/sasl-scram-tests.js +++ b/packages/pg/test/integration/client/sasl-scram-tests.js @@ -108,3 +108,82 @@ suite.test('sasl/scram fails when password is empty', async () => { ) assert.ok(usingSasl, 'Should be using SASL for authentication') }) + +/** + * SASLprep regression coverage. RFC 5802 / RFC 4013 require the SCRAM client + * to normalize the password (B.1 mapping → NFKC → prohibition + bidi check) + * before feeding it into PBKDF2. PostgreSQL's server applies the same + * SASLprep when computing the verifier, so any password whose NFKC form + * differs from the raw form would otherwise authenticate against psql/libpq + * but fail against pg with `28P01`. + * + * To exercise these tests, provision a role whose password contains an + * NFKC-asymmetric character. For example, in psql: + * + * SET password_encryption = 'scram-sha-256'; + * CREATE ROLE scram_unicode_test LOGIN PASSWORD U&'IX-\2168'; + * + * `\2168` is ROMAN NUMERAL IX; the server SASLprep-normalizes this to + * `IX-IX` when computing the verifier. Then export: + * + * SCRAM_TEST_PGUSER_UNICODE=scram_unicode_test + * SCRAM_TEST_PGPASSWORD_UNICODE='IX-\u2168' (i.e. the raw form) + * + * If either env var is unset the suite is skipped, matching the convention + * of the ASCII SCRAM block above. + */ +const unicodeConfig = { + user: process.env.SCRAM_TEST_PGUSER_UNICODE, + password: process.env.SCRAM_TEST_PGPASSWORD_UNICODE, + host: process.env.SCRAM_TEST_PGHOST, + port: process.env.SCRAM_TEST_PGPORT, + database: process.env.SCRAM_TEST_PGDATABASE, +} + +if (!unicodeConfig.user || !unicodeConfig.password) { + suite.test('skipping SCRAM unicode tests (missing env)', () => {}) +} else { + suite.test('sasl/scram authenticates a password requiring SASLprep (raw form)', async () => { + const client = new pg.Client(unicodeConfig) + let usingSasl = false + client.connection.once('authenticationSASL', () => { + usingSasl = true + }) + await client.connect() + assert.ok(usingSasl, 'Should be using SASL for authentication') + await client.end() + }) + + suite.test('sasl/scram authenticates the NFKC-equivalent ASCII form of the same password', async () => { + // The unicode password contains a codepoint that NFKC-decomposes to ASCII + // (e.g. U+2168 → "IX"). The server stored the verifier from the + // SASLprep'd ASCII form, so feeding the client the ASCII form directly + // must also authenticate. This proves that the prep step is symmetric: + // any NFKC-equivalent representation reaches the same PBKDF2 input. + const client = new pg.Client({ + ...unicodeConfig, + password: unicodeConfig.password.normalize('NFKC'), + }) + await client.connect() + await client.end() + }) + + suite.test('sasl/scram fails when unicode password is wrong', async () => { + const client = new pg.Client({ + ...unicodeConfig, + password: unicodeConfig.password + 'append-something-to-make-it-bad', + }) + let usingSasl = false + client.connection.once('authenticationSASL', () => { + usingSasl = true + }) + await assert.rejects( + () => client.connect(), + { + code: '28P01', + }, + 'Error code should be for a password error' + ) + assert.ok(usingSasl, 'Should be using SASL for authentication') + }) +} diff --git a/packages/pg/test/unit/client/sasl-scram-tests.js b/packages/pg/test/unit/client/sasl-scram-tests.js index 8b0376d67..fc75a748a 100644 --- a/packages/pg/test/unit/client/sasl-scram-tests.js +++ b/packages/pg/test/unit/client/sasl-scram-tests.js @@ -187,6 +187,64 @@ suite.test('sasl/scram', function () { assert.equal(session.response, 'c=eSws,r=ab,p=YVTEOwOD7khu/NulscjFegHrZoTXJBFI/7L61AN9khc=') }) + suite.test('SASLprep maps non-ASCII space characters (RFC 3454 C.1.2) to U+0020 SPACE', async function () { + // SASLprep probably misuses the C.1.2 table; U+200B, in particular, is listed in both the C.1.2 and B.1 tables. We treat it as a space for compatibility with PostgreSQL. + const sessionPrepped = { message: 'SASLInitialResponse', clientNonce: 'a' } + const sessionRef = { message: 'SASLInitialResponse', clientNonce: 'a' } + + await sasl.continueSession(sessionPrepped, '\u200bfoo\xa0bar', 'r=ab,s=abcd,i=1') + await sasl.continueSession(sessionRef, ' foo bar', 'r=ab,s=abcd,i=1') + + assert.equal(sessionPrepped.serverSignature, sessionRef.serverSignature) + assert.equal(sessionPrepped.response, sessionRef.response) + }) + + suite.test('SASLprep maps mapped-to-nothing characters before PBKDF2 (RFC 3454 B.1)', async function () { + // Soft hyphen U+00AD is mapped to nothing by SASLprep, so 'I\u00ADX' + // must produce identical SCRAM output to 'IX'. This proves the prep + // step is engaged on the SCRAM derivation path. Without the fix the + // two would diverge and this assertion would fail. + const sessionPrepped = { message: 'SASLInitialResponse', clientNonce: 'a' } + const sessionRef = { message: 'SASLInitialResponse', clientNonce: 'a' } + + await sasl.continueSession(sessionPrepped, 'I\u00ADX', 'r=ab,s=abcd,i=1') + await sasl.continueSession(sessionRef, 'IX', 'r=ab,s=abcd,i=1') + + assert.equal(sessionPrepped.serverSignature, sessionRef.serverSignature) + assert.equal(sessionPrepped.response, sessionRef.response) + }) + + suite.test('SASLprep NFKC-normalizes passwords before PBKDF2 (RFC 4013 §2.2)', async function () { + // ROMAN NUMERAL IX (U+2168) NFKC-decomposes to the ASCII letters 'IX'. + // PostgreSQL's server applies SASLprep when computing the verifier, so + // a role created with U+2168 is stored as if it were 'IX'. The client + // must do the same. + const sessionPrepped = { message: 'SASLInitialResponse', clientNonce: 'a' } + const sessionRef = { message: 'SASLInitialResponse', clientNonce: 'a' } + + await sasl.continueSession(sessionPrepped, '\u2168', 'r=ab,s=abcd,i=1') + await sasl.continueSession(sessionRef, 'IX', 'r=ab,s=abcd,i=1') + + assert.equal(sessionPrepped.serverSignature, sessionRef.serverSignature) + assert.equal(sessionPrepped.response, sessionRef.response) + }) + + suite.test('passes ASCII control characters through normalization unchanged', async function () { + // BEL (U+0007) is an ASCII control character. The minimal SASLprep + // implementation (B.1 mapping → C.1.2 mapping → NFKC) is the identity + // on ASCII control codes, so the bytes fed to PBKDF2 are exactly the + // raw password. We snapshot the resulting SCRAM output as a regression + // guard: if anyone ever swaps the order of operations, removes the + // NFKC step, or accidentally strips ASCII bytes, this assertion trips. + const session = { message: 'SASLInitialResponse', clientNonce: 'a' } + + await sasl.continueSession(session, '\u0007abc', 'r=ab,s=abcd,i=1') + + assert.equal(session.message, 'SASLResponse') + assert.equal(session.serverSignature, 'ytJN8GA+9TeZpeS28ix+u0cwaIB7iFlWgpAsmy+MmP0=') + assert.equal(session.response, 'c=biws,r=ab,p=04HAPnY4K2UhwiD2RJtFw9sU81SLcas8B1Uqdqv8SeQ=') + }) + suite.test('sets expected session data (SCRAM-SHA-256-PLUS)', async function () { const session = { message: 'SASLInitialResponse', From 2095247a7b10ebe19cd7d518e07ee2f259dda70a Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Wed, 13 May 2026 20:51:03 -0700 Subject: [PATCH 16/55] cleanup: Combine duplicated code in `Client#query` and avoid unneeded early non-const declarations (#3674) No behaviour change except for the negligible one of reading the `query_timeout` property before `submit`. --- packages/pg/lib/client.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index bb5e6d5f0..fb45649b3 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -605,14 +605,14 @@ class Client extends EventEmitter { // can take in strings, config object or query object let query let result - let readTimeout - let readTimeoutTimer - let queryCallback - if (config === null || config === undefined) { + if (config == null) { throw new TypeError('Client was passed a null or undefined query') - } else if (typeof config.submit === 'function') { - readTimeout = config.query_timeout || this.connectionParameters.query_timeout + } + + const readTimeout = config.query_timeout || this.connectionParameters.query_timeout + + if (typeof config.submit === 'function') { result = query = config if (!query.callback) { if (typeof values === 'function') { @@ -622,7 +622,6 @@ class Client extends EventEmitter { } } } else { - readTimeout = config.query_timeout || this.connectionParameters.query_timeout query = new Query(config, values, callback) if (!query.callback) { result = new this._Promise((resolve, reject) => { @@ -639,9 +638,9 @@ class Client extends EventEmitter { } if (readTimeout) { - queryCallback = query.callback || (() => {}) + const queryCallback = query.callback || (() => {}) - readTimeoutTimer = setTimeout(() => { + const readTimeoutTimer = setTimeout(() => { const error = new Error('Query read timeout') process.nextTick(() => { From 88a7e60c7191ce8061d6276b299895bf5511e042 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Thu, 14 May 2026 04:46:36 +0000 Subject: [PATCH 17/55] cleanup: Move declaration to more natural place Missed in #3674. --- packages/pg/lib/client.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index fb45649b3..3bfffc7d2 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -610,8 +610,6 @@ class Client extends EventEmitter { throw new TypeError('Client was passed a null or undefined query') } - const readTimeout = config.query_timeout || this.connectionParameters.query_timeout - if (typeof config.submit === 'function') { result = query = config if (!query.callback) { @@ -637,6 +635,7 @@ class Client extends EventEmitter { } } + const readTimeout = config.query_timeout || this.connectionParameters.query_timeout if (readTimeout) { const queryCallback = query.callback || (() => {}) From fa47e73349786c2a76db98801d60c05371b0a906 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Thu, 14 May 2026 07:40:29 -0700 Subject: [PATCH 18/55] fix: `Client#end` callback being called multiple times when first is no-op (#3676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: unintended listener after no-op `Client#end` callback * fix: `Client#end` callback being called multiple times when first is no-op (and unwanted retained listener even when not called multiple times) * fix: Prevent multiple callbacks in pg/native `Client#end`, and align pre-connect behaviour closer to pg As usual, the native client is extra full of bugs and inconsistencies, so this is just “good enough”. --- packages/pg/lib/client.js | 1 + packages/pg/lib/native/client.js | 6 ++++-- packages/pg/test/integration/client/api-tests.js | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 3bfffc7d2..3525cf5ac 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -716,6 +716,7 @@ class Client extends EventEmitter { if (!this.connection._connecting || this._ended) { if (cb) { cb() + return } else { return this._Promise.resolve() } diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index 6df471b83..fa17d9f65 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -249,8 +249,10 @@ Client.prototype.end = function (cb) { this._ending = true - if (!this._connected) { - this.once('connect', this.end.bind(this, cb)) + if (this._connecting && !this._connected) { + this.once('connect', () => { + this.end(() => {}) + }) } let result if (!cb) { diff --git a/packages/pg/test/integration/client/api-tests.js b/packages/pg/test/integration/client/api-tests.js index ab7ad6db8..2b0c3f85b 100644 --- a/packages/pg/test/integration/client/api-tests.js +++ b/packages/pg/test/integration/client/api-tests.js @@ -230,6 +230,21 @@ suite.test('callback is fired once and only once', function (done) { ) }) +suite.test('no-op Client#end callback is called exactly once', (done) => { + const client = new helper.Client() + let called = false + + client.end(() => { + assert(!called) + called = true + + client.connect((err) => { + assert.ifError(err) + client.end(done) + }) + }) +}) + suite.test('can provide callback and config object', function (done) { const pool = new pg.Pool() pool.connect( From c8da6ab9326d93005e6947217cf665f707e08ec7 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Thu, 14 May 2026 07:42:48 -0700 Subject: [PATCH 19/55] Assorted test cleanup (#3673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: Remove unused `assert.UTCDate` * test: Replace `equalBuffers` with `assert.deepStrictEqual` `spit` isn’t defined. * cleanup: Replace additional helper in test with stdlib `assert.rejects` * cleanup: Merge unused test suite `uncaughtException` handler into first one * cleanup: Remove unused `Suite#testAsync` * cleanup: Remove now-redundant `unhandledRejection` listener in tests All versions of Node (≥16.x) supported by pg 8 default to throwing. --- .../client/error-handling-tests.js | 11 ++-- packages/pg/test/suite.js | 16 ----- packages/pg/test/test-helper.js | 61 +------------------ .../unit/client/cleartext-password-tests.js | 2 +- .../pg/test/unit/client/md5-password-tests.js | 5 +- 5 files changed, 11 insertions(+), 84 deletions(-) diff --git a/packages/pg/test/integration/client/error-handling-tests.js b/packages/pg/test/integration/client/error-handling-tests.js index 7493ef68d..848839287 100644 --- a/packages/pg/test/integration/client/error-handling-tests.js +++ b/packages/pg/test/integration/client/error-handling-tests.js @@ -47,14 +47,11 @@ suite.test('re-using connections results in error callback', (done) => { }) }) -suite.test('re-using connections results in promise rejection', () => { +suite.test('re-using connections results in promise rejection', async () => { const client = new Client() - return client.connect().then(() => { - return helper.rejection(client.connect()).then((err) => { - assert(err instanceof Error) - return client.end() - }) - }) + await client.connect() + await assert.rejects(client.connect(), Error) + await client.end() }) suite.test('using a client after closing it results in error', (done) => { diff --git a/packages/pg/test/suite.js b/packages/pg/test/suite.js index 7a1c20008..e8d9d0834 100644 --- a/packages/pg/test/suite.js +++ b/packages/pg/test/suite.js @@ -1,11 +1,6 @@ 'use strict' const async = require('async') -const { deprecate } = require('util') - -const deprecatedTestAsync = deprecate(function (name, cb) { - this.test(name, cb) -}, 'Suite#testAsync is deprecated. Use Suite#test instead - it handles promises & async functions just fine.') class Test { constructor(name, cb) { @@ -75,17 +70,6 @@ class Suite { const test = new Test(name, cb) this._queue.push(test) } - - testAsync(name, cb) { - return deprecatedTestAsync.call(this, name, cb) - } } -process.on('unhandledRejection', (e) => { - setImmediate(() => { - console.error('Unhandled promise rejection') - throw e - }) -}) - module.exports = Suite diff --git a/packages/pg/test/test-helper.js b/packages/pg/test/test-helper.js index 8cd9dda36..3d2d4d4d8 100644 --- a/packages/pg/test/test-helper.js +++ b/packages/pg/test/test-helper.js @@ -21,7 +21,8 @@ process.on('uncaughtException', function (d) { } else { console.log(d) } - process.exit(-1) + // causes xargs to abort right away + process.exit(255) }) const expect = function (callback, timeout) { const executed = false @@ -66,12 +67,6 @@ process.on('exit', function () { console.log('') }) -process.on('uncaughtException', function (err) { - console.error('\n %s', err.stack || err.toString()) - // causes xargs to abort right away - process.exit(255) -}) - const getTimezoneOffset = Date.prototype.getTimezoneOffset const setTimezoneOffset = function (minutesOffset) { @@ -84,14 +79,6 @@ const resetTimezoneOffset = function () { Date.prototype.getTimezoneOffset = getTimezoneOffset } -const rejection = (promise) => - promise.then( - (value) => { - throw new Error(`Promise resolved when rejection was expected; value: ${sys.inspect(value)}`) - }, - (error) => error - ) - if (Object.isExtensible(assert)) { assert.same = function (actual, expected) { for (const key in expected) { @@ -124,49 +111,6 @@ if (Object.isExtensible(assert)) { }) } - assert.UTCDate = function (actual, year, month, day, hours, min, sec, milisecond) { - const actualYear = actual.getUTCFullYear() - assert.equal(actualYear, year, 'expected year ' + year + ' but got ' + actualYear) - - const actualMonth = actual.getUTCMonth() - assert.equal(actualMonth, month, 'expected month ' + month + ' but got ' + actualMonth) - - const actualDate = actual.getUTCDate() - assert.equal(actualDate, day, 'expected day ' + day + ' but got ' + actualDate) - - const actualHours = actual.getUTCHours() - assert.equal(actualHours, hours, 'expected hours ' + hours + ' but got ' + actualHours) - - const actualMin = actual.getUTCMinutes() - assert.equal(actualMin, min, 'expected min ' + min + ' but got ' + actualMin) - - const actualSec = actual.getUTCSeconds() - assert.equal(actualSec, sec, 'expected sec ' + sec + ' but got ' + actualSec) - - const actualMili = actual.getUTCMilliseconds() - assert.equal(actualMili, milisecond, 'expected milisecond ' + milisecond + ' but got ' + actualMili) - } - - const spit = function (actual, expected) { - console.log('') - console.log('actual ' + sys.inspect(actual)) - console.log('expect ' + sys.inspect(expected)) - console.log('') - } - - assert.equalBuffers = function (actual, expected) { - if (actual.length != expected.length) { - spit(actual, expected) - assert.equal(actual.length, expected.length) - } - for (let i = 0; i < actual.length; i++) { - if (actual[i] != expected[i]) { - spit(actual, expected) - } - assert.equal(actual[i], expected[i]) - } - } - assert.empty = function (actual) { assert.lengthIs(actual, 0) } @@ -257,6 +201,5 @@ module.exports = { Client: Client, setTimezoneOffset: setTimezoneOffset, resetTimezoneOffset: resetTimezoneOffset, - rejection: rejection, createPersonTable: createPersonTable, } diff --git a/packages/pg/test/unit/client/cleartext-password-tests.js b/packages/pg/test/unit/client/cleartext-password-tests.js index 388d94cf9..b844db5e6 100644 --- a/packages/pg/test/unit/client/cleartext-password-tests.js +++ b/packages/pg/test/unit/client/cleartext-password-tests.js @@ -14,7 +14,7 @@ suite.test('cleartext password auth responds with password', function () { const packets = client.connection.stream.packets assert.lengthIs(packets, 1) const packet = packets[0] - assert.equalBuffers(packet, [0x70, 0, 0, 0, 6, 33, 0]) + assert.deepStrictEqual(packet, Buffer.from([0x70, 0, 0, 0, 6, 33, 0])) }) suite.test('cleartext password auth does not crash with null password using pg-pass', function () { diff --git a/packages/pg/test/unit/client/md5-password-tests.js b/packages/pg/test/unit/client/md5-password-tests.js index 8fd2f7c2f..a00b15b1f 100644 --- a/packages/pg/test/unit/client/md5-password-tests.js +++ b/packages/pg/test/unit/client/md5-password-tests.js @@ -18,7 +18,10 @@ test('md5 authentication', async function () { test('should have correct encrypted data', async function () { const password = await crypto.postgresMd5PasswordHash(client.user, client.password, salt) // how do we want to test this? - assert.equalBuffers(client.connection.stream.packets[0], new BufferList().addCString(password).join(true, 'p')) + assert.deepStrictEqual( + client.connection.stream.packets[0], + new BufferList().addCString(password).join(true, 'p') + ) }) }) ) From f252870eba73c15449b57562e6698b5859e32095 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Thu, 14 May 2026 11:32:31 -0700 Subject: [PATCH 20/55] cleanup: pg utils (#3675) * cleanup: `arrayString` code style * cleanup: Move `Buffer.from` Node 4 compatibility code to common function Reviewed-by: brianc --- packages/pg/lib/utils.js | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index e23a55e9a..1bbdebaf9 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -11,6 +11,12 @@ function escapeElement(elementRepresentation) { return '"' + escaped + '"' } +// Node.js v4 does not support those Buffer.from params +const bufferFrom = + Buffer.from(new Uint8Array(1).buffer, 0, 0).length === 0 + ? Buffer.from + : (arrayBuffer, byteOffset, length) => Buffer.from(arrayBuffer).slice(byteOffset, byteOffset + length) + // convert a JS array to a postgres array literal // uses comma separator so won't work for types like box that use // a different array separator. @@ -18,28 +24,23 @@ function arrayString(val) { let result = '{' for (let i = 0; i < val.length; i++) { if (i > 0) { - result = result + ',' + result += ',' } - if (val[i] === null || typeof val[i] === 'undefined') { - result = result + 'NULL' - } else if (Array.isArray(val[i])) { - result = result + arrayString(val[i]) - } else if (ArrayBuffer.isView(val[i])) { - let item = val[i] + let item = val[i] + if (item == null) { + result += 'NULL' + } else if (Array.isArray(item)) { + result += arrayString(item) + } else if (ArrayBuffer.isView(item)) { if (!(item instanceof Buffer)) { - const buf = Buffer.from(item.buffer, item.byteOffset, item.byteLength) - if (buf.length === item.byteLength) { - item = buf - } else { - item = buf.slice(item.byteOffset, item.byteOffset + item.byteLength) - } + item = bufferFrom(item.buffer, item.byteOffset, item.byteLength) } result += '\\\\x' + item.toString('hex') } else { - result += escapeElement(prepareValue(val[i])) + result += escapeElement(prepareValue(item)) } } - result = result + '}' + result += '}' return result } @@ -57,11 +58,7 @@ const prepareValue = function (val, seen) { return val } if (ArrayBuffer.isView(val)) { - const buf = Buffer.from(val.buffer, val.byteOffset, val.byteLength) - if (buf.length === val.byteLength) { - return buf - } - return buf.slice(val.byteOffset, val.byteOffset + val.byteLength) // Node.js v4 does not support those Buffer.from params + return bufferFrom(val.buffer, val.byteOffset, val.byteLength) } if (isDate(val)) { if (defaults.parseInputDatesAsUTC) { From f776327b3fcdd997c67e866ef7c620ef9c26b3f2 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Mon, 18 May 2026 04:49:06 -0700 Subject: [PATCH 21/55] Remove compatibility code for unsupported versions of Node (<16) (#3678) * Remove compatibility code for unsupported versions of Node (<16) * cleanup: Remove remaining `unhandledRejection` handlers in pg tests Unhandled rejections are errors by default in all supported versions of Node. --- packages/pg/lib/crypto/utils-legacy.js | 43 --------- packages/pg/lib/crypto/utils-webcrypto.js | 89 ----------------- packages/pg/lib/crypto/utils.js | 96 +++++++++++++++++-- packages/pg/lib/utils.js | 13 +-- .../client/async-stack-trace-tests.js | 73 +++++++------- .../client/query-as-promise-tests.js | 5 - 6 files changed, 123 insertions(+), 196 deletions(-) delete mode 100644 packages/pg/lib/crypto/utils-legacy.js delete mode 100644 packages/pg/lib/crypto/utils-webcrypto.js diff --git a/packages/pg/lib/crypto/utils-legacy.js b/packages/pg/lib/crypto/utils-legacy.js deleted file mode 100644 index d70fdb638..000000000 --- a/packages/pg/lib/crypto/utils-legacy.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' -// This file contains crypto utility functions for versions of Node.js < 15.0.0, -// which does not support the WebCrypto.subtle API. - -const nodeCrypto = require('crypto') - -function md5(string) { - return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') -} - -// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html -function postgresMd5PasswordHash(user, password, salt) { - const inner = md5(password + user) - const outer = md5(Buffer.concat([Buffer.from(inner), salt])) - return 'md5' + outer -} - -function sha256(text) { - return nodeCrypto.createHash('sha256').update(text).digest() -} - -function hashByName(hashName, text) { - hashName = hashName.replace(/(\D)-/, '$1') // e.g. SHA-256 -> SHA256 - return nodeCrypto.createHash(hashName).update(text).digest() -} - -function hmacSha256(key, msg) { - return nodeCrypto.createHmac('sha256', key).update(msg).digest() -} - -async function deriveKey(password, salt, iterations) { - return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256') -} - -module.exports = { - postgresMd5PasswordHash, - randomBytes: nodeCrypto.randomBytes, - deriveKey, - sha256, - hashByName, - hmacSha256, - md5, -} diff --git a/packages/pg/lib/crypto/utils-webcrypto.js b/packages/pg/lib/crypto/utils-webcrypto.js deleted file mode 100644 index 65aa4a182..000000000 --- a/packages/pg/lib/crypto/utils-webcrypto.js +++ /dev/null @@ -1,89 +0,0 @@ -const nodeCrypto = require('crypto') - -module.exports = { - postgresMd5PasswordHash, - randomBytes, - deriveKey, - sha256, - hashByName, - hmacSha256, - md5, -} - -/** - * The Web Crypto API - grabbed from the Node.js library or the global - * @type Crypto - */ -// eslint-disable-next-line no-undef -const webCrypto = nodeCrypto.webcrypto || globalThis.crypto -/** - * The SubtleCrypto API for low level crypto operations. - * @type SubtleCrypto - */ -const subtleCrypto = webCrypto.subtle -const textEncoder = new TextEncoder() - -/** - * - * @param {*} length - * @returns - */ -function randomBytes(length) { - return webCrypto.getRandomValues(Buffer.alloc(length)) -} - -async function md5(string) { - try { - return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') - } catch (e) { - // `createHash()` failed so we are probably not in Node.js, use the WebCrypto API instead. - // Note that the MD5 algorithm on WebCrypto is not available in Node.js. - // This is why we cannot just use WebCrypto in all environments. - const data = typeof string === 'string' ? textEncoder.encode(string) : string - const hash = await subtleCrypto.digest('MD5', data) - return Array.from(new Uint8Array(hash)) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - } -} - -// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html -async function postgresMd5PasswordHash(user, password, salt) { - const inner = await md5(password + user) - const outer = await md5(Buffer.concat([Buffer.from(inner), salt])) - return 'md5' + outer -} - -/** - * Create a SHA-256 digest of the given data - * @param {Buffer} data - */ -async function sha256(text) { - return await subtleCrypto.digest('SHA-256', text) -} - -async function hashByName(hashName, text) { - return await subtleCrypto.digest(hashName, text) -} - -/** - * Sign the message with the given key - * @param {ArrayBuffer} keyBuffer - * @param {string} msg - */ -async function hmacSha256(keyBuffer, msg) { - const key = await subtleCrypto.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) - return await subtleCrypto.sign('HMAC', key, textEncoder.encode(msg)) -} - -/** - * Derive a key from the password and salt - * @param {string} password - * @param {Uint8Array} salt - * @param {number} iterations - */ -async function deriveKey(password, salt, iterations) { - const key = await subtleCrypto.importKey('raw', textEncoder.encode(password), 'PBKDF2', false, ['deriveBits']) - const params = { name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations: iterations } - return await subtleCrypto.deriveBits(params, key, 32 * 8, ['deriveBits']) -} diff --git a/packages/pg/lib/crypto/utils.js b/packages/pg/lib/crypto/utils.js index 9644b150f..65aa4a182 100644 --- a/packages/pg/lib/crypto/utils.js +++ b/packages/pg/lib/crypto/utils.js @@ -1,9 +1,89 @@ -'use strict' - -const useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split('.')[0]) < 15 -if (useLegacyCrypto) { - // We are on an old version of Node.js that requires legacy crypto utilities. - module.exports = require('./utils-legacy') -} else { - module.exports = require('./utils-webcrypto') +const nodeCrypto = require('crypto') + +module.exports = { + postgresMd5PasswordHash, + randomBytes, + deriveKey, + sha256, + hashByName, + hmacSha256, + md5, +} + +/** + * The Web Crypto API - grabbed from the Node.js library or the global + * @type Crypto + */ +// eslint-disable-next-line no-undef +const webCrypto = nodeCrypto.webcrypto || globalThis.crypto +/** + * The SubtleCrypto API for low level crypto operations. + * @type SubtleCrypto + */ +const subtleCrypto = webCrypto.subtle +const textEncoder = new TextEncoder() + +/** + * + * @param {*} length + * @returns + */ +function randomBytes(length) { + return webCrypto.getRandomValues(Buffer.alloc(length)) +} + +async function md5(string) { + try { + return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') + } catch (e) { + // `createHash()` failed so we are probably not in Node.js, use the WebCrypto API instead. + // Note that the MD5 algorithm on WebCrypto is not available in Node.js. + // This is why we cannot just use WebCrypto in all environments. + const data = typeof string === 'string' ? textEncoder.encode(string) : string + const hash = await subtleCrypto.digest('MD5', data) + return Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + } +} + +// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html +async function postgresMd5PasswordHash(user, password, salt) { + const inner = await md5(password + user) + const outer = await md5(Buffer.concat([Buffer.from(inner), salt])) + return 'md5' + outer +} + +/** + * Create a SHA-256 digest of the given data + * @param {Buffer} data + */ +async function sha256(text) { + return await subtleCrypto.digest('SHA-256', text) +} + +async function hashByName(hashName, text) { + return await subtleCrypto.digest(hashName, text) +} + +/** + * Sign the message with the given key + * @param {ArrayBuffer} keyBuffer + * @param {string} msg + */ +async function hmacSha256(keyBuffer, msg) { + const key = await subtleCrypto.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) + return await subtleCrypto.sign('HMAC', key, textEncoder.encode(msg)) +} + +/** + * Derive a key from the password and salt + * @param {string} password + * @param {Uint8Array} salt + * @param {number} iterations + */ +async function deriveKey(password, salt, iterations) { + const key = await subtleCrypto.importKey('raw', textEncoder.encode(password), 'PBKDF2', false, ['deriveBits']) + const params = { name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations: iterations } + return await subtleCrypto.deriveBits(params, key, 32 * 8, ['deriveBits']) } diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index 1bbdebaf9..638b43970 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -2,8 +2,7 @@ const defaults = require('./defaults') -const util = require('util') -const { isDate } = util.types || util // Node 8 doesn't have `util.types` +const { isDate } = require('util/types') function escapeElement(elementRepresentation) { const escaped = elementRepresentation.replace(/\\/g, '\\\\').replace(/"/g, '\\"') @@ -11,12 +10,6 @@ function escapeElement(elementRepresentation) { return '"' + escaped + '"' } -// Node.js v4 does not support those Buffer.from params -const bufferFrom = - Buffer.from(new Uint8Array(1).buffer, 0, 0).length === 0 - ? Buffer.from - : (arrayBuffer, byteOffset, length) => Buffer.from(arrayBuffer).slice(byteOffset, byteOffset + length) - // convert a JS array to a postgres array literal // uses comma separator so won't work for types like box that use // a different array separator. @@ -33,7 +26,7 @@ function arrayString(val) { result += arrayString(item) } else if (ArrayBuffer.isView(item)) { if (!(item instanceof Buffer)) { - item = bufferFrom(item.buffer, item.byteOffset, item.byteLength) + item = Buffer.from(item.buffer, item.byteOffset, item.byteLength) } result += '\\\\x' + item.toString('hex') } else { @@ -58,7 +51,7 @@ const prepareValue = function (val, seen) { return val } if (ArrayBuffer.isView(val)) { - return bufferFrom(val.buffer, val.byteOffset, val.byteLength) + return Buffer.from(val.buffer, val.byteOffset, val.byteLength) } if (isDate(val)) { if (defaults.parseInputDatesAsUTC) { diff --git a/packages/pg/test/integration/client/async-stack-trace-tests.js b/packages/pg/test/integration/client/async-stack-trace-tests.js index 8f289f5ad..567851a72 100644 --- a/packages/pg/test/integration/client/async-stack-trace-tests.js +++ b/packages/pg/test/integration/client/async-stack-trace-tests.js @@ -2,50 +2,41 @@ const helper = require('../test-helper') const pg = helper.pg -process.on('unhandledRejection', function (e) { - console.error(e, e.stack) - process.exit(1) -}) - const suite = new helper.Suite() -// these tests will only work for if --async-stack-traces is on, which is the default starting in node 16. -const NODE_MAJOR_VERSION = +process.versions.node.split('.')[0] -if (NODE_MAJOR_VERSION >= 16) { - suite.test('promise API async stack trace in pool', async function outerFunction() { - async function innerFunction() { - const pool = new pg.Pool() - await pool.query('SELECT test from nonexistent') - } - try { - await innerFunction() - throw Error('should have errored') - } catch (e) { - const stack = e.stack - if (!e.stack.includes('innerFunction') || !e.stack.includes('outerFunction')) { - throw Error('async stack trace does not contain wanted values: ' + stack, { cause: e }) - } +suite.test('promise API async stack trace in pool', async function outerFunction() { + async function innerFunction() { + const pool = new pg.Pool() + await pool.query('SELECT test from nonexistent') + } + try { + await innerFunction() + throw Error('should have errored') + } catch (e) { + const stack = e.stack + if (!e.stack.includes('innerFunction') || !e.stack.includes('outerFunction')) { + throw Error('async stack trace does not contain wanted values: ' + stack, { cause: e }) } - }) + } +}) - suite.test('promise API async stack trace in client', async function outerFunction() { - async function innerFunction() { - const client = new pg.Client() - await client.connect() - try { - await client.query('SELECT test from nonexistent') - } finally { - client.end() - } - } +suite.test('promise API async stack trace in client', async function outerFunction() { + async function innerFunction() { + const client = new pg.Client() + await client.connect() try { - await innerFunction() - throw Error('should have errored') - } catch (e) { - const stack = e.stack - if (!e.stack.includes('innerFunction') || !e.stack.includes('outerFunction')) { - throw Error('async stack trace does not contain wanted values: ' + stack, { cause: e }) - } + await client.query('SELECT test from nonexistent') + } finally { + client.end() } - }) -} + } + try { + await innerFunction() + throw Error('should have errored') + } catch (e) { + const stack = e.stack + if (!e.stack.includes('innerFunction') || !e.stack.includes('outerFunction')) { + throw Error('async stack trace does not contain wanted values: ' + stack, { cause: e }) + } + } +}) diff --git a/packages/pg/test/integration/client/query-as-promise-tests.js b/packages/pg/test/integration/client/query-as-promise-tests.js index 8e1ba5c71..8c0fcae72 100644 --- a/packages/pg/test/integration/client/query-as-promise-tests.js +++ b/packages/pg/test/integration/client/query-as-promise-tests.js @@ -4,11 +4,6 @@ const helper = require('../test-helper') const pg = helper.pg const assert = require('assert') -process.on('unhandledRejection', function (e) { - console.error(e, e.stack) - process.exit(1) -}) - const suite = new helper.Suite() suite.test('promise API', (cb) => { From cc03fa5cdf0f1e67b2518ebad5cf2269206aa49c Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Mon, 18 May 2026 07:50:22 -0400 Subject: [PATCH 22/55] Add scramMaxIterations option to limit SCRAM iteration count (#3677) Caps the number of SCRAM iterations the driver will perform during SASL auth, defaulting to 100000. Protects against malicious or misconfigured servers requesting unbounded PBKDF2 work. A value of zero disables the check entirely. --- packages/pg/lib/client.js | 18 ++++- packages/pg/lib/crypto/sasl.js | 18 ++++- .../pg/test/unit/client/sasl-scram-tests.js | 74 +++++++++++++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 3525cf5ac..d6c57194c 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -36,6 +36,17 @@ const queryQueueLengthDeprecationNotice = nodeUtils.deprecate( 'Calling client.query() when the client is already executing a query is deprecated and will be removed in pg@9.0. Use async/await or an external async flow control mechanism instead.' ) +function coerceNumberOrDefault(value, defaultValue) { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : defaultValue + } + if (typeof value === 'string' && value.trim() !== '') { + const n = Number(value) + return Number.isFinite(n) ? n : defaultValue + } + return defaultValue +} + class Client extends EventEmitter { constructor(config) { super() @@ -74,6 +85,7 @@ class Client extends EventEmitter { this._txStatus = null this.enableChannelBinding = Boolean(c.enableChannelBinding) // set true to use SCRAM-SHA-256-PLUS when offered + this.scramMaxIterations = coerceNumberOrDefault(c.scramMaxIterations, sasl.DEFAULT_MAX_SCRAM_ITERATIONS) this.connection = c.connection || new Connection({ @@ -307,7 +319,11 @@ class Client extends EventEmitter { _handleAuthSASL(msg) { this._getPassword(() => { try { - this.saslSession = sasl.startSession(msg.mechanisms, this.enableChannelBinding && this.connection.stream) + this.saslSession = sasl.startSession( + msg.mechanisms, + this.enableChannelBinding && this.connection.stream, + this.scramMaxIterations + ) this.connection.sendSASLInitialResponseMessage(this.saslSession.mechanism, this.saslSession.response) } catch (err) { this.connection.emit('error', err) diff --git a/packages/pg/lib/crypto/sasl.js b/packages/pg/lib/crypto/sasl.js index 39af4e4cf..ea63b2413 100644 --- a/packages/pg/lib/crypto/sasl.js +++ b/packages/pg/lib/crypto/sasl.js @@ -30,7 +30,9 @@ function saslprep(password) { return password.replace(nonAsciiSpace, ' ').replace(mappedToNothing, '').normalize('NFKC') } -function startSession(mechanisms, stream) { +const DEFAULT_MAX_SCRAM_ITERATIONS = 100000 + +function startSession(mechanisms, stream, scramMaxIterations = DEFAULT_MAX_SCRAM_ITERATIONS) { const candidates = ['SCRAM-SHA-256'] if (stream) candidates.unshift('SCRAM-SHA-256-PLUS') // higher-priority, so placed first @@ -53,6 +55,7 @@ function startSession(mechanisms, stream) { clientNonce, response: gs2Header + ',,n=*,r=' + clientNonce, message: 'SASLInitialResponse', + scramMaxIterations, } } @@ -78,6 +81,18 @@ async function continueSession(session, password, serverData, stream) { throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short') } + const scramMaxIterations = + typeof session.scramMaxIterations === 'number' ? session.scramMaxIterations : DEFAULT_MAX_SCRAM_ITERATIONS + // a value of 0 disables the iteration count check + if (scramMaxIterations !== 0 && sv.iteration > scramMaxIterations) { + throw new Error( + 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count ' + + sv.iteration + + ' exceeds scramMaxIterations of ' + + scramMaxIterations + ) + } + const clientFirstMessageBare = 'n=*,r=' + session.clientNonce const serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration @@ -243,4 +258,5 @@ module.exports = { startSession, continueSession, finalizeSession, + DEFAULT_MAX_SCRAM_ITERATIONS, } diff --git a/packages/pg/test/unit/client/sasl-scram-tests.js b/packages/pg/test/unit/client/sasl-scram-tests.js index fc75a748a..02b0d4e6d 100644 --- a/packages/pg/test/unit/client/sasl-scram-tests.js +++ b/packages/pg/test/unit/client/sasl-scram-tests.js @@ -55,6 +55,18 @@ suite.test('sasl/scram', function () { assert(session1.clientNonce != session2.clientNonce) }) + + suite.test('defaults scramMaxIterations to 100000', function () { + const session = sasl.startSession(['SCRAM-SHA-256']) + + assert.equal(session.scramMaxIterations, 100000) + }) + + suite.test('honors a custom scramMaxIterations', function () { + const session = sasl.startSession(['SCRAM-SHA-256'], null, 50) + + assert.equal(session.scramMaxIterations, 50) + }) }) suite.test('continueSession', function () { @@ -159,6 +171,68 @@ suite.test('sasl/scram', function () { ) }) + suite.test('fails when iteration count exceeds default scramMaxIterations', async function () { + await assert.rejects( + function () { + return sasl.continueSession( + { + message: 'SASLInitialResponse', + clientNonce: 'a', + scramMaxIterations: 100000, + }, + 'password', + 'r=ab,s=abcd,i=100001' + ) + }, + { + message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count 100001 exceeds scramMaxIterations of 100000', + } + ) + }) + + suite.test('fails when iteration count exceeds a custom scramMaxIterations', async function () { + await assert.rejects( + function () { + return sasl.continueSession( + { + message: 'SASLInitialResponse', + clientNonce: 'a', + scramMaxIterations: 10, + }, + 'password', + 'r=ab,s=abcd,i=11' + ) + }, + { + message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count 11 exceeds scramMaxIterations of 10', + } + ) + }) + + suite.test('allows iteration count at the scramMaxIterations limit', async function () { + const session = { + message: 'SASLInitialResponse', + clientNonce: 'a', + scramMaxIterations: 5, + } + + await sasl.continueSession(session, 'password', 'r=ab,s=abcd,i=5') + + assert.equal(session.message, 'SASLResponse') + }) + + suite.test('disables the iteration count check when scramMaxIterations is 0', async function () { + const session = { + message: 'SASLInitialResponse', + clientNonce: 'a', + scramMaxIterations: 0, + } + + await sasl.continueSession(session, 'password', 'r=ab,s=abcd,i=999999') + + assert.equal(session.message, 'SASLResponse') + }) + suite.test('sets expected session data (SCRAM-SHA-256)', async function () { const session = { message: 'SASLInitialResponse', From 8486337000f7ff5d430acdd60c19f68fa3130697 Mon Sep 17 00:00:00 2001 From: "Brian M. Carlson" Date: Mon, 18 May 2026 06:55:25 -0500 Subject: [PATCH 23/55] Sponsors & docs update --- SPONSORS.md | 1 + docs/theme.config.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SPONSORS.md b/SPONSORS.md index dfcbbd0ab..b482f3678 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -20,6 +20,7 @@ node-postgres is made possible by the helpful contributors from the community as - [loveland](https://github.com/loveland) - [gajus](https://github.com/gajus) - [thirdiron](https://github.com/thirdiron) +- [kiwicopple](https://github.com/kiwicopple) # Supporters diff --git a/docs/theme.config.js b/docs/theme.config.js index 03ba3665c..4c10dc5c2 100644 --- a/docs/theme.config.js +++ b/docs/theme.config.js @@ -17,14 +17,14 @@ export default { footer: { content: ( - As of 2026-03-01 I am taking a break from the workforce to focus entirely on this project! Please consider{' '} + Please consider{' '} - sponsoring this work on GitHub + sponsoring this project on GitHub! ! From d197d7b1093248bd6d94e1cd5cbf5c769f74252d Mon Sep 17 00:00:00 2001 From: "Brian M. Carlson" Date: Mon, 18 May 2026 06:59:08 -0500 Subject: [PATCH 24/55] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d167ceef..dd26374a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ For richer information consult the commit log on github with referenced pull req We do not include break-fix version release in this file. +## pg@8.21.0 + +- Handle [SASL SCRAM](https://github.com/brianc/node-postgres/pull/3521) server error responses properly. +- Add support for [node@26](https://github.com/brianc/node-postgres/pull/3667). +- Add `scramMaxIterations` [config option](https://github.com/brianc/node-postgres/pull/3677). +- Add `client.getTransactionStatus()` [method](https://github.com/brianc/node-postgres/pull/3645). + ## pg@8.20.0 - Add [onConnect](https://github.com/brianc/node-postgres/pull/3620) callback to pg.Pool constructor options allowing for async initialization of newly created & connected pooled clients. From 544b1ce8152bc280e398dc1e8a66920abe6a640e Mon Sep 17 00:00:00 2001 From: "Brian M. Carlson" Date: Mon, 18 May 2026 06:59:17 -0500 Subject: [PATCH 25/55] Publish - pg-bundler-test@0.3.0 - pg-cloudflare@1.4.0 - pg-connection-string@2.13.0 - pg-cursor@2.20.0 - pg-esm-test@1.7.0 - pg-native@3.8.0 - pg-pool@3.14.0 - pg-protocol@1.14.0 - pg-query-stream@4.15.0 - pg@8.21.0 --- packages/pg-bundler-test/package.json | 4 ++-- packages/pg-cloudflare/package.json | 2 +- packages/pg-connection-string/package.json | 2 +- packages/pg-cursor/package.json | 4 ++-- packages/pg-esm-test/package.json | 16 ++++++++-------- packages/pg-native/package.json | 2 +- packages/pg-pool/package.json | 2 +- packages/pg-protocol/package.json | 2 +- packages/pg-query-stream/package.json | 6 +++--- packages/pg/package.json | 10 +++++----- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/pg-bundler-test/package.json b/packages/pg-bundler-test/package.json index b81c6a24d..fc368c197 100644 --- a/packages/pg-bundler-test/package.json +++ b/packages/pg-bundler-test/package.json @@ -1,6 +1,6 @@ { "name": "pg-bundler-test", - "version": "0.2.0", + "version": "0.3.0", "description": "Test bundlers with pg-cloudflare, https://github.com/brianc/node-postgres/issues/3452", "license": "MIT", "private": true, @@ -9,7 +9,7 @@ "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-node-resolve": "^16.0.1", "esbuild": "^0.25.5", - "pg-cloudflare": "^1.3.0", + "pg-cloudflare": "^1.4.0", "rollup": "^4.41.1", "vite": "^7.1.7", "webpack": "^5.99.9", diff --git a/packages/pg-cloudflare/package.json b/packages/pg-cloudflare/package.json index ac68bb22e..4bc706c8a 100644 --- a/packages/pg-cloudflare/package.json +++ b/packages/pg-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "pg-cloudflare", - "version": "1.3.0", + "version": "1.4.0", "description": "A socket implementation that can run on Cloudflare Workers using native TCP connections.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/pg-connection-string/package.json b/packages/pg-connection-string/package.json index d02588a6c..aa075e154 100644 --- a/packages/pg-connection-string/package.json +++ b/packages/pg-connection-string/package.json @@ -1,6 +1,6 @@ { "name": "pg-connection-string", - "version": "2.12.0", + "version": "2.13.0", "description": "Functions for dealing with a PostgresSQL connection string", "main": "./index.js", "types": "./index.d.ts", diff --git a/packages/pg-cursor/package.json b/packages/pg-cursor/package.json index 332e51a2f..3949d50c8 100644 --- a/packages/pg-cursor/package.json +++ b/packages/pg-cursor/package.json @@ -1,6 +1,6 @@ { "name": "pg-cursor", - "version": "2.19.0", + "version": "2.20.0", "description": "Query cursor extension for node-postgres", "main": "index.js", "exports": { @@ -25,7 +25,7 @@ "license": "MIT", "devDependencies": { "mocha": "^11.7.5", - "pg": "^8.20.0" + "pg": "^8.21.0" }, "peerDependencies": { "pg": "^8" diff --git a/packages/pg-esm-test/package.json b/packages/pg-esm-test/package.json index 0f15f7ff2..ec3c4a811 100644 --- a/packages/pg-esm-test/package.json +++ b/packages/pg-esm-test/package.json @@ -1,6 +1,6 @@ { "name": "pg-esm-test", - "version": "1.6.0", + "version": "1.7.0", "description": "A test module for PostgreSQL with ESM support", "main": "index.js", "type": "module", @@ -14,13 +14,13 @@ "test" ], "devDependencies": { - "pg": "^8.20.0", - "pg-cloudflare": "^1.3.0", - "pg-cursor": "^2.19.0", - "pg-native": "^3.7.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", - "pg-query-stream": "^4.14.0" + "pg": "^8.21.0", + "pg-cloudflare": "^1.4.0", + "pg-cursor": "^2.20.0", + "pg-native": "^3.8.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-query-stream": "^4.15.0" }, "author": "Brian M. Carlson ", "license": "MIT" diff --git a/packages/pg-native/package.json b/packages/pg-native/package.json index 4c8148ac1..513f3bb3f 100644 --- a/packages/pg-native/package.json +++ b/packages/pg-native/package.json @@ -1,6 +1,6 @@ { "name": "pg-native", - "version": "3.7.0", + "version": "3.8.0", "description": "A slightly nicer interface to Postgres over node-libpq", "main": "index.js", "exports": { diff --git a/packages/pg-pool/package.json b/packages/pg-pool/package.json index 7ac434f97..6b9f60155 100644 --- a/packages/pg-pool/package.json +++ b/packages/pg-pool/package.json @@ -1,6 +1,6 @@ { "name": "pg-pool", - "version": "3.13.0", + "version": "3.14.0", "description": "Connection pool for node-postgres", "main": "index.js", "exports": { diff --git a/packages/pg-protocol/package.json b/packages/pg-protocol/package.json index d3326bdbc..4ee3e3f17 100644 --- a/packages/pg-protocol/package.json +++ b/packages/pg-protocol/package.json @@ -1,6 +1,6 @@ { "name": "pg-protocol", - "version": "1.13.0", + "version": "1.14.0", "description": "The postgres client/server binary protocol, implemented in TypeScript", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/pg-query-stream/package.json b/packages/pg-query-stream/package.json index 42ef8f268..6704fcfb9 100644 --- a/packages/pg-query-stream/package.json +++ b/packages/pg-query-stream/package.json @@ -1,6 +1,6 @@ { "name": "pg-query-stream", - "version": "4.14.0", + "version": "4.15.0", "description": "Postgres query result returned as readable stream", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -45,7 +45,7 @@ "concat-stream": "~1.0.1", "eslint-plugin-promise": "^7.3.0", "mocha": "^11.7.5", - "pg": "^8.20.0", + "pg": "^8.21.0", "stream-spec": "~0.3.5", "ts-node": "^8.5.4", "typescript": "^6.0.3" @@ -54,6 +54,6 @@ "pg": "^8" }, "dependencies": { - "pg-cursor": "^2.19.0" + "pg-cursor": "^2.20.0" } } diff --git a/packages/pg/package.json b/packages/pg/package.json index d14e448d6..61f9b7ede 100644 --- a/packages/pg/package.json +++ b/packages/pg/package.json @@ -1,6 +1,6 @@ { "name": "pg", - "version": "8.20.0", + "version": "8.21.0", "description": "PostgreSQL client - pure javascript & libpq with the same API", "keywords": [ "database", @@ -32,9 +32,9 @@ "./lib/*.js": "./lib/*.js" }, "dependencies": { - "pg-connection-string": "^2.12.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -50,7 +50,7 @@ "wrangler": "^3.x" }, "optionalDependencies": { - "pg-cloudflare": "^1.3.0" + "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" From b7d640a71d70edfbb4199c5d2563358c1fccb665 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 17 Jun 2026 22:12:49 +0200 Subject: [PATCH 26/55] Add docs for supported PostgreSQL versions (#3690) --- docs/pages/index.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index ff27662e6..95a3fb6a6 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -5,8 +5,14 @@ slug: / import { Logo } from '/components/logo.tsx' +## Introduction + node-postgres is a collection of node.js modules for interfacing with your PostgreSQL database. It has support for callbacks, promises, async/await, connection pooling, prepared statements, cursors, streaming results, C/C++ bindings, rich type parsing, and more! Just like PostgreSQL itself there are a lot of features: this documentation aims to get you up and running quickly and in the right direction. It also tries to provide guides for more advanced & edge-case topics allowing you to tap into the full power of PostgreSQL from node.js. +## Compatibility + +node-postgres supports every version of the PostgreSQL database from 8.x to the most recent version of PostgreSQL. + ## Install ```bash From c4dfbba79a1337746c109d849f3365c9271a5def Mon Sep 17 00:00:00 2001 From: Kyle Cannon Date: Wed, 17 Jun 2026 13:23:56 -0700 Subject: [PATCH 27/55] perf(pg-protocol): encode length-prefixed strings in a single pass (#3681) * perf(pg-protocol): encode length-prefixed strings in a single pass Add Writer.addInt32PrefixedString, which writes a value's Int32 byte-length prefix immediately followed by its UTF-8 bytes, computing Buffer.byteLength once. The previous `addInt32(Buffer.byteLength(s)).addString(s)` pairing scanned each string three times. Used for Bind parameter values and the SASL initial response. Wire output is byte-identical; addInt32/addString are unchanged for other callers. Benchmark (packages/pg-protocol/bench/write-bench.js, alternating-sampled vs base): bind(2 small) +16% bind(10 mixed) +23% bind(unicode) +14% full insert seq +9% Adds a multi-byte unicode bind unit test asserting the Int32 prefix equals the UTF-8 byte length, not the char length. Co-Authored-By: Claude Opus 4.8 (1M context) * Update packages/pg-protocol/src/outbound-serializer.test.ts Co-authored-by: Charmander <~@charmander.me> --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Charmander <~@charmander.me> --- packages/pg-protocol/src/buffer-writer.ts | 20 +++++++++++++++++ .../src/outbound-serializer.test.ts | 22 +++++++++++++++++++ packages/pg-protocol/src/serializer.ts | 6 ++--- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/pg-protocol/src/buffer-writer.ts b/packages/pg-protocol/src/buffer-writer.ts index cebb0d9ed..d206f322a 100644 --- a/packages/pg-protocol/src/buffer-writer.ts +++ b/packages/pg-protocol/src/buffer-writer.ts @@ -58,6 +58,26 @@ export class Writer { return this } + // Write an Int32 byte-length prefix immediately followed by the string's UTF-8 + // bytes. Postgres' Bind wire format prefixes every parameter with its length, + // and doing it in one method computes Buffer.byteLength ONCE — the previous + // `addInt32(Buffer.byteLength(s)).addString(s)` pairing scanned the string + // three times (byteLength for the prefix, byteLength again inside addString, + // then the encode), which is costly for large text parameters. + public addInt32PrefixedString(string: string): Writer { + const len = Buffer.byteLength(string) + this.ensure(4 + len) + const buffer = this.buffer + let offset = this.offset + buffer[offset++] = (len >>> 24) & 0xff + buffer[offset++] = (len >>> 16) & 0xff + buffer[offset++] = (len >>> 8) & 0xff + buffer[offset++] = (len >>> 0) & 0xff + buffer.write(string, offset, 'utf-8') + this.offset = offset + len + return this + } + public add(otherBuffer: Buffer): Writer { this.ensure(otherBuffer.length) otherBuffer.copy(this.buffer, this.offset) diff --git a/packages/pg-protocol/src/outbound-serializer.test.ts b/packages/pg-protocol/src/outbound-serializer.test.ts index 0d3e387e4..856ead7b9 100644 --- a/packages/pg-protocol/src/outbound-serializer.test.ts +++ b/packages/pg-protocol/src/outbound-serializer.test.ts @@ -129,6 +129,28 @@ describe('serializer', () => { .join(true, 'B') assert.deepEqual(actual, expectedBuffer) }) + + it('encodes a multi-byte string param with its UTF-8 byte length, not char length', function () { + // Guards the single-pass addInt32PrefixedString write path: the Int32 + // length prefix must be the UTF-8 byte count, not String.length. 'héllo中🎉' + // is 7 code points / 8 UTF-16 code units but 13 UTF-8 bytes. + const value = 'héllo中🎉' + const bytes = Buffer.from(value, 'utf8') + assert.notEqual(bytes.length, value.length) // sanity: the divergence we're testing + const actual = serialize.bind({ values: [value] }) + const expectedBuffer = new BufferList() + .addCString('') // portal + .addCString('') // statement + .addInt16(1) // param format code count + .addInt16(0) // format code for the one value (text) + .addInt16(1) // value count + .addInt32(bytes.length) // 13 — the UTF-8 byte length, NOT value.length (8) + .add(bytes) + .addInt16(1) // result format code count + .addInt16(0) // result format (text) + .join(true, 'B') + assert.deepEqual(actual, expectedBuffer) + }) }) it('with custom valueMapper', function () { diff --git a/packages/pg-protocol/src/serializer.ts b/packages/pg-protocol/src/serializer.ts index bb0441f56..137daad79 100644 --- a/packages/pg-protocol/src/serializer.ts +++ b/packages/pg-protocol/src/serializer.ts @@ -48,7 +48,7 @@ const password = (password: string): Buffer => { const sendSASLInitialResponseMessage = function (mechanism: string, initialResponse: string): Buffer { // 0x70 = 'p' - writer.addCString(mechanism).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse) + writer.addCString(mechanism).addInt32PrefixedString(initialResponse) return writer.flush(code.startup) } @@ -135,8 +135,8 @@ const writeValues = function (values: any[], valueMapper?: ValueMapper): void { } else { // add the param type (string) to the writer writer.addInt16(ParamType.STRING) - paramWriter.addInt32(Buffer.byteLength(mappedVal)) - paramWriter.addString(mappedVal) + // length prefix + UTF-8 bytes in one pass (Buffer.byteLength computed once) + paramWriter.addInt32PrefixedString(mappedVal) } } } From 882fc308cce7bf136cd1448e00395f760dad3e00 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Thu, 18 Jun 2026 05:24:06 +0900 Subject: [PATCH 28/55] Add support for sslnegotiation=direct (PostgreSQL 17) (#3688) PostgreSQL 17 added the `sslnegotiation` connection parameter, which allows clients to start the TLS handshake immediately after the TCP connection ("direct" negotiation) instead of first sending an SSLRequest packet and waiting for the server's S/N reply ("postgres" negotiation, the default and prior behavior). Direct negotiation saves one network round-trip and works with protocol-agnostic TLS tooling. - connection.js: extract the TLS upgrade into upgradeToSSL(); in direct mode upgrade the socket right after connect (skipping the SSLRequest exchange) and advertise the `postgresql` ALPN protocol as the server requires. - client.js: forward sslNegotiation to the Connection and skip requestSsl() in direct mode. - connection-parameters.js: read sslnegotiation from config / PGSSLNEGOTIATION, validate it is `postgres` or `direct`, require SSL to be enabled for `direct`, and include it in the libpq connection string. - pg-connection-string: parse the sslnegotiation query param and enable SSL automatically when `direct` is requested without other SSL config. - docs: document the new option. - tests: cover connection-string parsing, connection-parameters validation, and the direct-vs-traditional connection behavior (no SSLRequest packet, ALPN set only for direct). Closes #3346 --- docs/pages/features/ssl.mdx | 25 +++++++ packages/pg-connection-string/index.d.ts | 1 + packages/pg-connection-string/index.js | 6 ++ packages/pg-connection-string/test/parse.ts | 23 ++++++ packages/pg/lib/client.js | 8 ++- packages/pg/lib/connection-parameters.js | 13 ++++ packages/pg/lib/connection.js | 65 +++++++++++------ packages/pg/lib/defaults.js | 3 + .../connection-parameters/creation-tests.js | 59 +++++++++++++++ .../pg/test/unit/connection/error-tests.js | 71 +++++++++++++++++++ 10 files changed, 251 insertions(+), 23 deletions(-) diff --git a/docs/pages/features/ssl.mdx b/docs/pages/features/ssl.mdx index 9983c0434..6a29ed739 100644 --- a/docs/pages/features/ssl.mdx +++ b/docs/pages/features/ssl.mdx @@ -49,6 +49,31 @@ const config = { } ``` +## Direct SSL negotiation + +By default node-postgres uses the traditional PostgreSQL SSL negotiation: it sends an `SSLRequest` packet, waits for the server to acknowledge it, and only then starts the TLS handshake. PostgreSQL 17 and newer also support _direct_ SSL negotiation, where the TLS handshake begins immediately on connect (similar to HTTPS), saving one network round-trip. + +To use direct negotiation, set `sslnegotiation: 'direct'`. SSL must be enabled, and the server must be PostgreSQL 17+ configured to accept direct SSL connections. + +```js +const config = { + database: 'database-name', + host: 'host-or-ip', + ssl: { rejectUnauthorized: false }, + sslnegotiation: 'direct', +} +``` + +It can also be supplied via a connection string. When `sslnegotiation=direct` is present, SSL is enabled automatically if not otherwise configured: + +```js +const config = { + connectionString: 'postgres://user:password@host:port/db?sslmode=require&sslnegotiation=direct', +} +``` + +Direct negotiation requests the `postgresql` ALPN protocol during the TLS handshake, as required by the server. The default value is `'postgres'`, which preserves the traditional `SSLRequest` behavior. You can also set the `PGSSLNEGOTIATION` environment variable. + ## Channel binding If the PostgreSQL server offers SCRAM-SHA-256-PLUS (i.e. channel binding) for TLS/SSL connections, you can enable this as follows: diff --git a/packages/pg-connection-string/index.d.ts b/packages/pg-connection-string/index.d.ts index 2ebe67534..4b305299e 100644 --- a/packages/pg-connection-string/index.d.ts +++ b/packages/pg-connection-string/index.d.ts @@ -22,6 +22,7 @@ export interface ConnectionOptions { database: string | null | undefined client_encoding?: string ssl?: boolean | string | SSLConfig + sslnegotiation?: 'postgres' | 'direct' application_name?: string fallback_application_name?: string diff --git a/packages/pg-connection-string/index.js b/packages/pg-connection-string/index.js index 4b8d7afb9..7ee302976 100644 --- a/packages/pg-connection-string/index.js +++ b/packages/pg-connection-string/index.js @@ -78,6 +78,12 @@ function parse(str, options = {}) { config.ssl = {} } + // sslnegotiation=direct implies SSL is in use (libpq requires sslmode>=require), + // so enable SSL if the connection string did not otherwise configure it. + if (config.sslnegotiation === 'direct' && config.ssl === undefined) { + config.ssl = true + } + // Only try to load fs if we expect to read from the disk const fs = config.sslcert || config.sslkey || config.sslrootcert ? require('fs') : null diff --git a/packages/pg-connection-string/test/parse.ts b/packages/pg-connection-string/test/parse.ts index 814e49c58..c2a537581 100644 --- a/packages/pg-connection-string/test/parse.ts +++ b/packages/pg-connection-string/test/parse.ts @@ -216,6 +216,29 @@ describe('parse', function () { subject.ssl?.should.equal(true) }) + it('configuration parameter sslnegotiation=direct', function () { + const connectionString = 'pg:///?sslnegotiation=direct' + const subject = parse(connectionString) + subject.sslnegotiation?.should.equal('direct') + // direct negotiation implies SSL is enabled + subject.ssl?.should.equal(true) + }) + + it('configuration parameter sslnegotiation=postgres', function () { + const connectionString = 'pg:///?sslnegotiation=postgres' + const subject = parse(connectionString) + subject.sslnegotiation?.should.equal('postgres') + // traditional negotiation does not change ssl + ;(subject.ssl === undefined).should.equal(true) + }) + + it('sslnegotiation=direct keeps an explicit ssl config', function () { + const connectionString = 'pg:///?sslnegotiation=direct&sslmode=require' + const subject = parse(connectionString) + subject.sslnegotiation?.should.equal('direct') + subject.ssl?.should.eql({}) + }) + it('configuration parameter sslcert=/path/to/cert', function () { const connectionString = 'pg:///?sslcert=' + __dirname + '/example.cert' const subject = parse(connectionString) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index d6c57194c..18280f3c6 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -91,6 +91,7 @@ class Client extends EventEmitter { new Connection({ stream: c.stream, ssl: this.connectionParameters.ssl, + sslNegotiation: this.connectionParameters.sslnegotiation, keepAlive: c.keepAlive || false, keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0, encoding: this.connectionParameters.client_encoding || 'utf8', @@ -100,6 +101,7 @@ class Client extends EventEmitter { this.processID = null this.secretKey = null this.ssl = this.connectionParameters.ssl || false + this.sslNegotiation = this.connectionParameters.sslnegotiation || 'postgres' // As with Password, make SSL->Key (the private key) non-enumerable. // It won't show up in stack traces // or if the client is console.logged @@ -177,7 +179,11 @@ class Client extends EventEmitter { // once connection is established send startup message con.on('connect', function () { if (self.ssl) { - con.requestSsl() + // With direct SSL negotiation the connection upgrades to TLS without an + // SSLRequest packet, so the startup message is sent after 'sslconnect'. + if (self.sslNegotiation !== 'direct') { + con.requestSsl() + } } else { con.startup(self.getStartupConf()) } diff --git a/packages/pg/lib/connection-parameters.js b/packages/pg/lib/connection-parameters.js index c153932bb..37987fd68 100644 --- a/packages/pg/lib/connection-parameters.js +++ b/packages/pg/lib/connection-parameters.js @@ -99,6 +99,18 @@ class ConnectionParameters { }) } + // How to negotiate SSL: 'postgres' (default, the traditional SSLRequest + // handshake) or 'direct' (start the TLS handshake immediately on connect). + this.sslnegotiation = val('sslnegotiation', config, 'PGSSLNEGOTIATION') + if (this.sslnegotiation !== undefined && this.sslnegotiation !== 'postgres' && this.sslnegotiation !== 'direct') { + throw new Error( + `Invalid sslnegotiation value: "${this.sslnegotiation}". Valid values are "postgres" and "direct".` + ) + } + if (this.sslnegotiation === 'direct' && !this.ssl) { + throw new Error('sslnegotiation=direct requires SSL to be enabled') + } + this.client_encoding = val('client_encoding', config) this.replication = val('replication', config) // a domain socket begins with '/' @@ -144,6 +156,7 @@ class ConnectionParameters { add(params, ssl, 'sslkey') add(params, ssl, 'sslcert') add(params, ssl, 'sslrootcert') + add(params, this, 'sslnegotiation') if (this.database) { params.push('dbname=' + quoteParamValue(this.database)) diff --git a/packages/pg/lib/connection.js b/packages/pg/lib/connection.js index 027f93935..63cc13a53 100644 --- a/packages/pg/lib/connection.js +++ b/packages/pg/lib/connection.js @@ -3,7 +3,8 @@ const EventEmitter = require('events').EventEmitter const { parse, serialize } = require('pg-protocol') -const { getStream, getSecureStream } = require('./stream') +const stream = require('./stream') +const { getStream } = stream const flushBuffer = serialize.flush() const syncBuffer = serialize.sync() @@ -24,6 +25,7 @@ class Connection extends EventEmitter { this._keepAliveInitialDelayMillis = config.keepAliveInitialDelayMillis this.parsedStatements = {} this.ssl = config.ssl || false + this.sslNegotiation = config.sslNegotiation || 'postgres' this._ending = false this._emitMessage = false const self = this @@ -65,6 +67,14 @@ class Connection extends EventEmitter { return this.attachListeners(this.stream) } + // With direct SSL negotiation the TLS handshake starts immediately on the + // raw socket, skipping the SSLRequest packet and the server's 'S'/'N' reply. + if (this.sslNegotiation === 'direct') { + return this.stream.once('connect', function () { + self.upgradeToSSL(host, reportStreamError) + }) + } + this.stream.once('data', function (buffer) { const responseCode = buffer.toString('utf8') switch (responseCode) { @@ -78,32 +88,43 @@ class Connection extends EventEmitter { self.stream.end() return self.emit('error', new Error('There was an error establishing an SSL connection')) } - const options = { - socket: self.stream, - } + self.upgradeToSSL(host, reportStreamError) + }) + } - if (self.ssl !== true) { - Object.assign(options, self.ssl) + upgradeToSSL(host, reportStreamError) { + const self = this + const options = { + socket: self.stream, + } - if ('key' in self.ssl) { - options.key = self.ssl.key - } - } + if (self.ssl !== true) { + Object.assign(options, self.ssl) - const net = require('net') - if (net.isIP && net.isIP(host) === 0) { - options.servername = host + if ('key' in self.ssl) { + options.key = self.ssl.key } - try { - self.stream = getSecureStream(options) - } catch (err) { - return self.emit('error', err) - } - self.attachListeners(self.stream) - self.stream.on('error', reportStreamError) + } - self.emit('sslconnect') - }) + // Direct SSL negotiation requires ALPN so the server can confirm it is + // speaking the PostgreSQL protocol over the TLS connection. + if (self.sslNegotiation === 'direct') { + options.ALPNProtocols = ['postgresql'] + } + + const net = require('net') + if (net.isIP && net.isIP(host) === 0) { + options.servername = host + } + try { + self.stream = stream.getSecureStream(options) + } catch (err) { + return self.emit('error', err) + } + self.attachListeners(self.stream) + self.stream.on('error', reportStreamError) + + self.emit('sslconnect') } attachListeners(stream) { diff --git a/packages/pg/lib/defaults.js b/packages/pg/lib/defaults.js index 673696f79..427243f50 100644 --- a/packages/pg/lib/defaults.js +++ b/packages/pg/lib/defaults.js @@ -49,6 +49,9 @@ module.exports = { ssl: false, + // SSL negotiation style: 'postgres' (traditional SSLRequest) or 'direct' + sslnegotiation: undefined, + application_name: undefined, fallback_application_name: undefined, diff --git a/packages/pg/test/unit/connection-parameters/creation-tests.js b/packages/pg/test/unit/connection-parameters/creation-tests.js index bb6f815a0..e326e2630 100644 --- a/packages/pg/test/unit/connection-parameters/creation-tests.js +++ b/packages/pg/test/unit/connection-parameters/creation-tests.js @@ -358,3 +358,62 @@ suite.test('ssl is set on client', function () { }) ) }) + +suite.test('sslnegotiation defaults to undefined', function () { + const subject = new ConnectionParameters({}) + assert.strictEqual(subject.sslnegotiation, undefined) +}) + +suite.test('sslnegotiation=direct is read from config', function () { + const subject = new ConnectionParameters({ ssl: true, sslnegotiation: 'direct' }) + assert.strictEqual(subject.sslnegotiation, 'direct') +}) + +suite.test('sslnegotiation=postgres is read from config', function () { + const subject = new ConnectionParameters({ ssl: true, sslnegotiation: 'postgres' }) + assert.strictEqual(subject.sslnegotiation, 'postgres') +}) + +suite.test('sslnegotiation rejects invalid values', function () { + assert.throws(() => new ConnectionParameters({ ssl: true, sslnegotiation: 'bogus' }), /Invalid sslnegotiation value/) +}) + +suite.test('sslnegotiation=direct requires ssl', function () { + assert.throws(() => new ConnectionParameters({ ssl: false, sslnegotiation: 'direct' }), /requires SSL to be enabled/) +}) + +suite.test('sslnegotiation is read from PGSSLNEGOTIATION env var', function () { + const original = process.env.PGSSLNEGOTIATION + process.env.PGSSLNEGOTIATION = 'direct' + try { + const subject = new ConnectionParameters({ ssl: true }) + assert.strictEqual(subject.sslnegotiation, 'direct') + } finally { + if (original === undefined) { + delete process.env.PGSSLNEGOTIATION + } else { + process.env.PGSSLNEGOTIATION = original + } + } +}) + +suite.test('sslnegotiation is included in libpq connection string', function () { + const subject = new ConnectionParameters({ + user: 'brian', + host: 'localhost', + port: 5432, + database: 'postgres', + ssl: true, + sslnegotiation: 'direct', + }) + subject.getLibpqConnectionString( + assert.calls(function (err, pgCString) { + assert(!err) + assert.equal( + pgCString.indexOf("sslnegotiation='direct'") !== -1, + true, + 'libpqConnectionString should contain sslnegotiation' + ) + }) + ) +}) diff --git a/packages/pg/test/unit/connection/error-tests.js b/packages/pg/test/unit/connection/error-tests.js index 2171a25b6..04f1c3f4b 100644 --- a/packages/pg/test/unit/connection/error-tests.js +++ b/packages/pg/test/unit/connection/error-tests.js @@ -60,6 +60,77 @@ const SSLNegotiationPacketTests = [ }, ] +suite.test('direct SSL negotiation upgrades to TLS without an SSLRequest packet', function (done) { + const con = new Connection({ stream: new MemoryStream(), ssl: true, sslNegotiation: 'direct' }) + + // capture the upgrade instead of performing a real TLS handshake + let upgradeCalled = false + con.upgradeToSSL = function () { + upgradeCalled = true + } + + con.connect(1234, 'localhost') + + // simulate the raw socket connecting + con.stream.emit('connect') + + // no SSLRequest packet should have been written to the underlying stream + assert.equal(con.stream.packets.length, 0, 'direct negotiation must not send an SSLRequest packet') + assert.equal(upgradeCalled, true, 'direct negotiation must upgrade to TLS on connect') + done() +}) + +suite.test('direct SSL negotiation passes ALPN protocol to the secure stream', function (done) { + const streamModule = require('../../../lib/stream') + const originalGetSecureStream = streamModule.getSecureStream + + let capturedOptions = null + streamModule.getSecureStream = function (options) { + capturedOptions = options + return options.socket + } + + try { + const con = new Connection({ stream: new MemoryStream(), ssl: true, sslNegotiation: 'direct' }) + con.connect(1234, 'localhost') + con.stream.emit('connect') + + assert(capturedOptions, 'getSecureStream should have been called') + assert.deepEqual( + capturedOptions.ALPNProtocols, + ['postgresql'], + 'direct negotiation must request the postgresql ALPN protocol' + ) + done() + } finally { + streamModule.getSecureStream = originalGetSecureStream + } +}) + +suite.test('traditional SSL negotiation does not set ALPN protocol', function (done) { + const streamModule = require('../../../lib/stream') + const originalGetSecureStream = streamModule.getSecureStream + + let capturedOptions = null + streamModule.getSecureStream = function (options) { + capturedOptions = options + return options.socket + } + + try { + const con = new Connection({ stream: new MemoryStream(), ssl: true }) + con.connect(1234, 'localhost') + // traditional path: server signals SSL support with an 'S' byte + con.stream.emit('data', Buffer.from('S')) + + assert(capturedOptions, 'getSecureStream should have been called') + assert.equal(capturedOptions.ALPNProtocols, undefined, 'traditional negotiation must not request ALPN') + done() + } finally { + streamModule.getSecureStream = originalGetSecureStream + } +}) + for (const tc of SSLNegotiationPacketTests) { suite.test(tc.testName, function (done) { // our fake postgres server From d7175a4aa0347b7416109e9ecc61d4d235486d0e Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Thu, 18 Jun 2026 20:27:56 -0400 Subject: [PATCH 29/55] Expand CI matrix of PG versions and add direct SSL test (#3693) * Remove unused os block from GHA CI matrix * Add historical postgres versions to GHA test matrix Adds testing of historical postgres versions from 13 to 18 to the GitHub Actions CI matrix. They are each tested with the latest node version. Older node versions are only tested against the latest postgresql server version. Both the stable server version and latest node version are defined as YAML anchors to allow updating them in a single place in the future. For v18+ the container requires an explicit PGDATA directory parent path so we add that as well. This applies to older versions too but should not be an issue. * Add direct SSL test that only runs against v17+ --- .github/workflows/ci.yml | 33 +++++++---- .../pg/test/integration/client/ssl-tests.js | 55 +++++++++++++++++++ 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a266291d..6f0751c56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,28 +25,39 @@ jobs: needs: lint services: postgres: - image: ghcr.io/railwayapp-templates/postgres-ssl + image: ghcr.io/railwayapp-templates/postgres-ssl:${{ matrix.postgres }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_HOST_AUTH_METHOD: 'md5' POSTGRES_DB: ci_db_test + # PostgreSQL 18's official image defaults PGDATA to a versioned + # subdirectory (/var/lib/postgresql/18/docker), but the + # railwayapp-templates/postgres-ssl entrypoint requires PGDATA to + # start with /var/lib/postgresql/data so we pin it explicitly. This is + # also the default for the older images, so it is a no-op there. + PGDATA: /var/lib/postgresql/data ports: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 strategy: fail-fast: false matrix: - node: - - '16' - - '18' - - '20' - - '22' - - '24' - - '26' - os: - - ubuntu-latest - name: Node.js ${{ matrix.node }} + include: + # Historical Node.js versions tested against a single PostgreSQL version + - { node: '16', postgres: &stable_postgres '18' } + - { node: '18', postgres: *stable_postgres } + - { node: '20', postgres: *stable_postgres } + - { node: '22', postgres: *stable_postgres } + - { node: '24', postgres: *stable_postgres } + # Latest Node.js version tested against multiple PostgreSQL versions + - { node: &latest_node '26', postgres: '13' } + - { node: *latest_node, postgres: '14' } + - { node: *latest_node, postgres: '15' } + - { node: *latest_node, postgres: '16' } + - { node: *latest_node, postgres: '17' } + - { node: *latest_node, postgres: '18' } + name: Node.js ${{ matrix.node }} x PostgreSQL ${{ matrix.postgres }} runs-on: ubuntu-latest env: PGUSER: postgres diff --git a/packages/pg/test/integration/client/ssl-tests.js b/packages/pg/test/integration/client/ssl-tests.js index 33919cdf8..b5b32c5ba 100644 --- a/packages/pg/test/integration/client/ssl-tests.js +++ b/packages/pg/test/integration/client/ssl-tests.js @@ -22,3 +22,58 @@ suite.test('can connect with ssl', function (done) { }) ) }) + +async function getServerVersionNum() { + const client = new helper.pg.Client(helper.config) + await client.connect() + try { + const { + rows: [row], + } = await client.query('SHOW server_version_num') + return parseInt(row.server_version_num, 10) + } finally { + await client.end() + } +} + +// The native client forwards sslnegotiation=direct to libpq, whose support +// for direct SSL depends on the linked libpq version (17+) rather than on +// this library. It also does not expose the underlying TLS socket, so the +// direct-negotiation check below is impossible. So we only test the pure-JS client. +if (!helper.args.native) { + suite.test('can connect with direct SSL negotiation', async () => { + // Direct SSL negotiation (sslnegotiation=direct) is only supported by + // PostgreSQL 17 and newer servers. Probe the server version first and skip + // on older servers rather than failing the test. + const serverVersionNum = await getServerVersionNum() + if (serverVersionNum < 170000) { + console.log(`(skipped: direct SSL requires PostgreSQL 17+, server_version_num=${serverVersionNum}) `) + return + } + + const config = { + ...helper.config, + ssl: { rejectUnauthorized: false }, + sslnegotiation: 'direct', + } + const client = new helper.pg.Client(config) + await client.connect() + const { rows } = await client.query('SELECT NOW()') + assert.strictEqual(rows.length, 1) + + // Verify the connection actually used direct SSL negotiation rather than + // silently falling back to the traditional SSLRequest handshake. pg only + // sends the 'postgresql' ALPN protocol on a direct SSL handshake (see + // Connection#upgradeToSSL), and a PostgreSQL 17+ server echoes it back, so + // its presence on the negotiated TLS socket confirms direct negotiation. + const tlsSocket = client.connection.stream + assert.ok(tlsSocket.encrypted, 'expected the connection to be upgraded to a TLS socket') + assert.strictEqual( + tlsSocket.alpnProtocol, + 'postgresql', + 'expected direct SSL negotiation to select the "postgresql" ALPN protocol' + ) + + await client.end() + }) +} From f49ab4a9795ae0866409f9bfe52a68b4f65ef024 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Fri, 19 Jun 2026 09:29:00 +0900 Subject: [PATCH 30/55] fix: correct spelling mistakes across codebase (#3692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: correct spelling mistakes across codebase - "bye" → "byte" in parser.ts comment - "interal" → "internal" in CHANGELOG.md - "PostgresSQL" → "PostgreSQL" in pg-connection-string package.json and README.md - "immediatley" → "immediately" in network-partition-tests.js - "connectet" → "connected" in network-partition-tests.js * revert: restore original CHANGELOG.md spelling CHANGELOG is a historical record of past releases and should not be modified. Reverting the "interal" → "internal" fix from 7cc57c1d. --- packages/pg-connection-string/README.md | 2 +- packages/pg-connection-string/package.json | 2 +- packages/pg-protocol/src/parser.ts | 2 +- .../pg/test/integration/client/network-partition-tests.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pg-connection-string/README.md b/packages/pg-connection-string/README.md index e47adc816..5475f63bf 100644 --- a/packages/pg-connection-string/README.md +++ b/packages/pg-connection-string/README.md @@ -3,7 +3,7 @@ pg-connection-string [![NPM](https://nodei.co/npm/pg-connection-string.png?compact=true)](https://nodei.co/npm/pg-connection-string/) -Functions for dealing with a PostgresSQL connection string +Functions for dealing with a PostgreSQL connection string `parse` method taken from [node-postgres](https://github.com/brianc/node-postgres.git) Copyright (c) 2010-2014 Brian Carlson (brian.m.carlson@gmail.com) diff --git a/packages/pg-connection-string/package.json b/packages/pg-connection-string/package.json index aa075e154..ffa6705af 100644 --- a/packages/pg-connection-string/package.json +++ b/packages/pg-connection-string/package.json @@ -1,7 +1,7 @@ { "name": "pg-connection-string", "version": "2.13.0", - "description": "Functions for dealing with a PostgresSQL connection string", + "description": "Functions for dealing with a PostgreSQL connection string", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/packages/pg-protocol/src/parser.ts b/packages/pg-protocol/src/parser.ts index 998077a00..3d8ce80c7 100644 --- a/packages/pg-protocol/src/parser.ts +++ b/packages/pg-protocol/src/parser.ts @@ -28,7 +28,7 @@ import { } from './messages' import { BufferReader } from './buffer-reader' -// every message is prefixed with a single bye +// every message is prefixed with a single byte const CODE_LENGTH = 1 // every message has an int32 length which includes itself but does // NOT include the code in the length diff --git a/packages/pg/test/integration/client/network-partition-tests.js b/packages/pg/test/integration/client/network-partition-tests.js index 6ebdb8b45..362a40abc 100644 --- a/packages/pg/test/integration/client/network-partition-tests.js +++ b/packages/pg/test/integration/client/network-partition-tests.js @@ -14,8 +14,8 @@ const Server = function (response) { Server.prototype.start = function (cb) { // this is our fake postgres server - // it responds with our specified response immediatley after receiving every buffer - // this is sufficient into convincing the client its connectet to a valid backend + // it responds with our specified response immediately after receiving every buffer + // this is sufficient into convincing the client its connected to a valid backend // if we respond with a readyForQuery message this.server = net.createServer( function (socket) { From 695fe6180401e630a78f50f81b9909b9942f6035 Mon Sep 17 00:00:00 2001 From: Brian C Date: Thu, 18 Jun 2026 19:33:15 -0500 Subject: [PATCH 31/55] Stop cstring reading on any falsy value (#3691) --- .gitignore | 1 + packages/pg-protocol/src/buffer-reader.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8e242c10d..c6d5700ae 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ dist /.eslintcache .vscode/ manually-test-on-heroku.js +tsconfig.tsbuildinfo diff --git a/packages/pg-protocol/src/buffer-reader.ts b/packages/pg-protocol/src/buffer-reader.ts index c9d9c2b66..42a4a23fa 100644 --- a/packages/pg-protocol/src/buffer-reader.ts +++ b/packages/pg-protocol/src/buffer-reader.ts @@ -45,7 +45,7 @@ export class BufferReader { const start = this.offset let end = start // eslint-disable-next-line no-empty - while (this.buffer[end++] !== 0) {} + while (this.buffer[end++]) {} this.offset = end return this.buffer.toString(this.encoding, start, end - 1) } From 835fb83ab9e1cf30fa8367ba42bd633720d71832 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 19 Jun 2026 02:35:17 +0200 Subject: [PATCH 32/55] Fix error handling for exceptions on values parsing. (#3574) * fix(pg-protocol): reset Writer state when error throws from valueMapper in bind() When valueMapper throws during serialize.bind(), the module-level singleton Writer instances (writer and paramWriter) are left with partial data from the interrupted operation. This corrupts all subsequent serializer calls since they share the same Writer instances. Add Writer.clear() method that resets the write cursor without allocating a new buffer (zero overhead on the happy path). Wrap writeValues() in bind() with try-catch to clear both writers on error. * fix(pg): send Close and Sync when bind serialization throws When connection.bind() throws during prepare(), the catch block previously called handleError() without sending any protocol messages. Since PARSE was already buffered (due to cork), the server processes it and waits for more messages, leaving the connection hung. Send Close to clean up the parsed statement and Sync to return the connection to a usable state. --- packages/pg-protocol/src/buffer-writer.ts | 5 ++ .../src/outbound-serializer.test.ts | 58 +++++++++++++ packages/pg-protocol/src/serializer.ts | 8 +- packages/pg/lib/query.js | 4 + .../test/unit/client/throw-in-bind-tests.js | 86 +++++++++++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 packages/pg/test/unit/client/throw-in-bind-tests.js diff --git a/packages/pg-protocol/src/buffer-writer.ts b/packages/pg-protocol/src/buffer-writer.ts index d206f322a..9eae65859 100644 --- a/packages/pg-protocol/src/buffer-writer.ts +++ b/packages/pg-protocol/src/buffer-writer.ts @@ -102,4 +102,9 @@ export class Writer { this.buffer = Buffer.allocUnsafe(this.size) return result } + + public clear(): void { + this.offset = 5 + this.headerPosition = 0 + } } diff --git a/packages/pg-protocol/src/outbound-serializer.test.ts b/packages/pg-protocol/src/outbound-serializer.test.ts index 856ead7b9..aac0c57ba 100644 --- a/packages/pg-protocol/src/outbound-serializer.test.ts +++ b/packages/pg-protocol/src/outbound-serializer.test.ts @@ -295,4 +295,62 @@ describe('serializer', () => { const expected = new BufferList().addInt16(1234).addInt16(5678).addInt32(3).addInt32(4).join(true) assert.deepEqual(actual, expected) }) + + describe('bind error recovery', () => { + const throwingMapper = () => { + throw new Error('valueMapper error') + } + + it('produces correct bind output after a valueMapper exception', () => { + assert.throws(() => { + serialize.bind({ + values: ['fail'], + valueMapper: throwingMapper, + }) + }, /valueMapper error/) + + const actual = serialize.bind({ + portal: 'bang', + statement: 'woo', + values: ['1', 'hi', null, 'zing'], + }) + const expectedBuffer = new BufferList() + .addCString('bang') + .addCString('woo') + .addInt16(4) + .addInt16(0) + .addInt16(0) + .addInt16(0) + .addInt16(0) + .addInt16(4) + .addInt32(1) + .add(Buffer.from('1')) + .addInt32(2) + .add(Buffer.from('hi')) + .addInt32(-1) + .addInt32(4) + .add(Buffer.from('zing')) + .addInt16(1) + .addInt16(0) + .join(true, 'B') + assert.deepEqual(actual, expectedBuffer) + }) + + it('produces correct output from other serializer methods after a failed bind', () => { + assert.throws(() => { + serialize.bind({ + values: ['fail'], + valueMapper: throwingMapper, + }) + }, /valueMapper error/) + + const parseActual = serialize.parse({ text: '!' }) + const parseExpected = new BufferList().addCString('').addCString('!').addInt16(0).join(true, 'P') + assert.deepEqual(parseActual, parseExpected) + + const queryActual = serialize.query('select 1') + const queryExpected = new BufferList().addCString('select 1').join(true, 'Q') + assert.deepEqual(queryActual, queryExpected) + }) + }) }) diff --git a/packages/pg-protocol/src/serializer.ts b/packages/pg-protocol/src/serializer.ts index 137daad79..547c053f8 100644 --- a/packages/pg-protocol/src/serializer.ts +++ b/packages/pg-protocol/src/serializer.ts @@ -152,7 +152,13 @@ const bind = (config: BindOpts = {}): Buffer => { writer.addCString(portal).addCString(statement) writer.addInt16(len) - writeValues(values, config.valueMapper) + try { + writeValues(values, config.valueMapper) + } catch (err) { + writer.clear() + paramWriter.clear() + throw err + } writer.addInt16(len) writer.add(paramWriter.flush()) diff --git a/packages/pg/lib/query.js b/packages/pg/lib/query.js index 64aab5ff2..04e1c1d65 100644 --- a/packages/pg/lib/query.js +++ b/packages/pg/lib/query.js @@ -228,6 +228,10 @@ class Query extends EventEmitter { valueMapper: utils.prepareValue, }) } catch (err) { + // we should close parse to avoid leaking connections + connection.close({ type: 'S', name: this.name }) + connection.sync() + this.handleError(err, connection) return } diff --git a/packages/pg/test/unit/client/throw-in-bind-tests.js b/packages/pg/test/unit/client/throw-in-bind-tests.js new file mode 100644 index 000000000..8b460b9e4 --- /dev/null +++ b/packages/pg/test/unit/client/throw-in-bind-tests.js @@ -0,0 +1,86 @@ +'use strict' +const helper = require('./test-helper') +const Query = require('../../../lib/query') +const assert = require('assert') + +const suite = new helper.Suite() + +const bindError = new Error('TEST: Throw in bind') + +const setupClient = function () { + const client = helper.client() + const con = client.connection + const calls = { parse: 0, sync: 0, describe: 0, execute: 0, close: 0 } + + con.parse = function () { + calls.parse++ + } + con.bind = function () { + throw bindError + } + con.describe = function () { + calls.describe++ + assert.fail('describe should not be called when bind throws') + } + con.execute = function () { + calls.execute++ + assert.fail('execute should not be called when bind throws') + } + con.close = function () { + calls.close++ + } + con.sync = function () { + calls.sync++ + } + + return { client, con, calls } +} + +suite.test('calls callback with error when bind throws', function (done) { + const { client, con, calls } = setupClient() + con.emit('readyForQuery') + client.query( + new Query({ + text: 'select $1', + values: ['x'], + callback: function (err) { + assert.equal(err, bindError) + assert.equal(calls.sync, 1, 'sync should be called once') + assert.equal(calls.describe, 0, 'describe should not be called') + assert.equal(calls.execute, 0, 'execute should not be called') + done() + }, + }) + ) +}) + +suite.test('emits error event when bind throws (no callback)', function (done) { + const { client, con, calls } = setupClient() + con.emit('readyForQuery') + const query = new Query({ + text: 'select $1', + values: ['x'], + }) + query.on('error', function (err) { + assert.equal(err, bindError) + assert.equal(calls.sync, 1, 'sync should be called once') + done() + }) + client.query(query) +}) + +suite.test('send close when bind throws', function (done) { + const { client, con, calls } = setupClient() + con.emit('readyForQuery') + client.query( + new Query({ + text: 'select $1', + values: ['x'], + callback: function (err) { + assert.equal(err, bindError) + assert.equal(calls.close, 1, 'close should be called') + done() + }, + }) + ) +}) From 7bc35ca5c300ab3dc4763bc7c50a4051aec86b85 Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Fri, 19 Jun 2026 02:38:09 +0200 Subject: [PATCH 33/55] docs: add fastest (#3540) see https://github.com/brianc/node-postgres/issues/3538#issuecomment-3261371631 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ca28ce5d..c2f5c247c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ NPM version NPM downloads -Non-blocking PostgreSQL client for Node.js. Pure JavaScript and optional native libpq bindings. +Non-blocking [Truly Fastest](https://github.com/nigrosimone/postgres-benchmarks) PostgreSQL client for Node.js. Pure JavaScript and optional native libpq bindings. ## Monorepo From d80b2612fbe83ed8234637f20b943d85e4331094 Mon Sep 17 00:00:00 2001 From: "Brian M. Carlson" Date: Thu, 18 Jun 2026 19:52:30 -0500 Subject: [PATCH 34/55] Update docs & changelog --- CHANGELOG.md | 4 ++++ README.md | 3 ++- docs/pages/index.mdx | 10 ++++++---- packages/pg/README.md | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd26374a1..d4cf0fedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ For richer information consult the commit log on github with referenced pull req We do not include break-fix version release in this file. +## pg@8.22.0 + +- Add support for [sslnegotiation=direct](https://github.com/brianc/node-postgres/pull/3688) for PostgreSQL 17+. + ## pg@8.21.0 - Handle [SASL SCRAM](https://github.com/brianc/node-postgres/pull/3521) server error responses properly. diff --git a/README.md b/README.md index c2f5c247c..ecd94e792 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ NPM version NPM downloads -Non-blocking [Truly Fastest](https://github.com/nigrosimone/postgres-benchmarks) PostgreSQL client for Node.js. Pure JavaScript and optional native libpq bindings. +Non-blocking PostgreSQL client for Node.js (and bun, deno, cloudflare, etc...). Pure JavaScript and optional native libpq bindings. ## Monorepo @@ -34,6 +34,7 @@ The source repo for the documentation is available for contribution [here](https ### Features +- [Fastest PostgreSQL client for Node.js](https://github.com/nigrosimone/postgres-benchmarks) - Pure JavaScript client and native libpq bindings share _the same API_ - Connection pooling - Extensible JS ↔ PostgreSQL data-type coercion diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index 95a3fb6a6..d0ab0edc6 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -13,6 +13,8 @@ node-postgres is a collection of node.js modules for interfacing with your Postg node-postgres supports every version of the PostgreSQL database from 8.x to the most recent version of PostgreSQL. +node-postgres supports all current and LTS versions of node as well as bun, deno, and cloudflare workers. + ## Install ```bash @@ -66,12 +68,12 @@ import { Client } from 'pg' const client = await new Client().connect() try { - const res = await client.query('SELECT $1::text as message', ['Hello world!']) - console.log(res.rows[0].message) // Hello world! + const res = await client.query('SELECT $1::text as message', ['Hello world!']) + console.log(res.rows[0].message) // Hello world! } catch (err) { - console.error(err); + console.error(err) } finally { - await client.end() + await client.end() } ``` diff --git a/packages/pg/README.md b/packages/pg/README.md index 75242374c..2fca0eb10 100644 --- a/packages/pg/README.md +++ b/packages/pg/README.md @@ -18,6 +18,7 @@ $ npm install pg ### Features +- [Fastest PostgreSQL client for Node.js](https://github.com/nigrosimone/postgres-benchmarks) - Pure JavaScript client and native libpq bindings share _the same API_ - Connection pooling - Extensible JS ↔ PostgreSQL data-type coercion From b617619f9fb6fbd231731823e2732a2927ded4be Mon Sep 17 00:00:00 2001 From: "Brian M. Carlson" Date: Thu, 18 Jun 2026 19:52:53 -0500 Subject: [PATCH 35/55] Publish - pg-connection-string@2.14.0 - pg-cursor@2.21.0 - pg-esm-test@1.8.0 - pg-protocol@1.15.0 - pg-query-stream@4.16.0 - pg@8.22.0 --- packages/pg-connection-string/package.json | 2 +- packages/pg-cursor/package.json | 4 ++-- packages/pg-esm-test/package.json | 10 +++++----- packages/pg-protocol/package.json | 2 +- packages/pg-query-stream/package.json | 6 +++--- packages/pg/package.json | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/pg-connection-string/package.json b/packages/pg-connection-string/package.json index ffa6705af..cf0ac743b 100644 --- a/packages/pg-connection-string/package.json +++ b/packages/pg-connection-string/package.json @@ -1,6 +1,6 @@ { "name": "pg-connection-string", - "version": "2.13.0", + "version": "2.14.0", "description": "Functions for dealing with a PostgreSQL connection string", "main": "./index.js", "types": "./index.d.ts", diff --git a/packages/pg-cursor/package.json b/packages/pg-cursor/package.json index 3949d50c8..9dc4ef7ff 100644 --- a/packages/pg-cursor/package.json +++ b/packages/pg-cursor/package.json @@ -1,6 +1,6 @@ { "name": "pg-cursor", - "version": "2.20.0", + "version": "2.21.0", "description": "Query cursor extension for node-postgres", "main": "index.js", "exports": { @@ -25,7 +25,7 @@ "license": "MIT", "devDependencies": { "mocha": "^11.7.5", - "pg": "^8.21.0" + "pg": "^8.22.0" }, "peerDependencies": { "pg": "^8" diff --git a/packages/pg-esm-test/package.json b/packages/pg-esm-test/package.json index ec3c4a811..88370652a 100644 --- a/packages/pg-esm-test/package.json +++ b/packages/pg-esm-test/package.json @@ -1,6 +1,6 @@ { "name": "pg-esm-test", - "version": "1.7.0", + "version": "1.8.0", "description": "A test module for PostgreSQL with ESM support", "main": "index.js", "type": "module", @@ -14,13 +14,13 @@ "test" ], "devDependencies": { - "pg": "^8.21.0", + "pg": "^8.22.0", "pg-cloudflare": "^1.4.0", - "pg-cursor": "^2.20.0", + "pg-cursor": "^2.21.0", "pg-native": "^3.8.0", "pg-pool": "^3.14.0", - "pg-protocol": "^1.14.0", - "pg-query-stream": "^4.15.0" + "pg-protocol": "^1.15.0", + "pg-query-stream": "^4.16.0" }, "author": "Brian M. Carlson ", "license": "MIT" diff --git a/packages/pg-protocol/package.json b/packages/pg-protocol/package.json index 4ee3e3f17..5279b7456 100644 --- a/packages/pg-protocol/package.json +++ b/packages/pg-protocol/package.json @@ -1,6 +1,6 @@ { "name": "pg-protocol", - "version": "1.14.0", + "version": "1.15.0", "description": "The postgres client/server binary protocol, implemented in TypeScript", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/pg-query-stream/package.json b/packages/pg-query-stream/package.json index 6704fcfb9..00969c496 100644 --- a/packages/pg-query-stream/package.json +++ b/packages/pg-query-stream/package.json @@ -1,6 +1,6 @@ { "name": "pg-query-stream", - "version": "4.15.0", + "version": "4.16.0", "description": "Postgres query result returned as readable stream", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -45,7 +45,7 @@ "concat-stream": "~1.0.1", "eslint-plugin-promise": "^7.3.0", "mocha": "^11.7.5", - "pg": "^8.21.0", + "pg": "^8.22.0", "stream-spec": "~0.3.5", "ts-node": "^8.5.4", "typescript": "^6.0.3" @@ -54,6 +54,6 @@ "pg": "^8" }, "dependencies": { - "pg-cursor": "^2.20.0" + "pg-cursor": "^2.21.0" } } diff --git a/packages/pg/package.json b/packages/pg/package.json index 61f9b7ede..f8f614804 100644 --- a/packages/pg/package.json +++ b/packages/pg/package.json @@ -1,6 +1,6 @@ { "name": "pg", - "version": "8.21.0", + "version": "8.22.0", "description": "PostgreSQL client - pure javascript & libpq with the same API", "keywords": [ "database", @@ -32,9 +32,9 @@ "./lib/*.js": "./lib/*.js" }, "dependencies": { - "pg-connection-string": "^2.13.0", + "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", - "pg-protocol": "^1.14.0", + "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, From 1a38e1d774fd77a5f2ab37baa2d35720a1146672 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Mon, 22 Jun 2026 13:49:57 -0700 Subject: [PATCH 36/55] =?UTF-8?q?Remove=20unused=20=E2=80=9Cchunky?= =?UTF-8?q?=E2=80=9D=20dev=20dependency=20and=20associated=20leftovers=20(?= =?UTF-8?q?#3697)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unused since 766e48f34a5efaf52cfdc545230aaccfbb3d5107 (Update types & move some configs around). --- packages/pg-protocol/package.json | 1 - packages/pg-protocol/src/types/chunky.d.ts | 1 - packages/pg-protocol/tsconfig.json | 3 +-- yarn.lock | 5 ----- 4 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 packages/pg-protocol/src/types/chunky.d.ts diff --git a/packages/pg-protocol/package.json b/packages/pg-protocol/package.json index 5279b7456..979b2f13a 100644 --- a/packages/pg-protocol/package.json +++ b/packages/pg-protocol/package.json @@ -19,7 +19,6 @@ "@types/mocha": "^10.0.10", "@types/node": "^16", "chai": "^4.2.0", - "chunky": "^0.0.0", "mocha": "^11.7.5", "ts-node": "^8.5.4", "typescript": "^6.0.3" diff --git a/packages/pg-protocol/src/types/chunky.d.ts b/packages/pg-protocol/src/types/chunky.d.ts deleted file mode 100644 index 7389bda66..000000000 --- a/packages/pg-protocol/src/types/chunky.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'chunky' diff --git a/packages/pg-protocol/tsconfig.json b/packages/pg-protocol/tsconfig.json index e09c03cd7..7b31d4d92 100644 --- a/packages/pg-protocol/tsconfig.json +++ b/packages/pg-protocol/tsconfig.json @@ -14,8 +14,7 @@ "declaration": true, "paths": { "*": [ - "./node_modules/*", - "./src/types/*" + "./node_modules/*" ] }, "types": [ diff --git a/yarn.lock b/yarn.lock index 92028ecc8..e91121be7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3195,11 +3195,6 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -chunky@^0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/chunky/-/chunky-0.0.0.tgz" - integrity sha1-HnWAojwIOJfSrWYkWefv2EZfYIo= - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" From e35aed91cb8f04116b3557e09adf33e2677af1ed Mon Sep 17 00:00:00 2001 From: Robert Nilsson <45368533+bobnil@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:40:31 +0200 Subject: [PATCH 37/55] feat(types): expose QueryStream.Config (#3710) --- packages/pg-query-stream/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pg-query-stream/src/index.ts b/packages/pg-query-stream/src/index.ts index 2a4509e09..752e881ca 100644 --- a/packages/pg-query-stream/src/index.ts +++ b/packages/pg-query-stream/src/index.ts @@ -72,4 +72,8 @@ class QueryStream extends Readable implements Submittable { } } +namespace QueryStream { + export type Config = QueryStreamConfig +} + export = QueryStream From c5e8c9a57bff6d9160ec5dbd5c4f4c1e4c460711 Mon Sep 17 00:00:00 2001 From: Noritaka Kobayashi Date: Wed, 22 Jul 2026 00:38:20 +0900 Subject: [PATCH 38/55] fix(lint): support TypeScript declaration merging (#3715) --- eslint.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 3f95083a0..2d969deb0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -71,6 +71,8 @@ export default defineConfig([ rules: { 'no-undef': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'error', }, }, ]) From eb19d0fe6d7da11e7f1c5e73e4026350e42f9156 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 8 Aug 2026 20:13:54 +0100 Subject: [PATCH 39/55] Add opt-in query pipelining (#3652) * Add opt-in query pipelining support Allow multiple queries to be sent on the wire before waiting for responses, reducing round-trip latency. Enabled via client.pipelining = true. Each query gets its own Sync boundary so errors are isolated. Tracks in-flight named statements (submittedNamedStatements) to prevent duplicate Parse messages when pipelining queries with the same prepared statement name. Handles error/disconnect cleanup for the sent queue. * Fix pipelining edge cases and add benchmark - Clean up submittedNamedStatements on error in _handleErrorMessage to prevent stale entries from blocking future re-preparation of the same named statement after a parse failure - Guard _pulsePipelinedQueryQueue against non-queryable connections - Fix cancel() and readTimeout for sent queries: removing an already-sent query from _sentQueryQueue corrupts the pipeline response mapping since the server will still respond to it; no-op the callback instead - Add bench-pipelining.js comparing serial vs pipelined throughput * Fix pipelining activation race and add edge-case tests Gate _sentQueryQueue activation on readyForQuery=true inside _pulsePipelinedQueryQueue (and remove the redundant promotion block from _handleReadyForQuery) to eliminate the microtask/macrotask race where the next query could be activated as _activeQuery before the error's ReadyForQuery arrived, causing that RFQ to be handled by the wrong query. Also adds the error-listener fix for the query_timeout integration test so the expected stream-destroy doesn't leak as an unhandled 'error'. * Add pipelining documentation and pool-level integration - New features/pipelining.mdx documenting the opt-in flag - Client and Pool API reference updated - Pool accepts `pipelining: true` and sets it on every client it creates * Fix prettier formatting * Add pipelining support to native client via libpq pipeline mode - pg-native: handle PGRES_PIPELINE_SYNC/PGRES_PIPELINE_ABORTED in _emitResult; add pipeline() batch method using libpq 14+ pipeline mode (enterPipelineMode, pipelineSync, exitPipelineMode) - pg-native: bump libpq dependency to ^1.9.0 (has pipeline bindings) - native client: add _pulsePipelinedQueryQueue that batches all queued queries through pg-native pipeline(), delivering results per-query - native client: suppress queue length deprecation when pipelining * Fix pipeline mode to use extended query protocol and add JS vs native benchmarks Pipeline mode requires sendQueryParams (extended query protocol), not sendQuery (simple query protocol). PostgreSQL rejects PQsendQuery in pipeline mode. Benchmark script now tests all four combinations: JS serial, JS pipelined, native serial, and native pipelined. * Fix native client end() to wait for in-flight pipeline queries The native client end() was immediately terminating the connection, causing "Connection terminated" errors for queries still in the pipeline. Now waits for the drain event before closing when pipelining is active. Also fix pipeline mode to use sendQueryParams instead of sendQuery, since PostgreSQL rejects simple query protocol in pipeline mode. * Fix native pipelining: guard handleError when query.native is unset and skip JS-only tests handleError in native/query.js crashes when this.native is undefined (e.g. query_timeout fires before pipeline callback sets it). Skip named statement cleanup and query_timeout tests for native client since those features rely on JS-specific internals. * Make pipelining a constructor option named 'pipeline' Move from post-construction property (client.pipelining = true) to constructor option (new Client({ pipeline: true })). Same for the pool: new Pool({ pipeline: true }). Renames the property and internal _pipeliningInFlight to _pipelineInFlight. Tests, docs, and benchmarks updated accordingly. --------- Co-authored-by: Brian C --- docs/pages/apis/client.mdx | 14 +- docs/pages/apis/pool.mdx | 5 + docs/pages/features/_meta.js | 1 + docs/pages/features/pipelining.mdx | 132 +++++++++++ packages/pg-native/index.js | 156 +++++++++++++ packages/pg-pool/test/index.js | 27 +++ packages/pg/bench-pipelining.js | 216 ++++++++++++++++++ packages/pg/lib/client.js | 66 +++++- packages/pg/lib/connection.js | 1 + packages/pg/lib/native/client.js | 107 ++++++++- packages/pg/lib/native/query.js | 2 +- packages/pg/lib/query.js | 7 +- .../integration/client/pipelining-tests.js | 154 +++++++++++++ packages/pg/test/integration/test-helper.js | 4 +- .../pg/test/unit/client/simple-query-tests.js | 94 ++++++++ packages/pg/test/unit/client/test-helper.js | 15 ++ 16 files changed, 982 insertions(+), 19 deletions(-) create mode 100644 docs/pages/features/pipelining.mdx create mode 100644 packages/pg/bench-pipelining.js create mode 100644 packages/pg/test/integration/client/pipelining-tests.js diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index ecfd67fca..973b1a832 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -29,7 +29,8 @@ type Config = { idle_in_transaction_session_timeout?: number, // number of milliseconds before terminating any session with an open idle transaction, default is no timeout client_encoding?: string, // specifies the character set encoding that the database uses for sending data to the client fallback_application_name?: string, // provide an application name to use if application_name is not set - options?: string // command-line options to be sent to the server + options?: string, // command-line options to be sent to the server + pipeline?: boolean // when true, enables query pipelining. See /features/pipelining for details. Default false. } ``` @@ -56,6 +57,17 @@ const client = new Client() await client.connect() ``` +## client.pipeline + +`client.pipeline: boolean` (read-only) + +Whether this client has pipelining enabled. Set via the `pipeline` config option to the `Client` constructor. Defaults to `false`. See [Pipelining](/features/pipelining) for details and examples. + +```js +const client = new Client({ pipeline: true }) +await client.connect() +``` + ## client.query ### QueryConfig diff --git a/docs/pages/apis/pool.mdx b/docs/pages/apis/pool.mdx index 123bc8ba4..3627dd1c5 100644 --- a/docs/pages/apis/pool.mdx +++ b/docs/pages/apis/pool.mdx @@ -69,6 +69,11 @@ type Config = { // If the function throws or returns a promise that rejects, the client is destroyed // and the error is returned to the caller requesting the connection. onConnect?: (client: Client) => void | Promise + + // When set to true, enables query pipelining on every client the pool creates. + // Pipelined clients send queries to the server without waiting for previous responses. + // Default is false. See /features/pipelining for details. + pipeline?: boolean } ``` diff --git a/docs/pages/features/_meta.js b/docs/pages/features/_meta.js index 62f1660ca..7ddd35a5c 100644 --- a/docs/pages/features/_meta.js +++ b/docs/pages/features/_meta.js @@ -1,6 +1,7 @@ export default { connecting: 'Connecting', queries: 'Queries', + pipelining: 'Pipelining', pooling: 'Pooling', transactions: 'Transactions', types: 'Data Types', diff --git a/docs/pages/features/pipelining.mdx b/docs/pages/features/pipelining.mdx new file mode 100644 index 000000000..7943aa490 --- /dev/null +++ b/docs/pages/features/pipelining.mdx @@ -0,0 +1,132 @@ +--- +title: Pipelining +--- + +import { Alert } from '/components/alert.tsx' + +## What is pipelining? + +By default node-postgres waits for each query to complete before sending the next one. This means every query pays a full network round-trip of latency. **Query pipelining** sends multiple queries to the server without waiting for responses, and the server processes them in order. Each query still gets its own result (or error), but you avoid the idle time between them. + +``` +sequential (default) pipelined +───────────────────── ───────────────────── + client ──Parse──▶ server client ──Parse──▶ server + client ◀──Ready── server ──Parse──▶ + client ──Parse──▶ server ──Parse──▶ + client ◀──Ready── server client ◀──Ready── server + client ──Parse──▶ server client ◀──Ready── server + client ◀──Ready── server client ◀──Ready── server +``` + +In benchmarks, pipelining typically delivers **2-3x throughput** for batches of simple queries on a local connection, with larger gains over higher-latency links. + +## Enabling pipelining + +Pipelining is opt-in. Pass `pipeline: true` to the `Client` constructor: + +```js +import { Client } from 'pg' + +const client = new Client({ pipeline: true }) +await client.connect() + +const [r1, r2, r3] = await Promise.all([ + client.query('SELECT 1 AS num'), + client.query('SELECT 2 AS num'), + client.query('SELECT 3 AS num'), +]) + +console.log(r1.rows[0].num, r2.rows[0].num, r3.rows[0].num) // 1 2 3 + +await client.end() +``` + +All query types work with pipelining: plain text, parameterized, and named prepared statements. + +## Pipelining with a pool + +Pass `pipeline: true` in the pool config to enable it on every client the pool creates: + +```js +import { Pool } from 'pg' + +const pool = new Pool({ pipeline: true }) + +const client = await pool.connect() +// client.pipeline is already true + +const [users, orders] = await Promise.all([ + client.query('SELECT * FROM users WHERE id = $1', [1]), + client.query('SELECT * FROM orders WHERE user_id = $1', [1]), +]) + +client.release() +``` + + +
+ pool.query() checks out a client for a single query and releases it immediately, so pipelining has no effect there. Use pool.connect() to check out a client and send multiple queries on it. +
+
+ +## Error isolation + +Each pipelined query gets its own error boundary. A failing query in the middle of a batch does not break the other queries: + +```js +const results = await Promise.allSettled([ + client.query('SELECT 1 AS num'), + client.query('SELECT INVALID SYNTAX'), + client.query('SELECT 3 AS num'), +]) + +console.log(results[0].status) // 'fulfilled' +console.log(results[1].status) // 'rejected' +console.log(results[2].status) // 'fulfilled' +``` + +This works because node-postgres sends a `Sync` message after each query, which is how PostgreSQL delimits error boundaries in the extended query protocol. + +## Named prepared statements + +Named prepared statements work with pipelining. When two pipelined queries share the same statement name, node-postgres sends `Parse` only once and reuses the prepared statement for subsequent queries: + +```js +const queries = Array.from({ length: 100 }, (_, i) => ({ + name: 'get-user', + text: 'SELECT * FROM users WHERE id = $1', + values: [i], +})) + +const results = await Promise.all(queries.map(q => client.query(q))) +``` + +## Graceful shutdown + +Calling `client.end()` while pipelined queries are in flight will wait for all of them to complete before closing the connection: + +```js +const client = new Client({ pipeline: true }) +await client.connect() + +const p1 = client.query('SELECT 1') +const p2 = client.query('SELECT 2') +const endPromise = client.end() + +// Both queries will resolve normally +const [r1, r2] = await Promise.all([p1, p2]) +await endPromise +``` + +## When to use pipelining + +Pipelining is most useful when you have multiple **independent** queries that don't depend on each other's results. Common use cases: + +- Fetching data from multiple tables in parallel for a page load +- Inserting or updating multiple rows simultaneously +- Running a batch of analytics queries + +
+ Do not use pipelining inside a transaction if you need to read the result of one query before issuing the next. Pipelined queries are all sent before any responses arrive, so you cannot branch on intermediate results. For dependent queries within a transaction, use sequential await calls instead. +
diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js index 1c18241db..7fcc26303 100644 --- a/packages/pg-native/index.js +++ b/packages/pg-native/index.js @@ -199,6 +199,10 @@ Client.prototype._emitResult = function (pq) { break } + case 'PGRES_PIPELINE_SYNC': + case 'PGRES_PIPELINE_ABORTED': + break + default: this._readError('unrecognized command status: ' + status) break @@ -314,6 +318,158 @@ Client.prototype._onResult = function (result) { this._resultCount++ } +// Send a batch of queries in pipeline mode and collect results in order. +// Each entry in `queries` is {text, values?, name?}. +// `cb(err, results)` where results is an array, one per query, +// of {err, rows, result} objects. +Client.prototype.pipeline = function (queries, cb) { + const pq = this.pq + + if (!pq.pipelineModeSupported || !pq.pipelineModeSupported()) { + return cb(new Error('Pipeline mode is not supported. Requires PostgreSQL 14+ client libraries.')) + } + + if (!pq.enterPipelineMode()) { + return cb(new Error(pq.errorMessage() || 'Failed to enter pipeline mode')) + } + + pq.setNonBlocking(true) + + // Send all queries, each followed by a sync + for (let i = 0; i < queries.length; i++) { + const q = queries[i] + let sent + if (q.name) { + if (q._alreadyPrepared) { + sent = pq.sendQueryPrepared(q.name, q.values || []) + } else { + // send prepare then execute in same pipeline batch + sent = pq.sendPrepare(q.name, q.text, (q.values || []).length) + if (sent) { + sent = pq.sendQueryPrepared(q.name, q.values || []) + } + } + } else { + // In pipeline mode, simple query protocol (sendQuery) is not allowed. + // Always use extended query protocol (sendQueryParams). + sent = pq.sendQueryParams(q.text, q.values || []) + } + + if (!sent) { + const err = new Error(pq.errorMessage() || 'Failed to send pipelined query') + pq.exitPipelineMode() + return cb(err) + } + + pq.pipelineSync() + } + + // Flush all queued data to the socket + this._waitForDrain(pq, (err) => { + if (err) { + pq.exitPipelineMode() + return cb(err) + } + this._readPipelineResults(queries, cb) + }) +} + +// Read pipeline results for `queries.length` sync points. +// Calls cb(null, results) when all syncs have been received. +Client.prototype._readPipelineResults = function (queries, cb) { + const pq = this.pq + const self = this + const results = [] + let queryIndex = 0 + let currentResult = null + let currentError = null + + const processResults = function () { + if (!pq.consumeInput()) { + pq.exitPipelineMode() + return cb(new Error(pq.errorMessage() || 'Failed to consume input')) + } + + while (!pq.isBusy()) { + if (!pq.getResult()) { + // null between result groups in pipeline — try again + if (pq.isBusy()) return // more data needed + if (!pq.getResult()) { + // truly no more results — should not happen before all syncs + break + } + } + + const status = pq.resultStatus() + + if (status === 'PGRES_PIPELINE_SYNC') { + // End of one query's results + sync + if (currentError) { + results.push({ err: currentError, rows: null, result: null }) + } else if (currentResult) { + results.push({ err: null, rows: currentResult.rows, result: currentResult }) + } else { + results.push({ err: null, rows: [], result: null }) + } + currentResult = null + currentError = null + queryIndex++ + + if (queryIndex >= queries.length) { + // All queries processed + pq.exitPipelineMode() + return cb(null, results) + } + continue + } + + if (status === 'PGRES_FATAL_ERROR') { + currentError = new Error(pq.resultErrorMessage()) + // Extract error fields + const fields = pq.resultErrorFields() + if (fields) { + for (const key in fields) { + currentError[key] = fields[key] + } + } + continue + } + + if (status === 'PGRES_PIPELINE_ABORTED') { + // Query skipped due to previous error in same sync group + continue + } + + if (status === 'PGRES_TUPLES_OK' || status === 'PGRES_COMMAND_OK' || status === 'PGRES_EMPTY_QUERY') { + currentResult = self._consumeQueryResults(pq) + continue + } + } + + // Still waiting for more data — will be called again when readable + } + + // Use the libuv readable watcher + this._stopReading() + let done = false + const origCb = cb + cb = function (err, results) { + if (done) return + done = true + pq.removeListener('readable', onReadable) + self._stopReading() + origCb(err, results) + } + const onReadable = function () { + processResults() + } + pq.on('readable', onReadable) + pq.startReader() + + // Try an initial read in case data is already available + processResults() +} + Client.prototype._onReadyForQuery = function () { // remove instance callback const cb = this._queryCallback diff --git a/packages/pg-pool/test/index.js b/packages/pg-pool/test/index.js index 57a68e01e..cc1e9d905 100644 --- a/packages/pg-pool/test/index.js +++ b/packages/pg-pool/test/index.js @@ -203,6 +203,33 @@ describe('pool', function () { }) }) + it('enables pipeline on clients when configured', async function () { + const pool = new Pool({ pipeline: true }) + const client = await pool.connect() + expect(client.pipeline).to.be(true) + + const [r1, r2, r3] = await Promise.all([ + client.query('SELECT 1 AS num'), + client.query('SELECT 2 AS num'), + client.query('SELECT 3 AS num'), + ]) + + expect(r1.rows[0].num).to.eql(1) + expect(r2.rows[0].num).to.eql(2) + expect(r3.rows[0].num).to.eql(3) + + client.release() + return pool.end() + }) + + it('does not enable pipeline by default', async function () { + const pool = new Pool() + const client = await pool.connect() + expect(client.pipeline).to.be(false) + client.release() + return pool.end() + }) + it('recovers from query errors', function () { const pool = new Pool() diff --git a/packages/pg/bench-pipelining.js b/packages/pg/bench-pipelining.js new file mode 100644 index 000000000..90f384806 --- /dev/null +++ b/packages/pg/bench-pipelining.js @@ -0,0 +1,216 @@ +'use strict' +const pg = require('./lib') + +let Native +try { + Native = require('pg-native') +} catch (e) { + // pg-native not available — skip native benchmarks +} + +const SECONDS = 5 +const BATCH = 10 + +async function bench(label, fn, seconds) { + // warmup + for (let i = 0; i < 100; i++) await fn() + + const deadline = Date.now() + seconds * 1000 + let count = 0 + while (Date.now() < deadline) { + await fn() + count++ + } + const qps = (count / seconds).toFixed(0) + console.log(` ${label}: ${qps} qps (${count} queries in ${seconds}s)`) + return count / seconds +} + +// --- JS client helpers --- + +async function jsSerial(label, query, seconds) { + const client = new pg.Client() + await client.connect() + const qps = await bench(label, () => client.query(query), seconds) + await client.end() + return qps +} + +async function jsPipelined(label, makeQueries, batchSize, seconds) { + const client = new pg.Client({ pipeline: true }) + await client.connect() + + // warmup + for (let i = 0; i < 10; i++) { + await Promise.all(makeQueries(batchSize).map((q) => client.query(q))) + } + + const deadline = Date.now() + seconds * 1000 + let count = 0 + while (Date.now() < deadline) { + await Promise.all(makeQueries(batchSize).map((q) => client.query(q))) + count += batchSize + } + const qps = (count / seconds).toFixed(0) + console.log(` ${label} (batch=${batchSize}): ${qps} qps`) + await client.end() + return count / seconds +} + +// --- Native client helpers --- + +function nativeConnect() { + return new Promise((resolve, reject) => { + const client = new Native() + client.connect((err) => { + if (err) return reject(err) + resolve(client) + }) + }) +} + +function nativeQuery(client, text, values) { + return new Promise((resolve, reject) => { + client.query(text, values, (err, rows) => { + if (err) return reject(err) + resolve(rows) + }) + }) +} + +function nativeEnd(client) { + return new Promise((resolve) => { + client.end(() => resolve()) + }) +} + +function nativePipeline(client, queries) { + return new Promise((resolve, reject) => { + client.pipeline(queries, (err, results) => { + if (err) return reject(err) + resolve(results) + }) + }) +} + +async function nativeSerial(label, text, values, seconds) { + const client = await nativeConnect() + + // warmup + for (let i = 0; i < 100; i++) await nativeQuery(client, text, values) + + const deadline = Date.now() + seconds * 1000 + let count = 0 + while (Date.now() < deadline) { + await nativeQuery(client, text, values) + count++ + } + const qps = (count / seconds).toFixed(0) + console.log(` ${label}: ${qps} qps (${count} queries in ${seconds}s)`) + await nativeEnd(client) + return count / seconds +} + +async function nativePipelined(label, makeQueries, batchSize, seconds) { + const client = await nativeConnect() + + // warmup + for (let i = 0; i < 10; i++) { + await nativePipeline(client, makeQueries(batchSize)) + } + + const deadline = Date.now() + seconds * 1000 + let count = 0 + while (Date.now() < deadline) { + await nativePipeline(client, makeQueries(batchSize)) + count += batchSize + } + const qps = (count / seconds).toFixed(0) + console.log(` ${label} (batch=${batchSize}): ${qps} qps`) + await nativeEnd(client) + return count / seconds +} + +// --- Main --- + +async function run() { + const results = {} + + console.log('\n=== JS Client — Serial ===') + results.jsSerialSimple = await jsSerial('simple SELECT 1', { text: 'SELECT 1' }, SECONDS) + results.jsSerialParam = await jsSerial('parameterized', { text: 'SELECT $1::int AS n', values: [42] }, SECONDS) + results.jsSerialNamed = await jsSerial( + 'named prepared', + { name: 'bench-named', text: 'SELECT $1::int AS n', values: [42] }, + SECONDS + ) + + console.log('\n=== JS Client — Pipelined ===') + results.jsPipedSimple = await jsPipelined( + 'simple SELECT 1', + (n) => Array.from({ length: n }, () => ({ text: 'SELECT 1' })), + BATCH, + SECONDS + ) + results.jsPipedParam = await jsPipelined( + 'parameterized', + (n) => Array.from({ length: n }, () => ({ text: 'SELECT $1::int AS n', values: [42] })), + BATCH, + SECONDS + ) + results.jsPipedNamed = await jsPipelined( + 'named prepared', + (n) => + Array.from({ length: n }, (_, i) => ({ name: `bench-named-${i}`, text: 'SELECT $1::int AS n', values: [42] })), + BATCH, + SECONDS + ) + + if (Native) { + console.log('\n=== Native Client — Serial ===') + results.nativeSerialSimple = await nativeSerial('simple SELECT 1', 'SELECT 1', undefined, SECONDS) + results.nativeSerialParam = await nativeSerial('parameterized', 'SELECT $1::int AS n', [42], SECONDS) + + console.log('\n=== Native Client — Pipelined ===') + results.nativePipedSimple = await nativePipelined( + 'simple SELECT 1', + (n) => Array.from({ length: n }, () => ({ text: 'SELECT 1' })), + BATCH, + SECONDS + ) + results.nativePipedParam = await nativePipelined( + 'parameterized', + (n) => Array.from({ length: n }, () => ({ text: 'SELECT $1::int AS n', values: [42] })), + BATCH, + SECONDS + ) + } else { + console.log('\n(pg-native not available — skipping native benchmarks)') + } + + // --- Summary --- + console.log('\n=== Speedup Summary ===') + console.log('JS pipelining vs serial:') + console.log(` simple: ${(results.jsPipedSimple / results.jsSerialSimple).toFixed(2)}x`) + console.log(` parameterized: ${(results.jsPipedParam / results.jsSerialParam).toFixed(2)}x`) + console.log(` named: ${(results.jsPipedNamed / results.jsSerialNamed).toFixed(2)}x`) + + if (Native) { + console.log('Native pipelining vs serial:') + console.log(` simple: ${(results.nativePipedSimple / results.nativeSerialSimple).toFixed(2)}x`) + console.log(` parameterized: ${(results.nativePipedParam / results.nativeSerialParam).toFixed(2)}x`) + + console.log('Native serial vs JS serial:') + console.log(` simple: ${(results.nativeSerialSimple / results.jsSerialSimple).toFixed(2)}x`) + console.log(` parameterized: ${(results.nativeSerialParam / results.jsSerialParam).toFixed(2)}x`) + + console.log('Native pipelined vs JS pipelined:') + console.log(` simple: ${(results.nativePipedSimple / results.jsPipedSimple).toFixed(2)}x`) + console.log(` parameterized: ${(results.nativePipedParam / results.jsPipedParam).toFixed(2)}x`) + } +} + +run().catch((e) => { + console.error(e) + process.exit(1) +}) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 18280f3c6..7a2fc9a64 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -97,6 +97,8 @@ class Client extends EventEmitter { encoding: this.connectionParameters.client_encoding || 'utf8', }) this._queryQueue = [] + this._sentQueryQueue = [] + this.pipeline = Boolean(c.pipeline) this.binary = c.binary || defaults.binary this.processID = null this.secretKey = null @@ -141,6 +143,9 @@ class Client extends EventEmitter { this._activeQuery = null } + this._sentQueryQueue.forEach(enqueueError) + this._sentQueryQueue.length = 0 + this._queryQueue.forEach(enqueueError) this._queryQueue.length = 0 } @@ -430,6 +435,9 @@ class Client extends EventEmitter { } this._activeQuery = null + if (activeQuery.name) { + delete this.connection.submittedNamedStatements[activeQuery.name] + } activeQuery.handleError(msg, this.connection) } @@ -500,6 +508,7 @@ class Client extends EventEmitter { // it again on the same client if (activeQuery.name) { this.connection.parsedStatements[activeQuery.name] = activeQuery.text + delete this.connection.submittedNamedStatements[activeQuery.name] } } @@ -578,6 +587,10 @@ class Client extends EventEmitter { }) } else if (client._queryQueue.indexOf(query) !== -1) { client._queryQueue.splice(client._queryQueue.indexOf(query), 1) + } else if (client._sentQueryQueue.indexOf(query) !== -1) { + // Query already sent on wire — can't remove it without corrupting the + // pipeline. No-op the callback so the result is silently discarded. + query.callback = () => {} } } @@ -601,6 +614,10 @@ class Client extends EventEmitter { } _pulseQueryQueue() { + if (this.pipeline) { + this._pulsePipelinedQueryQueue() + return + } if (this.readyForQuery === true) { this._activeQuery = this._queryQueue.shift() const activeQuery = this._getActiveQuery() @@ -623,6 +640,31 @@ class Client extends EventEmitter { } } + _pulsePipelinedQueryQueue() { + if (!this._connected || !this._queryable) { + return + } + while (this._queryQueue.length > 0) { + const query = this._queryQueue.shift() + this.hasExecuted = true + const queryError = query.submit(this.connection) + if (queryError) { + process.nextTick(() => { + query.handleError(queryError, this.connection) + }) + continue + } + this._sentQueryQueue.push(query) + } + if (this.readyForQuery && !this._activeQuery && this._sentQueryQueue.length > 0) { + this._activeQuery = this._sentQueryQueue.shift() + this.readyForQuery = false + } + if (!this._activeQuery && this._sentQueryQueue.length === 0 && this._queryQueue.length === 0 && this.hasExecuted) { + this.emit('drain') + } + } + query(config, values, callback) { // can take in strings, config object or query object let query @@ -674,10 +716,15 @@ class Client extends EventEmitter { // just do nothing if query completes query.callback = () => {} - // Remove from queue + // Remove from queue (only safe if not yet sent) const index = this._queryQueue.indexOf(query) if (index > -1) { this._queryQueue.splice(index, 1) + } else if (this.pipeline) { + // Query already sent — the pipeline is blocked until it completes. + // Destroy the connection to unblock all remaining pipelined queries. + this.connection.stream.destroy() + return } this._pulseQueryQueue() @@ -711,7 +758,7 @@ class Client extends EventEmitter { return result } - if (this._queryQueue.length > 0) { + if (this._queryQueue.length > 0 && !this.pipeline) { queryQueueLengthDeprecationNotice() } this._queryQueue.push(query) @@ -744,9 +791,18 @@ class Client extends EventEmitter { } } - if (this._getActiveQuery() || !this._queryable) { - // if we have an active query we need to force a disconnect - // on the socket - otherwise a hung query could block end forever + if (!this._queryable) { + // socket is dead — force close + this.connection.stream.destroy() + } else if ( + this.pipeline && + (this._getActiveQuery() || this._sentQueryQueue.length > 0 || this._queryQueue.length > 0) + ) { + // pipelined queries are already on the wire (or queued to send) and will + // complete normally; wait for drain then do a graceful goodbye + this.once('drain', () => this.connection.end()) + } else if (this._getActiveQuery()) { + // non-pipeline: a hung query could block end forever — force disconnect this.connection.stream.destroy() } else { this.connection.end() diff --git a/packages/pg/lib/connection.js b/packages/pg/lib/connection.js index 63cc13a53..62d38fa69 100644 --- a/packages/pg/lib/connection.js +++ b/packages/pg/lib/connection.js @@ -24,6 +24,7 @@ class Connection extends EventEmitter { this._keepAlive = config.keepAlive this._keepAliveInitialDelayMillis = config.keepAliveInitialDelayMillis this.parsedStatements = {} + this.submittedNamedStatements = {} this.ssl = config.ssl || false this.sslNegotiation = config.sslNegotiation || 'postgres' this._ending = false diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index fa17d9f65..d305713d6 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -36,6 +36,8 @@ const Client = (module.exports = function (config) { this._connecting = false this._connected = false this._queryable = true + this.pipeline = Boolean(config.pipeline) + this._pipelineInFlight = false // keep these on the object for legacy reasons // for the time being. TODO: deprecate all this jazz @@ -234,7 +236,7 @@ Client.prototype.query = function (config, values, callback) { return result } - if (this._queryQueue.length > 0) { + if (this._queryQueue.length > 0 && !this.pipeline) { queryQueueLengthDeprecationNotice() } @@ -261,16 +263,25 @@ Client.prototype.end = function (cb) { }) } - this.native.end(function () { - self._connected = false + const doEnd = function () { + self.native.end(function () { + self._connected = false - self._errorAllQueries(new Error('Connection terminated')) + self._errorAllQueries(new Error('Connection terminated')) - process.nextTick(() => { - self.emit('end') - if (cb) cb() + process.nextTick(() => { + self.emit('end') + if (cb) cb() + }) }) - }) + } + + // If pipeline has in-flight or queued queries, wait for them to drain before closing + if (this.pipeline && (this._pipelineInFlight || this._queryQueue.length > 0)) { + this.once('drain', doEnd) + } else { + doEnd() + } return result } @@ -282,6 +293,9 @@ Client.prototype._pulseQueryQueue = function (initialConnection) { if (!this._connected) { return } + if (this.pipeline && !initialConnection) { + return this._pulsePipelinedQueryQueue() + } if (this._hasActiveQuery()) { return } @@ -300,6 +314,83 @@ Client.prototype._pulseQueryQueue = function (initialConnection) { }) } +Client.prototype._pulsePipelinedQueryQueue = function () { + if (!this._connected || this._pipelineInFlight) { + return + } + if (this._queryQueue.length === 0) { + if (this.hasExecuted) { + this.emit('drain') + } + return + } + + this._pipelineInFlight = true + const self = this + const queries = [] + const nativeQueries = [] + const utils = require('../utils') + + while (this._queryQueue.length > 0) { + const query = this._queryQueue.shift() + this.hasExecuted = true + nativeQueries.push(query) + + const values = query.values ? query.values.map(utils.prepareValue) : null + const pipelineEntry = { text: query.text, name: query.name } + if (values) { + pipelineEntry.values = values + } + if (query.name && this.namedQueries[query.name]) { + pipelineEntry._alreadyPrepared = true + } + queries.push(pipelineEntry) + } + + this.native.pipeline(queries, function (err, results) { + self._pipelineInFlight = false + + if (err) { + // Total pipeline failure — error all queries + for (let i = 0; i < nativeQueries.length; i++) { + const q = nativeQueries[i] + q.native = self.native + q.handleError(err) + } + self._pulsePipelinedQueryQueue() + return + } + + // Deliver results to each query + for (let i = 0; i < nativeQueries.length; i++) { + const q = nativeQueries[i] + const r = results[i] + q.native = self.native + + if (r.err) { + q.handleError(r.err) + } else { + // Track named queries on success + if (q.name) { + self.namedQueries[q.name] = q.text + } + q.state = 'end' + q.emit('end', r.result) + if (q.callback) { + q.callback(null, r.result) + } + } + + setImmediate(function () { + q.emit('_done') + }) + } + + // Process any queries that arrived while we were reading + self._pulsePipelinedQueryQueue() + }) +} + // attempt to cancel an in-progress query Client.prototype.cancel = function (query) { if (this._activeQuery === query) { diff --git a/packages/pg/lib/native/query.js b/packages/pg/lib/native/query.js index e02294f63..8cb561979 100644 --- a/packages/pg/lib/native/query.js +++ b/packages/pg/lib/native/query.js @@ -48,7 +48,7 @@ const errorFieldMap = { NativeQuery.prototype.handleError = function (err) { // copy pq error fields into the error object - const fields = this.native.pq.resultErrorFields() + const fields = this.native && this.native.pq.resultErrorFields() if (fields) { for (const key in fields) { const normalizedFieldName = errorFieldMap[key] || key diff --git a/packages/pg/lib/query.js b/packages/pg/lib/query.js index 04e1c1d65..6b9214199 100644 --- a/packages/pg/lib/query.js +++ b/packages/pg/lib/query.js @@ -153,7 +153,7 @@ class Query extends EventEmitter { if (typeof this.text !== 'string' && typeof this.name !== 'string') { return new Error('A query must have either text or a name. Supplying neither is unsupported.') } - const previous = connection.parsedStatements[this.name] + const previous = connection.parsedStatements[this.name] || connection.submittedNamedStatements[this.name] if (this.text && previous && this.text !== previous) { return new Error(`Prepared statements must be unique - '${this.name}' was used for a different statement`) } @@ -183,7 +183,7 @@ class Query extends EventEmitter { } hasBeenParsed(connection) { - return this.name && connection.parsedStatements[this.name] + return this.name && (connection.parsedStatements[this.name] || connection.submittedNamedStatements[this.name]) } handlePortalSuspended(connection) { @@ -214,6 +214,9 @@ class Query extends EventEmitter { name: this.name, types: this.types, }) + if (this.name) { + connection.submittedNamedStatements[this.name] = this.text + } } // because we're mapping user supplied values to diff --git a/packages/pg/test/integration/client/pipelining-tests.js b/packages/pg/test/integration/client/pipelining-tests.js new file mode 100644 index 000000000..957047d7d --- /dev/null +++ b/packages/pg/test/integration/client/pipelining-tests.js @@ -0,0 +1,154 @@ +'use strict' +const helper = require('./test-helper') +const assert = require('assert') +const suite = new helper.Suite() + +suite.test('basic pipeline with simple queries', async function () { + const client = helper.client(undefined, { pipeline: true }) + + const [r1, r2, r3] = await Promise.all([ + client.query('SELECT 1 AS num'), + client.query('SELECT 2 AS num'), + client.query('SELECT 3 AS num'), + ]) + + assert.equal(r1.rows[0].num, 1) + assert.equal(r2.rows[0].num, 2) + assert.equal(r3.rows[0].num, 3) + + await client.end() +}) + +suite.test('pipeline with parameterized queries', async function () { + const client = helper.client(undefined, { pipeline: true }) + + const [r1, r2, r3] = await Promise.all([ + client.query('SELECT $1::int AS num', [10]), + client.query('SELECT $1::text AS name', ['hello']), + client.query('SELECT $1::int + $2::int AS sum', [3, 4]), + ]) + + assert.equal(r1.rows[0].num, 10) + assert.equal(r2.rows[0].name, 'hello') + assert.equal(r3.rows[0].sum, 7) + + await client.end() +}) + +suite.test('pipeline with named prepared statements', async function () { + const client = helper.client(undefined, { pipeline: true }) + + const [r1, r2] = await Promise.all([ + client.query({ name: 'fetch-num', text: 'SELECT $1::int AS num', values: [42] }), + client.query({ name: 'fetch-num', text: 'SELECT $1::int AS num', values: [99] }), + ]) + + assert.equal(r1.rows[0].num, 42) + assert.equal(r2.rows[0].num, 99) + + await client.end() +}) + +suite.test('pipeline error isolation', async function () { + const client = helper.client(undefined, { pipeline: true }) + + const results = await Promise.allSettled([ + client.query('SELECT 1 AS num'), + client.query('SELECT INVALID SYNTAX'), + client.query('SELECT 3 AS num'), + ]) + + assert.equal(results[0].status, 'fulfilled') + assert.equal(results[0].value.rows[0].num, 1) + assert.equal(results[1].status, 'rejected') + assert.equal(results[2].status, 'fulfilled') + assert.equal(results[2].value.rows[0].num, 3) + + await client.end() +}) + +suite.test('pipeline drain event', async function () { + const client = helper.client(undefined, { pipeline: true }) + + const drainPromise = new Promise((resolve) => { + client.on('drain', resolve) + }) + + client.query('SELECT 1') + client.query('SELECT 2') + client.query('SELECT 3') + + await drainPromise + await client.end() +}) + +// #12: end() during active pipeline — should drain gracefully, not destroy +suite.test('end() waits for in-flight pipelined queries to complete', async function () { + const client = helper.client(undefined, { pipeline: true }) + + // Fire queries then call end() immediately without awaiting them + const p1 = client.query('SELECT 1 AS num') + const p2 = client.query('SELECT 2 AS num') + const endPromise = client.end() + + // All queries should resolve (not error) because end() drains gracefully + const [r1, r2] = await Promise.all([p1, p2]) + assert.equal(r1.rows[0].num, 1) + assert.equal(r2.rows[0].num, 2) + await endPromise +}) + +// #13: named statement error cleanup — submittedNamedStatements not left stale +// This relies on submittedNamedStatements tracking which only exists in the JS client +suite.test( + 'named statement parse error cleans up and allows re-preparation', + !helper.args.native && + async function () { + const client = helper.client(undefined, { pipeline: true }) + + // Use an invalid type to force a server-side parse error + const err = await client + .query({ name: 'bad-stmt', text: 'SELECT $1::nonexistent_type_xyz', values: [1] }) + .then(() => null) + .catch((e) => e) + + assert.ok(err, 'expected parse to fail') + + // The stale submittedNamedStatements entry should be gone. + // Re-using the same name with valid SQL should work. + const result = await client.query({ name: 'bad-stmt', text: 'SELECT $1::int AS n', values: [42] }) + assert.equal(result.rows[0].n, 42) + + await client.end() + } +) + +// #14: query_timeout with pipelining +// When an already-sent pipelined query times out, the connection is destroyed +// to unblock the pipeline — subsequent queries error rather than hanging. +// Native client does not support query_timeout in pipeline mode. +suite.test( + 'query_timeout on sent pipelined query destroys connection to unblock', + !helper.args.native && + async function () { + const client = helper.client(undefined, { pipeline: true }) + client.on('error', () => {}) // absorb the 'error' event emitted when stream is destroyed + + const results = await Promise.allSettled([ + client.query('SELECT 1 AS num'), + client.query({ text: 'SELECT pg_sleep(30)', query_timeout: 100 }), + client.query('SELECT 3 AS num'), + ]) + + // Query 1 completes before the slow query enters the pipeline + assert.equal(results[0].status, 'fulfilled') + assert.equal(results[0].value.rows[0].num, 1) + + // Query 2 times out + assert.equal(results[1].status, 'rejected') + assert.ok(results[1].reason.message.includes('timeout'), `unexpected error: ${results[1].reason.message}`) + + // Query 3 errors because the connection was destroyed to unblock the pipeline + assert.equal(results[2].status, 'rejected') + } +) diff --git a/packages/pg/test/integration/test-helper.js b/packages/pg/test/integration/test-helper.js index 9dab8843a..fe2044f60 100644 --- a/packages/pg/test/integration/test-helper.js +++ b/packages/pg/test/integration/test-helper.js @@ -10,8 +10,8 @@ if (helper.args.native) { } // creates a client from cli parameters -helper.client = function (cb) { - const client = new Client() +helper.client = function (cb, options) { + const client = new Client(options) client.connect(cb) return client } diff --git a/packages/pg/test/unit/client/simple-query-tests.js b/packages/pg/test/unit/client/simple-query-tests.js index 8cc550830..efb705aa1 100644 --- a/packages/pg/test/unit/client/simple-query-tests.js +++ b/packages/pg/test/unit/client/simple-query-tests.js @@ -114,6 +114,100 @@ test('executing query', function () { }) }) + test('pipeline', function () { + test('sends all queries immediately after readyForQuery', function () { + const client = helper.client({ pipeline: true }) + client.connection.emit('readyForQuery') + client.query('one') + client.query('two') + client.query('three') + assert.lengthIs(client.connection.queries, 3) + assert.equal(client.connection.queries[0], 'one') + assert.equal(client.connection.queries[1], 'two') + assert.equal(client.connection.queries[2], 'three') + }) + + test('completes queries in order', function (done) { + const client = helper.client({ pipeline: true }) + const con = client.connection + con.emit('readyForQuery') + + const results = [] + client.query('one', (err, res) => { + results.push('one') + }) + client.query('two', (err, res) => { + results.push('two') + }) + client.query('three', (err, res) => { + results.push('three') + }) + + // simulate server responding to each query in order + con.emit('readyForQuery') + con.emit('readyForQuery') + con.emit('readyForQuery') + + process.nextTick(() => { + assert.deepStrictEqual(results, ['one', 'two', 'three']) + done() + }) + }) + + test('emits drain after all queries complete', function (done) { + const client = helper.client({ pipeline: true }) + const con = client.connection + con.emit('readyForQuery') + + client.query('one') + client.query('two') + + client.on('drain', () => { + done() + }) + + con.emit('readyForQuery') + con.emit('readyForQuery') + }) + + test('extended protocol: sends parse/bind/sync for each pipelined parameterized query', function () { + const client = helper.client({ pipeline: true }) + const con = client.connection + con.emit('readyForQuery') + + client.query({ text: 'SELECT $1::int', values: [1] }) + client.query({ text: 'SELECT $1::int', values: [2] }) + + // both parse messages should have been sent immediately + assert.lengthIs(con.parseMessages, 2) + assert.equal(con.parseMessages[0].text, 'SELECT $1::int') + assert.equal(con.parseMessages[1].text, 'SELECT $1::int') + // both bind messages too + assert.lengthIs(con.bindMessages, 2) + // each query sends its own sync + assert.equal(con.syncCount, 2) + }) + + test('named statement: parse sent only once when pipelining the same name', function () { + const client = helper.client({ pipeline: true }) + const con = client.connection + con.emit('readyForQuery') + + client.query({ name: 'my-stmt', text: 'SELECT $1::int', values: [1] }) + client.query({ name: 'my-stmt', text: 'SELECT $1::int', values: [2] }) + + // parse sent only once — second query reuses the submitted statement + assert.lengthIs(con.parseMessages, 1) + // both bind messages sent + assert.lengthIs(con.bindMessages, 2) + }) + + test('pipeline disabled by default', function () { + const client = helper.client() + assert.equal(client.pipeline, false) + }) + }) + test('handles errors', function () { const client = helper.client() diff --git a/packages/pg/test/unit/client/test-helper.js b/packages/pg/test/unit/client/test-helper.js index 4a3fa9687..8c3c3304e 100644 --- a/packages/pg/test/unit/client/test-helper.js +++ b/packages/pg/test/unit/client/test-helper.js @@ -10,7 +10,22 @@ const makeClient = function (config) { connection.query = function (text) { this.queries.push(text) } + connection.parse = function (msg) { + this.parseMessages.push(msg) + } + connection.bind = function (msg) { + this.bindMessages.push(msg) + } + connection.describe = function (msg) {} + connection.execute = function (msg) {} + connection.sync = function () { + this.syncCount++ + } + connection.flush = function () {} connection.queries = [] + connection.parseMessages = [] + connection.bindMessages = [] + connection.syncCount = 0 const client = new Client({ connection: connection, ...config }) client.connect() client.connection.emit('connect') From 1be86af091caefdc07abec07242694c4f969ce5b Mon Sep 17 00:00:00 2001 From: Pratik Dulal <72141032+pratik-desgn@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:06:05 +0545 Subject: [PATCH 40/55] fix(pg-protocol): read ParameterDescription type OIDs as unsigned (#3728) parseField() already reads dataTypeID as uint32 for RowDescription (with a regression test covering OIDs above 2^31-1), but parseParameterDescriptionMessage() was still using the signed int32 reader for the same kind of value, so custom extension types with a high OID would come back negative in ParameterDescriptionMessage.dataTypeIDs. --- packages/pg-protocol/src/inbound-parser.test.ts | 10 ++++++++++ packages/pg-protocol/src/parser.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/pg-protocol/src/inbound-parser.test.ts b/packages/pg-protocol/src/inbound-parser.test.ts index 285f4bf2b..8687194c3 100644 --- a/packages/pg-protocol/src/inbound-parser.test.ts +++ b/packages/pg-protocol/src/inbound-parser.test.ts @@ -161,6 +161,8 @@ const oneParameterDescBuf = buffers.parameterDescription([1111]) const twoParameterDescBuf = buffers.parameterDescription([2222, 3333]) +const bigOidParameterDescBuf = buffers.parameterDescription([3000000003]) + const expectedEmptyParameterDescriptionMessage = { name: 'parameterDescription', length: 6, @@ -182,6 +184,13 @@ const expectedTwoParameterMessage = { dataTypeIDs: [2222, 3333], } +const expectedBigOidParameterMessage = { + name: 'parameterDescription', + length: 10, + parameterCount: 1, + dataTypeIDs: [3000000003], +} + const testForMessage = function (buffer: Buffer, expectedMessage: any) { it('receives and parses ' + expectedMessage.name, async () => { const messages = await parseBuffers([buffer]) @@ -288,6 +297,7 @@ describe('PgPacketStream', function () { testForMessage(emptyParameterDescriptionBuffer, expectedEmptyParameterDescriptionMessage) testForMessage(oneParameterDescBuf, expectedOneParameterMessage) testForMessage(twoParameterDescBuf, expectedTwoParameterMessage) + testForMessage(bigOidParameterDescBuf, expectedBigOidParameterMessage) }) describe('parsing rows', function () { diff --git a/packages/pg-protocol/src/parser.ts b/packages/pg-protocol/src/parser.ts index 3d8ce80c7..df48ca4a1 100644 --- a/packages/pg-protocol/src/parser.ts +++ b/packages/pg-protocol/src/parser.ts @@ -300,7 +300,8 @@ const parseParameterDescriptionMessage = (reader: BufferReader) => { const parameterCount = reader.int16() const message = new ParameterDescriptionMessage(LATEINIT_LENGTH, parameterCount) for (let i = 0; i < parameterCount; i++) { - message.dataTypeIDs[i] = reader.int32() + // OIDs are unsigned, same as dataTypeID in parseField above + message.dataTypeIDs[i] = reader.uint32() } return message } From 747a68ce273f8cdbb7c9c619f211ac383aaebdab Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Sat, 8 Aug 2026 12:21:33 -0700 Subject: [PATCH 41/55] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cf0fedf..f8158747e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ For richer information consult the commit log on github with referenced pull req We do not include break-fix version release in this file. +## pg@8.23.0 + +- Add support for query [`pipelineing`](https://github.com/brianc/node-postgres/pull/3652). + ## pg@8.22.0 - Add support for [sslnegotiation=direct](https://github.com/brianc/node-postgres/pull/3688) for PostgreSQL 17+. From df274d1ba9ad9d11a8f1079314faeafde7208207 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Sat, 8 Aug 2026 12:22:18 -0700 Subject: [PATCH 42/55] Publish - pg-cursor@2.22.0 - pg-esm-test@1.9.0 - pg-native@3.9.0 - pg-protocol@1.16.0 - pg-query-stream@4.17.0 - pg@8.23.0 --- packages/pg-cursor/package.json | 4 ++-- packages/pg-esm-test/package.json | 12 ++++++------ packages/pg-native/package.json | 2 +- packages/pg-protocol/package.json | 2 +- packages/pg-query-stream/package.json | 6 +++--- packages/pg/package.json | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/pg-cursor/package.json b/packages/pg-cursor/package.json index 9dc4ef7ff..46f3406c2 100644 --- a/packages/pg-cursor/package.json +++ b/packages/pg-cursor/package.json @@ -1,6 +1,6 @@ { "name": "pg-cursor", - "version": "2.21.0", + "version": "2.22.0", "description": "Query cursor extension for node-postgres", "main": "index.js", "exports": { @@ -25,7 +25,7 @@ "license": "MIT", "devDependencies": { "mocha": "^11.7.5", - "pg": "^8.22.0" + "pg": "^8.23.0" }, "peerDependencies": { "pg": "^8" diff --git a/packages/pg-esm-test/package.json b/packages/pg-esm-test/package.json index 88370652a..23a688bf9 100644 --- a/packages/pg-esm-test/package.json +++ b/packages/pg-esm-test/package.json @@ -1,6 +1,6 @@ { "name": "pg-esm-test", - "version": "1.8.0", + "version": "1.9.0", "description": "A test module for PostgreSQL with ESM support", "main": "index.js", "type": "module", @@ -14,13 +14,13 @@ "test" ], "devDependencies": { - "pg": "^8.22.0", + "pg": "^8.23.0", "pg-cloudflare": "^1.4.0", - "pg-cursor": "^2.21.0", - "pg-native": "^3.8.0", + "pg-cursor": "^2.22.0", + "pg-native": "^3.9.0", "pg-pool": "^3.14.0", - "pg-protocol": "^1.15.0", - "pg-query-stream": "^4.16.0" + "pg-protocol": "^1.16.0", + "pg-query-stream": "^4.17.0" }, "author": "Brian M. Carlson ", "license": "MIT" diff --git a/packages/pg-native/package.json b/packages/pg-native/package.json index 513f3bb3f..63d7569f3 100644 --- a/packages/pg-native/package.json +++ b/packages/pg-native/package.json @@ -1,6 +1,6 @@ { "name": "pg-native", - "version": "3.8.0", + "version": "3.9.0", "description": "A slightly nicer interface to Postgres over node-libpq", "main": "index.js", "exports": { diff --git a/packages/pg-protocol/package.json b/packages/pg-protocol/package.json index 979b2f13a..f2920d569 100644 --- a/packages/pg-protocol/package.json +++ b/packages/pg-protocol/package.json @@ -1,6 +1,6 @@ { "name": "pg-protocol", - "version": "1.15.0", + "version": "1.16.0", "description": "The postgres client/server binary protocol, implemented in TypeScript", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/pg-query-stream/package.json b/packages/pg-query-stream/package.json index 00969c496..30c416e19 100644 --- a/packages/pg-query-stream/package.json +++ b/packages/pg-query-stream/package.json @@ -1,6 +1,6 @@ { "name": "pg-query-stream", - "version": "4.16.0", + "version": "4.17.0", "description": "Postgres query result returned as readable stream", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -45,7 +45,7 @@ "concat-stream": "~1.0.1", "eslint-plugin-promise": "^7.3.0", "mocha": "^11.7.5", - "pg": "^8.22.0", + "pg": "^8.23.0", "stream-spec": "~0.3.5", "ts-node": "^8.5.4", "typescript": "^6.0.3" @@ -54,6 +54,6 @@ "pg": "^8" }, "dependencies": { - "pg-cursor": "^2.21.0" + "pg-cursor": "^2.22.0" } } diff --git a/packages/pg/package.json b/packages/pg/package.json index f8f614804..d028179ca 100644 --- a/packages/pg/package.json +++ b/packages/pg/package.json @@ -1,6 +1,6 @@ { "name": "pg", - "version": "8.22.0", + "version": "8.23.0", "description": "PostgreSQL client - pure javascript & libpq with the same API", "keywords": [ "database", @@ -34,7 +34,7 @@ "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", - "pg-protocol": "^1.15.0", + "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, From 6f1cf81ecd877708ea554ecaa38368ceafc92542 Mon Sep 17 00:00:00 2001 From: Noritaka Kobayashi Date: Tue, 11 Aug 2026 15:18:50 +0900 Subject: [PATCH 43/55] ci: run lint on Node.js 22 (#3714) [skip ci] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f0751c56..551388fcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Setup node uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 cache: yarn - run: yarn install --frozen-lockfile - run: yarn lint From 203db1da197b46739cb4c654a8c83338f4ea3628 Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Tue, 11 Aug 2026 07:16:40 +0000 Subject: [PATCH 44/55] cleanup: Regenerate lockfile, workspace package.json with Yarn 1.22.22 (#3741) For ease of review of #3713 (and previously other PRs). [skip ci] --- package.json | 4 ++-- yarn.lock | 31 ++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9285ad142..68e42e353 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", + "@types/node": "^16", "@typescript-eslint/eslint-plugin": "^8.58.0", "@typescript-eslint/parser": "^8.58.0", "eslint": "^10.2.1", @@ -30,8 +31,7 @@ "eslint-plugin-prettier": "^5.1.2", "lerna": "^3.19.0", "prettier": "3.0.3", - "typescript": "^6.0.3", - "@types/node": "^16" + "typescript": "^6.0.3" }, "prettier": { "semi": false, diff --git a/yarn.lock b/yarn.lock index e91121be7..e2977ba3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8482,7 +8482,7 @@ stream-spec@~0.3.5: dependencies: macgyver "~1.10" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8517,6 +8517,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -8556,7 +8565,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8584,6 +8593,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9501,7 +9517,7 @@ wrangler@^3.x: fsevents "~2.3.2" sharp "^0.33.5" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9528,6 +9544,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From ff9d775abd12f29dd6df03945253b54eabbb29f2 Mon Sep 17 00:00:00 2001 From: Noritaka Kobayashi Date: Tue, 11 Aug 2026 16:30:39 +0900 Subject: [PATCH 45/55] chore: remove unused eslint-plugin-node (#3713) [skip ci] --- package.json | 1 - yarn.lock | 43 +++---------------------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 68e42e353..e30454007 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@typescript-eslint/parser": "^8.58.0", "eslint": "^10.2.1", "eslint-config-prettier": "^10.1.2", - "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.2", "lerna": "^3.19.0", "prettier": "3.0.3", diff --git a/yarn.lock b/yarn.lock index e2977ba3b..a826fd60a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4144,26 +4144,6 @@ eslint-config-prettier@^10.1.2: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276" integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA== -eslint-plugin-es@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz" - integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== - dependencies: - eslint-utils "^2.0.0" - regexpp "^3.0.0" - -eslint-plugin-node@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz" - integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== - dependencies: - eslint-plugin-es "^3.0.0" - eslint-utils "^2.0.0" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - eslint-plugin-prettier@^5.1.2: version "5.5.5" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz#9eae11593faa108859c26f9a9c367d619a0769c0" @@ -4197,18 +4177,6 @@ eslint-scope@^9.1.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" @@ -5187,7 +5155,7 @@ ignore@^4.0.3: resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.2.0: +ignore@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== @@ -7783,11 +7751,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexpp@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== - release-zalgo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" @@ -7897,7 +7860,7 @@ resolve@1.1.x: resolved "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.10.1: +resolve@^1.10.0: version "1.17.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -8060,7 +8023,7 @@ schema-utils@^4.3.0, schema-utils@^4.3.2: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== From 816d073267d2b5b5f04f9aaecd8989e3172a334a Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Wed, 12 Aug 2026 02:28:41 +0000 Subject: [PATCH 46/55] perf: Copy less when serializing `Bind` messages (#3740) * Skip copying unused space at end of writer buffer when resizing * perf: Write `Bind` parameter values directly to message writer instead of creating a separate `paramWriter`. * fix: Avoid creating message with uninitialized memory when `valueMapper` shrinks values It would have created an incomplete message anyway, but I figured this was vaguely plausible, bad enough, and easy enough to fix. No test though, because the test is annoying to write. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/pg-protocol/src/buffer-writer.ts | 14 ++++++- packages/pg-protocol/src/serializer.ts | 46 +++++++++++++---------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/pg-protocol/src/buffer-writer.ts b/packages/pg-protocol/src/buffer-writer.ts index 9eae65859..eb69cc8ce 100644 --- a/packages/pg-protocol/src/buffer-writer.ts +++ b/packages/pg-protocol/src/buffer-writer.ts @@ -1,7 +1,7 @@ //binary data writer tuned for encoding binary specific to the postgres binary protocol export class Writer { - private buffer: Buffer + public buffer: Buffer private offset: number = 5 private headerPosition: number = 0 constructor(private size = 256) { @@ -16,7 +16,7 @@ export class Writer { // https://stackoverflow.com/questions/2269063/buffer-growth-strategy const newSize = oldBuffer.length + (oldBuffer.length >> 1) + size this.buffer = Buffer.allocUnsafe(newSize) - oldBuffer.copy(this.buffer) + oldBuffer.copy(this.buffer, 0, 0, this.offset) } } @@ -85,6 +85,16 @@ export class Writer { return this } + /** + * Appends an uninitialized block of {@link size} bytes to the buffer and returns its offset. + */ + public reserveUnsafe(size: number): number { + const offset = this.offset + this.ensure(size) + this.offset += size + return offset + } + private join(code?: number): Buffer { if (code) { this.buffer[this.headerPosition] = code diff --git a/packages/pg-protocol/src/serializer.ts b/packages/pg-protocol/src/serializer.ts index 547c053f8..bbd59623e 100644 --- a/packages/pg-protocol/src/serializer.ts +++ b/packages/pg-protocol/src/serializer.ts @@ -110,34 +110,36 @@ type BindOpts = { valueMapper?: ValueMapper } -const paramWriter = new Writer() - // make this a const enum so typescript will inline the value const enum ParamType { STRING = 0, BINARY = 1, } -const writeValues = function (values: any[], valueMapper?: ValueMapper): void { - for (let i = 0; i < values.length; i++) { +const writeValues = function (values: any[], valueMapper: ValueMapper | undefined, formatsOffset: number): void { + const len = values.length + for (let i = 0; i < len; i++) { const mappedVal = valueMapper ? valueMapper(values[i], i) : values[i] + let formatByte = ParamType.STRING + if (mappedVal == null) { - // add the param type (string) to the writer - writer.addInt16(ParamType.STRING) - // write -1 to the param writer to indicate null - paramWriter.addInt32(-1) + // write -1 to indicate null + writer.addInt32(-1) } else if (mappedVal instanceof Buffer) { - // add the param type (binary) to the writer - writer.addInt16(ParamType.BINARY) + formatByte = ParamType.BINARY + // add the buffer to the param writer - paramWriter.addInt32(mappedVal.length) - paramWriter.add(mappedVal) + writer.addInt32(mappedVal.length) + writer.add(mappedVal) } else { - // add the param type (string) to the writer - writer.addInt16(ParamType.STRING) // length prefix + UTF-8 bytes in one pass (Buffer.byteLength computed once) - paramWriter.addInt32PrefixedString(mappedVal) + writer.addInt32PrefixedString(mappedVal) } + + // beware: `writer` operations can replace `writer.buffer` with a new buffer + const buf = writer.buffer + buf[formatsOffset++] = 0 + buf[formatsOffset++] = formatByte } } @@ -150,19 +152,23 @@ const bind = (config: BindOpts = {}): Buffer => { const len = values.length writer.addCString(portal).addCString(statement) + + // number of parameter format codes + writer.addInt16(len) + + // space for those codes, filled by `writeValues` + const formatsOffset = writer.reserveUnsafe(len * 2) + + // number of parameter values writer.addInt16(len) try { - writeValues(values, config.valueMapper) + writeValues(values, config.valueMapper, formatsOffset) } catch (err) { writer.clear() - paramWriter.clear() throw err } - writer.addInt16(len) - writer.add(paramWriter.flush()) - // all results use the same format code writer.addInt16(1) // format code From cd5ec59255dc5ff179c4929fa2eb40ad881d1d07 Mon Sep 17 00:00:00 2001 From: Pratik Dulal <72141032+pratik-desgn@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:15:22 +0545 Subject: [PATCH 47/55] fix(pg-cloudflare): safely end closed sockets (#3735) --- packages/pg-cloudflare/src/index.ts | 2 +- packages/pg-esm-test/pg-cloudflare.test.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/pg-cloudflare/src/index.ts b/packages/pg-cloudflare/src/index.ts index 9b1e517ba..357131911 100644 --- a/packages/pg-cloudflare/src/index.ts +++ b/packages/pg-cloudflare/src/index.ts @@ -107,7 +107,7 @@ export class CloudflareSocket extends EventEmitter { end(data = Buffer.alloc(0), encoding: BufferEncoding = 'utf8', callback: (...args: unknown[]) => void = () => {}) { log('ending CF socket') this.write(data, encoding, (err) => { - this._cfSocket!.close() + this._cfSocket?.close() if (callback) callback(err) }) return this diff --git a/packages/pg-esm-test/pg-cloudflare.test.js b/packages/pg-esm-test/pg-cloudflare.test.js index a42620253..c140f0bb1 100644 --- a/packages/pg-esm-test/pg-cloudflare.test.js +++ b/packages/pg-esm-test/pg-cloudflare.test.js @@ -6,4 +6,16 @@ describe('pg-cloudflare', () => { it('should export CloudflareSocket constructor', () => { assert.ok(new CloudflareSocket()) }) + + it('should safely end after the underlying socket has closed', async () => { + const socket = new CloudflareSocket() + const underlyingSocket = { closed: Promise.resolve() } + socket._cfSocket = underlyingSocket + socket._addClosedHandler() + + await underlyingSocket.closed + assert.equal(socket._cfSocket, null) + + assert.doesNotThrow(() => socket.end()) + }) }) From 7bee4db0dbecfc26ae0daf888d34912e8c8c982e Mon Sep 17 00:00:00 2001 From: Clio Date: Wed, 12 Aug 2026 10:33:29 +0800 Subject: [PATCH 48/55] fix: preserve row mode in native pipelines (#3742) Co-authored-by: zfaustk <4340287+zfaustk@users.noreply.github.com> --- packages/pg-native/index.js | 6 +++--- packages/pg/lib/native/client.js | 2 +- .../test/integration/client/pipelining-tests.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js index 7fcc26303..fafd66aaf 100644 --- a/packages/pg-native/index.js +++ b/packages/pg-native/index.js @@ -174,8 +174,8 @@ Client.prototype._stopReading = function () { this.pq.removeListener('readable', this._read) } -Client.prototype._consumeQueryResults = function (pq) { - return buildResult(pq, this._types, this.arrayMode) +Client.prototype._consumeQueryResults = function (pq, arrayMode = this.arrayMode) { + return buildResult(pq, this._types, arrayMode) } Client.prototype._emitResult = function (pq) { @@ -441,7 +441,7 @@ Client.prototype._readPipelineResults = function (queries, cb) { } if (status === 'PGRES_TUPLES_OK' || status === 'PGRES_COMMAND_OK' || status === 'PGRES_EMPTY_QUERY') { - currentResult = self._consumeQueryResults(pq) + currentResult = self._consumeQueryResults(pq, queries[queryIndex].arrayMode) continue } } diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index d305713d6..2edfff720 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -337,7 +337,7 @@ Client.prototype._pulsePipelinedQueryQueue = function () { nativeQueries.push(query) const values = query.values ? query.values.map(utils.prepareValue) : null - const pipelineEntry = { text: query.text, name: query.name } + const pipelineEntry = { text: query.text, name: query.name, arrayMode: query._arrayMode } if (values) { pipelineEntry.values = values } diff --git a/packages/pg/test/integration/client/pipelining-tests.js b/packages/pg/test/integration/client/pipelining-tests.js index 957047d7d..7f927d2b0 100644 --- a/packages/pg/test/integration/client/pipelining-tests.js +++ b/packages/pg/test/integration/client/pipelining-tests.js @@ -35,6 +35,21 @@ suite.test('pipeline with parameterized queries', async function () { await client.end() }) +suite.test('pipeline preserves row mode for each query', async function () { + const client = new helper.Client({ pipeline: true }) + await client.connect() + + const [arrayResult, objectResult] = await Promise.all([ + client.query({ text: 'SELECT $1::int AS num', values: [10], rowMode: 'array' }), + client.query({ text: 'SELECT $1::int AS num', values: [20] }), + ]) + + assert.deepStrictEqual(arrayResult.rows, [[10]]) + assert.deepStrictEqual(objectResult.rows, [{ num: 20 }]) + + await client.end() +}) + suite.test('pipeline with named prepared statements', async function () { const client = helper.client(undefined, { pipeline: true }) From c940d7c206c8545cb195df2ecde230d9ca0279c8 Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Wed, 12 Aug 2026 04:40:51 +0200 Subject: [PATCH 49/55] Fail pipelined queries when the connection dies instead of hanging (#3736) --- packages/pg-native/index.js | 27 +++++-- .../test/pipeline-connection-loss.js | 76 +++++++++++++++++++ packages/pg/lib/native/client.js | 12 ++- 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 packages/pg-native/test/pipeline-connection-loss.js diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js index fafd66aaf..451daeb7d 100644 --- a/packages/pg-native/index.js +++ b/packages/pg-native/index.js @@ -386,8 +386,12 @@ Client.prototype._readPipelineResults = function (queries, cb) { const processResults = function () { if (!pq.consumeInput()) { - pq.exitPipelineMode() - return cb(new Error(pq.errorMessage() || 'Failed to consume input')) + // read the message before anything else touches the connection: libpq appends to a single + // error buffer, and exiting pipeline mode on a connection that is still busy adds its own + // "cannot exit pipeline mode while busy" to the end of the reason the caller actually wants. + // The connection is finished either way, so there is nothing to exit cleanly for. + const message = pq.errorMessage() + return cb(new Error(message || 'Failed to consume input')) } while (!pq.isBusy()) { @@ -395,8 +399,10 @@ Client.prototype._readPipelineResults = function (queries, cb) { // null between result groups in pipeline — try again if (pq.isBusy()) return // more data needed if (!pq.getResult()) { - // truly no more results — should not happen before all syncs - break + // libpq has no result left and is not waiting for one, yet we have not seen a sync for + // every query. Nothing further is owed on this connection, so breaking out would return + // without ever calling cb and strand the caller. Fail the batch instead. + return cb(new Error(pq.errorMessage() || 'Connection ended before the pipeline completed')) } } @@ -436,7 +442,13 @@ Client.prototype._readPipelineResults = function (queries, cb) { } if (status === 'PGRES_PIPELINE_ABORTED') { - // Query skipped due to previous error in same sync group + // The server refused to run this one: an earlier query in the same sync group failed and + // the pipeline is aborted until the next sync. Falling through left currentError and + // currentResult null, so the sync branch below handed back {err: null, rows: []} and a + // statement that never ran looked like one that matched no rows. + if (!currentError) { + currentError = new Error('Query was not executed: an earlier query in the same pipeline failed') + } continue } @@ -465,6 +477,11 @@ Client.prototype._readPipelineResults = function (queries, cb) { } pq.on('readable', onReadable) pq.startReader() + // startReader() has to be recorded, or _stopReading() short-circuits on the flag and leaves the + // poll watcher running after the batch is over. A later finish() then closes the handle with the + // watcher still armed, and the next startReader() on it aborts the process with + // "uv_poll_start: Assertion `!uv__is_closing(handle)' failed". + this._reading = true // Try an initial read in case data is already available processResults() diff --git a/packages/pg-native/test/pipeline-connection-loss.js b/packages/pg-native/test/pipeline-connection-loss.js new file mode 100644 index 000000000..eb5e779a5 --- /dev/null +++ b/packages/pg-native/test/pipeline-connection-loss.js @@ -0,0 +1,76 @@ +const net = require('net') +const assert = require('assert') +const Client = require('../') + +describe('pipeline reader', function () { + let proxy + let proxyPort + let clientSockets + + beforeEach(function (done) { + clientSockets = [] + proxy = net.createServer(function (client) { + const upstream = net.connect(Number(process.env.PGPORT || 5432), process.env.PGHOST || 'localhost') + clientSockets.push(client) + client.pipe(upstream) + upstream.pipe(client) + client.on('error', function () {}) + upstream.on('error', function () {}) + }) + proxy.listen(0, '127.0.0.1', function () { + proxyPort = proxy.address().port + done() + }) + }) + + afterEach(function (done) { + proxy.close(function () { + done() + }) + }) + + // the batch starts the reader, so it has to stop it too. It used to leave the poll watcher armed, + // and a later finish() then closed the handle under it. + it('stops the reader it started once the batch is done', function (done) { + const client = new Client() + client.connect(`host=127.0.0.1 port=${proxyPort}`, function (err) { + assert.ifError(err) + let stopped = 0 + const stopReader = client.pq.stopReader.bind(client.pq) + client.pq.stopReader = function () { + stopped++ + return stopReader() + } + client.pipeline([{ text: 'SELECT 1' }, { text: 'SELECT 2' }], function (err) { + assert.ifError(err) + assert(stopped > 0, 'the reader started for the batch was never stopped') + client.end() + done() + }) + }) + }) + + // exitPipelineMode() on a busy connection appends its own complaint to libpq's error buffer, so + // reading the message afterwards buried the reason the caller wanted. + it('reports why the connection went away', function (done) { + this.timeout(10000) + const client = new Client() + client.connect(`host=127.0.0.1 port=${proxyPort}`, function (err) { + assert.ifError(err) + client.pipeline([{ text: 'SELECT pg_sleep(10)' }], function (err) { + assert(err, 'a batch cut off mid flight must fail') + assert( + !/cannot exit pipeline mode/.test(err.message), + `error should say why the connection ended, got: ${err.message}` + ) + client.end() + done() + }) + setTimeout(function () { + clientSockets.forEach(function (socket) { + socket.end() + }) + }, 100) + }) + }) +}) diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index 2edfff720..9ec3c8c03 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -351,13 +351,21 @@ Client.prototype._pulsePipelinedQueryQueue = function () { self._pipelineInFlight = false if (err) { - // Total pipeline failure — error all queries + // Total pipeline failure. Per-query errors arrive on results[i].err, so reaching here means + // the connection itself is gone: mark it unusable and say so, the way the JS client and the + // non-pipelined native path both do. Without this the client looks healthy after losing its + // backend, a Pool never discards it, and everything routed to it fails one query at a time + // for the life of the process. + self._connected = false + self._queryable = false for (let i = 0; i < nativeQueries.length; i++) { const q = nativeQueries[i] q.native = self.native q.handleError(err) } - self._pulsePipelinedQueryQueue() + self._errorAllQueries(err) + self.emit('error', err) + self.emit('end') return } From 299148045f921060b5df91343ab4710d46674b08 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:41:23 -0700 Subject: [PATCH 50/55] Deprecate serializing invalid `Date`s (#3731) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: charmander <1889843+charmander@users.noreply.github.com> --- packages/pg/lib/utils.js | 10 ++++++++++ packages/pg/test/unit/utils-tests.js | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index 638b43970..4649405a8 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -1,9 +1,16 @@ 'use strict' const defaults = require('./defaults') +const nodeUtils = require('util') const { isDate } = require('util/types') +const invalidDateDeprecationNotice = nodeUtils.deprecate( + () => {}, + 'Sending an invalid date to Postgres is deprecated and will throw an error in the next major version of pg. Ensure any Date object passed as a query parameter is valid.', + 'PG_INVALID_DATE' +) + function escapeElement(elementRepresentation) { const escaped = elementRepresentation.replace(/\\/g, '\\\\').replace(/"/g, '\\"') @@ -54,6 +61,9 @@ const prepareValue = function (val, seen) { return Buffer.from(val.buffer, val.byteOffset, val.byteLength) } if (isDate(val)) { + if (isNaN(val.getTime())) { + invalidDateDeprecationNotice() + } if (defaults.parseInputDatesAsUTC) { return dateToStringUTC(val) } else { diff --git a/packages/pg/test/unit/utils-tests.js b/packages/pg/test/unit/utils-tests.js index 5f75f6c2d..edb01f414 100644 --- a/packages/pg/test/unit/utils-tests.js +++ b/packages/pg/test/unit/utils-tests.js @@ -89,6 +89,23 @@ test('prepareValues: 1 BC date prepared properly', function () { helper.resetTimezoneOffset() }) +test('prepareValue: invalid date emits deprecation warning', function () { + const warningSeen = new Promise((resolve) => { + const onWarning = (warning) => { + if (warning.code === 'PG_INVALID_DATE') { + process.removeListener('warning', onWarning) + resolve() + } + } + process.on('warning', onWarning) + }) + + const out = utils.prepareValue(new Date(NaN)) + assert.strictEqual(out, '0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN') + + return warningSeen +}) + test('prepareValues: undefined prepared properly', function () { const out = utils.prepareValue(void 0) assert.strictEqual(out, null) From 0cef6afdf781e4cbe7eb5ac943df3c14b49c33de Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Wed, 12 Aug 2026 04:42:22 +0200 Subject: [PATCH 51/55] Reject portal based queries in pipeline mode instead of misrouting rows (#3737) --- packages/pg/lib/client.js | 18 ++++ .../client/pipeline-portal-tests.js | 83 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 packages/pg/test/integration/client/pipeline-portal-tests.js diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 7a2fc9a64..2b13c1de7 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -744,6 +744,24 @@ class Client extends EventEmitter { query._result._types = this._types } + // A query that keeps a portal open across round trips cannot share a pipelined connection: the + // queries written behind it are answered out of its portal, so rows land on the wrong query and + // the reads that follow fail with 'portal does not exist'. Refuse it instead of corrupting. + if (this.pipeline) { + const portalQuery = + typeof config.submit === 'function' && !(query instanceof Query) + ? 'Custom query classes such as pg-cursor and pg-query-stream are' + : query.rows + ? 'The `rows` option is' + : null + if (portalQuery) { + process.nextTick(() => { + query.handleError(new Error(`${portalQuery} not supported in pipeline mode`), this.connection) + }) + return result + } + } + if (!this._queryable) { process.nextTick(() => { query.handleError(new Error('Client has encountered a connection error and is not queryable'), this.connection) diff --git a/packages/pg/test/integration/client/pipeline-portal-tests.js b/packages/pg/test/integration/client/pipeline-portal-tests.js new file mode 100644 index 000000000..b478a461e --- /dev/null +++ b/packages/pg/test/integration/client/pipeline-portal-tests.js @@ -0,0 +1,83 @@ +'use strict' +const helper = require('./test-helper') +const assert = require('assert') +const pg = helper.pg + +// A portal stays open across round trips, so on a pipelined connection the queries written behind +// it were answered out of that portal: rows arrived on the wrong query, later reads failed with +// 'portal does not exist' and pg-cursor crashed on a null row buffer. These must be refused. +const suite = new helper.Suite('pipeline mode portal queries') + +if (helper.args.native) { + return +} + +// stands in for pg-cursor and pg-query-stream, so the test does not need either package +class FakeCursor { + constructor() { + this.error = null + } + submit() {} + handleError(err) { + this.error = err + if (this.onError) this.onError(err) + } +} + +suite.test('rejects a custom query class', (done) => { + const client = new pg.Client({ pipeline: true }) + client.connect((err) => { + if (err) return done(err) + const cursor = client.query(new FakeCursor()) + cursor.onError = (err) => { + assert.ok(/pipeline mode/.test(err.message), `expected a pipeline mode error, got: ${err.message}`) + client.end(done) + } + }) +}) + +suite.test('rejects the rows option', (done) => { + const client = new pg.Client({ pipeline: true }) + client.connect((err) => { + if (err) return done(err) + client + .query({ text: 'SELECT generate_series(1, 10) as num', rows: 3 }) + .then(() => { + client.end(() => done(new Error('a paged query should not be accepted in pipeline mode'))) + }) + .catch((err) => { + assert.ok(/pipeline mode/.test(err.message), `expected a pipeline mode error, got: ${err.message}`) + client.end(done) + }) + }) +}) + +suite.test('accepts both when pipeline mode is off', (done) => { + const client = new pg.Client() + client.connect((err) => { + if (err) return done(err) + client + .query({ text: 'SELECT generate_series(1, 10) as num', rows: 3 }) + .then((res) => { + assert.equal(res.rows.length, 10) + client.end(done) + }) + .catch((err) => client.end(() => done(err))) + }) +}) + +suite.test('leaves normal queries alone', (done) => { + const client = new pg.Client({ pipeline: true }) + client.connect((err) => { + if (err) return done(err) + Promise.all([1, 2, 3].map((i) => client.query('SELECT $1::int as v', [i]))) + .then((results) => { + assert.deepStrictEqual( + results.map((r) => Number(r.rows[0].v)), + [1, 2, 3] + ) + client.end(done) + }) + .catch((err) => client.end(() => done(err))) + }) +}) From 2b02f645687b8536f209aecc24f1b4bdb4c32d16 Mon Sep 17 00:00:00 2001 From: Dustin <85157729+GiHoon1123@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:44:18 +0900 Subject: [PATCH 52/55] fix: avoid mutating query config (#3720) * fix: don't mutate the caller's query config object Fixes #2651. normalizeQueryConfig() wrote values/callback directly onto the object it was given instead of copying it first. Reusing the same config object across calls (callback style, then promise style) left a stale callback on it, so a later promise-style call would see query.callback already set, skip creating the promise, and silently return undefined while the real result went to the old callback instead. Copy the object before writing to it so the caller's config is never touched, and add regression tests for both the direct case and the callback-then-promise reuse scenario. * test: simplify query config comments * fix: preserve query config property access --- packages/pg/lib/utils.js | 10 +++++- .../pg/test/unit/client/simple-query-tests.js | 14 ++++++++ packages/pg/test/unit/utils-tests.js | 35 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index 4649405a8..ba51c82c8 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -153,7 +153,8 @@ function dateToStringUTC(date) { function normalizeQueryConfig(config, values, callback) { // can take in strings or config objects - config = typeof config === 'string' ? { text: config } : config + // Copy config so normalization does not mutate the caller's object. + config = typeof config === 'string' ? { text: config } : cloneQueryConfig(config) if (values) { if (typeof values === 'function') { config.callback = values @@ -167,6 +168,13 @@ function normalizeQueryConfig(config, values, callback) { return config } +function cloneQueryConfig(config) { + if (config == null) { + return config + } + return Object.defineProperties(Object.create(Object.getPrototypeOf(config)), Object.getOwnPropertyDescriptors(config)) +} + // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c const escapeIdentifier = function (str) { return '"' + str.replace(/"/g, '""') + '"' diff --git a/packages/pg/test/unit/client/simple-query-tests.js b/packages/pg/test/unit/client/simple-query-tests.js index efb705aa1..3e3918773 100644 --- a/packages/pg/test/unit/client/simple-query-tests.js +++ b/packages/pg/test/unit/client/simple-query-tests.js @@ -244,4 +244,18 @@ test('executing query', function () { ) }) }) + + test('reusing a config object across calls', function () { + // Regression test for https://github.com/brianc/node-postgres/issues/2651. + test('does not leak callback state into a later promise-style call', function () { + const client = helper.client() + const config = { text: 'SELECT $1', values: [1] } + + client.query(config, function () {}) + const result = client.query(config) + + assert.ok(result instanceof Promise, 'expected client.query() to return a Promise') + result.catch(() => {}) + }) + }) }) diff --git a/packages/pg/test/unit/utils-tests.js b/packages/pg/test/unit/utils-tests.js index edb01f414..ff0d92944 100644 --- a/packages/pg/test/unit/utils-tests.js +++ b/packages/pg/test/unit/utils-tests.js @@ -33,6 +33,41 @@ test('normalizing query configs', function () { assert.deepEqual(config, { text: 'TEXT', values: [10], callback: callback }) }) +test('normalizeQueryConfig does not mutate the passed-in config object', function () { + // Regression test for https://github.com/brianc/node-postgres/issues/2651. + const original = { text: 'TEXT' } + const callback = function () {} + + const normalized = utils.normalizeQueryConfig(original, [10], callback) + + assert.equal(original.callback, undefined) + assert.equal(original.values, undefined) + assert.deepEqual(normalized, { text: 'TEXT', values: [10], callback: callback }) +}) + +test('normalizeQueryConfig preserves inherited config properties', function () { + class QueryConfig { + constructor() { + this._text = 'TEXT' + } + + get text() { + return this._text + } + } + + const original = new QueryConfig() + const callback = function () {} + + const normalized = utils.normalizeQueryConfig(original, [10], callback) + + assert.equal(original.callback, undefined) + assert.equal(original.values, undefined) + assert.equal(normalized.text, 'TEXT') + assert.deepEqual(normalized.values, [10]) + assert.equal(normalized.callback, callback) +}) + test('prepareValues: buffer prepared properly', function () { const buf = Buffer.from('quack') const out = utils.prepareValue(buf) From c9e57617bc92c2ded23a75345f50eadc527bd131 Mon Sep 17 00:00:00 2001 From: Clio Date: Sat, 15 Aug 2026 03:35:15 +0800 Subject: [PATCH 53/55] fix: support callback as second argument to `CloudflareSocket.write` (#3747) `Connection.end()` calls `write(data, callback)`. `CloudflareSocket.write` previously treated the callback as an encoding, so it never ran. --- packages/pg-cloudflare/src/index.ts | 10 +++++++--- packages/pg-esm-test/pg-cloudflare.test.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/pg-cloudflare/src/index.ts b/packages/pg-cloudflare/src/index.ts index 357131911..d625beee0 100644 --- a/packages/pg-cloudflare/src/index.ts +++ b/packages/pg-cloudflare/src/index.ts @@ -82,11 +82,15 @@ export class CloudflareSocket extends EventEmitter { this.emit('data', Buffer.from(value)) } + write(data: Uint8Array | string, callback?: (error?: unknown) => void): true | void + write(data: Uint8Array | string, encoding?: BufferEncoding, callback?: (error?: unknown) => void): true | void write( data: Uint8Array | string, - encoding: BufferEncoding = 'utf8', - callback: (...args: unknown[]) => void = () => {} - ) { + encodingOrCallback: BufferEncoding | ((error?: unknown) => void) = 'utf8', + callback: (error?: unknown) => void = () => {} + ): true | void { + const encoding = typeof encodingOrCallback === 'function' ? 'utf8' : encodingOrCallback + if (typeof encodingOrCallback === 'function') callback = encodingOrCallback if (data.length === 0) return callback() if (typeof data === 'string') data = Buffer.from(data, encoding) diff --git a/packages/pg-esm-test/pg-cloudflare.test.js b/packages/pg-esm-test/pg-cloudflare.test.js index c140f0bb1..75d1f9957 100644 --- a/packages/pg-esm-test/pg-cloudflare.test.js +++ b/packages/pg-esm-test/pg-cloudflare.test.js @@ -18,4 +18,23 @@ describe('pg-cloudflare', () => { assert.doesNotThrow(() => socket.end()) }) + + it('should call the write(data, callback) callback exactly once', async () => { + const socket = new CloudflareSocket() + socket._cfWriter = { write: () => Promise.resolve() } + + let resolve + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise + }) + let called = false + socket.write(Buffer.from('x'), (error) => { + assert.ifError(error) + assert(!called) + called = true + resolve() + }) + + await promise + }) }) From 9808955838dd84835c4ca904228c9c2eaeda047e Mon Sep 17 00:00:00 2001 From: Charmander <~@charmander.me> Date: Tue, 1 Sep 2026 17:26:05 -0700 Subject: [PATCH 54/55] cleanup: Fix typo in comment [skip ci] --- packages/pg/lib/crypto/cert-signatures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pg/lib/crypto/cert-signatures.js b/packages/pg/lib/crypto/cert-signatures.js index 8d8df3425..5f8650624 100644 --- a/packages/pg/lib/crypto/cert-signatures.js +++ b/packages/pg/lib/crypto/cert-signatures.js @@ -107,7 +107,7 @@ function signatureAlgorithmHashFromCertificate(data, index) { } throw x509Error('unknown hash OID ' + hashOID, data) } - // Ed25519 -- see https: return//github.com/openssl/openssl/issues/15477 + // Ed25519 -- see https://github.com/openssl/openssl/issues/15477 case '1.3.101.110': case '1.3.101.112': // ph return 'SHA-512' From 458903894f3514465b74d623f6b92f713b6b952f Mon Sep 17 00:00:00 2001 From: Johannes Ewald Date: Thu, 10 Sep 2026 21:55:06 +0200 Subject: [PATCH 55/55] fix: validate server certificate against host when connecting to an IP address (#3756) tls.connect verifies the server identity against `servername`, falling back to `host` and then to 'localhost'. Since `servername` must not be set to an IP address (RFC 6066 section 3), certificates were validated against 'localhost' whenever the connection host was an IP address. Passing `host` to tls.connect fixes this. Continues #2273 (originally by Frazer McLean) with tests. Fixes #2263 Co-authored-by: Frazer McLean --- packages/pg/lib/connection.js | 6 + packages/pg/test/unit/connection/ssl-tests.js | 115 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 packages/pg/test/unit/connection/ssl-tests.js diff --git a/packages/pg/lib/connection.js b/packages/pg/lib/connection.js index 62d38fa69..099e2d4f0 100644 --- a/packages/pg/lib/connection.js +++ b/packages/pg/lib/connection.js @@ -97,6 +97,11 @@ class Connection extends EventEmitter { const self = this const options = { socket: self.stream, + // tls.connect checks the server identity against `servername`, falling + // back to `host` and then to 'localhost'. `servername` must stay unset + // for IP addresses (see below), so `host` is needed to keep certificate + // validation working when connecting to an IP address. + host, } if (self.ssl !== true) { @@ -113,6 +118,7 @@ class Connection extends EventEmitter { options.ALPNProtocols = ['postgresql'] } + // SNI must not be set to an IP address (RFC 6066 section 3) const net = require('net') if (net.isIP && net.isIP(host) === 0) { options.servername = host diff --git a/packages/pg/test/unit/connection/ssl-tests.js b/packages/pg/test/unit/connection/ssl-tests.js new file mode 100644 index 000000000..930c58ce5 --- /dev/null +++ b/packages/pg/test/unit/connection/ssl-tests.js @@ -0,0 +1,115 @@ +'use strict' +const helper = require('./test-helper') +const Connection = require('../../../lib/connection') +const net = require('net') +const tls = require('tls') +const fs = require('fs') +const path = require('path') +const assert = require('assert') + +const suite = new helper.Suite() +const { MemoryStream } = helper + +// tls.connect verifies the server identity against `servername`, falling back +// to `host` and then to 'localhost'. Since `servername` must not be set to an +// IP address, `host` has to be passed as well or certificates would be +// validated against 'localhost' when connecting to an IP address. +// See https://github.com/brianc/node-postgres/issues/2263 + +suite.test('SSL upgrade passes the host to the secure stream when connecting to an IP address', function (done) { + const streamModule = require('../../../lib/stream') + const originalGetSecureStream = streamModule.getSecureStream + + let capturedOptions = null + streamModule.getSecureStream = function (options) { + capturedOptions = options + return options.socket + } + + try { + const con = new Connection({ stream: new MemoryStream(), ssl: true }) + con.connect(1234, '127.0.0.1') + // server signals SSL support with an 'S' byte + con.stream.emit('data', Buffer.from('S')) + + assert(capturedOptions, 'getSecureStream should have been called') + assert.equal(capturedOptions.host, '127.0.0.1', 'the host must be passed for certificate validation') + assert.equal(capturedOptions.servername, undefined, 'SNI must not be set to an IP address') + done() + } finally { + streamModule.getSecureStream = originalGetSecureStream + } +}) + +suite.test( + 'SSL upgrade passes the host and servername to the secure stream when connecting to a hostname', + function (done) { + const streamModule = require('../../../lib/stream') + const originalGetSecureStream = streamModule.getSecureStream + + let capturedOptions = null + streamModule.getSecureStream = function (options) { + capturedOptions = options + return options.socket + } + + try { + const con = new Connection({ stream: new MemoryStream(), ssl: true }) + con.connect(1234, 'example.com') + con.stream.emit('data', Buffer.from('S')) + + assert(capturedOptions, 'getSecureStream should have been called') + assert.equal(capturedOptions.host, 'example.com') + assert.equal(capturedOptions.servername, 'example.com') + done() + } finally { + streamModule.getSecureStream = originalGetSecureStream + } + } +) + +suite.test('TLS verifies the server certificate against the IP address being connected to', function (done) { + const tlsDir = path.join(__dirname, '..', '..', 'tls') + const serverKey = fs.readFileSync(path.join(tlsDir, 'test-server.key')) + const serverCert = fs.readFileSync(path.join(tlsDir, 'test-server.crt')) + const serverCa = fs.readFileSync(path.join(tlsDir, 'test-server-ca.crt')) + + // our fake postgres server: reply 'S' to the SSLRequest packet, then + // perform the server side of the TLS handshake on the raw socket + let socket + const server = net.createServer(function (c) { + socket = c + c.once('data', function () { + c.write(Buffer.from('S')) + socket = new tls.TLSSocket(c, { isServer: true, key: serverKey, cert: serverCert }) + }) + }) + + server.listen(0, '127.0.0.1', function () { + // capture which host tls.connect checks the server identity against; + // without the fix from https://github.com/brianc/node-postgres/pull/2273 + // this was 'localhost' instead of the IP address being connected to + let verifiedHost = null + const con = new Connection({ + ssl: { + ca: serverCa, + checkServerIdentity: function (host) { + verifiedHost = host + return undefined + }, + }, + }) + con.connect(server.address().port, '127.0.0.1') + assert.emits(con, 'sslconnect', function () { + // 'sslconnect' fires before the TLS handshake completes, so wait for it + con.stream.on('secureConnect', function () { + assert.equal(verifiedHost, '127.0.0.1', 'the server identity must be verified against the IP address') + con.end() + socket.destroy() + server.close() + done() + }) + }) + con.requestSsl() + }) +})