From b43552f8c84d01fd7ae0a5f27205b7d32d7930a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Bracht?= Date: Sat, 25 Apr 2026 15:23:23 -0700 Subject: [PATCH 1/3] fix codeql regex findings and bump mqdb-wasm to 0.3.2 --- CHANGELOG.md | 5 +++++ package-lock.json | 18 +++++++++++------- package.json | 2 +- scripts/extract-changelog.mjs | 11 +++++++++-- src/memory-store.ts | 8 ++++++-- src/persistence-layer.ts | 12 ++++++++---- 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 067bcb5..12c17d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - **Open-source release.** Apache-2.0 license; `package.json` now carries `license`, `author`, `repository`, `bugs`, `homepage`, `keywords`, `engines`, `sideEffects`, and a `files` allowlist. `private: true` removed. - **Build pipeline.** New `tsconfig.build.json` + `npm run build` emit ESM `dist/` via `tsc` with `rewriteRelativeImportExtensions` (rewrites `.ts` → `.js`). `exports` map points at `dist/index.js`, `dist/react/index.js`, `dist/vue/index.js` with matching `.d.ts` declarations. `prepublishOnly` runs `clean` + `check` + `build` so the npm tarball cannot ship without a green gate. +- **`mqdb-wasm` bumped to `0.3.2`** (from `0.3.1`). + +### Fixed + +- **Polynomial-time backtracking in WASM corruption detection.** `MemoryStore.isWasmCorrupted` and `PersistenceLayer.isDbCorrupted` previously matched error messages with a regex containing `transaction.*null`, which has polynomial complexity on inputs with many `transaction` prefixes (CodeQL `js/polynomial-redos`). Replaced with substring `indexOf` checks — same semantics, linear time. ## 0.3.0 diff --git a/package-lock.json b/package-lock.json index b04b994..6c1be3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@laboverwire/stitch", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@laboverwire/stitch", - "version": "0.2.0", + "version": "0.3.0", + "license": "Apache-2.0", "dependencies": { - "mqdb-wasm": "^0.3.1", + "mqdb-wasm": "^0.3.2", "mqtt5-wasm": "^1.0.0" }, "devDependencies": { @@ -30,6 +31,9 @@ "vitest": "^4.1.5", "vue": "^3.3.0" }, + "engines": { + "node": ">=18" + }, "peerDependencies": { "react": "^19.0.0", "vue": "^3.3.0" @@ -3264,10 +3268,10 @@ } }, "node_modules/mqdb-wasm": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/mqdb-wasm/-/mqdb-wasm-0.3.1.tgz", - "integrity": "sha512-QkN2bsyaRunt9zh7LqamA2SnPfEV1K39WoDVOAjK/RhJmkYMft13L2cM1CQjKaKYOqc88grzhsgkUbz7WRAYnA==", - "license": "AGPL-3.0-only" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/mqdb-wasm/-/mqdb-wasm-0.3.2.tgz", + "integrity": "sha512-1ByPFa9Nr1i+sdXcXGE7610d7pS3SBj8BOqyvRTdBogX+6Ityfhq6eKO2w4QBACIwLch4SLmsrSMh0VcaI9rJA==", + "license": "Apache-2.0" }, "node_modules/mqtt5-wasm": { "version": "1.0.0", diff --git a/package.json b/package.json index e87c259..91ac617 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ } }, "dependencies": { - "mqdb-wasm": "^0.3.1", + "mqdb-wasm": "^0.3.2", "mqtt5-wasm": "^1.0.0" }, "peerDependencies": { diff --git a/scripts/extract-changelog.mjs b/scripts/extract-changelog.mjs index 9f311dd..af463d9 100755 --- a/scripts/extract-changelog.mjs +++ b/scripts/extract-changelog.mjs @@ -9,13 +9,20 @@ if (!version) { process.exit(1); } +if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/.test(version)) { + console.error(`extract-changelog.mjs: "${version}" is not a valid SemVer version.`); + process.exit(1); +} + const here = dirname(fileURLToPath(import.meta.url)); const changelogPath = resolve(here, '..', 'CHANGELOG.md'); const text = readFileSync(changelogPath, 'utf8'); const lines = text.split('\n'); -const headingPattern = new RegExp(`^##\\s+${version.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\b`); -const startIdx = lines.findIndex((l) => headingPattern.test(l)); +const heading = `## ${version}`; +const startIdx = lines.findIndex( + (l) => l === heading || l.startsWith(`${heading} `) || l.startsWith(`${heading}\t`) +); if (startIdx === -1) { console.error(`CHANGELOG.md: no "## ${version}" section found.`); process.exit(1); diff --git a/src/memory-store.ts b/src/memory-store.ts index 8d63796..5acb52e 100644 --- a/src/memory-store.ts +++ b/src/memory-store.ts @@ -176,8 +176,12 @@ class MemoryStoreImpl implements MemoryStore { private isWasmCorrupted(err: unknown): boolean { if (err instanceof Error && err.name === 'RuntimeError') return true; - const msg = err instanceof Error ? err.message : String(err); - return /transaction.*null|arg0 is null|transaction error|unreachable/i.test(msg); + const msg = (err instanceof Error ? err.message : String(err)).toLowerCase(); + if (msg.includes('arg0 is null')) return true; + if (msg.includes('transaction error')) return true; + if (msg.includes('unreachable')) return true; + const txIdx = msg.indexOf('transaction'); + return txIdx !== -1 && msg.indexOf('null', txIdx) !== -1; } private notifyCorruption(): void { diff --git a/src/persistence-layer.ts b/src/persistence-layer.ts index f850249..b5dd20d 100644 --- a/src/persistence-layer.ts +++ b/src/persistence-layer.ts @@ -398,10 +398,14 @@ class PersistenceLayerImpl implements PersistenceLayer { private isDbCorrupted(err: unknown): boolean { const inner = err instanceof MqdbError ? err.cause : err; if (inner instanceof Error && inner.name === 'RuntimeError') return true; - const msg = inner instanceof Error ? inner.message : String(inner); - return /transaction.*null|arg0 is null|transaction error|index out of bounds|database is busy|unreachable/i.test( - msg - ); + const msg = (inner instanceof Error ? inner.message : String(inner)).toLowerCase(); + if (msg.includes('arg0 is null')) return true; + if (msg.includes('transaction error')) return true; + if (msg.includes('index out of bounds')) return true; + if (msg.includes('database is busy')) return true; + if (msg.includes('unreachable')) return true; + const txIdx = msg.indexOf('transaction'); + return txIdx !== -1 && msg.indexOf('null', txIdx) !== -1; } private async recoverDb(): Promise { From 54406fbbde3759698f9ff1ef00dc14420926f2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Bracht?= Date: Sat, 25 Apr 2026 15:36:46 -0700 Subject: [PATCH 2/3] consolidate corruption detection into shared helper --- CHANGELOG.md | 2 +- src/internal-wasm-error.ts | 16 ++++++++++++++++ src/memory-store.ts | 10 ++-------- src/persistence-layer.ts | 13 ++----------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c17d2..cd99151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixed -- **Polynomial-time backtracking in WASM corruption detection.** `MemoryStore.isWasmCorrupted` and `PersistenceLayer.isDbCorrupted` previously matched error messages with a regex containing `transaction.*null`, which has polynomial complexity on inputs with many `transaction` prefixes (CodeQL `js/polynomial-redos`). Replaced with substring `indexOf` checks — same semantics, linear time. +- **Polynomial-time backtracking in WASM corruption detection.** `MemoryStore.isWasmCorrupted` and `PersistenceLayer.isDbCorrupted` previously matched error messages with a regex containing `transaction.*null`, which has polynomial complexity on inputs with many `transaction` prefixes (CodeQL `js/polynomial-redos`). Both helpers now delegate to a shared `isCorruptionError(err, extraPatterns)` exported from `internal-wasm-error.ts`, which uses substring `indexOf` checks — same semantics, linear time, and the corruption-pattern list lives in one place. ## 0.3.0 diff --git a/src/internal-wasm-error.ts b/src/internal-wasm-error.ts index 72aebbf..6eec749 100644 --- a/src/internal-wasm-error.ts +++ b/src/internal-wasm-error.ts @@ -29,3 +29,19 @@ export async function wrapAsync(method: string, fn: () => Promise): Promis throw wrapWasmError(method, err); } } + +const BASE_CORRUPTION_PATTERNS = ['arg0 is null', 'transaction error', 'unreachable'] as const; + +export function isCorruptionError(err: unknown, extraPatterns: readonly string[] = []): boolean { + const inner = err instanceof MqdbError ? err.cause : err; + if (inner instanceof Error && inner.name === 'RuntimeError') return true; + const msg = (inner instanceof Error ? inner.message : String(inner)).toLowerCase(); + for (const pattern of BASE_CORRUPTION_PATTERNS) { + if (msg.includes(pattern)) return true; + } + for (const pattern of extraPatterns) { + if (msg.includes(pattern)) return true; + } + const txIdx = msg.indexOf('transaction'); + return txIdx !== -1 && msg.indexOf('null', txIdx) !== -1; +} diff --git a/src/memory-store.ts b/src/memory-store.ts index 5acb52e..73095ce 100644 --- a/src/memory-store.ts +++ b/src/memory-store.ts @@ -1,6 +1,6 @@ import type { Database } from 'mqdb-wasm'; import type initWasm from 'mqdb-wasm'; -import { wrapWasmError } from './internal-wasm-error.ts'; +import { wrapWasmError, isCorruptionError } from './internal-wasm-error.ts'; import type { StoreConfig, MutationEvent, MemoryStore, OriginTag } from './types.ts'; type SubscriptionCallback = () => void; @@ -175,13 +175,7 @@ class MemoryStoreImpl implements MemoryStore { } private isWasmCorrupted(err: unknown): boolean { - if (err instanceof Error && err.name === 'RuntimeError') return true; - const msg = (err instanceof Error ? err.message : String(err)).toLowerCase(); - if (msg.includes('arg0 is null')) return true; - if (msg.includes('transaction error')) return true; - if (msg.includes('unreachable')) return true; - const txIdx = msg.indexOf('transaction'); - return txIdx !== -1 && msg.indexOf('null', txIdx) !== -1; + return isCorruptionError(err); } private notifyCorruption(): void { diff --git a/src/persistence-layer.ts b/src/persistence-layer.ts index b5dd20d..d4d594e 100644 --- a/src/persistence-layer.ts +++ b/src/persistence-layer.ts @@ -1,6 +1,6 @@ import type { Database } from 'mqdb-wasm'; import type { StoreConfig, PersistenceLayer, EntityDefinition } from './types.ts'; -import { wrapWasmError, MqdbError } from './internal-wasm-error.ts'; +import { wrapWasmError, isCorruptionError } from './internal-wasm-error.ts'; const PENDING_SYNC_DEFINITION: EntityDefinition = { fields: [ @@ -396,16 +396,7 @@ class PersistenceLayerImpl implements PersistenceLayer { } private isDbCorrupted(err: unknown): boolean { - const inner = err instanceof MqdbError ? err.cause : err; - if (inner instanceof Error && inner.name === 'RuntimeError') return true; - const msg = (inner instanceof Error ? inner.message : String(inner)).toLowerCase(); - if (msg.includes('arg0 is null')) return true; - if (msg.includes('transaction error')) return true; - if (msg.includes('index out of bounds')) return true; - if (msg.includes('database is busy')) return true; - if (msg.includes('unreachable')) return true; - const txIdx = msg.indexOf('transaction'); - return txIdx !== -1 && msg.indexOf('null', txIdx) !== -1; + return isCorruptionError(err, ['index out of bounds', 'database is busy']); } private async recoverDb(): Promise { From a041bfff9aaeceff1cf0846ab38ac7fe4b5fb179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Bracht?= Date: Sat, 25 Apr 2026 15:40:52 -0700 Subject: [PATCH 3/3] fix exponential backtracking in semver validation regex --- scripts/extract-changelog.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/extract-changelog.mjs b/scripts/extract-changelog.mjs index af463d9..cc86c42 100755 --- a/scripts/extract-changelog.mjs +++ b/scripts/extract-changelog.mjs @@ -9,7 +9,7 @@ if (!version) { process.exit(1); } -if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/.test(version)) { +if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?(?:\+[0-9A-Za-z.]+)?$/.test(version)) { console.error(`extract-changelog.mjs: "${version}" is not a valid SemVer version.`); process.exit(1); }