diff --git a/apps/web/lib/api/plugin-pairing.ts b/apps/web/lib/api/plugin-pairing.ts index d13ff0b..93b74ef 100644 --- a/apps/web/lib/api/plugin-pairing.ts +++ b/apps/web/lib/api/plugin-pairing.ts @@ -4,6 +4,7 @@ import { db } from "@/lib/db/index" import { pluginPairing } from "@/lib/db/schema" import { eq, and, lt } from "drizzle-orm" import { auth } from "@/lib/auth" +import { authenticateWithApiKey } from "@/lib/auth/api-key-guard" const PAIRING_TTL_MS = 10 * 60 * 1000 // 10 minutes @@ -26,6 +27,28 @@ export const pluginPairingRoutes = new Elysia({ prefix: "/plugin", detail: { tags: ["Plugin"] }, }) + // ── Verify API key: plugin checks if its key is still valid ── + .get( + "/verify-key", + async ({ request, set }) => { + const guard = await authenticateWithApiKey(request.headers) + if (!guard.ok) { + set.status = guard.status + return { valid: false, error: guard.error } + } + return { + valid: true, + user: { name: guard.user.name, image: guard.user.image }, + } + }, + { + detail: { + summary: "Verify a Decky plugin API key", + description: + "Checks whether the x-api-key header contains a valid, enabled API key. Used by the plugin to test its saved key.", + }, + }, + ) // ── Initiate: plugin requests a pairing token (no auth) ────── .post( "/pair/initiate", diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 3f673c7..979c2c3 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -599,31 +599,40 @@ exec mangohud "$@" return {"success": False, "error": str(e), "status": 0} async def test_api_key(self, api_key: str, base_url: str = "https://deckyvault.xyz") -> dict: - """RPC: Test if an API key is valid by calling the games lookup endpoint. - Returns {valid: bool, error: str?}.""" + """RPC: Test if an API key is valid by calling the dedicated verify endpoint. + Returns {valid: bool, error: str?, userName?: str, userImage?: str}.""" import urllib.request import urllib.error + if not api_key: + return {"valid": False, "error": "No API key provided"} + try: - url = f"{base_url}/api/games/lookup?steamAppId=0" + url = f"{base_url}/api/plugin/verify-key" req = urllib.request.Request( url, headers={ "x-api-key": api_key, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0", + "Accept": "application/json", }, method="GET" ) context = _get_ssl_context() with urllib.request.urlopen(req, timeout=10, context=context) as response: - # A 404 (game not found) still means the API key is valid - return {"valid": True} + result = json.loads(response.read().decode('utf-8')) + return { + "valid": True, + "userName": result.get("user", {}).get("name"), + "userImage": result.get("user", {}).get("image"), + } except urllib.error.HTTPError as e: if e.code == 401: - return {"valid": False, "error": "Invalid API key"} - elif e.code in (400, 404): - return {"valid": True} # Key works, just bad request or no game with ID 0 - else: + return {"valid": False, "error": "Invalid or revoked API key"} + try: + err = json.loads(e.read().decode('utf-8')) + return {"valid": False, "error": err.get("error", f"Server returned status {e.code}")} + except Exception: return {"valid": False, "error": f"Server returned status {e.code}"} except urllib.error.URLError as e: return {"valid": False, "error": f"Network error: {str(e.reason)}"}