Skip to content

Leaderboard api - #561

Open
btfcookies wants to merge 2 commits into
qbreader:mainfrom
btfcookies:feat/public-leaderboard-api
Open

Leaderboard api#561
btfcookies wants to merge 2 commits into
qbreader:mainfrom
btfcookies:feat/public-leaderboard-api

Conversation

@btfcookies

Copy link
Copy Markdown
Contributor

Summary

  • Exposes user data via a GET api/leaderboard endpoint. This is different from the existing api/admin/leaderboard, I expanded it to include numCorrect, powers, tens, negs, tossupPoints, bonusPoints, points, pptu, ppb, accuracy, and averageCorrectCelerity.
  • api/admin/leaderboard and client/admin/leaderboard are unaffected
  • api/leaderboard is gated behind a shared secret (check your dms)
  • Adds api docs for the new endpoint
  • I also fixed 3 minor bugs related to database/account-info/leaderboard.js: slice(0, limit) returned [] when limit was omitted, deleted users left rows that didn't have usernames, mergeTwoSortedArrays didn't match MongoDB's sort outside of ASCII, which split not ASCII usernames into 2 rows.

btfcookies and others added 2 commits August 6, 2026 20:59
Ranks every user by the number of questions they have heard. The data
already existed behind /api/admin/leaderboard; this exposes it on the
public, CORS-enabled API and widens it beyond raw play counts.

database/account-info/leaderboard.js now also reports numCorrect,
powers, tens, negs, tossupPoints, bonusPoints, points, pptu, ppb,
accuracy, and averageCorrectCelerity. The existing fields are kept, so
/api/admin/leaderboard and client/admin/leaderboard are unaffected.

Also fixes three problems in that aggregation:

- slice(0, limit) returned [] when limit was omitted, since
  Array.prototype.slice treats a null end index as 0. Omitting the
  limit now returns every row.
- Users deleted since their buzzes were recorded produced rows with no
  username. They are dropped.
- mergeTwoSortedArrays compares usernames with < and >, but the inputs
  were ordered by MongoDB's byte-wise sort. The two disagree outside
  ASCII, which split a non-ASCII user into two rows. Both inputs are
  now sorted in JS with the same comparison the merge uses, and the
  unindexed $sort stages are gone.

The route catches rejections from the aggregation and returns 503.
Express 4 does not forward a rejected promise to the error handler, so
without this a database error takes down the process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This endpoint returns every user's username alongside their play stats.
Rather than deciding on the maintainers' behalf that this is fine to
publish openly, require a ?key= that matches LEADERBOARD_KEY in the
environment. Requests with a missing or wrong key get 401.

If LEADERBOARD_KEY is unset, every request is rejected rather than the
endpoint quietly falling back to public — a deploy that forgets to set
it fails closed, not open.

The comparison hashes both sides with SHA-256 before calling
timingSafeEqual, since that function throws on unequal-length buffers
and comparing raw lengths would leak the true key length to a timing
attack.

Cache-Control changes from public to private: a shared or CDN cache
must not be allowed to serve this response to a caller that supplied
no key of its own.

Docs updated to describe the key requirement and the new 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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