// Client-side GitHub access. The token lives only in the engineer's browser // (localStorage) and is sent only to api.github.com — the dashboard itself // stays a static site with no server-held secrets. import { FEEDBACK_LABEL, FEEDBACK_REPO } from "./config"; import type { FeedbackIssue } from "./types"; const TOKEN_KEY = "rfp-tracker:github-token"; export const TOKEN_EVENT = "rfp-tracker:token-changed"; export function getStoredToken(): string | null { if (typeof window === "undefined") return null; return window.localStorage.getItem(TOKEN_KEY); } export function storeToken(token: string | null) { if (token) window.localStorage.setItem(TOKEN_KEY, token); else window.localStorage.removeItem(TOKEN_KEY); window.dispatchEvent(new Event(TOKEN_EVENT)); } async function gh(path: string, init: RequestInit = {}) { const token = getStoredToken(); if (!token) throw new Error("No GitHub token connected"); const res = await fetch(`https://api.github.com${path}`, { ...init, headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${token}`, ...(init.body ? { "Content-Type": "application/json" } : {}), }, }); if (!res.ok) { let detail = ""; try { detail = ((await res.json()) as { message?: string }).message ?? ""; } catch { /* non-JSON error body */ } const hint = res.status === 401 ? "Token is invalid or expired" : res.status === 403 || res.status === 404 ? "Token lacks permission on this repository" : `GitHub returned ${res.status}`; throw new Error(detail ? `${hint} — ${detail}` : hint); } return res.status === 204 ? null : res.json(); } export async function fetchViewer(): Promise<{ login: string }> { return gh("/user") as Promise<{ login: string }>; } // Files community feedback as a `feedback`-labeled issue in FEEDBACK_REPO // under the connected user's account. Labels only stick when the token's // user has triage rights on the repo. export async function createFeedbackIssue( title: string, body: string, ): Promise { const it = (await gh(`/repos/${FEEDBACK_REPO}/issues`, { method: "POST", body: JSON.stringify({ title, body, labels: [FEEDBACK_LABEL] }), })) as { number: number; title: string; body?: string | null; html_url: string; state: string; created_at: string; comments: number; user: { login: string }; labels: { name: string }[]; }; return { repo: FEEDBACK_REPO, number: it.number, title: it.title, body: it.body ?? "", url: it.html_url, state: it.state === "closed" ? "closed" : "open", author: it.user.login, created_at: it.created_at, labels: it.labels.map((l) => l.name).filter((n) => n !== FEEDBACK_LABEL), comments: it.comments, }; }