import "server-only"; import { createHash, randomBytes } from "node:crypto"; import { z } from "zod"; import { admin, required, user } from "./supabase"; import { accountProfile } from "./profile"; import { browserAccount } from "./request-security"; import { verifyOrigin } from "./request-origin"; import { ServiceError } from "./service-error"; import { developerKeyInput } from "@/shared/developer-api"; import { developerError, developerJson } from "./developer-response"; async function body(request: Request) { if (!request.headers.get("content-type")?.includes("application/json")) throw new ServiceError("Use application/json for key management.", 400); const reader = request.body?.getReader(); if (!reader) throw new ServiceError("JSON body required.", 400); const chunks: Uint8Array[] = []; let bytes = 0; while (true) { const next = await reader.read(); if (next.done) break; bytes += next.value.byteLength; if (bytes > 8192) { await reader.cancel(); throw new ServiceError("Request too large.", 413); } chunks.push(next.value); } try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { throw new ServiceError("Valid JSON required.", 400); } } export async function developerKeys(request: Request) { try { if (!["GET", "POST", "DELETE"].includes(request.method)) return developerError( "method_not_allowed", "Use GET, POST or DELETE.", 405, { Allow: "GET, POST, DELETE" }, ); // API/device/metrics credentials cannot mint keys, even alongside account cookies. const owner = await browserAccount(request, user); if (request.headers.get("sec-fetch-site") === "cross-site") return developerError( "invalid_origin", "Use the developer panel on MineGeo.", 403, ); try { verifyOrigin(request); } catch { return developerError( "invalid_origin", "Request origin is not allowed.", 403, ); } await accountProfile(owner); if (request.method === "GET") { const keys = required( await admin().rpc("developer_keys_list", { p_owner: owner.id }), ); return developerJson({ keys }); } if (request.method === "DELETE") { const { id } = z .object({ id: z.string().uuid() }) .strict() .parse(await body(request)); const ok = required( await admin().rpc("developer_key_revoke", { p_owner: owner.id, p_key: id, }), ); return ok ? developerJson({ ok: true }) : developerError("not_found", "API key not found.", 404); } const input = developerKeyInput.parse(await body(request)); const allowed = required( await admin().rpc("check_rate", { p_key: `developer:create:${owner.id}`, p_limit: 10, p_seconds: 3600, }), ); if (!allowed) return developerError( "rate_limited", "Too many key creations. Try again later.", 429, { "Retry-After": "3600" }, ); const token = `mg_dev_${randomBytes(32).toString("hex")}`; const result = await admin().rpc("developer_key_create", { p_owner: owner.id, p_name: input.name, p_scopes: input.scopes, p_token_hash: createHash("sha256").update(token).digest("hex"), p_prefix: token.slice(0, 17), p_expires_days: input.expiresDays, }); if ( result.error?.code === "22023" && result.error.message === "Maximum 5 active API keys" ) return developerError( "key_limit", "You can have at most five active API keys. Revoke an unused key first.", 409, ); const key = required(result); return developerJson({ key, token }, 201); } catch (error) { if (error instanceof z.ZodError) return developerError( "invalid_request", "Check the key name, permissions and expiry.", 400, ); if (error instanceof ServiceError) return developerError( error.status === 401 ? "sign_in_required" : "invalid_request", error.message, error.status, ); return developerError( "service_unavailable", "API key management is temporarily unavailable.", 503, ); } }