mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
Macht mana-analytics zur Backend-Heimat des Public-Community-Hubs. - Pseudonym: createDisplayHash(userId, secret) + generateDisplayName() produzieren deterministisch "Wachsame Eule #4528" pro User. 100 Adjektive × 80 Tieren × 10000 Suffixe → ~80M Kombinationen. 7 unit tests, 0 PII im Output. - Schema-Erweiterung user_feedback: display_hash, display_name, module_context, parent_id (1-level Threading), reactions jsonb (cached emoji→count), score (cached weighted sort). - feedback_votes ersetzt durch feedback_reactions (Slack-Pattern: unique(feedback_id, user_id, emoji), pro User mehrere Emojis möglich). - Service: createFeedback stempelt display_hash + display_name. Neue Methoden getPublicFeed (redacted), getReplies, toggleReaction (rebuilt reactions+score). Admin-Methoden adminListAll/adminUpdate founder-tier-gated im Route-Layer. - Routes: /api/v1/public/feedback/* — anonymous reads (kein Auth, kein userId/displayHash/deviceInfo im Output) /api/v1/feedback/* — auth-required Submit/React/Manage, plus :id/replies, :id/react, /admin - Config: neuer FEEDBACK_PSEUDONYM_SECRET-Env-Var seedet die Hashes; Rotation re-keyt nur künftige Pseudonyme, alte Records bleiben stabil. Migration 0002_public-community-foundation.sql idempotent, lokal + prod (mana-server) eingespielt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
241 lines
4.1 KiB
TypeScript
241 lines
4.1 KiB
TypeScript
/**
|
||
* Pseudonym generator for the public-community surface.
|
||
*
|
||
* Same hash-input always yields the same display_name → users see "their"
|
||
* pseudonym consistently across submissions, but real userId is never
|
||
* exposed.
|
||
*
|
||
* Generation:
|
||
* "{Adjektiv} {Tier} #{HashSuffix}"
|
||
* e.g. "Wachsame Eule #4528", "Heimliche Otter #0091"
|
||
*
|
||
* Naming space: 100 adjectives × 80 animals × 10000 numeric suffixes
|
||
* = 80 million combinations. Collision risk irrelevant in practice.
|
||
*
|
||
* The 4-digit suffix is the last 4 hex characters of the hash interpreted
|
||
* as decimal (modulo 10000). Adjective and animal indices are derived
|
||
* from disjoint hash slices so they vary independently.
|
||
*/
|
||
|
||
import { createHash } from 'crypto';
|
||
|
||
const ADJECTIVES = [
|
||
'Wachsame',
|
||
'Heimliche',
|
||
'Stille',
|
||
'Kühne',
|
||
'Sanfte',
|
||
'Wilde',
|
||
'Listige',
|
||
'Weise',
|
||
'Mutige',
|
||
'Träumerische',
|
||
'Neugierige',
|
||
'Treue',
|
||
'Verspielte',
|
||
'Bedächtige',
|
||
'Flinke',
|
||
'Helle',
|
||
'Dunkle',
|
||
'Goldene',
|
||
'Silberne',
|
||
'Funkelnde',
|
||
'Glühende',
|
||
'Kühle',
|
||
'Warme',
|
||
'Singende',
|
||
'Tanzende',
|
||
'Schweigende',
|
||
'Lauschende',
|
||
'Suchende',
|
||
'Findende',
|
||
'Wandernde',
|
||
'Schwebende',
|
||
'Tauchende',
|
||
'Träumende',
|
||
'Wache',
|
||
'Frohe',
|
||
'Ernste',
|
||
'Leise',
|
||
'Laute',
|
||
'Sanfte',
|
||
'Schnelle',
|
||
'Langsame',
|
||
'Geheime',
|
||
'Offene',
|
||
'Versteckte',
|
||
'Sichtbare',
|
||
'Strahlende',
|
||
'Glänzende',
|
||
'Matte',
|
||
'Helle',
|
||
'Schimmernde',
|
||
'Flackernde',
|
||
'Stetige',
|
||
'Bewegte',
|
||
'Ruhige',
|
||
'Stürmische',
|
||
'Friedliche',
|
||
'Kämpferische',
|
||
'Freundliche',
|
||
'Misstrauische',
|
||
'Vertrauende',
|
||
'Hoffende',
|
||
'Zweifelnde',
|
||
'Glaubende',
|
||
'Fragende',
|
||
'Antwortende',
|
||
'Lernende',
|
||
'Lehrende',
|
||
'Wachsende',
|
||
'Reife',
|
||
'Junge',
|
||
'Alte',
|
||
'Zeitlose',
|
||
'Frische',
|
||
'Müde',
|
||
'Wache',
|
||
'Schlafende',
|
||
'Erwachende',
|
||
'Träumende',
|
||
'Erinnernde',
|
||
'Vergessende',
|
||
'Zählende',
|
||
'Sammelnde',
|
||
'Verschenkende',
|
||
'Bewahrende',
|
||
'Suchende',
|
||
'Wartende',
|
||
'Eilende',
|
||
'Bleibende',
|
||
'Reisende',
|
||
'Heimkehrende',
|
||
'Aufbrechende',
|
||
'Ankommende',
|
||
'Lauschende',
|
||
'Singende',
|
||
'Pfeifende',
|
||
'Summende',
|
||
'Knurrende',
|
||
'Schnurrende',
|
||
'Lachende',
|
||
'Weinende',
|
||
'Schmunzelnde',
|
||
'Staunende',
|
||
];
|
||
|
||
const ANIMALS = [
|
||
'Eule',
|
||
'Otter',
|
||
'Fuchs',
|
||
'Wolf',
|
||
'Bär',
|
||
'Luchs',
|
||
'Adler',
|
||
'Reiher',
|
||
'Kranich',
|
||
'Schwalbe',
|
||
'Lerche',
|
||
'Amsel',
|
||
'Specht',
|
||
'Falke',
|
||
'Habicht',
|
||
'Sperber',
|
||
'Krähe',
|
||
'Rabe',
|
||
'Elster',
|
||
'Häher',
|
||
'Star',
|
||
'Drossel',
|
||
'Meise',
|
||
'Fink',
|
||
'Sperling',
|
||
'Schmetterling',
|
||
'Libelle',
|
||
'Hirsch',
|
||
'Reh',
|
||
'Gämse',
|
||
'Steinbock',
|
||
'Murmeltier',
|
||
'Eichhörnchen',
|
||
'Iltis',
|
||
'Marder',
|
||
'Dachs',
|
||
'Igel',
|
||
'Wiesel',
|
||
'Hermelin',
|
||
'Salamander',
|
||
'Molch',
|
||
'Frosch',
|
||
'Kröte',
|
||
'Eidechse',
|
||
'Schlange',
|
||
'Schildkröte',
|
||
'Karpfen',
|
||
'Hecht',
|
||
'Forelle',
|
||
'Lachs',
|
||
'Stör',
|
||
'Aal',
|
||
'Krebs',
|
||
'Schnecke',
|
||
'Spinne',
|
||
'Käfer',
|
||
'Hummel',
|
||
'Biene',
|
||
'Wespe',
|
||
'Ameise',
|
||
'Grashüpfer',
|
||
'Grille',
|
||
'Heuschrecke',
|
||
'Marienkäfer',
|
||
'Hirschkäfer',
|
||
'Robbe',
|
||
'Seehund',
|
||
'Delfin',
|
||
'Wal',
|
||
'Hai',
|
||
'Möwe',
|
||
'Albatros',
|
||
'Pelikan',
|
||
'Kormoran',
|
||
'Pinguin',
|
||
'Schwan',
|
||
'Gans',
|
||
'Ente',
|
||
'Storch',
|
||
'Ibis',
|
||
];
|
||
|
||
function hashToBigInt(hash: string): bigint {
|
||
// Strip non-hex chars, take first 16 hex chars (64-bit slice).
|
||
const slice = hash.replace(/[^0-9a-f]/gi, '').slice(0, 16);
|
||
return BigInt('0x' + slice);
|
||
}
|
||
|
||
/**
|
||
* Deterministically derive a display name from a display-hash.
|
||
*
|
||
* @param displayHash hex string from createDisplayHash().
|
||
* @returns "{Adjektiv} {Tier} #{NNNN}" — stable across calls for same hash.
|
||
*/
|
||
export function generateDisplayName(displayHash: string): string {
|
||
const big = hashToBigInt(displayHash);
|
||
const adj = ADJECTIVES[Number(big % BigInt(ADJECTIVES.length))];
|
||
const animal = ANIMALS[Number((big / BigInt(ADJECTIVES.length)) % BigInt(ANIMALS.length))];
|
||
const suffix = Number((big / BigInt(ADJECTIVES.length * ANIMALS.length)) % 10000n)
|
||
.toString()
|
||
.padStart(4, '0');
|
||
return `${adj} ${animal} #${suffix}`;
|
||
}
|
||
|
||
/**
|
||
* Derive a non-reversible display hash for a given userId.
|
||
* Same userId + same secret always produces the same hash.
|
||
*/
|
||
export function createDisplayHash(userId: string, secret: string): string {
|
||
return createHash('sha256').update(`${userId}:${secret}`).digest('hex');
|
||
}
|
||
|
||
// Exported for tests.
|
||
export const __TEST__ = { ADJECTIVES, ANIMALS };
|