Hash the 40k hidden-players list and drop the old PayPal block
Deploy 40k-rankings / build-and-deploy (push) Successful in 4s

Hidden players are now matched by a hash of their full name or nickname
(src/hide.js) so the names aren't readable in the source or bundle.
Also removes the commented-out PayPal section from the Kingmaker
sign-up page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 19:14:12 -05:00
co-authored by Claude Opus 5.5
parent 95a54e9ec2
commit da2a5c42e7
5 changed files with 46 additions and 26 deletions
+6 -2
View File
@@ -53,8 +53,11 @@ Everything you'd normally change is in the `CONFIG` block at the top of
- `teamId`: the BCP team whose members are shown - `teamId`: the BCP team whose members are shown
- `leagueId` / `regionId`: which ITC league and region to rank against - `leagueId` / `regionId`: which ITC league and region to rank against
- `lmcLeagueId`: the Lord Marshal Conference league used for the LMC columns - `lmcLeagueId`: the Lord Marshal Conference league used for the LMC columns
- `blacklistNames`: players to hide from the board (full name or nickname, - `hiddenNameHashes`: players to hide from the board, stored as hashes of
case-insensitive) their full name or nickname so the names aren't readable in the source. The
comment at the top of `src/hide.js` has a one-line command to hash a name.
This only obfuscates: the roster is public, so anyone could hash every
member's name to find who's hidden.
- `memberIds`: fallback roster, used if the team endpoint ever stops returning - `memberIds`: fallback roster, used if the team endpoint ever stops returning
members without auth members without auth
- `useSample`: set to `true` to render placeholder rows with no network calls - `useSample`: set to `true` to render placeholder rows with no network calls
@@ -62,6 +65,7 @@ Everything you'd normally change is in the `CONFIG` block at the top of
## Files ## Files
- `index.html`, `src/main.js`, `src/style.css`: the page - `index.html`, `src/main.js`, `src/style.css`: the page
- `src/hide.js`: name hashing for `hiddenNameHashes`
- `nav.html`: this site's header nav entries (see the root README) - `nav.html`: this site's header nav entries (see the root README)
- `public/elo-data.json`: generated ELO data. Don't edit it by hand; the - `public/elo-data.json`: generated ELO data. Don't edit it by hand; the
weekly sync overwrites it. weekly sync overwrites it.
+27
View File
@@ -0,0 +1,27 @@
/* Hashing for CONFIG.hiddenNameHashes, so hidden players' names aren't
readable in the source or the deployed bundle. This is obfuscation, not
secrecy: the team roster is public, so anyone could hash every member's
name and find a match.
Get the hash for a name (full name or nickname) from sites/40k-rankings:
node --input-type=module -e "import { nameHash } from './src/hide.js'; console.log(nameHash('First Last'))"
*/
// cyrb53: small, fast, synchronous 53-bit string hash.
function cyrb53(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
// Case- and whitespace-insensitive, so "jane DOE " matches "Jane Doe".
export function nameHash(name) {
const norm = String(name || "").trim().replace(/\s+/g, " ").toLowerCase();
return norm ? cyrb53(norm).toString(16) : "";
}
+12 -9
View File
@@ -1,3 +1,5 @@
import { nameHash } from './hide.js';
/* ---- Shared: header / footer ---- */ /* ---- Shared: header / footer ---- */
function loadHTML(file, elementId, callback) { function loadHTML(file, elementId, callback) {
fetch(file) fetch(file)
@@ -48,19 +50,20 @@ const CONFIG = {
// Fallback roster if the team endpoint requires auth. // Fallback roster if the team endpoint requires auth.
memberIds: [], memberIds: [],
// Names to hide from the leaderboard entirely (case-insensitive). // Players to hide from the leaderboard entirely, as nameHash() values of
// Match on full name ("First Last") or nickname, whichever the player goes by. // their full name ("First Last") or nickname — see src/hide.js for how to
blacklistNames: ["Hidden Player"], // generate one. Hashed so the names aren't readable in the source.
hiddenNameHashes: ["5b64057a31512"],
// Flip to true to preview the layout with placeholder rows (no network). // Flip to true to preview the layout with placeholder rows (no network).
useSample: false, useSample: false,
}; };
const BLACKLIST = new Set(CONFIG.blacklistNames.map(n => n.trim().toLowerCase())); const HIDDEN = new Set(CONFIG.hiddenNameHashes);
function isBlacklisted(u = {}) { function isHidden(u = {}) {
const full = fullName(u).toLowerCase(); const full = nameHash(fullName(u));
const nick = String(u.nickname || "").trim().toLowerCase(); const nick = nameHash(u.nickname);
return (full && BLACKLIST.has(full)) || (nick && BLACKLIST.has(nick)); return (full && HIDDEN.has(full)) || (nick && HIDDEN.has(nick));
} }
const API = "https://newprod-api.bestcoastpairings.com/v1"; const API = "https://newprod-api.bestcoastpairings.com/v1";
@@ -290,7 +293,7 @@ function build(rows, ids) {
const kept = []; const kept = [];
for (const rec of rows) { for (const rec of rows) {
const uid = rec.userId || (rec.user && rec.user.id); const uid = rec.userId || (rec.user && rec.user.id);
if (uid && ids.has(uid) && !isBlacklisted(rec.user || {})) kept.push(rec); if (uid && ids.has(uid) && !isHidden(rec.user || {})) kept.push(rec);
} }
kept.sort((a, b) => Number(b.ITCPoints || 0) - Number(a.ITCPoints || 0)); kept.sort((a, b) => Number(b.ITCPoints || 0) - Number(a.ITCPoints || 0));
return kept; return kept;
+1 -2
View File
@@ -29,8 +29,7 @@ different form or sheet, replace the iframe `src` in the relevant HTML file.
## Updating each season ## Updating each season
Season name, dates and sign-up status are hardcoded in `index.html`. The entry Season name, dates and sign-up status are hardcoded in `index.html`. The entry
fee note is in `signups.html`. There's also a commented-out PayPal block there fee note is in `signups.html`.
that can be re-enabled if fees move back to PayPal.
## Files ## Files
-13
View File
@@ -18,19 +18,6 @@
<div class="divider"></div> <div class="divider"></div>
<!-- commenting out paypal information
<section class="mb-8">
<h2 class="text-2xl font-semibold mb-3">League Entry Fee</h2>
<p class="mb-4">In addition to signing-up below, please pay your league entry fee of $## via PayPal to secure your spot.</p>
<a href="https://paypal.me/REDACTED" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
Pay Entry Fee via PayPal
</a>
</section>
<div class="divider"></div>
-->
<!-- Google Form embed — replace the src URL with your form's embed link --> <!-- Google Form embed — replace the src URL with your form's embed link -->
<div class="w-full"> <div class="w-full">
<iframe <iframe