Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
43a91fd
chore(sec): 1.17.0 — clear CRITICAL + transitive findings via overrid…
vikrantpuppala Jul 10, 2026
208527f
fix: coherent dev-toolchain for 1.17.0 overrides (mocha/eslint bump)
vikrantpuppala Jul 10, 2026
320ed33
style: prettier --write on CHANGELOG.md + package.json
vikrantpuppala Jul 10, 2026
b2aa909
2.0.0: breaking security bump — Node >=20, thrift 0.23, uuid 11 (0 OS…
vikrantpuppala Jul 10, 2026
94afd9d
ci: trigger checks after clean force-update
vikrantpuppala Jul 13, 2026
7635c4c
Merge remote-tracking branch 'origin/main' into vp/security-bump-runt…
vikrantpuppala Jul 13, 2026
ae38bab
test(ci): fix ts-node on Node 24+ (strip-types) and add Node 26 to ma…
vikrantpuppala Jul 13, 2026
22b49cc
test(ci): drop invalid node-option flag; ts-node/register alone fixes…
vikrantpuppala Jul 13, 2026
02994a4
test(ci): add ts-node/esm loader so Node 24+ handles .ts on the impor…
vikrantpuppala Jul 13, 2026
20dbce1
fix(deps): trim redundant overrides to minimal 3; fail-fast:false for…
vikrantpuppala Jul 14, 2026
2c194ad
Revert override trim — restore 7-override set consistent with the loc…
vikrantpuppala Jul 14, 2026
6593cb5
lz4: replace unmaintained `lz4` with maintained `lz4-napi` (frame-com…
vikrantpuppala Jul 14, 2026
c619b2f
ci: complete lz4-napi lockfile, restore npm ci, fix prettier
vikrantpuppala Jul 14, 2026
c357037
review: address code-review findings on the lz4-napi swap
vikrantpuppala Jul 14, 2026
3318065
review: gate real-codec lz4 tests on availability, not the environment
vikrantpuppala Jul 14, 2026
7aa00b7
Merge branch 'main' into vp/security-bump-runtime-and-dev
vikrantpuppala Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/actions/setup-jfrog/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,38 @@ runs:
always-auth=true
EOF
echo "npm configured to use JFrog registry"

# Rewrite lockfile `resolved:` URLs to JFrog. `npm ci` honors the
# lockfile's `resolved:` URL over the `.npmrc` registry; protected
# runners cannot reach `registry.npmjs.org` or the internal
# `npm-proxy.dev.databricks.com` directly, so without this rewrite
# npm hangs ~8 minutes on any package whose `resolved:` points
# elsewhere, then dies with "Exit handler never called".
# The committed lockfile is kept as-generated locally (mixed
# public-npm + dev-proxy URLs) so contributors can `npm ci`
# locally; this step adapts it for CI.
- name: Rewrite lockfile to JFrog registry
shell: bash
run: |
set -euo pipefail
if [ -f package-lock.json ]; then
# Rewrite both public npm and the internal dev-proxy (the
# latter is written by `npm install` when run on a dev
# workstation whose .npmrc points at npm-proxy.dev).
sed -i \
-e 's|https://registry.npmjs.org|https://databricks.jfrog.io/artifactory/api/npm/db-npm|g' \
-e 's|https://npm-proxy.dev.databricks.com|https://databricks.jfrog.io/artifactory/api/npm/db-npm|g' \
package-lock.json
echo "package-lock.json resolved: URLs rewritten to JFrog"
echo "Resolved URL distribution after rewrite:"
grep -oE '"resolved": "https://[^/]+' package-lock.json | sort | uniq -c
# Fail loud if any non-JFrog host remains — a new registry hostname
# added to the tree would otherwise silently hang `npm ci` for 8min.
LEAKS=$(grep -oE '"resolved": "https://[^/]+' package-lock.json | grep -v 'databricks\.jfrog\.io' | sort -u || true)
if [ -n "$LEAKS" ]; then
echo "::error::JFrog rewrite incomplete; non-JFrog hosts remain: $LEAKS"
exit 1
fi
else
echo "no package-lock.json found; skipping rewrite"
fi
56 changes: 42 additions & 14 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ jobs:
labels: linux-ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Pin lockfileVersion to 2
# See CONTRIBUTING.md "Dependency Pins". Modern npm writes v3
# by default; catching drift here prevents silent format upgrades
# from sneaking in via `npm install`.
run: |
actual=$(jq -r '.lockfileVersion' package-lock.json)
if [ "$actual" != "2" ]; then
echo "::error::package-lock.json lockfileVersion is $actual; expected 2. Regenerate with 'npm install --lockfile-version=2'."
exit 1
fi
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
Expand All @@ -49,10 +59,16 @@ jobs:
runs-on:
group: databricks-protected-runner-group
labels: linux-ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
# only LTS versions starting from the lowest we support
node-version: ['14', '16', '18', '20']
# LTS versions: 16/18/20 are the currently-supported floor; 22
# is the active LTS and 24 is the new LTS. Node 14 was dropped
# because the modern npm ecosystem (e.g. @dabh/diagnostics@2.0.7+
# via winston) ships ES2021 syntax (||=) that Node 14's V8 cannot
# parse. Node 14 has been EOL upstream since April 2023.
node-version: ['20', '22', '24', '26']
env:
cache-name: cache-node-modules
NYC_REPORT_DIR: coverage_unit_node${{ matrix.node-version }}
Expand All @@ -62,11 +78,6 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python 3.10 for Node 14
if: ${{ matrix.node-version == '14' }}
uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4
with:
python-version: '3.10'
- uses: ./.github/actions/setup-jfrog
- name: Cache node modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
Expand Down Expand Up @@ -94,32 +105,49 @@ jobs:
group: databricks-protected-runner-group
labels: linux-ubuntu-latest
environment: azure-prod
# Cap job time so a wedged matrix entry doesn't hold a runner +
# warehouse session for GitHub's 6-hour default. Tests historically
# complete in <15min; 30min leaves room for warehouse cold-start.
timeout-minutes: 30
strategy:
# Run all matrix entries even if one fails so a Node-version-specific
# network/TLS regression doesn't hide other versions' results.
fail-fast: false
matrix:
# Matches the unit-test matrix so e2e catches Node-version-specific
# behaviors (TLS/cipher defaults, native fetch interaction, lz4 ABI,
# OAuth crypto APIs) that unit tests with mocked I/O can't surface.
node-version: ['20', '22', '24', '26']
env:
E2E_HOST: ${{ secrets.DATABRICKS_HOST }}
E2E_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }}
E2E_ACCESS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
E2E_TABLE_SUFFIX: ${{github.sha}}
# Include Node version in the suffix so parallel matrix entries
# don't collide on shared E2E table names. Use underscore (not
# hyphen) — hyphens aren't valid in SQL unquoted identifiers,
# and tests build identifiers as `..._${E2E_TABLE_SUFFIX}`.
E2E_TABLE_SUFFIX: ${{ github.sha }}_node${{ matrix.node-version }}
E2E_CATALOG: peco
E2E_SCHEMA: default
E2E_VOLUME: e2etests
cache-name: cache-node-modules
NYC_REPORT_DIR: coverage_e2e
NYC_REPORT_DIR: coverage_e2e_node${{ matrix.node-version }}

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
node-version: ${{ matrix.node-version }}
- uses: ./.github/actions/setup-jfrog
- name: Cache node modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ matrix.node-version }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
${{ runner.os }}-${{ matrix.node-version }}-build-${{ env.cache-name }}-
${{ runner.os }}-${{ matrix.node-version }}-build-
${{ runner.os }}-${{ matrix.node-version }}-
- name: Run e2e tests
run: |
npm ci
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Release History

