Three code-level correctness gaps surfaced while fact-checking the EQL v3 docs restructure (#658) — each a case where the docs (and in two cases the code's own comments/signatures) described behaviour the implementation didn't provide.
1. Supabase matches() had no short-needle guard 🔴 (fail-open)
A free-text needle shorter than the tokenizer's token_length blooms to zero tokens, so bloom @> {} is true for every row — the query silently returns and the caller decrypts the whole table. matchNeedleError was wired into the Drizzle adapter only; the Supabase adapter's own class doc claimed the guard existed.
Expected: reject the too-short needle at assertTermQueryable (the single choke point every term spelling funnels through, where the JSON-containment guard already lives), so both first-party surfaces reject identically. (The authoritative encryptQuery-path fix belongs in protect-ffi — protectjs-ffi#138 — but can't cover Supabase's storage-encrypt path, so the client-side guard stays.)
2. .withLockContext({ identityClaim }) didn't typecheck on the Supabase builder
The builder accepted only a LockContext instance (TS2353 on the canonical identity example #642/#658 document as the primary form). Expected: widen the stored field + method to LockContextInput (LockContext | { identityClaim }), the type the stack-level operations already accept.
3. EncryptionErrorTypes lacked as const
Every member's value widened to string, so the StackError union's type fields were all string and the union never discriminated: switch (error.type) couldn't narrow and error.code was TS2339. The documented exhaustive error-handling pattern didn't compile. Expected: one-line as const.
Resolution
Fixed in #659, with unit + type-level regression coverage for all three. Found during the #658 code-review pass.
Three code-level correctness gaps surfaced while fact-checking the EQL v3 docs restructure (#658) — each a case where the docs (and in two cases the code's own comments/signatures) described behaviour the implementation didn't provide.
1. Supabase
matches()had no short-needle guard 🔴 (fail-open)A free-text needle shorter than the tokenizer's
token_lengthblooms to zero tokens, sobloom @> {}is true for every row — the query silently returns and the caller decrypts the whole table.matchNeedleErrorwas wired into the Drizzle adapter only; the Supabase adapter's own class doc claimed the guard existed.Expected: reject the too-short needle at
assertTermQueryable(the single choke point every term spelling funnels through, where the JSON-containment guard already lives), so both first-party surfaces reject identically. (The authoritativeencryptQuery-path fix belongs in protect-ffi — protectjs-ffi#138 — but can't cover Supabase's storage-encrypt path, so the client-side guard stays.)2.
.withLockContext({ identityClaim })didn't typecheck on the Supabase builderThe builder accepted only a
LockContextinstance (TS2353on the canonical identity example #642/#658 document as the primary form). Expected: widen the stored field + method toLockContextInput(LockContext | { identityClaim }), the type the stack-level operations already accept.3.
EncryptionErrorTypeslackedas constEvery member's value widened to
string, so theStackErrorunion'stypefields were allstringand the union never discriminated:switch (error.type)couldn't narrow anderror.codewasTS2339. The documented exhaustive error-handling pattern didn't compile. Expected: one-lineas const.Resolution
Fixed in #659, with unit + type-level regression coverage for all three. Found during the #658 code-review pass.