fix(privacy): stop the mirror writing legal names into tubafrenzy DJ_HANDLE - #2292
Merged
Conversation
5 tasks
Schema constraint shape reportno new constraints detected in this diff (uniqueIndex, .unique(), SET NOT NULL, CHECK, FK) |
This was referenced Aug 27, 2026
…HANDLE http-mirror.ts's mapShowToTubafrenzy fell back to auth_user.name for handle-less DJs when computing djHandle. auth_user.name is written once at provisioning as realName || username and never maintained, making it a hidden, stale copy of the DJ's legal name (see shared/database/src/dj-name.ts for the history). Reading it here leaked legal names into tubafrenzy's public DJ_HANDLE field on wxyc.info for every DJ without a stage handle set. djHandle now resolves as: per-show override -> resolveDjDisplayName(djName) -> username -> ''. username is the terminal fallback rather than name because it is the least-surprising non-PII stable identifier and matches the auth_user.name policy chosen for the broader PII safeguards plan. The all-absent '' case is pinned explicitly (mirroring the showName ?? '' convention) even though it's unreachable today, so the behavior is chosen rather than an emergent undefined on a field tubafrenzy treats as required. Line 514 (djName: dj.realName || dj.name) is deliberately left untouched: it targets tubafrenzy's distinct DJ_NAME (real-name) field, where name remains the correct fallback until the auth_user.name backfill makes realName reliably populated. Folding it into this fix would touch a field that is not the leak. Rewrote the http.mirror.test.ts case that pinned the removed behavior (djHandle falling through to name) as the negative case, added coverage for the override/handle/username/all-absent matrix, and added a sentinel test proving djHandle can never equal a value planted only in name/realName. Updated the legacy-mirror-reconcile orchestrate.test.ts DJ fixture to carry username, matching what selectDj's full-row select actually returns. Corrected the shows.legacy_dj_name docblock in schema.ts, which described the old djHandle fallback chain.
…tract Code-review finding on #2292: the terminal fallback `dj.username ?? ''` applied no trim/blank filter, so a whitespace-only or padded username — impossible via current write paths (usernames validate against /^[a-zA-Z0-9_.]+$/) but expressible in the unconstrained varchar — would reach tubafrenzy's public DJ_HANDLE, contradicting the docblock's "if all three are unusable, djHandle is ''" claim. `.trim()` with the existing `?? ''` tail closes it; test covers padded and whitespace-only usernames.
jakebromberg
force-pushed
the
bugfix/mirror-djhandle-pii
branch
from
August 27, 2026 17:25
c628dc4 to
f802f85
Compare
…nical override link
jakebromberg
added a commit
that referenced
this pull request
Aug 28, 2026
…tract Code-review finding on #2292: the terminal fallback `dj.username ?? ''` applied no trim/blank filter, so a whitespace-only or padded username — impossible via current write paths (usernames validate against /^[a-zA-Z0-9_.]+$/) but expressible in the unconstrained varchar — would reach tubafrenzy's public DJ_HANDLE, contradicting the docblock's "if all three are unusable, djHandle is ''" claim. `.trim()` with the existing `?? ''` tail closes it; test covers padded and whitespace-only usernames.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2291.
What
mapShowToTubafrenzyfed tubafrenzy's publicDJ_HANDLEfield fromdj.namewhenever a DJ had no stage handle:dj.nameisauth_user.name, written once at provisioning asrealName || usernameand never maintained afterwards — a stale, hidden copy of the DJ's legal name (shared/database/src/dj-name.ts:1-13). So every handle-less DJ starting a show published their legal name to wxyc.info, and the flowsheet ETL round-tripped it back intoshows.legacy_dj_name.New chain at
http-mirror.ts:531-532:resolveDjDisplayNameis imported from@wxyc/databaserather than re-derived locally, so the trim + literal-"Anonymous"filter stays in one place (the?? nullcoercion is required — the helper is typed(djName: string | null)andMirrorDJ.djNameis optional under this package'sstrict: true).MirrorDJgainsusername?: string | nullat:496; both callers —apps/backend/middleware/legacy/flowsheet.mirror.ts:168andjobs/legacy-mirror-reconcile/orchestrate.ts:285— alreadyselect()the fullauth_userrow, so no query changes.Why now
The mirror has days to live before the 2026-08-31 turndown, and this still earns its place: it stops adding rows that the final tubafrenzy dump scrub (#1543) would otherwise have to chase, and stops growing the misattributed share of the
shows.legacy_dj_nameremediation cohort.This is Track 0 of
plans/dj-name-pii-safeguards.md, sliced to ship alone. The plan file lands in a sibling PR.Deliberate non-changes
http-mirror.ts:534(djName: dj.realName || dj.name) is untouched. That is tubafrenzy's separateDJ_NAMElegal-name field — the legitimate real-name flow, not the leak.nameremains the correct fallback there until theauth_user.namebackfill (Track 2d) makesreal_namereliably populated, at which point it flips torealName-only — or dies with the mirror. Changing it here would touch a field this PR is not about.'', matching the siblingshowName ?? ''convention, rather than left to emerge asundefinedon a field tubafrenzy treats as required. Unreachable in practice (every provisioned user has a username; anonymous users cannot own shows) — pinned so the behavior is chosen rather than accidental.The expanded docblock at
:499-527states both decisions inline, including "do not re-addname, do not re-derive the trim/Anonymous logic locally."Tests
tests/unit/middleware/legacy/http.mirror.test.ts:'falls back to name when realName/djName are null', assertingdjHandle === dj.name) is rewritten as the negative case: it now falls tousername, and the assertion ondjName(the real-name field) is retained unchanged to prove that half did not move.resolveDjDisplayName, the literal"Anonymous"handle, a whitespace-only handle, and all-absent ->''.name/realNameand assertsdjHandlenever equals it, across the full override/handle/username/all-absent matrix.tests/unit/jobs/legacy-mirror-reconcile/orchestrate.test.ts: the DJ fixture gainsusername, matching whatselectDj's full-rowselect()actually returns.Docs
Corrects the
shows.legacy_dj_namedocblock atshared/database/src/schema.ts:2497-2510, which described the outbound chain this PR removes. A stale doctrine comment on exactly this column is how the previous incident propagated, so it is corrected in the same commit rather than swept later.Local checks
The lint warnings are the pre-existing
security/detect-object-injectionbaseline; this branch adds none.