Skip to content

fix(identity): resolve + reconcile duplicate Users rows — cat-game counter/leaderboard desync (#2651) - #2661

Merged
jung-thomas merged 11 commits into
DEVfrom
fix/identity-dedup-resolver-2651
Oct 5, 2026
Merged

jung-thomas merged 11 commits into
DEVfrom
fix/identity-dedup-resolver-2651

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Summary

Fixes #2651 — the Devtoberfest cat-game (Kasimir) counter and leaderboard desync, root-caused to duplicate Users rows for the same human.

Root cause: After the IAS migration, SAP ID Service issues IAS tokens that carry the user's email in xs.user.attributes (surfaced as user.attr.email) rather than the JWT root payload.email claim. tokenEmail() only read payload.email, so Tier-3 email matching in resolveUser was skipped for IAS logins → resolveUser returned null → provisionDbUser minted a second Users row for the same human. Cat-game points earned on a forked row have no EventRegistration, so the GAMEBOARD_PARTICIPANT_V1 inner join hides them from the leaderboard while the in-game counter still counts them. ~349 duplicate-email clusters in PROD.

This is why forks continued at ~4–8%/day even after the #2552 identity-linkage layer shipped.

Two logical halves (reviewed together)

Prevention (stops new forks):

  • tokenEmail() falls back to user.attr?.email for IAS tokens — the root-cause fix (XSUAA payload.email still wins; no regression)
  • pickCanonicalRow() — deterministic canonical-row pick; exported from the package barrel so the CF bundle includes it
  • provisionDbUser return block hardened against concurrent-insert races
  • Tier-3 tags __hasRegistration so the resolver and the reconciliation job pick the same canonical row

Reconciliation (fixes the 349 existing forks):

  • mergeAccounts extended to repoint UserIdentities (dedupe issuer+subject), EventRegistrations (dedupe event, keep earliest joinedAt), and CatGameAwards (sum per-day to the 5-cap, trim per-event to the 100-cap — matches the live game's hard caps)
  • srv/jobs/identity-dedup-job.js — reconcileDuplicateEmails({dryRun}), dry-run by default, computes real point deltas. Deliberately NOT registered in JOB_REGISTRY (registry jobs get a one-click admin button + cron; a mutating 349-cluster reconciliation must stay operator-invoked). Operator runbook in the SDD workspace.

Test plan

  • Unit: identity resolver (43/43), mergeAccounts extension (5/5), reconciliation job (4/4) — all green
  • XSUAA token path unregressed (precedence test)
  • Cat-game cap math verified (daily 5, event 100, no negative points)
  • Final whole-branch review (cross-file / CF-runtime bundle chain): SOUND
  • On DEV: sign in with an IAS/SAP-ID account, earn a cat-game award, confirm counter == leaderboard bonus (no new forked row)
  • On DEV: run reconcileDuplicateEmails({dryRun:true}) against DEV data, review cluster/point counts before any execute

Notes

  • The revert pair in history (a5966cbf + d6e1f0925) is the honest trail of an abandoned eager-merge approach — the real fix is the tokenEmail change.
  • Reconciliation on PROD data is a separate operator step (dry-run → review with maintainer → execute), gated behind the runbook; nothing auto-runs on merge.

@jung-thomas
jung-thomas merged commit bae1ec8 into DEV Oct 5, 2026
4 of 5 checks passed
@jung-thomas
jung-thomas deleted the fix/identity-dedup-resolver-2651 branch October 5, 2026 19:01
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.

1 participant