diff --git a/CHANGELOG.md b/CHANGELOG.md index 067bcb5..cd99151 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`). 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/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..cc86c42 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.]+)?(?:\+[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/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 8d63796..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,9 +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); - return /transaction.*null|arg0 is null|transaction error|unreachable/i.test(msg); + return isCorruptionError(err); } private notifyCorruption(): void { diff --git a/src/persistence-layer.ts b/src/persistence-layer.ts index f850249..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,12 +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); - return /transaction.*null|arg0 is null|transaction error|index out of bounds|database is busy|unreachable/i.test( - msg - ); + return isCorruptionError(err, ['index out of bounds', 'database is busy']); } private async recoverDb(): Promise {