- F1 [medium — the substantive one]: hasRestrictedPagesInWorkspace is now UNCACHED
(a plain EXISTS per call, like its sibling hasRestrictedPagesInSpace). Caching it
(even 5s) reintroduced an access-control leak the space path never had: a
concurrent whole-workspace read in the insert->commit window of the FIRST
restricted page could re-populate `false` under withCache (read-then-set, no
del-during-read guard) and override the insert-time bust, leaking that page to
unauthorized users for up to the TTL. Uncaching removes both the DB/cache
asymmetry and the TOCTOU race; the space path already accepts this per-call cost.
Reverted the now-unnecessary insertPageAccess cache-bust and removed the dead
HAS_RESTRICTED_PAGES_IN_WORKSPACE cache key.
- F2 [test]: page-permission-workspace-filter.int-spec.ts (real PG) — the
short-circuit returns the full input set with zero restrictions AND filters out
the page the user can't reach when a restriction is present (proving the authz
behavior is unchanged), the 0->1 transition flips immediately, and the flag is
per-workspace scoped.
- F3 [doc]: documented the deploy-time write-lock in the migration header — the
non-CONCURRENT GIN trigram builds take a SHARE lock that blocks writes on
pages/users/… for minutes on a large tenant; run in a maintenance window or
build CONCURRENTLY out-of-band for big installs.
- F4 [doc]: corrected the jwt.strategy comment — the reused req.raw.workspace is
the middleware's selectAll superset (not "the exact row this query returns"),
harmless because AuthWorkspace already preferred that object.
Gate: server tsc 0; the new int-spec 3/3 on real Postgres.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One migration + targeted hot-path fixes. API behavior 1:1 (schema change = added
indexes + a byte-identical f_unaccent function-body swap, see below).
- Trigram + composite indexes (20260705T120000-perf-indexes.ts): GIN trigram on
LOWER(f_unaccent(title/name)) for pages/users/groups (the /search/suggest
leading-wildcard LIKE did a seq scan per keystroke — EXPLAIN now confirms
Bitmap Index Scan on idx_pages_title_trgm), + page_history(page_id,id DESC),
comments(page_id,id). DEVIATION (verified byte-identical): PG18 cannot inline
the two-arg f_unaccent body during index creation, so up() swaps it to the
schema-qualified single-arg `SELECT public.unaccent($1)` — same dictionary,
identical output for all inputs, so the tsvector trigger + main @@ search stay
consistent with NO reindex; down() restores the exact two-arg body.
- Auth path: jwt.strategy reuses req.raw.workspace when workspaceId matches (the
middleware already validated it) instead of re-querying; domain.middleware
caches the workspace lookup (withCache 15s, invalidated in all 8 WorkspaceRepo
mutators, with a Date reviver for the JSON-serialized cache). USER + SESSION
caching DEFERRED — the invalidation surface (role change doesn't revoke
sessions; revocation includes background jobs) can't be safely covered, and a
missed hook on a security path is worse than the win.
- AI re-embed coalescing: aiQueue.add gets {jobId: embed-<id>, delay: 30s} so
active editing collapses to one job (worker reads current page state).
- filterAccessiblePageIds: hasRestrictedPagesInWorkspace short-circuit skips the
recursive-ancestor CTE when a workspace has zero restricted pages (wired from
search/favorites/notifications/recent/created-by). EXISTS on the same pageAccess
table the CTE anti-joins → no false-positive / no access leak. Busts the cache
on insertPageAccess so a 0->1 restricted transition takes effect immediately
(review F1).
- Small: syncTransclusion guarded by a family-node probe (both old+new content, so
the removal path is preserved); mention notifications enqueue only when the set
gained a member; redis maintainLock clears a prior interval (leak fix).
Skipped as risky (flagged): global ValidationPipe transform change; a pool-wide
statement_timeout (would kill long CREATE INDEX migrations on the same pool).
NOTE: kept the trash query's `content` select — the trash UI reads page.content
for its preview modal (review F3, would have regressed).
Gate: server tsc 0; jest page-permission/auth/search/persistence 15 suites pass;
migration up+down+idempotency verified on real PG18 with EXPLAIN confirming index
use. No new deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>