diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-06-03 01:38:53 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-06-03 01:38:53 -0700 |
| commit | 4a2f82f06490b7fb277dc6c7558d10c34503a495 (patch) | |
| tree | a206b7dd9777371d07ba1599cf3eff7f52df4d97 /src/helpers | |
| parent | 0cc204ae751a69f20925d2791c930bbf45e4eeed (diff) | |
add basic daily answer encryption
Diffstat (limited to 'src/helpers')
| -rw-r--r-- | src/helpers/fetchSolution.ts | 52 | ||||
| -rw-r--r-- | src/helpers/fetchSongs.ts | 51 |
2 files changed, 103 insertions, 0 deletions
diff --git a/src/helpers/fetchSolution.ts b/src/helpers/fetchSolution.ts new file mode 100644 index 0000000..b43722a --- /dev/null +++ b/src/helpers/fetchSolution.ts @@ -0,0 +1,52 @@ +import { Song } from "../types/song"; + +const SALT = process.env.HEARDLE_SALT ?? 'changeme'; +const API_URL = process.env.HEARDLE_API_URL ?? 'http://localhost:3001'; + +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); + } + return bytes; +} + + +async function getDailyKey(): Promise<CryptoKey> { + const enc = new TextEncoder(); + const date = new Date().toISOString().split('T')[0]; + const keyMaterial = await crypto.subtle.importKey( + 'raw', + enc.encode(date), + 'PBKDF2', + false, + ['deriveKey'], + ); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: enc.encode(SALT), iterations: 100_000, hash: 'SHA-256' }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['decrypt'], + ); +} + +export async function getDailySolution(): Promise<Song> { + const solutionData = await fetch(`${API_URL}/today`); + if (!solutionData.ok) { + throw new Error(`Failed to fetch solution: ${solutionData.statusText}`); + } + const { iv, tag, data } = await solutionData.json(); + const key = await getDailyKey(); + const ciphertext = hexToBytes(data); + const authTag = hexToBytes(tag); + const combined = new Uint8Array(ciphertext.length + authTag.length); + combined.set(ciphertext); + combined.set(authTag, ciphertext.length); + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: hexToBytes(iv) }, + key, + combined, + ); + return JSON.parse(new TextDecoder().decode(decrypted)) as Song; +} diff --git a/src/helpers/fetchSongs.ts b/src/helpers/fetchSongs.ts new file mode 100644 index 0000000..5ae0d83 --- /dev/null +++ b/src/helpers/fetchSongs.ts @@ -0,0 +1,51 @@ +import { Song } from "../types/song"; +let cachedSongs: Song[] | null = null; +function fuzzyMatch(input: string): string { + return input.toLowerCase().replace(/[^0-9a-z ]/gi, ''); +} + + +export async function fetchSongs(useCache=true): Promise<Song[]> { + if (useCache && cachedSongs) { + return cachedSongs; + } + + try { + const response = await fetch('/songs'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const songsData: Song[] = await response.json(); + cachedSongs = songsData; + return songsData; + } catch (error) { + console.error("Failed to fetch songs:", error); + throw error; + } +} + +export async function searchSongs(searchTerm: string): Promise<Song[]> { + const songsToSearch = await fetchSongs(); + + const processedSearchTerm = fuzzyMatch(searchTerm); + + const matchingSongs = songsToSearch + .filter((song: Song) => { + const songName = fuzzyMatch(song.name); + const songArtist = fuzzyMatch(song.artist); + + if (songArtist.includes(processedSearchTerm) || songName.includes(processedSearchTerm)) { + return true; + } + return false; + }) + .sort((a, b) => + a.artist.toLowerCase().localeCompare(b.artist.toLocaleLowerCase()) + || a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase()) + ); + + return matchingSongs; +} +export function getCachedSongs(): Song[] | null { + return cachedSongs; +} |
