Pete news: salted per-event GUID token + zone_clear taxonomy

Closes the two deferred code-review follow-ups on the adventure-news
seam, plus folds in two pre-committed WIP fixes.

A. Privacy — the public GUID no longer leaks a stable per-player id.
   Replaced userHash(userID)=sha256(userID)[:6] with
   eventToken(userID, discriminator)=HMAC-SHA256(salt, userID‖disc).
   The salt is 32 random bytes, auto-generated once and persisted in the
   durable news_config table (cached via sync.Once). Because each event
   uses a distinct HMAC message, tokens are a PRF output and are BOTH
   uncomputable from a Matrix handle (no enumeration of a player's
   events, incl. ones anonymized after !news optout) AND mutually
   unlinkable (a named event can't be walked back to a player's other,
   anonymized events). Updated all emit sites: pete.go zone, dnd_combat
   death, adventure_duel rival, dnd_setup arrival, achievements
   milestone, bootstrap x3.

B. Taxonomy — repeat zone clears were mislabeled zone_first. Now emit
   zone_clear (bulletin) vs zone_first (realm-first, priority). Adopted
   the invariant GUID-prefix == event_type, which also fixes latent
   permalink mislabels (achv->milestone, rival->rival_result rendered a
   neutral "Dispatch" on their permalink pages).

Folded-in WIP fixes: create the news_config table b42beec's
newsEmissionOn reads but never created; reap sent pete_emit_queue rows
in RunMaintenance; don't burn a retry attempt when shutdown cancels an
in-flight send.

Tests: TestEventToken (salted/stable/per-event/persisted),
TestEmitZoneClearTaxonomy (first->zone_first, repeat->zone_clear),
updated pete_test.go prefixes. Full internal suite + vet green.

Unshipped. Deploy Pete first (it must know zone_clear), then gogobee.
This commit is contained in:
prosolis
2026-07-11 07:43:57 -07:00
parent b42beec348
commit ae7ff38996
9 changed files with 206 additions and 45 deletions

View File

@@ -616,6 +616,10 @@ func RunMaintenance() {
{"urban_cache", `DELETE FROM urban_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
{"url_cache", `DELETE FROM url_cache WHERE cached_at < ?`, []interface{}{cutoff30d}},
// Pete adventure-news queue — reap delivered rows (kept only for
// idempotency); undelivered rows stay so the sender can retry/park them.
{"pete_emit_queue", `DELETE FROM pete_emit_queue WHERE sent_at IS NOT NULL AND sent_at < ?`, []interface{}{cutoff7d}},
// Rate limits — purge entries older than today
{"rate_limits", `DELETE FROM rate_limits WHERE date < ?`, []interface{}{today}},
@@ -979,6 +983,14 @@ CREATE TABLE IF NOT EXISTS news_realm_firsts (
PRIMARY KEY (kind, target)
);
-- Durable runtime config for adventure news (e.g. the emission kill-switch).
-- Deliberately NOT stored in api_cache: RunMaintenance prunes that table after
-- 7 days, which would silently revert an operator's !news off back to on.
CREATE TABLE IF NOT EXISTS news_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-- Birthdays
CREATE TABLE IF NOT EXISTS birthdays (
user_id TEXT PRIMARY KEY,