import type { ClientPuzzle, GenerateResponse } from './types' const BASE = '/api' export interface ThemeInfo { id: string name: string word_count: number } export interface MeInfo { display_name: string | null is_new: boolean has_passkey: boolean preferred_preset: string | null streak: { current: number; best: number; last_date: string | null } streak_freezes: number } export interface ValidateResult { correct: boolean cells: string[][] rank: number | null percentile: number | null total_solvers: number | null already_solved: boolean solve_time: number solve_hints: number cell_results: string | null new_achievements: string[] flagged: boolean receipt: ReceiptPayload | null } export interface HintLetter { row: number col: number letter: string } export interface HintResult { is_correct: boolean row: number col: number letter: string letters: HintLetter[] } export interface LeaderboardEntry { name: string | null time: number hints: number date: string | null count: number current: number best: number instance: string } export interface LeaderboardResult { entries: LeaderboardEntry[] } export type ServerSettings = Record export interface HistoryEntry { date: string preset: string | null time: number hints: number short_id: string | null } export interface RatingResult { average: number count: number my_rating: number | null } export type DailyRatings = Record export interface CommunityClueItem { id: number clue: string upvotes: number downvotes: number status: string my_vote: number origin_domain: string | null created_at: string } export interface ClueSubmissionItem { id: number word: string clue: string upvotes: number downvotes: number status: string } export interface EditorLockedCell { row: number col: number letter: string } export interface EditorGeneratedClue { number: number direction: string text: string answer: string } export interface EditorAutofillResult { ok: boolean error: string letters: (string | null)[][] clues: EditorGeneratedClue[] } export interface EditorLoadedClue { number: number direction: string text: string answer: string startRow?: number startCol?: number } export interface EditorLoadedPuzzle { id: string title: string width: number height: number grid: string[][] clues: EditorLoadedClue[] puzzle: { grid?: { cells?: { Letter?: string }[][] | null } } | null status: string } export interface SavedPuzzleItem { id: string title: string width: number height: number status: string updated_at: string } export interface CommunityPuzzleItem { id: string title: string author: string | null origin: string | null low_reputation: boolean origin_reputation: number | null federated?: boolean width: number height: number updated_at: string } export interface CommunityPlayResult { id: string short_id: string title: string community: boolean puzzle: ClientPuzzle } export interface ConstructorAnalytics { total_solvers: number total_attempts: number avg_time: number completion_rate: number total_hints: number width: number height: number hint_heatmap?: number[][] max_hints?: number } export interface ArchiveEntry { date: string preset: string short_id: string width: number height: number solved: boolean } export interface PasskeyItem { id: string name: string created_at: string } export interface PackClue { text: string difficulty?: number } export interface PackWord { word: string clues: PackClue[] } export interface PackItem { id: string name: string description: string | null author: string | null tags: string[] word_count: number downloads: number rating: number | null rating_count: number created_at: string words?: PackWord[] } export interface PackActionResult { id: string word_count: number } export interface ImportResult { imported: number skipped: string[] } export interface MigrateResult { ok: boolean target: string export: unknown } export interface ReportResult { id: number } export interface RoomPlayer { id: string name: string color: string cursor: [number, number] | null direction: string order: number } export interface RoomInfo { code: string mode: string puzzle_id: string | null players: RoomPlayer[] player_count: number max_players: number started: boolean current_turn: string | null party: { phase: string round: number total_rounds: number sub_mode: string } | null } export interface ReceiptBody { receipt_id: string puzzle_hash: string solve_time_secs: number hints_used: number solve_date: string instance_url: string player_name: string room_code: string | null mode: string | null players: string[] created_at: string expires_at: string } export interface ReceiptPayload { receipt: ReceiptBody signature: string key_id: string receipt_url?: string receipt_id?: string } export interface ClaimReceiptResult { claimed: boolean solve_time: number hints: number } export interface TrainerClueResult { clue: string length: number pattern: string difficulty: string } export interface AnalyticsSummary { total_solves: number clean_solves: number total_time_secs: number avg_time_secs: number avg_hints: number current_streak: number best_streak: number first_solve: string | null puzzles_rated: number clues_submitted: number } export interface SolveEntry { date: string preset: string time: number hints: number } export interface TrendEntry { date: string avg_time?: number avg_hints?: number } export interface PresetStats { count: number best: number avg: number clean: number } export interface CleanRateEntry { date: string rate: number } export interface MonthlyEntry { month: string count: number } export interface DiffEntry { date: string avg_difficulty: number } export interface FastestRecord { time: number date: string } export interface Analytics { summary: AnalyticsSummary by_preset: Record solve_history: SolveEntry[] activity_calendar: Record by_hour: number[] by_day_of_week: number[] time_trend: TrendEntry[] hints_trend: TrendEntry[] improvement_rate: number | null clean_rate_trend: CleanRateEntry[] fastest_by_preset: Record solve_time_distribution: number[] monthly_totals: MonthlyEntry[] completion_rate: number words_encountered: number avg_time_by_hour: Record difficulty_progression: DiffEntry[] } export interface WordOfTheDayResult { word: string definition: string clue: string length: number } export type ChallengeCompletionMap = Record export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } export type ImportPayload = Record async function getJson(res: Response): Promise { // SAFETY: every caller passes its endpoint's documented JSON shape and decodes at the call site return res.json() as T } interface PasskeyParam { type: 'public-key' alg: number } interface RegistrationStartOptions { challenge: string rp: { id: string; name: string } user: { id: string; name: string; displayName: string } pubKeyCredParams: PasskeyParam[] timeout: number authenticatorSelection: AuthenticatorSelectionCriteria attestation: AttestationConveyancePreference } interface AuthenticationStartOptions { challenge: string rp_id: string timeout: number allowCredentials: { type: 'public-key'; id: string }[] } interface ValidateBody { cells: (string | null)[][] elapsed: number hints: number local_date: string room_code?: string cell_results?: string } interface CreateRoomBody { puzzle_id: string mode: string party_sub?: string } export async function fetchThemes(): Promise { const res = await fetch(`${BASE}/themes`) if (!res.ok) return [] const data = await getJson<{ themes?: ThemeInfo[] }>(res) return data.themes || [] } export async function generatePuzzle( difficulty?: string, size?: number, density?: number, clue_difficulty?: number, theme?: string, cryptic?: boolean, pack?: string, ): Promise { const res = await fetch(`${BASE}/puzzles/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ difficulty, size, density, clue_difficulty, theme, cryptic, pack }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchDaily(date: string, preset: string): Promise { const res = await fetch(`${BASE}/puzzles/daily/${date}/${preset}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchPuzzleByShortId(shortId: string): Promise { const res = await fetch(`${BASE}/puzzles/s/${shortId}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchPuzzleById(id: string): Promise { const res = await fetch(`${BASE}/puzzles/${id}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function validateSolution( id: string, cells: (string | null)[][], elapsed?: number, hints?: number, roomCode?: string | null, cellResults?: string, ): Promise { const localDate = new Date().toISOString().split('T')[0] const body: ValidateBody = { cells, elapsed: elapsed ?? 0, hints: hints ?? 0, local_date: localDate } if (roomCode) body.room_code = roomCode if (cellResults) body.cell_results = cellResults const res = await fetch(`${BASE}/puzzles/${id}/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function requestHint( id: string, kind: string, row: number, col: number, direction?: string, letter?: string, ): Promise { const res = await fetch(`${BASE}/puzzles/${id}/hint`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind, row, col, direction, letter }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchMe(): Promise { const res = await fetch(`${BASE}/me`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function updateMe(data: { display_name?: string }): Promise { await fetch(`${BASE}/me`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) } export async function saveProgress(id: string, cells: (string | null)[][]): Promise { await fetch(`${BASE}/puzzles/${id}/progress`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cells }), }) } export async function fetchLeaderboard(puzzleId: string): Promise { const res = await fetch(`${BASE}/leaderboards/${puzzleId}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchDailyLeaderboard(date: string, preset: string): Promise { const res = await fetch(`${BASE}/leaderboards/daily/${date}/${preset}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchFederatedLeaderboard(date: string, preset: string): Promise { const res = await fetch(`${BASE}/leaderboards/federated/${date}/${preset}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchAlltimeLeaderboard(preset: string): Promise { const res = await fetch(`${BASE}/leaderboards/alltime/${preset}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchWeeklyLeaderboard(preset: string): Promise { const res = await fetch(`${BASE}/leaderboards/weekly/${preset}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchStreakLeaderboard(): Promise { const res = await fetch(`${BASE}/leaderboards/streaks`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchCompletionistLeaderboard(): Promise { const res = await fetch(`${BASE}/leaderboards/completionist`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchCleanLeaderboard(date: string, preset: string): Promise { const res = await fetch(`${BASE}/leaderboards/clean/${date}/${preset}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchAnalytics(): Promise { const res = await fetch(`${BASE}/me/analytics`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchSettings(): Promise { const res = await fetch(`${BASE}/me/settings`) if (!res.ok) return {} return res.json() } export async function saveSettings(settings: ServerSettings): Promise { await fetch(`${BASE}/me/settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings), }) } // passkey / WebAuthn helpers function bufToBase64url(buf: ArrayBuffer): string { return btoa(String.fromCharCode(...new Uint8Array(buf))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } function base64urlToBuf(s: string): ArrayBuffer { const padded = s.replace(/-/g, '+').replace(/_/g, '/') + '=='.slice(0, (4 - s.length % 4) % 4) const binary = atob(padded) const buf = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i) return buf.buffer } export async function registerPasskey(): Promise { const res = await fetch(`${BASE}/passkey/register/start`, { method: 'POST' }) if (!res.ok) throw new Error(await res.text()) const { options } = await getJson<{ options: RegistrationStartOptions }>(res) const publicKey: PublicKeyCredentialCreationOptions = { challenge: base64urlToBuf(options.challenge), rp: options.rp, user: { id: base64urlToBuf(options.user.id), name: options.user.name, displayName: options.user.displayName, }, pubKeyCredParams: options.pubKeyCredParams, timeout: options.timeout, authenticatorSelection: options.authenticatorSelection, attestation: options.attestation, } const credential = await navigator.credentials.create({ publicKey }) if (!(credential instanceof PublicKeyCredential)) return false const attestation = credential.response if (!(attestation instanceof AuthenticatorAttestationResponse)) return false const finish = await fetch(`${BASE}/passkey/register/finish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: credential.id, clientDataJSON: bufToBase64url(attestation.clientDataJSON), attestationObject: bufToBase64url(attestation.attestationObject), }), }) if (!finish.ok) throw new Error(await finish.text()) return true } export async function authenticatePasskey(): Promise<{ ok: boolean; display_name: string | null } | null> { const res = await fetch(`${BASE}/passkey/authenticate/start`, { method: 'POST' }) if (!res.ok) throw new Error(await res.text()) const { options } = await getJson<{ options: AuthenticationStartOptions }>(res) const publicKey: PublicKeyCredentialRequestOptions = { challenge: base64urlToBuf(options.challenge), rpId: options.rp_id, timeout: options.timeout, allowCredentials: options.allowCredentials.map(c => ({ type: c.type, id: base64urlToBuf(c.id), })), } const assertion = await navigator.credentials.get({ publicKey }) if (!(assertion instanceof PublicKeyCredential)) return null const resp = assertion.response if (!(resp instanceof AuthenticatorAssertionResponse)) return null const finish = await fetch(`${BASE}/passkey/authenticate/finish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: assertion.id, clientDataJSON: bufToBase64url(resp.clientDataJSON), authenticatorData: bufToBase64url(resp.authenticatorData), signature: bufToBase64url(resp.signature), }), }) if (!finish.ok) throw new Error(await finish.text()) return finish.json() } // history export async function fetchHistory(): Promise { const res = await fetch(`${BASE}/me/history`) if (!res.ok) return [] const data = await getJson<{ history?: HistoryEntry[] }>(res) return data.history || [] } // puzzle ratings export async function ratePuzzle(id: string, rating: number): Promise { const res = await fetch(`${BASE}/puzzles/${id}/rate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rating }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchRating(id: string): Promise { const res = await fetch(`${BASE}/puzzles/${id}/rating`) if (!res.ok) return { average: 0, count: 0, my_rating: null } return res.json() } export async function fetchDailyRatings(date: string): Promise { const res = await fetch(`${BASE}/puzzles/daily/${date}/ratings`) if (!res.ok) return {} return res.json() } // community clues export async function submitClue(word: string, clue: string): Promise<{ id: number }> { const res = await fetch(`${BASE}/clues/community`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ word, clue }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchCommunityClues(word: string): Promise<{ clues: CommunityClueItem[] }> { const res = await fetch(`${BASE}/clues/community/${word.toUpperCase()}`) if (!res.ok) return { clues: [] } return res.json() } export async function voteClue(id: number, vote: number): Promise { await fetch(`${BASE}/clues/community/vote/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vote }), }) } export async function deleteSubmission(id: number): Promise { await fetch(`${BASE}/me/submissions/${id}`, { method: 'DELETE' }) } export async function fetchMySubmissions(): Promise { const res = await fetch(`${BASE}/me/submissions`) if (!res.ok) return [] const data = await getJson<{ submissions?: ClueSubmissionItem[] }>(res) return data.submissions || [] } // puzzle import (.puz / .ipuz) export async function importPuzzle(file: File): Promise { const buf = await file.arrayBuffer() const res = await fetch(`${BASE}/puzzles/import`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: buf, }) if (!res.ok) throw new Error(await res.text()) return res.json() } // editor export async function editorAutofill(width: number, height: number, cells: string[][], locked?: EditorLockedCell[]): Promise { const res = await fetch(`${BASE}/editor/autofill`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ width, height, cells, locked }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function editorClueSuggestions(word: string): Promise { const res = await fetch(`${BASE}/editor/clue-suggestions/${word.toUpperCase()}`) if (!res.ok) return [] const data = await getJson<{ suggestions?: string[] }>(res) return data.suggestions || [] } export async function editorCheckWords(words: string[]): Promise<{ blocked: string[] }> { const res = await fetch(`${BASE}/editor/check-words`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ words }), }) if (!res.ok) return { blocked: [] } return res.json() } export async function editorSave(data: { id?: string title: string width: number height: number grid: string[][] clues?: EditorLoadedClue[] puzzle?: { grid: { cells: ({ Letter: string } | null)[][] } } status?: string }): Promise<{ id: string }> { const res = await fetch(`${BASE}/editor/puzzles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function editorLoad(id: string): Promise { const res = await fetch(`${BASE}/editor/puzzles/${id}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function editorList(): Promise { const res = await fetch(`${BASE}/editor/puzzles`) if (!res.ok) return [] const data = await getJson<{ puzzles?: SavedPuzzleItem[] }>(res) return data.puzzles || [] } export async function editorDelete(id: string): Promise { await fetch(`${BASE}/editor/puzzles/${id}`, { method: 'DELETE' }) } export async function fetchCommunityPuzzles(): Promise { const res = await fetch(`${BASE}/community-puzzles`) if (!res.ok) return [] const data = await getJson<{ puzzles?: CommunityPuzzleItem[] }>(res) return data.puzzles || [] } export async function fetchFederatedPuzzles(): Promise<{ puzzles: CommunityPuzzleItem[] }> { const res = await fetch(`${BASE}/federated-puzzles`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function loadCommunityPuzzle(id: string): Promise { const res = await fetch(`${BASE}/community-puzzles/${id}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function editorExport(id: string): Promise { const res = await fetch(`${BASE}/editor/puzzles/${id}/export`) if (!res.ok) throw new Error(await res.text()) const blob = await res.blob() const cd = res.headers.get('content-disposition') || '' const match = cd.match(/filename="(.+)"/) const filename = match ? match[1] : 'puzzle.ipuz' const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = filename; a.click() URL.revokeObjectURL(url) } export async function editorExportAll(): Promise { const res = await fetch(`${BASE}/editor/export-all`) if (!res.ok) throw new Error(await res.text()) const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = 'cruciverb-puzzles-export.json'; a.click() URL.revokeObjectURL(url) } export async function editorImport(data: ImportPayload): Promise { const res = await fetch(`${BASE}/editor/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function editorPublish(id: string): Promise<{ ok: boolean }> { const res = await fetch(`${BASE}/editor/puzzles/${id}/publish`, { method: 'POST' }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchPuzzleAnalytics(id: string): Promise { const res = await fetch(`${BASE}/editor/puzzles/${id}/analytics`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchArchive(preset?: string, page?: number): Promise<{ entries: ArchiveEntry[] }> { const params = new URLSearchParams() if (preset && preset !== 'all') params.set('preset', preset) if (page) params.set('page', String(page)) const qs = params.toString() const res = await fetch(`${BASE}/puzzles/archive${qs ? '?' + qs : ''}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function fetchPasskeys(): Promise { const res = await fetch(`${BASE}/me/passkeys`) if (!res.ok) return [] const data = await getJson<{ passkeys?: PasskeyItem[] }>(res) return data.passkeys || [] } // privacy / account export async function deleteAccount(): Promise { await fetch(`${BASE}/me/account`, { method: 'DELETE' }) } export async function exportData(): Promise { const res = await fetch(`${BASE}/me/export`) if (!res.ok) throw new Error(await res.text()) const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = 'cruciverb-export.json'; a.click() URL.revokeObjectURL(url) } export async function importData(data: ImportPayload): Promise { const res = await fetch(`${BASE}/me/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // user migration export async function migrateAccount(targetDomain: string): Promise { const res = await fetch(`${BASE}/me/migrate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target_domain: targetDomain }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // user blocklist import/export export async function exportUserBlocklist(): Promise { const res = await fetch(`${BASE}/me/blocklist`) if (!res.ok) throw new Error(await res.text()) const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = 'my-blocklist.json'; a.click() URL.revokeObjectURL(url) } export async function importUserBlocklist(data: ImportPayload): Promise { const res = await fetch(`${BASE}/me/blocklist/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // word of the day export async function fetchWordOfTheDay(): Promise { const res = await fetch(`${BASE}/word-of-the-day`) if (!res.ok) return { word: '', definition: '', clue: '', length: 0 } return res.json() } // clue trainer export async function fetchTrainerClue(difficulty: string): Promise { const res = await fetch(`${BASE}/trainer/clue?difficulty=${difficulty}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function checkTrainerAnswer(clue: string, answer: string): Promise<{ correct: boolean; answer: string }> { const res = await fetch(`${BASE}/trainer/check`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clue, answer }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // content reporting export async function submitReport(contentType: string, contentId: string, reason?: string): Promise { const res = await fetch(`${BASE}/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content_type: contentType, content_id: contentId, reason }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // multiplayer rooms export async function createRoom(puzzleId: string, mode: string, partySub?: string): Promise<{ code: string }> { const body: CreateRoomBody = { puzzle_id: puzzleId, mode } if (partySub) body.party_sub = partySub const res = await fetch(`${BASE}/rooms`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function getRoomInfo(code: string): Promise { const res = await fetch(`${BASE}/rooms/${code}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export function connectRoom(code: string): WebSocket { const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:' return new WebSocket(`${proto}//${window.location.host}/api/rooms/${code}/ws`) } // wordlist packs export async function fetchPacks(search?: string): Promise<{ packs: PackItem[] }> { const params = search ? `?search=${encodeURIComponent(search)}` : '' const res = await fetch(`${BASE}/packs${params}`) return res.json() } export async function createPack(pack: { name: string description?: string tags?: string[] words: PackWord[] }): Promise { const res = await fetch(`${BASE}/packs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pack), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // admin setup export async function fetchAdminStatus(): Promise<{ configured: boolean }> { const res = await fetch(`${BASE}/admin/status`) if (!res.ok) return { configured: true } return res.json() } export async function setupAdmin(username: string, password: string): Promise<{ ok: boolean }> { const res = await fetch(`${BASE}/admin/setup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } // daily challenges export async function fetchDailyChallenge(date: string, preset: string): Promise<{ challenge: string; completed: boolean }> { const res = await fetch(`${BASE}/puzzles/daily/${date}/${preset}/challenge`) if (!res.ok) return { challenge: '', completed: false } return res.json() } export async function completeDailyChallenge(date: string, preset: string, challengeType: string): Promise { await fetch(`${BASE}/puzzles/daily/${date}/${preset}/challenge/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ challenge_type: challengeType }), }) } export async function fetchTodayChallenges(): Promise { const res = await fetch(`${BASE}/me/challenges/today`) if (!res.ok) return {} return res.json() } // receipts export async function getReceipt(id: string): Promise { const res = await fetch(`${BASE}/receipts/${id}`) if (!res.ok) throw new Error(await res.text()) return res.json() } export async function claimReceipt(receipt: ReceiptPayload): Promise { const res = await fetch(`${BASE}/receipts/claim`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(receipt), }) if (!res.ok) throw new Error(await res.text()) return res.json() }