## 2.0.0

**Breaking changes — completes the security cleanup that 1.17.0 could not do without breaking changes.**

- **`engines.node` raised to `>=20.0.0`** (was `>=14`). Node 14/16/18 are dropped. Node 14 (EOL 2023-04), 16 (EOL 2023-09), and 18 (EOL 2025-04) are all past upstream end-of-life. The `>=20` floor is also the minimum for the patched `serialize-javascript` (dev) — see below — and comfortably clears the kernel backend's own Node 18 requirement.
- **`thrift` `^0.16.0` → `^0.23.0`** — clears GHSA-r67j-r569-jrwp (HIGH) and GHSA-526f-jxpj-jmg2. A direct runtime dep whose fix is a major bump.
- **`uuid` `^9.0.0` → `^11.1.1`** — clears GHSA-w5hq-g745-h8pq (HIGH 7.5). uuid 11 dual-publishes ESM+CJS; the driver's CJS `dist/` continues to `require('uuid')` unchanged. An `overrides` pin holds thrift's transitive uuid at 11 (uuid 13 is ESM-only and would break `require()`).

**Security:** with the above plus the `overrides` block carried from 1.17.0 (and `serialize-javascript` pinned to the patched `^7.0.5`, which requires Node ≥20), OSV-Scanner reports **zero findings** — 0 CRITICAL / 0 HIGH / 0 MED / 0 LOW.

