+
+ // 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/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/docs/pages/index.mdx b/docs/pages/index.mdx
index ff27662e6..d0ab0edc6 100644
--- a/docs/pages/index.mdx
+++ b/docs/pages/index.mdx
@@ -5,8 +5,16 @@ 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.
+
+node-postgres supports all current and LTS versions of node as well as bun, deno, and cloudflare workers.
+
## Install
```bash
@@ -60,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/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!
!
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 000000000..2d969deb0
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,78 @@
+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',
+ 'no-redeclare': 'off',
+ '@typescript-eslint/no-redeclare': 'error',
+ },
+ },
+])
diff --git a/package.json b/package.json
index 1f40662b9..e30454007 100644
--- a/package.json
+++ b/package.json
@@ -20,15 +20,17 @@
"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",
+ "@types/node": "^16",
+ "@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"
},
"prettier": {
"semi": false,
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 c1584fc09..4bc706c8a 100644
--- a/packages/pg-cloudflare/package.json
+++ b/packages/pg-cloudflare/package.json
@@ -1,13 +1,13 @@
{
"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",
"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..d625beee0 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'
/**
@@ -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)
@@ -107,7 +111,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
@@ -153,7 +157,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/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
[](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/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 29ffeafd7..7ee302976 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)) {
@@ -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
@@ -164,7 +170,7 @@ function toConnectionOptions(sslConfig) {
}
return c
- }, {})
+ }, Object.create(null))
return connectionOptions
}
@@ -200,7 +206,7 @@ function toClientConfig(config) {
}
return c
- }, {})
+ }, Object.create(null))
return poolConfig
}
diff --git a/packages/pg-connection-string/package.json b/packages/pg-connection-string/package.json
index a60131456..cf0ac743b 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.12.0",
- "description": "Functions for dealing with a PostgresSQL connection string",
+ "version": "2.14.0",
+ "description": "Functions for dealing with a PostgreSQL connection string",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
@@ -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-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..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)
@@ -467,4 +490,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-cursor/package.json b/packages/pg-cursor/package.json
index 332e51a2f..46f3406c2 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.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.20.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 0f15f7ff2..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.6.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.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.23.0",
+ "pg-cloudflare": "^1.4.0",
+ "pg-cursor": "^2.22.0",
+ "pg-native": "^3.9.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.16.0",
+ "pg-query-stream": "^4.17.0"
},
"author": "Brian M. Carlson ",
"license": "MIT"
diff --git a/packages/pg-esm-test/pg-cloudflare.test.js b/packages/pg-esm-test/pg-cloudflare.test.js
index a42620253..75d1f9957 100644
--- a/packages/pg-esm-test/pg-cloudflare.test.js
+++ b/packages/pg-esm-test/pg-cloudflare.test.js
@@ -6,4 +6,35 @@ 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())
+ })
+
+ 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
+ })
})
diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js
index 8c83406bb..451daeb7d 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
@@ -166,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) {
@@ -191,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
@@ -306,6 +318,175 @@ 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()) {
+ // 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()) {
+ if (!pq.getResult()) {
+ // null between result groups in pipeline — try again
+ if (pq.isBusy()) return // more data needed
+ if (!pq.getResult()) {
+ // 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'))
+ }
+ }
+
+ 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') {
+ // 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
+ }
+
+ if (status === 'PGRES_TUPLES_OK' || status === 'PGRES_COMMAND_OK' || status === 'PGRES_EMPTY_QUERY') {
+ currentResult = self._consumeQueryResults(pq, queries[queryIndex].arrayMode)
+ 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()
+ // 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()
+}
+
Client.prototype._onReadyForQuery = function () {
// remove instance callback
const cb = this._queryCallback
diff --git a/packages/pg-native/package.json b/packages/pg-native/package.json
index 4c8148ac1..63d7569f3 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.9.0",
"description": "A slightly nicer interface to Postgres over node-libpq",
"main": "index.js",
"exports": {
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-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-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-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-protocol/package.json b/packages/pg-protocol/package.json
index 896c21e69..f2920d569 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.16.0",
"description": "The postgres client/server binary protocol, implemented in TypeScript",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -17,12 +17,11 @@
"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..42a4a23fa 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) {}
@@ -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)
}
diff --git a/packages/pg-protocol/src/buffer-writer.ts b/packages/pg-protocol/src/buffer-writer.ts
index cebb0d9ed..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)
}
}
@@ -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)
@@ -65,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
@@ -82,4 +112,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/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/outbound-serializer.test.ts b/packages/pg-protocol/src/outbound-serializer.test.ts
index 0d3e387e4..aac0c57ba 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 () {
@@ -273,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/parser.ts b/packages/pg-protocol/src/parser.ts
index 998077a00..df48ca4a1 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
@@ -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
}
diff --git a/packages/pg-protocol/src/serializer.ts b/packages/pg-protocol/src/serializer.ts
index bb0441f56..bbd59623e 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)
}
@@ -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)
- paramWriter.addInt32(Buffer.byteLength(mappedVal))
- paramWriter.addString(mappedVal)
+ // length prefix + UTF-8 bytes in one pass (Buffer.byteLength computed once)
+ 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,12 +152,22 @@ const bind = (config: BindOpts = {}): Buffer => {
const len = values.length
writer.addCString(portal).addCString(statement)
+
+ // number of parameter format codes
writer.addInt16(len)
- writeValues(values, config.valueMapper)
+ // space for those codes, filled by `writeValues`
+ const formatsOffset = writer.reserveUnsafe(len * 2)
+ // number of parameter values
writer.addInt16(len)
- writer.add(paramWriter.flush())
+
+ try {
+ writeValues(values, config.valueMapper, formatsOffset)
+ } catch (err) {
+ writer.clear()
+ throw err
+ }
// all results use the same format code
writer.addInt16(1)
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 0ae32c8dc..7b31d4d92 100644
--- a/packages/pg-protocol/tsconfig.json
+++ b/packages/pg-protocol/tsconfig.json
@@ -9,15 +9,18 @@
"moduleResolution": "node16",
"sourceMap": true,
"outDir": "dist",
+ "rootDir": "./src",
"incremental": true,
- "baseUrl": ".",
"declaration": true,
"paths": {
"*": [
- "node_modules/*",
- "src/types/*"
+ "./node_modules/*"
]
- }
+ },
+ "types": [
+ "node",
+ "mocha"
+ ]
},
"include": [
"src/**/*"
diff --git a/packages/pg-query-stream/package.json b/packages/pg-query-stream/package.json
index 5369a3c4c..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.14.0",
+ "version": "4.17.0",
"description": "Postgres query result returned as readable stream",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -39,21 +39,21 @@
"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",
+ "pg": "^8.23.0",
"stream-spec": "~0.3.5",
"ts-node": "^8.5.4",
- "typescript": "^4.0.3"
+ "typescript": "^6.0.3"
},
"peerDependencies": {
"pg": "^8"
},
"dependencies": {
- "pg-cursor": "^2.19.0"
+ "pg-cursor": "^2.22.0"
}
}
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
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/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
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 9200dded6..2b13c1de7 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()
@@ -71,22 +82,28 @@ 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.scramMaxIterations = coerceNumberOrDefault(c.scramMaxIterations, sasl.DEFAULT_MAX_SCRAM_ITERATIONS)
this.connection =
c.connection ||
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',
})
this._queryQueue = []
+ this._sentQueryQueue = []
+ this.pipeline = Boolean(c.pipeline)
this.binary = c.binary || defaults.binary
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
@@ -126,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
}
@@ -164,7 +184,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())
}
@@ -306,7 +330,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)
@@ -359,6 +387,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)
@@ -406,6 +435,9 @@ class Client extends EventEmitter {
}
this._activeQuery = null
+ if (activeQuery.name) {
+ delete this.connection.submittedNamedStatements[activeQuery.name]
+ }
activeQuery.handleError(msg, this.connection)
}
@@ -476,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]
}
}
@@ -554,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 = () => {}
}
}
@@ -577,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()
@@ -599,18 +640,41 @@ 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
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
+ }
+
+ if (typeof config.submit === 'function') {
result = query = config
if (!query.callback) {
if (typeof values === 'function') {
@@ -620,7 +684,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) => {
@@ -631,13 +694,16 @@ class Client extends EventEmitter {
Error.captureStackTrace(err)
throw err
})
+ } else if (typeof query.callback !== 'function') {
+ throw new TypeError('callback is not a function')
}
}
+ const readTimeout = config.query_timeout || this.connectionParameters.query_timeout
if (readTimeout) {
- queryCallback = query.callback || (() => {})
+ const queryCallback = query.callback || (() => {})
- readTimeoutTimer = setTimeout(() => {
+ const readTimeoutTimer = setTimeout(() => {
const error = new Error('Query read timeout')
process.nextTick(() => {
@@ -650,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()
@@ -673,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)
@@ -687,7 +776,7 @@ class Client extends EventEmitter {
return result
}
- if (this._queryQueue.length > 0) {
+ if (this._queryQueue.length > 0 && !this.pipeline) {
queryQueueLengthDeprecationNotice()
}
this._queryQueue.push(query)
@@ -703,6 +792,10 @@ class Client extends EventEmitter {
this.connection.unref()
}
+ getTransactionStatus() {
+ return this._txStatus
+ }
+
end(cb) {
this._ending = true
@@ -710,14 +803,24 @@ class Client extends EventEmitter {
if (!this.connection._connecting || this._ended) {
if (cb) {
cb()
+ return
} else {
return this._Promise.resolve()
}
}
- 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-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..099e2d4f0 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()
@@ -23,7 +24,9 @@ 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
this._emitMessage = false
const self = this
@@ -65,6 +68,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 +89,49 @@ 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,
+ // 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 ('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']
+ }
+
+ // 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
+ }
+ 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/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'
diff --git a/packages/pg/lib/crypto/sasl.js b/packages/pg/lib/crypto/sasl.js
index 47b77610c..ea63b2413 100644
--- a/packages/pg/lib/crypto/sasl.js
+++ b/packages/pg/lib/crypto/sasl.js
@@ -2,7 +2,37 @@
const crypto = require('./utils')
const { signatureAlgorithmHashFromCertificate } = require('./cert-signatures')
-function startSession(mechanisms, stream) {
+// 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')
+}
+
+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
@@ -25,6 +55,7 @@ function startSession(mechanisms, stream) {
clientNonce,
response: gs2Header + ',,n=*,r=' + clientNonce,
message: 'SASLInitialResponse',
+ scramMaxIterations,
}
}
@@ -50,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
@@ -70,7 +113,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)
@@ -178,7 +221,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)) {
@@ -209,4 +258,5 @@ module.exports = {
startSession,
continueSession,
finalizeSession,
+ DEFAULT_MAX_SCRAM_ITERATIONS,
}
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/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/lib/native/client.js b/packages/pg/lib/native/client.js
index d8bb4dce5..9ec3c8c03 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()
}
@@ -249,8 +251,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) {
@@ -259,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
}
@@ -280,6 +293,9 @@ Client.prototype._pulseQueryQueue = function (initialConnection) {
if (!this._connected) {
return
}
+ if (this.pipeline && !initialConnection) {
+ return this._pulsePipelinedQueryQueue()
+ }
if (this._hasActiveQuery()) {
return
}
@@ -298,6 +314,91 @@ 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, arrayMode: query._arrayMode }
+ 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. 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._errorAllQueries(err)
+ self.emit('error', err)
+ self.emit('end')
+ 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) {
@@ -321,3 +422,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/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 64aab5ff2..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
@@ -228,6 +231,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/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/lib/utils.js b/packages/pg/lib/utils.js
index e23a55e9a..ba51c82c8 100644
--- a/packages/pg/lib/utils.js
+++ b/packages/pg/lib/utils.js
@@ -1,9 +1,15 @@
'use strict'
const defaults = require('./defaults')
+const nodeUtils = require('util')
-const util = require('util')
-const { isDate } = util.types || util // Node 8 doesn't have `util.types`
+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, '\\"')
@@ -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 = Buffer.from(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,13 +58,12 @@ 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 Buffer.from(val.buffer, val.byteOffset, val.byteLength)
}
if (isDate(val)) {
+ if (isNaN(val.getTime())) {
+ invalidDateDeprecationNotice()
+ }
if (defaults.parseInputDatesAsUTC) {
return dateToStringUTC(val)
} else {
@@ -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/package.json b/packages/pg/package.json
index 6be526ee8..d028179ca 100644
--- a/packages/pg/package.json
+++ b/packages/pg/package.json
@@ -1,6 +1,6 @@
{
"name": "pg",
- "version": "8.20.0",
+ "version": "8.23.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.14.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.16.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
@@ -45,12 +45,12 @@
"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"
},
"optionalDependencies": {
- "pg-cloudflare": "^1.3.0"
+ "pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
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(
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..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)
- }
+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)
- }
+ 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/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/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) {
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)))
+ })
+})
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..7f927d2b0
--- /dev/null
+++ b/packages/pg/test/integration/client/pipelining-tests.js
@@ -0,0 +1,169 @@
+'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 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 })
+
+ 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/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/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) => {
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/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()
+ })
+}
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/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/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/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')
+ )
})
})
)
diff --git a/packages/pg/test/unit/client/sasl-scram-tests.js b/packages/pg/test/unit/client/sasl-scram-tests.js
index 2df0f1860..02b0d4e6d 100644
--- a/packages/pg/test/unit/client/sasl-scram-tests.js
+++ b/packages/pg/test/unit/client/sasl-scram-tests.js
@@ -55,67 +55,68 @@ 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 () {
- 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,59 +124,115 @@ 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(
+ 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', 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',
+ }
+ )
+ })
+
+ suite.test('fails when iteration count exceeds default scramMaxIterations', async function () {
+ await assert.rejects(
function () {
return sasl.continueSession(
{
message: 'SASLInitialResponse',
+ clientNonce: 'a',
+ scramMaxIterations: 100000,
},
- 'bad-password',
- 'r=1,s=abcd'
+ 'password',
+ 'r=ab,s=abcd,i=100001'
)
},
{
- message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing',
+ message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count 100001 exceeds scramMaxIterations of 100000',
}
)
})
- suite.test('fails when server nonce does not start with client nonce', function () {
- assert.rejects(
+ suite.test('fails when iteration count exceeds a custom scramMaxIterations', async function () {
+ await assert.rejects(
function () {
return sasl.continueSession(
{
message: 'SASLInitialResponse',
- clientNonce: '2',
+ clientNonce: 'a',
+ scramMaxIterations: 10,
},
- 'bad-password',
- 'r=1,s=abcd,i=1'
+ 'password',
+ 'r=ab,s=abcd,i=11'
)
},
{
- message: 'SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce',
+ 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',
@@ -204,6 +261,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',
@@ -284,6 +399,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 () {
diff --git a/packages/pg/test/unit/client/simple-query-tests.js b/packages/pg/test/unit/client/simple-query-tests.js
index d7d938992..3e3918773 100644
--- a/packages/pg/test/unit/client/simple-query-tests.js
+++ b/packages/pg/test/unit/client/simple-query-tests.js
@@ -114,31 +114,148 @@ 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()
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 () {
+ assert.throws(
+ () => {
+ client.query('SELECT $1', [1], 'notafunction')
+ },
+ {
+ message: 'callback is not a 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/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')
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()
+ },
+ })
+ )
+})
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
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()
+ })
+})
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)
+})
diff --git a/packages/pg/test/unit/utils-tests.js b/packages/pg/test/unit/utils-tests.js
index 5f75f6c2d..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)
@@ -89,6 +124,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)
diff --git a/yarn.lock b/yarn.lock
index dd6662852..a826fd60a 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"
@@ -1841,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"
@@ -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==
-
-"@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==
- 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"
+ 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@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/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" "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"
+ "@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/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==
+"@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:
- "@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"
@@ -3204,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"
@@ -3590,7 +3576,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 +3585,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 +3665,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 +3830,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"
@@ -4172,38 +4144,18 @@ 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.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.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,83 +4167,84 @@ 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"
-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.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 +4256,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 +4443,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 +4463,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 +4480,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 +4502,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 +4549,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 +4844,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 +4919,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 +4943,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 +5155,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.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 +5174,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 +5453,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 +5705,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 +5794,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==
@@ -5956,12 +5871,12 @@ levn@~0.3.0:
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==
+ 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.22.2"
+ nan "~2.26.2"
lines-and-columns@^1.1.6:
version "1.1.6"
@@ -6060,11 +5975,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 +6227,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 +6251,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 +6324,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"
@@ -6681,10 +6590,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.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"
@@ -7220,7 +7129,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 +7258,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 +7329,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"
@@ -7549,10 +7448,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"
@@ -7852,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"
@@ -7966,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==
@@ -8005,11 +7899,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 +7906,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 +7972,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"
@@ -8139,12 +8023,12 @@ 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==
-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 +8142,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"
@@ -8779,12 +8658,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"
@@ -8868,11 +8747,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 +8837,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 +8892,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 +8964,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 +8991,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"