The database store review found two Critical defects and several Important
ones, all reachable in production.
grant() was wrapped in db.tx for atomicity. The sqlite driver runs a bare
BEGIN on one shared connection with no serialization, so an open transaction
swallows any concurrent write from another method and discards it on
rollback. Demonstrated: revokeRole resolved with no error while the role
survived - a security-critical revoke reporting success with the privilege
retained. Concurrent grants also rejected outright with "cannot start a
transaction within a transaction". Replaced with single-statement upserts,
which are atomic without a transaction; assignRole likewise drops its
check-then-act SELECT for ON CONFLICT DO NOTHING, which was rejecting 19 of
20 concurrent identical calls.
effect had no CHECK constraint and assignmentsFor classified by exact
equality, so a mis-cased or corrupted value was dropped from BOTH buckets -
a deny row that silently stopped denying. Added the constraint and made
anything that is not literally "allow" count as a deny.
scopeKey now refuses an explicitly empty tenantId rather than treating it as
global, which otherwise let a caller who controls the tenant id read and
write global assignments.
Also: ensureAuthzTables takes the dialect from db.driver.dialect instead of
defaulting to sqlite; the DDL is a list of statements rather than a blob
split on a formatting-dependent separator; MySQL identity columns get a
binary collation so tenant "T1" cannot match "t1"; and postgres placeholders
are numbered.
Adds four conformance tests for the concurrency and empty-scope cases. The
suite was entirely sequential and structurally could not catch any of this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>