Closes #370.

## 1.17.0

**Security (non-breaking):** clears the non-breaking OSV-Scanner findings via a `package.json` `overrides` block plus a coherent dev-toolchain bump (`mocha`, `eslint`) — **2 CRITICAL** (`basic-ftp`, `form-data`) plus the transitively-fixable HIGH/MED/LOW (`ws`, `@75lb/deep-merge`, `ip-address`, and the mocha-tree deps `braces`/`micromatch`/`cross-spawn`/`minimatch`/`glob`/`@babel/*`/`js-yaml`/`path-to-regexp`). No engine or runtime-API changes — a drop-in patch so consumers can clear critical/high scanner alerts without adopting the breaking `2.0.0` bump.
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ npm run lint:fix
npm run type-check
```

## Dependency Pins

A few entries in `package.json` are pinned more tightly than usual. Don't relax these without understanding why.

- **`typescript: "5.5.4"`** (exact, no caret). This pin has both a floor and a ceiling:

- Floor (TS >= 5.0) is required because `uuid@11`'s shipped `.d.ts` uses `export type * from './types.js'`, a TS 5.0+ feature.
- Ceiling (TS < 5.6) is required because TS 5.6 changed how `@types/node`'s generic `Buffer<TArrayBuffer extends ArrayBufferLike>` declarations get emitted into our published `dist/*.d.ts`. Allowing TS 5.6+ would leak `Buffer<ArrayBufferLike>` into the published types, which fails to compile for consumers on stale `@types/node`.
- If you bump TS, run `npm run build` and `git diff dist/` and verify no `Buffer<...>` generics appear in any `.d.ts`. If they do, you need to either roll back or also bump `@types/node` consumer expectations (a customer-facing change).

- **`overrides.uuid: "^11.1.1"`**. Forces `thrift@0.23.0`'s declared `uuid: ^13.0.0` (ESM-only) down to v11 (dual ESM+CJS). Without this override, the driver's CJS-compiled `dist/` would crash on `require('uuid')` at runtime. Remove this override only after migrating the driver to ESM or when `thrift` drops the uuid dep.

- **`package-lock.json` is pinned to `lockfileVersion: 2`.** Modern npm writes v3 by default. To regenerate the lockfile, run `npm install --lockfile-version=2` so CI's lint step doesn't reject your PR. v2 is kept for compat with older toolchains; revisit when the team is ready to drop them.

## Pull Request Process

1. Update the [CHANGELOG](CHANGELOG.md) with details of your changes, if applicable.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The Databricks SQL Driver for Node.js is a Javascript driver for applications th

## Requirements

- Node.js 14 or newer
- Node.js 20 or newer

## Installation

Expand Down
7 changes: 6 additions & 1 deletion lib/connection/auth/tokenProvider/FederationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ export default class FederationProvider implements ITokenProvider {
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
signal: controller.signal,
// node-fetch ships its own AbortSignal shim that differs slightly
// from the native AbortSignal (subtle `this`-typing mismatch on
// onabort handler). TS 4 didn't catch this; TS 5+ does. The two
// are runtime-compatible, so cast through `unknown` (rather than
// `any`) to keep the assertion narrow.
signal: controller.signal as unknown as import('node-fetch').RequestInit['signal'],
});

if (!response.ok) {
Expand Down
2 changes: 1 addition & 1 deletion lib/result/ArrowResultHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default class ArrowResultHandler implements IResultsProvider<ArrowBatch>
this.isLZ4Compressed = lz4Compressed ?? false;

if (this.isLZ4Compressed && !LZ4()) {
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4` not installed');
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4-napi` not installed');
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/result/CloudFetchResultHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class CloudFetchResultHandler implements IResultsProvider<ArrowBa
this.statementId = statementId;

if (this.isLZ4Compressed && !LZ4()) {
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4` not installed');
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4-napi` not installed');
}
}

Expand Down
68 changes: 45 additions & 23 deletions lib/utils/lz4.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,60 @@
import type LZ4Namespace from 'lz4';
// LZ4 frame codec, backed by `lz4-napi`.
//
// Historical note: this driver previously depended on the unmaintained
// `lz4` package (last published 2021), a node-gyp native addon with no
// prebuilt binaries. It failed to compile on Node 22+, so it was silently
// skipped as an optional dependency and LZ4-compressed results could not be
// decoded. `lz4-napi` is maintained and ships prebuilt binaries for all
// supported platforms/Node versions, so no local toolchain build is needed.
//
// The Databricks server sends LZ4-compressed Arrow/CloudFetch payloads in
// the LZ4 **frame** format (magic 0x184D2204). `lz4-napi`'s
// `compressFrameSync`/`decompressFrameSync` are byte-compatible with the
// frames the old `lz4` package produced/consumed — verified by round-trip
// and cross-decoding both directions. We expose the same `{ encode, decode }`
// surface the rest of the driver already calls, so call sites are unchanged.

type LZ4Module = typeof LZ4Namespace;
type Lz4Napi = {
compressFrameSync(data: Buffer): Buffer;
decompressFrameSync(data: Buffer): Buffer;
};

function tryLoadLZ4Module(): LZ4Module | undefined {
// Stable interface used across the driver (lib call sites do `LZ4()!.decode(buf)`).
export interface LZ4Codec {
encode(data: Buffer): Buffer;
decode(data: Buffer): Buffer;
}

function tryLoadLZ4Module(): LZ4Codec | undefined {
let napi: Lz4Napi;
try {
return require('lz4'); // eslint-disable-line global-require
napi = require('lz4-napi'); // eslint-disable-line global-require
} catch (err) {
if (!(err instanceof Error) || !('code' in err)) {
// eslint-disable-next-line no-console
console.warn('Unexpected error loading LZ4 module: Invalid error object', err);
return undefined;
}

if (err.code === 'MODULE_NOT_FOUND') {
// lz4-napi ships prebuilds so this should not normally happen, but keep
// the load-failure-tolerant contract: if it can't load, LZ4 support is
// reported as unavailable rather than crashing the driver.
if (err instanceof Error && 'code' in err && err.code === 'MODULE_NOT_FOUND') {
return undefined;
}

if (err.code === 'ERR_DLOPEN_FAILED') {
// eslint-disable-next-line no-console
console.warn('LZ4 native module failed to load: Architecture or version mismatch', err);
return undefined;
}

// If it's not a known error, return undefined
// eslint-disable-next-line no-console
console.warn('Unknown error loading LZ4 module: Unhandled error code', err);
console.warn('LZ4 module (lz4-napi) failed to load; LZ4-compressed results unavailable', err);
return undefined;
}

// lz4-napi's frame APIs already return freshly-allocated, owned `Buffer`s
// (napi-rs materializes Node Buffers), so we return them directly rather
// than copying again with `Buffer.from` — decode runs on the CloudFetch
// hot path (once per Arrow batch).
return {
encode: (data: Buffer): Buffer => napi.compressFrameSync(data),
decode: (data: Buffer): Buffer => napi.decompressFrameSync(data),
};
}

// The null is already tried resolving that failed
let resolvedModule: LZ4Module | null | undefined;
// The null means we already tried resolving and it failed.
let resolvedModule: LZ4Codec | null | undefined;

function getResolvedModule() {
function getResolvedModule(): LZ4Codec | undefined {
if (resolvedModule === undefined) {
resolvedModule = tryLoadLZ4Module() ?? null;
}
Expand Down
Loading
Loading