Skip to content

fix(privacy): stop the mirror writing legal names into tubafrenzy DJ_HANDLE - #2292

Merged
jakebromberg merged 3 commits into
mainfrom
bugfix/mirror-djhandle-pii
Aug 28, 2026
Merged

fix(privacy): stop the mirror writing legal names into tubafrenzy DJ_HANDLE#2292
jakebromberg merged 3 commits into
mainfrom
bugfix/mirror-djhandle-pii

Conversation

@jakebromberg

Copy link
Copy Markdown
Member

Closes #2291.

What

mapShowToTubafrenzy fed tubafrenzy's public DJ_HANDLE field from dj.name whenever a DJ had no stage handle:

// shared/legacy-mirror/src/http-mirror.ts:512 (before)
const djHandle = override && override.length > 0 ? override : dj.djName || dj.name;

dj.name is auth_user.name, written once at provisioning as realName || username and 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 into shows.legacy_dj_name.

New chain at http-mirror.ts:531-532:

per-show override -> resolveDjDisplayName(dj.djName ?? null) -> dj.username -> ''

resolveDjDisplayName is imported from @wxyc/database rather than re-derived locally, so the trim + literal-"Anonymous" filter stays in one place (the ?? null coercion is required — the helper is typed (djName: string | null) and MirrorDJ.djName is optional under this package's strict: true). MirrorDJ gains username?: string | null at :496; both callers — apps/backend/middleware/legacy/flowsheet.mirror.ts:168 and jobs/legacy-mirror-reconcile/orchestrate.ts:285 — already select() the full auth_user row, 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_name remediation 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 separate DJ_NAME legal-name field — the legitimate real-name flow, not the leak. name remains the correct fallback there until the auth_user.name backfill (Track 2d) makes real_name reliably populated, at which point it flips to realName-only — or dies with the mirror. Changing it here would touch a field this PR is not about.
  • The all-absent case is pinned to '', matching the sibling showName ?? '' convention, rather than left to emerge as undefined on 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-527 states both decisions inline, including "do not re-add name, do not re-derive the trim/Anonymous logic locally."

Tests

tests/unit/middleware/legacy/http.mirror.test.ts:

  • The existing case that pinned the removed behavior ('falls back to name when realName/djName are null', asserting djHandle === dj.name) is rewritten as the negative case: it now falls to username, and the assertion on djName (the real-name field) is retained unchanged to prove that half did not move.
  • New coverage for handle-via-resolveDjDisplayName, the literal "Anonymous" handle, a whitespace-only handle, and all-absent -> ''.
  • Sentinel test: plants one obviously-fake sentinel string in name/realName and asserts djHandle never equals it, across the full override/handle/username/all-absent matrix.

tests/unit/jobs/legacy-mirror-reconcile/orchestrate.test.ts: the DJ fixture gains username, matching what selectDj's full-row select() actually returns.

Docs

Corrects the shows.legacy_dj_name docblock at shared/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

npm run format:check          All matched files use Prettier code style!
npm run typecheck             clean across all 9 workspaces
npm run lint                  959 problems (0 errors, 959 warnings) — exit 0
npx jest --config jest.unit.config.ts \
  --testPathIgnorePatterns "tests/unit/jobs/flowsheet-etl"
                              Test Suites: 472 passed, 472 total
                              Tests:       8289 passed, 8289 total  — exit 0
npx jest --config jest.unit.config.ts tests/unit/jobs/flowsheet-etl
                              Test Suites: 9 passed, 9 total
                              Tests:       197 passed, 197 total    — exit 0

The lint warnings are the pre-existing security/detect-object-injection baseline; this branch adds none.

@github-actions

Copy link
Copy Markdown

Schema constraint shape report

no new constraints detected in this diff (uniqueIndex, .unique(), SET NOT NULL, CHECK, FK)

…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
jakebromberg force-pushed the bugfix/mirror-djhandle-pii branch from c628dc4 to f802f85 Compare August 27, 2026 17:25
@jakebromberg
jakebromberg merged commit 3f60f0c into main Aug 28, 2026
7 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mirror writes DJ legal names into tubafrenzy's public DJ_HANDLE for handle-less DJs

1 participant