From 0a57778219574713b25fed2ad4db563e2d841b32 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sat, 11 Jul 2026 07:47:18 -0600 Subject: [PATCH 1/2] feat(security): add RE2-backed safe-regex substrate (re2 dependency) Introduce compileSafeRegex (security/safeRegex.ts), a linear-time regex compiler backed by the re2 native addon, as the safe substrate for regex whose source is operator- or attacker-influenced. RE2's finite-automaton engine cannot catastrophically backtrack, so a pattern like (a+)+$ cannot stall the event loop (ReDoS) the way new RegExp(...) can. The signature mirrors an error-collecting validator; RE2-unsupported features (backreferences, lookaround) surface as compile errors rather than running unsafely -- that rejection is part of the contract. This lands the native dependency on its own so its prebuilt-matrix resolution and the linear-time/feature-rejection contract are reviewed independently of the WAF logic that will consume it. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 208 ++++++++++++++++++++++++-- package.json | 1 + security/safeRegex.ts | 29 ++++ unitTests/security/safeRegex.test.mjs | 66 ++++++++ 4 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 security/safeRegex.ts create mode 100644 unitTests/security/safeRegex.test.mjs diff --git a/package-lock.json b/package-lock.json index ef816ff10..306a8b624 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "pkijs": "3.4.0", "prompt": "1.3.0", "properties-reader": "2.3.0", + "re2": "^1.26.0", "recursive-iterator": "3.3.0", "semver": "7.8.5", "send": "^1.2.0", @@ -2954,6 +2955,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -5401,6 +5414,15 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/abbrev": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", @@ -6422,6 +6444,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", @@ -7068,6 +7099,15 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -7498,9 +7538,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "optional": true, - "peer": true + "license": "Apache-2.0" }, "node_modules/express": { "version": "5.2.1", @@ -9590,6 +9628,24 @@ "node": ">= 6" } }, + "node_modules/install-artifact-from-github": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.7.0.tgz", + "integrity": "sha512-qAb91yAKVF9rFY4rVP21ZtYUyCScxAFt9udwzVWNLBE1pQcdQeB2gd1HlNPcQNYCzCDvJ/QJQPuWQ6aTmSlU8g==", + "license": "BSD-3-Clause", + "bin": { + "hash-github-cache": "bin/hash-github-cache.js", + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/uhop" + } + }, "node_modules/intercept-stdout": { "version": "0.1.2", "dev": true, @@ -11213,6 +11269,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkcert": { "version": "3.2.0", "dev": true, @@ -11538,9 +11606,10 @@ "license": "ISC" }, "node_modules/nan": { - "version": "2.25.0", - "license": "MIT", - "optional": true + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -11613,6 +11682,30 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-13.0.1.tgz", + "integrity": "sha512-piOr0S10qy5THB+q5BdqkoOx65XL/tjTMUAit3vciPNp+snTOBnGunWH1Rz7XZUxf2T9uFrfT/Ty4+aC3yPeyg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^10.0.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^8.4.1", + "which": "^7.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -11637,6 +11730,39 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz", + "integrity": "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11811,6 +11937,21 @@ "node": ">= 10" } }, + "node_modules/nopt": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", + "license": "ISC", + "dependencies": { + "abbrev": "^5.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "license": "MIT", @@ -12634,6 +12775,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "license": "MIT", @@ -12858,6 +13008,24 @@ "quickselect": "^2.0.0" } }, + "node_modules/re2": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.26.0.tgz", + "integrity": "sha512-QTEfFp0nu1eT/HKKZC+2BKM9Lb8uqprGkcHhro9r3giiKS+1SnNZBZ1m7C7Axi/VR1YGXiu3mp35UA53yHAuNw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "install-artifact-from-github": "^1.7.0", + "nan": "^2.27.0", + "node-gyp": "^13.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/uhop" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -14287,6 +14455,22 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", @@ -14311,6 +14495,15 @@ "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/teex": { "version": "1.0.1", "license": "MIT", @@ -14461,7 +14654,6 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -14478,7 +14670,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -14496,7 +14687,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index f136b4483..28ea90bea 100644 --- a/package.json +++ b/package.json @@ -191,6 +191,7 @@ "pkijs": "3.4.0", "prompt": "1.3.0", "properties-reader": "2.3.0", + "re2": "^1.26.0", "recursive-iterator": "3.3.0", "semver": "7.8.5", "send": "^1.2.0", diff --git a/security/safeRegex.ts b/security/safeRegex.ts new file mode 100644 index 000000000..9142b5b4b --- /dev/null +++ b/security/safeRegex.ts @@ -0,0 +1,29 @@ +import RE2 from 're2'; + +/** + * Compile an operator- or attacker-influenced regex source with RE2 instead of `new RegExp(...)`. + * + * RE2 is a finite-automaton engine with a linear-time matching guarantee: a pathological pattern + * such as `(a+)+$` cannot trigger catastrophic backtracking and stall the event loop the way the + * built-in `RegExp` can (ReDoS). Any regex whose source is not fully trusted should be compiled + * through here rather than through `RegExp`. + * + * The signature mirrors an error-collecting validator: on success it returns the compiled matcher; + * on failure it pushes a human-readable reason onto `errors` and returns `undefined`. RE2 rejects + * patterns that use features it cannot evaluate in linear time — backreferences (`\1`) and + * lookaround (`(?=…)`, `(?<=…)`) — so those surface here as compile errors. That rejection is part + * of the contract, not a bug: it is the price of the linear-time guarantee, and callers should + * treat "unsupported feature" and "invalid syntax" the same way (the rule does not compile). + * + * The returned object is an `RE2`, which extends `RegExp`, so it is drop-in for the `.test()` / + * `.exec()` surface at existing call sites. For matching many patterns at once, prefer `RE2.Set` + * (a native multi-pattern scan) over combining sources into one alternation. + */ +export function compileSafeRegex(source: string, where: string, errors: string[]): RegExp | undefined { + try { + return new RE2(source); + } catch (error) { + errors.push(`${where}: invalid or unsupported regex ${JSON.stringify(source)}: ${(error as Error).message}`); + return undefined; + } +} diff --git a/unitTests/security/safeRegex.test.mjs b/unitTests/security/safeRegex.test.mjs new file mode 100644 index 000000000..fb5fc78f1 --- /dev/null +++ b/unitTests/security/safeRegex.test.mjs @@ -0,0 +1,66 @@ +/** + * Unit coverage for compileSafeRegex (security/safeRegex.ts) — the RE2-backed compiler for + * regex whose source is operator- or attacker-influenced. This is the safe substrate that the WAF + * (and any future untrusted-regex call site) compiles through instead of `new RegExp(...)`. + * + * The suite pins the two properties that justify the native dependency: (1) linear-time matching, + * so a catastrophic-backtracking pattern that hangs `RegExp` returns immediately here (ReDoS + * immunity), and (2) the deliberate contract that RE2-unsupported features (backreferences, + * lookaround) are reported as compile errors rather than silently downgraded. Loading the module + * at all also exercises that the re2 native binary resolves on the CI matrix. + */ + +import { expect } from 'chai'; +import { compileSafeRegex } from '#src/security/safeRegex'; + +describe('compileSafeRegex', () => { + it('compiles a valid pattern into a working matcher', () => { + const errors = []; + const re = compileSafeRegex('^/api/', 'test', errors); + expect(errors).to.deep.equal([]); + expect(re).to.not.equal(undefined); + expect(re.test('/api/users')).to.equal(true); + expect(re.test('/public')).to.equal(false); + }); + + it('is immune to catastrophic backtracking (ReDoS)', () => { + const errors = []; + // `(a+)+$` against a long non-matching input is the canonical ReDoS trigger: the built-in + // RegExp explores exponentially many partitions and hangs for seconds-to-minutes. RE2 + // evaluates it in linear time, so this must complete near-instantly. + const re = compileSafeRegex('(a+)+$', 'redos', errors); + expect(errors).to.deep.equal([]); + const input = 'a'.repeat(50_000) + '!'; + const start = process.hrtime.bigint(); + const matched = re.test(input); + const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6; + expect(matched).to.equal(false); + // Generous ceiling: linear-time RE2 finishes in well under a millisecond; RegExp would not + // return within the test timeout. The bound only needs to distinguish linear from explosive. + expect(elapsedMs).to.be.lessThan(250); + }); + + it('rejects backreferences (unsupported in linear-time RE2) as a compile error', () => { + const errors = []; + const re = compileSafeRegex('(a)\\1', 'backref', errors); + expect(re).to.equal(undefined); + expect(errors).to.have.lengthOf(1); + expect(errors[0]).to.include('backref'); + }); + + it('rejects lookaround as a compile error', () => { + const errors = []; + const re = compileSafeRegex('foo(?=bar)', 'lookahead', errors); + expect(re).to.equal(undefined); + expect(errors).to.have.lengthOf(1); + expect(errors[0]).to.include('lookahead'); + }); + + it('rejects invalid regex syntax as a compile error', () => { + const errors = []; + const re = compileSafeRegex('(unclosed', 'syntax', errors); + expect(re).to.equal(undefined); + expect(errors).to.have.lengthOf(1); + expect(errors[0]).to.include('syntax'); + }); +}); From f73a67c515de53a4b2f3d83cb0b47f9f6aaf9f2a Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sat, 11 Jul 2026 10:40:55 -0600 Subject: [PATCH 2/2] ci(waf): rebuild re2 native binary after --ignore-scripts installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit/integration/stress workflows install with `npm install --ignore-scripts` and then run Harper. lmdb/msgpackr/rocksdb survive that because they bundle their prebuilt binary in the npm package; re2 instead downloads its binary from a post-install script, so --ignore-scripts leaves re2.node absent and every Harper process that loads the WAF fails with "Cannot find module './build/Release/re2.node'". Add an explicit `npm rebuild re2` step after each such install to materialize the binary (fetches the prebuilt where one exists, source-builds otherwise — e.g. on Node majors re2 has not yet published a prebuilt for). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/integration-tests.yaml | 5 +++++ .github/workflows/stress-large-data.yaml | 4 ++++ .github/workflows/stress-tests.yaml | 4 ++++ .github/workflows/unit-tests.yaml | 5 +++++ 4 files changed, 18 insertions(+) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index fab887e14..309dbd8fa 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -109,6 +109,11 @@ jobs: - name: Relink bin scripts run: npm install --ignore-scripts + # re2 downloads its native binary from a post-install script (unlike lmdb/msgpackr/rocksdb, + # which bundle prebuilds), so --ignore-scripts above leaves it absent. Materialize it here. + - name: Build re2 native binary + run: npm rebuild re2 + - name: Install harperdb@4 for legacy tests run: | mkdir -p /tmp/harperdb-legacy diff --git a/.github/workflows/stress-large-data.yaml b/.github/workflows/stress-large-data.yaml index 85441aa2a..61662c654 100644 --- a/.github/workflows/stress-large-data.yaml +++ b/.github/workflows/stress-large-data.yaml @@ -105,6 +105,10 @@ jobs: name: harper-large-stress-build-${{ inputs.node-version || '24' }} - name: Relink bin scripts run: npm install --ignore-scripts + # re2 downloads its native binary from a post-install script (unlike lmdb/msgpackr/rocksdb, + # which bundle prebuilds), so --ignore-scripts above leaves it absent. Materialize it here. + - name: Build re2 native binary + run: npm rebuild re2 - name: Run ${{ matrix.test.name }} env: HARPER_RUN_STRESS_TESTS: '1' diff --git a/.github/workflows/stress-tests.yaml b/.github/workflows/stress-tests.yaml index cb126dd3d..f88f45a6b 100644 --- a/.github/workflows/stress-tests.yaml +++ b/.github/workflows/stress-tests.yaml @@ -145,6 +145,10 @@ jobs: name: harper-stress-build-${{ inputs.node-version || '24' }} - name: Relink bin scripts run: npm install --ignore-scripts + # re2 downloads its native binary from a post-install script (unlike lmdb/msgpackr/rocksdb, + # which bundle prebuilds), so --ignore-scripts above leaves it absent. Materialize it here. + - name: Build re2 native binary + run: npm rebuild re2 - name: Run ${{ matrix.test.name }} env: HARPER_RUN_STRESS_TESTS: '1' diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 2c2bfa021..52471e19e 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -30,6 +30,11 @@ jobs: - name: Install dependencies run: npm install --ignore-scripts + # re2 downloads its native binary from a post-install script (unlike lmdb/msgpackr/rocksdb, + # which bundle prebuilds), so --ignore-scripts above leaves it absent. Materialize it here. + - name: Build re2 native binary + run: npm rebuild re2 + - name: Build run: npm run build || true