{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pokerstudy-api",
  "title": "Poker Study AI API proxy",
  "description": "Your server's window onto the Poker Study AI API: one /api/pokerstudy/* route that adds your psk_ key, a server-only client, and the fetch hook the data blocks share. Routes that return your own hands and opponents are locked outside development until you edit authorizePrivate.",
  "files": [
    {
      "path": "src/lib/pokerstudy/api.ts",
      "content": "/**\n * Server-side client for the Poker Study AI API. Reads your Studio key from\n * `POKERSTUDY_API_KEY` and sends it as a Bearer token. Import this only from\n * route handlers, server actions or server components: a `psk_` key\n * authenticates as you, so it must never reach the browser.\n */\n\nconst DEFAULT_BASE = \"https://www.pokerstudy.ai\";\n\nexport class PokerStudyConfigError extends Error {}\n\nexport async function pokerstudyFetch(path: string, init: RequestInit = {}): Promise<Response> {\n  if (typeof window !== \"undefined\") {\n    throw new PokerStudyConfigError(\"pokerstudyFetch is server-only: it reads your API key.\");\n  }\n  const key = process.env.POKERSTUDY_API_KEY;\n  if (!key) {\n    throw new PokerStudyConfigError(\n      \"POKERSTUDY_API_KEY is not set. Create a key at https://www.pokerstudy.ai/settings/api and add it to .env.local.\",\n    );\n  }\n  const url = new URL(path, process.env.POKERSTUDY_API_URL || DEFAULT_BASE);\n  const headers = new Headers(init.headers);\n  headers.set(\"Authorization\", `Bearer ${key}`);\n  return fetch(url, { ...init, headers, cache: \"no-store\" });\n}\n\n/**\n * Who may read the key owner's private data (uploaded sessions, opponents,\n * drill results) through `/api/pokerstudy/*`. Those routes answer as the key's\n * owner, so on a public deployment anyone could read them.\n *\n * The default allows it in development only. Replace the body with your own\n * check, for example your auth library's session, before you ship. Solver\n * lookups and grading are not private and skip this check.\n */\nexport async function authorizePrivate(req: Request): Promise<boolean> {\n  void req; // e.g. `return Boolean((await auth()).userId)` with your auth library\n  return process.env.NODE_ENV !== \"production\" || process.env.POKERSTUDY_ALLOW_PRIVATE === \"true\";\n}\n\n/** Relay an upstream response as JSON, keeping its status. */\nexport async function relay(res: Response, transform?: (body: unknown) => unknown): Promise<Response> {\n  const text = await res.text();\n  if (!transform || !res.ok) {\n    return new Response(text, { status: res.status, headers: { \"content-type\": \"application/json\" } });\n  }\n  return Response.json(transform(JSON.parse(text)), { status: res.status });\n}\n\nexport function jsonError(message: string, status: number): Response {\n  return Response.json({ error: message }, { status });\n}\n",
      "type": "registry:lib",
      "target": "lib/pokerstudy/api.ts"
    },
    {
      "path": "registry/pokerstudy/proxy-route.ts",
      "content": "import { authorizePrivate, jsonError, pokerstudyFetch, relay } from \"@/lib/pokerstudy/api\";\n\n/**\n * /api/pokerstudy/* : your app's window onto the Poker Study AI API. Each\n * path below forwards to one documented endpoint with POKERSTUDY_API_KEY\n * added on the server; nothing else is reachable through it.\n *\n * Routes marked `private` answer with the key owner's own hands, opponents\n * and drill results, so they go through `authorizePrivate` in\n * lib/pokerstudy/api.ts. Edit that function before you deploy.\n */\n\nconst NL_GAMES: Record<string, string> = { nl: \"nl/v2\", nl9: \"nl9\", nlmtt8: \"nlmtt8\" };\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst MAX_BODY = 16_000;\n\ntype Ctx = { params: Promise<{ path?: string[] }> };\n\nasync function readJson(req: Request): Promise<string | Response> {\n  const body = await req.text();\n  if (body.length > MAX_BODY) return jsonError(\"request body too large\", 413);\n  try {\n    JSON.parse(body || \"{}\");\n  } catch {\n    return jsonError(\"invalid JSON body\", 400);\n  }\n  return body || \"{}\";\n}\n\nconst post = (body: string): RequestInit => ({\n  method: \"POST\",\n  headers: { \"content-type\": \"application/json\" },\n  body,\n});\n\nasync function guard(req: Request): Promise<Response | null> {\n  return (await authorizePrivate(req)) ? null : jsonError(\"not authorized\", 403);\n}\n\nasync function handle(req: Request, ctx: Ctx): Promise<Response> {\n  const [head, id, ...rest] = (await ctx.params).path ?? [];\n  const method = req.method;\n  if (rest.length > 0) return jsonError(\"not found\", 404);\n\n  try {\n    // Solver strategy for one preflop decision. Public data.\n    if (method === \"GET\" && head === \"node\" && !id) {\n      const params = new URL(req.url).searchParams;\n      const game = params.get(\"game\") ?? \"nl\";\n      const path = NL_GAMES[game];\n      if (!path) return jsonError(`unknown game \"${game}\". Use one of: ${Object.keys(NL_GAMES).join(\", \")}`, 400);\n      const upstream = new URLSearchParams();\n      for (const name of [\"stack\", \"history\", \"hand\"]) {\n        const value = params.get(name);\n        if (value) upstream.set(name, value.slice(0, 500));\n      }\n      return relay(await pokerstudyFetch(`/api/ranges/${path}/node?${upstream}`));\n    }\n\n    // The agent's action for a table snapshot, and a grade for yours.\n    if (method === \"POST\" && (head === \"decide\" || head === \"evaluate\") && !id) {\n      const body = await readJson(req);\n      if (typeof body !== \"string\") return body;\n      return relay(await pokerstudyFetch(`/api/play/${head}`, post(body)));\n    }\n\n    // Everything below is the key owner's own data.\n    const denied = await guard(req);\n    if (denied) return denied;\n\n    if (method === \"GET\" && head === \"trainer-stats\" && !id) {\n      return relay(await pokerstudyFetch(\"/api/trainer/stats\"));\n    }\n    if (method === \"GET\" && head === \"sessions\" && !id) {\n      return relay(await pokerstudyFetch(\"/api/sessions\"));\n    }\n    if (method === \"GET\" && head === \"sessions\" && id && UUID.test(id)) {\n      // The upstream body carries every hand of the session; the browser only\n      // needs the summary and the hero's stats.\n      return relay(await pokerstudyFetch(`/api/sessions/${id}`), (b) => {\n        const body = b as { id: string; createdAt: string; hero: string; heroStats: unknown; session?: Record<string, unknown> };\n        const s = body.session ?? {};\n        return {\n          id: body.id,\n          createdAt: body.createdAt,\n          hero: body.hero,\n          heroStats: body.heroStats,\n          session: {\n            totalHands: s.totalHands,\n            heroHandsPlayed: s.heroHandsPlayed,\n            firstTs: s.firstTs,\n            lastTs: s.lastTs,\n            gameType: s.gameType,\n          },\n        };\n      });\n    }\n    if (method === \"GET\" && head === \"opponents\" && !id) {\n      return relay(await pokerstudyFetch(\"/api/opponents\"));\n    }\n    if (method === \"GET\" && head === \"opponents\" && id && UUID.test(id)) {\n      return relay(await pokerstudyFetch(`/api/opponents/${id}`));\n    }\n    if (method === \"POST\" && head === \"limp-range\" && !id) {\n      const body = await readJson(req);\n      if (typeof body !== \"string\") return body;\n      return relay(await pokerstudyFetch(\"/api/analytics/opponent-limp-range\", post(body)));\n    }\n  } catch (err) {\n    return jsonError((err as Error).message, 500);\n  }\n  return jsonError(\"not found\", 404);\n}\n\nexport const GET = handle;\nexport const POST = handle;\n",
      "type": "registry:file",
      "target": "app/api/pokerstudy/[...path]/route.ts"
    },
    {
      "path": "src/components/pokerstudy/use-pokerstudy.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nexport type PokerStudyState<T> =\n  | { status: \"loading\" }\n  | { status: \"error\"; message: string; httpStatus?: number }\n  | { status: \"ready\"; data: T };\n\ntype Settled<T> = { key: string; state: Exclude<PokerStudyState<T>, { status: \"loading\" }> };\n\n/**\n * Fetch JSON from your `/api/pokerstudy/*` proxy (or any URL). Pass `null` to\n * skip the request, and `initialData` to render without fetching at all.\n * Refetches when `url` or `body` changes.\n */\nexport function usePokerStudy<T>(\n  url: string | null,\n  { body, initialData }: { body?: unknown; initialData?: T } = {},\n): PokerStudyState<T> {\n  const [settled, setSettled] = useState<Settled<T> | null>(null);\n  const payload = body === undefined ? null : JSON.stringify(body);\n  const key = `${url}|${payload}`;\n\n  useEffect(() => {\n    if (!url || initialData !== undefined) return;\n    const ctrl = new AbortController();\n    const done = (state: Settled<T>[\"state\"]) => setSettled({ key, state });\n    fetch(url, {\n      signal: ctrl.signal,\n      ...(payload === null\n        ? {}\n        : { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: payload }),\n    })\n      .then(async (res) => {\n        const json = (await res.json().catch(() => null)) as (T & { error?: string }) | null;\n        if (!res.ok || !json || (typeof json === \"object\" && \"error\" in json && json.error)) {\n          done({ status: \"error\", message: json?.error ?? `HTTP ${res.status}`, httpStatus: res.status });\n        } else {\n          done({ status: \"ready\", data: json });\n        }\n      })\n      .catch((err: Error) => {\n        if (err.name !== \"AbortError\") done({ status: \"error\", message: err.message });\n      });\n    return () => ctrl.abort();\n  }, [initialData, key, payload, url]);\n\n  if (initialData !== undefined) return { status: \"ready\", data: initialData };\n  return settled?.key === key ? settled.state : { status: \"loading\" };\n}\n\n/**\n * The loading and error states every data block shares. A 403 from the proxy\n * means `authorizePrivate` turned the request away, so it says where to look.\n */\nexport function PokerStudyStatus({ state }: { state: Exclude<PokerStudyState<unknown>, { status: \"ready\" }> }) {\n  if (state.status === \"loading\") {\n    return <div aria-busy className=\"h-40 w-full max-w-[560px] animate-pulse rounded-md bg-white/[0.04]\" />;\n  }\n  const hint =\n    state.httpStatus === 403\n      ? \" Private data is off outside development; allow it in authorizePrivate (lib/pokerstudy/api.ts).\"\n      : state.httpStatus === 401\n        ? \" Check POKERSTUDY_API_KEY: it must be a Studio key.\"\n        : \"\";\n  return (\n    <p role=\"alert\" className=\"max-w-[560px] rounded-md border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-300\">\n      {state.message}.{hint}\n    </p>\n  );\n}\n",
      "type": "registry:hook",
      "target": "components/pokerstudy/use-pokerstudy.tsx"
    }
  ],
  "envVars": {
    "POKERSTUDY_API_KEY": ""
  },
  "docs": "Set POKERSTUDY_API_KEY in .env.local to a Studio key from https://www.pokerstudy.ai/settings/api. app/api/pokerstudy answers as you: before deploying, edit authorizePrivate in lib/pokerstudy/api.ts so only your signed-in users can read your sessions, opponents and drill stats.",
  "type": "registry:lib"
}