* fix: security hardening — search DoS, slug hijack, symlink traversal, content bombs, stdin guard 4 security vulnerabilities closed: - Search limit clamped to 100 (MAX_SEARCH_LIMIT) with statement_timeout 8s - Frontmatter slug authority enforced (path-derived, mismatch rejected) - Symlink traversal blocked (lstatSync in walker + importFromFile) - Content size guard on importFromContent (Buffer.byteLength, 5MB) - Stdin size guard in parseOpArgs (5MB cap) Search pagination added (--offset param on search + query operations). Clamp warning emitted when limit is capped. Co-Authored-By: garagon <garagon@users.noreply.github.com> * fix: PGLite concurrent access lock — prevent Aborted() crash File-based advisory lock using atomic mkdir with PID tracking and 5-minute stale detection. Clear error messages show which process holds the lock and how to recover. Co-Authored-By: danbr <danbr@users.noreply.github.com> * fix: 12 data integrity fixes + stale embedding prevention CTE searchKeyword rewrite (SQL-level LIMIT, not JS splice). Write validation on addLink/addTag/addTimelineEntry/putRawData/createVersion. Health metrics now measure real problems (stale_pages, orphan_pages, dead_links). Orphan chunk cleanup on empty pages. Embedding error logging. contentHash now covers all PageInput fields. Stale embedding NULL'd when chunk_text changes (prevents wrong vector on new text). hybridSearch stops double-embedding query. MCP param validation. type/exclude_slugs search filters now work. pgcrypto extension for Postgres <13. Co-Authored-By: win4r <win4r@users.noreply.github.com> * perf: 30x embedAll speedup + O(n²) fix + ask alias Sliding worker pool (concurrency 20, tunable via GBRAIN_EMBED_CONCURRENCY). O(n²) chunk lookup in embedPage replaced with Map. gbrain ask alias for query (CLI-only, not in MCP tools-json). .idea added to .gitignore. Co-Authored-By: stephenhungg <stephenhungg@users.noreply.github.com> Co-Authored-By: sharziki <sharziki@users.noreply.github.com> Co-Authored-By: hnshah <hnshah@users.noreply.github.com> Co-Authored-By: doguabaris <doguabaris@users.noreply.github.com> * chore: bump version and changelog (v0.9.1) Community fix wave: 10 PRs, 7 contributors. 4 security fixes, PGLite crash fix, 12 data integrity fixes, 30x embed speedup, search pagination, ask alias. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: garagon <garagon@users.noreply.github.com> Co-authored-by: danbr <danbr@users.noreply.github.com> Co-authored-by: win4r <win4r@users.noreply.github.com> Co-authored-by: stephenhungg <stephenhungg@users.noreply.github.com> Co-authored-by: sharziki <sharziki@users.noreply.github.com> Co-authored-by: hnshah <hnshah@users.noreply.github.com> Co-authored-by: doguabaris <doguabaris@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
import { describe, it, expect } from 'bun:test';
|
|
import { MAX_SEARCH_LIMIT, clampSearchLimit } from '../src/core/engine.ts';
|
|
|
|
describe('clampSearchLimit', () => {
|
|
it('uses default when undefined', () => {
|
|
expect(clampSearchLimit(undefined)).toBe(20);
|
|
});
|
|
|
|
it('uses custom default when provided', () => {
|
|
expect(clampSearchLimit(undefined, 10)).toBe(10);
|
|
});
|
|
|
|
it('passes through in-range values', () => {
|
|
expect(clampSearchLimit(50)).toBe(50);
|
|
});
|
|
|
|
it('clamps oversized values to MAX_SEARCH_LIMIT', () => {
|
|
expect(clampSearchLimit(10_000_000)).toBe(MAX_SEARCH_LIMIT);
|
|
});
|
|
|
|
it('uses default for zero', () => {
|
|
expect(clampSearchLimit(0)).toBe(20);
|
|
});
|
|
|
|
it('uses default for negative', () => {
|
|
expect(clampSearchLimit(-5)).toBe(20);
|
|
});
|
|
|
|
it('floors fractional values', () => {
|
|
expect(clampSearchLimit(7.9)).toBe(7);
|
|
});
|
|
|
|
it('uses default for NaN', () => {
|
|
expect(clampSearchLimit(NaN)).toBe(20);
|
|
});
|
|
|
|
it('clamps Infinity to MAX_SEARCH_LIMIT', () => {
|
|
expect(clampSearchLimit(Infinity)).toBe(20); // !isFinite → default
|
|
});
|
|
|
|
it('MAX_SEARCH_LIMIT is 100', () => {
|
|
expect(MAX_SEARCH_LIMIT).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe('listPages is NOT affected by search clamp', () => {
|
|
it('listPages accepts limit > MAX_SEARCH_LIMIT (regression test)', async () => {
|
|
// listPages uses PageFilters.limit, NOT clampSearchLimit.
|
|
// This test verifies the clamp is scoped to search operations only.
|
|
// We import the PGLite engine and check that listPages with limit 100000 works.
|
|
const { PGLiteEngine } = await import('../src/core/pglite-engine.ts');
|
|
const engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
|
|
// Insert a page
|
|
await engine.putPage('test/big-list', {
|
|
title: 'Test', type: 'concept', compiled_truth: 'test content', timeline: '',
|
|
});
|
|
|
|
// listPages with limit 100000 should NOT be clamped
|
|
const pages = await engine.listPages({ limit: 100000 });
|
|
expect(pages.length).toBeGreaterThanOrEqual(1);
|
|
|
|
await engine.disconnect();
|
|
});
|
|
});
|