Index: node_modules/pg/lib/crypto/cert-signatures.js
===================================================================
--- node_modules/pg/lib/crypto/cert-signatures.js	(revision 62b296460f935ff5cc47a75f6567f97df5a244a7)
+++ node_modules/pg/lib/crypto/cert-signatures.js	(revision 62b296460f935ff5cc47a75f6567f97df5a244a7)
@@ -0,0 +1,122 @@
+function x509Error(msg, cert) {
+  return new Error('SASL channel binding: ' + msg + ' when parsing public certificate ' + cert.toString('base64'))
+}
+
+function readASN1Length(data, index) {
+  let length = data[index++]
+  if (length < 0x80) return { length, index }
+
+  const lengthBytes = length & 0x7f
+  if (lengthBytes > 4) throw x509Error('bad length', data)
+
+  length = 0
+  for (let i = 0; i < lengthBytes; i++) {
+    length = (length << 8) | data[index++]
+  }
+
+  return { length, index }
+}
+
+function readASN1OID(data, index) {
+  if (data[index++] !== 0x6) throw x509Error('non-OID data', data) // 6 = OID
+
+  const { length: OIDLength, index: indexAfterOIDLength } = readASN1Length(data, index)
+  index = indexAfterOIDLength
+  const lastIndex = index + OIDLength
+
+  const byte1 = data[index++]
+  let oid = ((byte1 / 40) >> 0) + '.' + (byte1 % 40)
+
+  while (index < lastIndex) {
+    // loop over numbers in OID
+    let value = 0
+    while (index < lastIndex) {
+      // loop over bytes in number
+      const nextByte = data[index++]
+      value = (value << 7) | (nextByte & 0x7f)
+      if (nextByte < 0x80) break
+    }
+    oid += '.' + value
+  }
+
+  return { oid, index }
+}
+
+function expectASN1Seq(data, index) {
+  if (data[index++] !== 0x30) throw x509Error('non-sequence data', data) // 30 = Sequence
+  return readASN1Length(data, index)
+}
+
+function signatureAlgorithmHashFromCertificate(data, index) {
+  // read this thread: https://www.postgresql.org/message-id/17760-b6c61e752ec07060%40postgresql.org
+  if (index === undefined) index = 0
+  index = expectASN1Seq(data, index).index
+  const { length: certInfoLength, index: indexAfterCertInfoLength } = expectASN1Seq(data, index)
+  index = indexAfterCertInfoLength + certInfoLength // skip over certificate info
+  index = expectASN1Seq(data, index).index // skip over signature length field
+  const { oid, index: indexAfterOID } = readASN1OID(data, index)
+  switch (oid) {
+    // RSA
+    case '1.2.840.113549.1.1.4':
+      return 'MD5'
+    case '1.2.840.113549.1.1.5':
+      return 'SHA-1'
+    case '1.2.840.113549.1.1.11':
+      return 'SHA-256'
+    case '1.2.840.113549.1.1.12':
+      return 'SHA-384'
+    case '1.2.840.113549.1.1.13':
+      return 'SHA-512'
+    case '1.2.840.113549.1.1.14':
+      return 'SHA-224'
+    case '1.2.840.113549.1.1.15':
+      return 'SHA512-224'
+    case '1.2.840.113549.1.1.16':
+      return 'SHA512-256'
+    // ECDSA
+    case '1.2.840.10045.4.1':
+      return 'SHA-1'
+    case '1.2.840.10045.4.3.1':
+      return 'SHA-224'
+    case '1.2.840.10045.4.3.2':
+      return 'SHA-256'
+    case '1.2.840.10045.4.3.3':
+      return 'SHA-384'
+    case '1.2.840.10045.4.3.4':
+      return 'SHA-512'
+    // RSASSA-PSS: hash is indicated separately
+    case '1.2.840.113549.1.1.10': {
+      index = indexAfterOID
+      index = expectASN1Seq(data, index).index
+      if (data[index++] !== 0xa0) throw x509Error('non-tag data', data) // a0 = constructed tag 0
+      index = readASN1Length(data, index).index // skip over tag length field
+      index = expectASN1Seq(data, index).index // skip over sequence length field
+      const { oid: hashOID } = readASN1OID(data, index)
+      switch (hashOID) {
+        // standalone hash OIDs
+        case '1.2.840.113549.2.5':
+          return 'MD5'
+        case '1.3.14.3.2.26':
+          return 'SHA-1'
+        case '2.16.840.1.101.3.4.2.1':
+          return 'SHA-256'
+        case '2.16.840.1.101.3.4.2.2':
+          return 'SHA-384'
+        case '2.16.840.1.101.3.4.2.3':
+          return 'SHA-512'
+      }
+      throw x509Error('unknown hash OID ' + hashOID, data)
+    }
+    // Ed25519 -- see https: return//github.com/openssl/openssl/issues/15477
+    case '1.3.101.110':
+    case '1.3.101.112': // ph
+      return 'SHA-512'
+    // Ed448 -- still not in pg 17.2 (if supported, digest would be SHAKE256 x 64 bytes)
+    case '1.3.101.111':
+    case '1.3.101.113': // ph
+      throw x509Error('Ed448 certificate channel binding is not currently supported by Postgres')
+  }
+  throw x509Error('unknown OID ' + oid, data)
+}
+
+module.exports = { signatureAlgorithmHashFromCertificate }
Index: node_modules/pg/lib/crypto/sasl.js
===================================================================
--- node_modules/pg/lib/crypto/sasl.js	(revision 62b296460f935ff5cc47a75f6567f97df5a244a7)
+++ node_modules/pg/lib/crypto/sasl.js	(revision 62b296460f935ff5cc47a75f6567f97df5a244a7)
@@ -0,0 +1,262 @@
+'use strict'
+const crypto = require('./utils')
+const { signatureAlgorithmHashFromCertificate } = require('./cert-signatures')
+
+// SASLprep (RFC 4013) — minimal in-tree implementation.
+//
+// Per RFC 5802 §2.2, the SCRAM-SHA-256 client must normalize the password via
+// SASLprep before feeding it into PBKDF2. PostgreSQL's server applies the same
+// SASLprep when computing the stored verifier, and libpq does the same client
+// side, so passwords whose NFKC form differs from the raw form
+// would otherwise authenticate against psql/libpq but fail against pg with `28P01`.
+//
+// We deliberately implement only the three steps that change the byte content:
+//   1. RFC 3454 Table C.1.2 (non-ASCII space) → U+0020 SPACE.
+//   2. RFC 3454 Table B.1 (commonly mapped to nothing) → empty.
+//   3. NFKC normalization.
+// We skip the prohibition (RFC 4013 §2.3) and bidi (RFC 3454 §6) checks.
+// libpq is forgiving on those paths and Postgres's own SASLprep matches that
+// leniency for legacy roles, so omitting the rejection logic keeps existing
+// roles working without adding complexity.
+function saslprep(password) {
+  // RFC 3454 Table C.1.2 — non-ASCII space characters, mapped to U+0020.
+  const nonAsciiSpace = /[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]/g
+  // RFC 3454 Table B.1 — "commonly mapped to nothing". The set intentionally
+  // contains zero-width joiners and variation selectors — the very characters
+  // ESLint's no-misleading-character-class warns about — because they combine
+  // with their neighbors and the RFC strips them for that reason.
+  // eslint-disable-next-line no-misleading-character-class
+  const mappedToNothing = /[\u00AD\u034F\u1806\u180B\u180C\u180D\u200C\u200D\u2060\uFE00-\uFE0F\uFEFF]/g
+  return password.replace(nonAsciiSpace, ' ').replace(mappedToNothing, '').normalize('NFKC')
+}
+
+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
+
+  const mechanism = candidates.find((candidate) => mechanisms.includes(candidate))
+
+  if (!mechanism) {
+    throw new Error('SASL: Only mechanism(s) ' + candidates.join(' and ') + ' are supported')
+  }
+
+  if (mechanism === 'SCRAM-SHA-256-PLUS' && typeof stream.getPeerCertificate !== 'function') {
+    // this should never happen if we are really talking to a Postgres server
+    throw new Error('SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate')
+  }
+
+  const clientNonce = crypto.randomBytes(18).toString('base64')
+  const gs2Header = mechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : stream ? 'y' : 'n'
+
+  return {
+    mechanism,
+    clientNonce,
+    response: gs2Header + ',,n=*,r=' + clientNonce,
+    message: 'SASLInitialResponse',
+    scramMaxIterations,
+  }
+}
+
+async function continueSession(session, password, serverData, stream) {
+  if (session.message !== 'SASLInitialResponse') {
+    throw new Error('SASL: Last message was not SASLInitialResponse')
+  }
+  if (typeof password !== 'string') {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string')
+  }
+  if (password === '') {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a non-empty string')
+  }
+  if (typeof serverData !== 'string') {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: serverData must be a string')
+  }
+
+  const sv = parseServerFirstMessage(serverData)
+
+  if (!sv.nonce.startsWith(session.clientNonce)) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce')
+  } else if (sv.nonce.length === session.clientNonce.length) {
+    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
+
+  // without channel binding:
+  let channelBinding = stream ? 'eSws' : 'biws' // 'y,,' or 'n,,', base64-encoded
+
+  // override if channel binding is in use:
+  if (session.mechanism === 'SCRAM-SHA-256-PLUS') {
+    const peerCert = stream.getPeerCertificate().raw
+    let hashName = signatureAlgorithmHashFromCertificate(peerCert)
+    if (hashName === 'MD5' || hashName === 'SHA-1') hashName = 'SHA-256'
+    const certHash = await crypto.hashByName(hashName, peerCert)
+    const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)])
+    channelBinding = bindingData.toString('base64')
+  }
+
+  const clientFinalMessageWithoutProof = 'c=' + channelBinding + ',r=' + sv.nonce
+  const authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
+
+  const saltBytes = Buffer.from(sv.salt, 'base64')
+  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)
+  const clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString('base64')
+  const serverKey = await crypto.hmacSha256(saltedPassword, 'Server Key')
+  const serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage)
+
+  session.message = 'SASLResponse'
+  session.serverSignature = Buffer.from(serverSignatureBytes).toString('base64')
+  session.response = clientFinalMessageWithoutProof + ',p=' + clientProof
+}
+
+function finalizeSession(session, serverData) {
+  if (session.message !== 'SASLResponse') {
+    throw new Error('SASL: Last message was not SASLResponse')
+  }
+  if (typeof serverData !== 'string') {
+    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string')
+  }
+
+  const { serverSignature } = parseServerFinalMessage(serverData)
+
+  if (serverSignature !== session.serverSignature) {
+    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match')
+  }
+}
+
+/**
+ * printable       = %x21-2B / %x2D-7E
+ *                   ;; Printable ASCII except ",".
+ *                   ;; Note that any "printable" is also
+ *                   ;; a valid "value".
+ */
+function isPrintableChars(text) {
+  if (typeof text !== 'string') {
+    throw new TypeError('SASL: text must be a string')
+  }
+  return text
+    .split('')
+    .map((_, i) => text.charCodeAt(i))
+    .every((c) => (c >= 0x21 && c <= 0x2b) || (c >= 0x2d && c <= 0x7e))
+}
+
+/**
+ * base64-char     = ALPHA / DIGIT / "/" / "+"
+ *
+ * base64-4        = 4base64-char
+ *
+ * base64-3        = 3base64-char "="
+ *
+ * base64-2        = 2base64-char "=="
+ *
+ * base64          = *base64-4 [base64-3 / base64-2]
+ */
+function isBase64(text) {
+  return /^(?:[a-zA-Z0-9+/]{4})*(?:[a-zA-Z0-9+/]{2}==|[a-zA-Z0-9+/]{3}=)?$/.test(text)
+}
+
+function parseAttributePairs(text) {
+  if (typeof text !== 'string') {
+    throw new TypeError('SASL: attribute pairs text must be a string')
+  }
+
+  return new Map(
+    text.split(',').map((attrValue) => {
+      if (!/^.=/.test(attrValue)) {
+        throw new Error('SASL: Invalid attribute pair entry')
+      }
+      const name = attrValue[0]
+      const value = attrValue.substring(2)
+      return [name, value]
+    })
+  )
+}
+
+function parseServerFirstMessage(data) {
+  const attrPairs = parseAttributePairs(data)
+
+  const nonce = attrPairs.get('r')
+  if (!nonce) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing')
+  } else if (!isPrintableChars(nonce)) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce must only contain printable characters')
+  }
+  const salt = attrPairs.get('s')
+  if (!salt) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing')
+  } else if (!isBase64(salt)) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64')
+  }
+  const iterationText = attrPairs.get('i')
+  if (!iterationText) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing')
+  } else if (!/^[1-9][0-9]*$/.test(iterationText)) {
+    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count')
+  }
+  const iteration = parseInt(iterationText, 10)
+
+  return {
+    nonce,
+    salt,
+    iteration,
+  }
+}
+
+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)) {
+    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature must be base64')
+  }
+  return {
+    serverSignature,
+  }
+}
+
+function xorBuffers(a, b) {
+  if (!Buffer.isBuffer(a)) {
+    throw new TypeError('first argument must be a Buffer')
+  }
+  if (!Buffer.isBuffer(b)) {
+    throw new TypeError('second argument must be a Buffer')
+  }
+  if (a.length !== b.length) {
+    throw new Error('Buffer lengths must match')
+  }
+  if (a.length === 0) {
+    throw new Error('Buffers cannot be empty')
+  }
+  return Buffer.from(a.map((_, i) => a[i] ^ b[i]))
+}
+
+module.exports = {
+  startSession,
+  continueSession,
+  finalizeSession,
+  DEFAULT_MAX_SCRAM_ITERATIONS,
+}
Index: node_modules/pg/lib/crypto/utils.js
===================================================================
--- node_modules/pg/lib/crypto/utils.js	(revision 62b296460f935ff5cc47a75f6567f97df5a244a7)
+++ node_modules/pg/lib/crypto/utils.js	(revision 62b296460f935ff5cc47a75f6567f97df5a244a7)
@@ -0,0 +1,89 @@
+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'])
+}
