Files
civitai__civitai/src/server/services/image-scanner-flag.ts
T
Zachary Lowden 47462793f7 fix(sysredis): coerce Buffer reply before strict-equality compare in isRunCancelled + isImageScannerNewEnabled (sentinel-mode gap) (#2711)
Two residual silent `===` mismatches the #2700 Buffer-coercion sweep missed.
The legacy single-pod sysRedis returns strings, but the HA/Sentinel sysRedis
(preview now, prod after the upcoming cutover) returns BLOB_STRING replies as a
Buffer. A Buffer is never strictly-equal to a string literal, so `value === '1'`
silently evaluates false under Sentinel — no throw, just wrong behavior. Mirrors
the canonical coercion from #2697/#2700.

scanner-policies.service.ts (isRunCancelled): markRunCancelled sets the key to
'1'. Pre-fix `v === '1'` compared a Buffer to the string → always false → run
cancellation silently never detected in sentinel mode. Coerce to utf8 before the
compare; try/catch fail-open behavior unchanged.

image.service.ts (isImageScannerNewEnabled) — higher priority, it's destructive:
a Buffer matched none of the four literals ('1'/'true'/'0'/'false') → fell
through and DESTRUCTIVELY overwrote the operator's '1' with 'false', then
returned false. The pure decision is extracted into image-scanner-flag.ts
(parseScannerFlag → true/false/null) so the coercion + the seed-only-on-unset
behavior is unit-testable without loading the ~7.9K-line image.service module
(which drags in Prisma/env/auth at import and can't load under vitest). null ==
genuinely-unset → the only case the seed('false') still fires. Default-seeding
behavior for the genuinely-null case is unchanged.

Tests: scanner-policies.buffer.test.ts asserts Buffer('1')→true (the bug),
string('1')→true (unchanged), Buffer('0')/null/other→false, thrown→false
(fail-open). image-scanner-flag.buffer.test.ts asserts parseScannerFlag over
Buffer/string/null/unknown inputs. Both confirmed non-vacuous (reverting a
coercion turns the Buffer cases red).

Pre-Sentinel-cutover hardening alongside #2697/#2700/#2710.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:02:21 -05:00

26 lines
1.1 KiB
TypeScript

/**
* Pure decision helper for the `image-scanner-new` sysRedis kill-switch.
*
* Lives in its own tiny module so the Buffer-coercion + seed-only-on-unset
* behavior is unit-testable without importing the ~7.9K-line image.service
* (which drags in Prisma/env/auth at load time and can't load under vitest).
*
* sysRedis.get is typed `string` but the HA/Sentinel client returns a Buffer
* for BLOB_STRING replies, matching none of the four literals → pre-fix
* `isImageScannerNewEnabled` fell through and DESTRUCTIVELY overwrote the
* operator's '1' with 'false' (then returned false). Coerce once before
* comparing. See PR #2697/#2700 for the canonical Buffer-vs-string regression.
*
* Returns:
* - `true` for '1' / 'true'
* - `false` for '0' / 'false'
* - `null` for a genuinely-unset/unknown key — the ONLY case in which the
* caller should default-seed 'false'.
*/
export function parseScannerFlag(raw: unknown): boolean | null {
const value = Buffer.isBuffer(raw) ? raw.toString('utf8') : raw;
if (value === '1' || value === 'true') return true;
if (value === '0' || value === 'false') return false;
return null;
}