import { createHash } from "node:crypto"; import { z } from "zod"; import { developerScopes, developerPagination, type DeveloperPagination, } from "../shared/developer-api"; const quota = z.object({ limit: z.literal(60), remaining: z.number().int().min(0).max(60), ownerLimit: z.literal(180), ownerRemaining: z.number().int().min(0).max(180), resetAt: z.number().int().positive(), }); const authorization = z.discriminatedUnion("ok", [ quota.extend({ ok: z.literal(true), keyId: z.string().uuid(), ownerId: z.string().uuid(), scopes: z.array(z.enum(developerScopes)), }), z.object({ ok: z.literal(false), error: z.enum(["invalid_key", "rate_limited"]), limit: z.literal(60).optional(), remaining: z.number().optional(), ownerLimit: z.literal(180).optional(), ownerRemaining: z.number().optional(), resetAt: z.number().optional(), }), ]); export type DeveloperData = { consume(hash: string): Promise; maps(page: DeveloperPagination, owner?: string): Promise; map(id: string): Promise; account(owner: string): Promise; games(owner: string, page: DeveloperPagination): Promise; }; export function developerJson( body: unknown, status = 200, extra: Record = {}, ) { return Response.json(body, { status, headers: { "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff", ...extra, }, }); } export function developerError( code: string, message: string, status: number, headers: Record = {}, ) { return developerJson({ error: { code, message } }, status, headers); } export async function developerResponse( request: Request, path: string[], data: DeveloperData, ) { let headers: Record = {}; if (request.method !== "GET") return developerError( "method_not_allowed", "This API version supports GET requests only.", 405, { Allow: "GET" }, ); const match = /^Bearer (mg_dev_[a-f0-9]{64})$/.exec( request.headers.get("authorization") ?? "", ); if (!match) return developerError( "invalid_key", "A valid developer Bearer key is required.", 401, { "WWW-Authenticate": "Bearer" }, ); try { const auth = authorization.parse( await data.consume(createHash("sha256").update(match[1]).digest("hex")), ); if (!auth.ok && auth.error === "invalid_key") return developerError( "invalid_key", "The developer key is invalid, expired, or revoked.", 401, { "WWW-Authenticate": "Bearer" }, ); const validatedQuota = quota.parse(auth); const reset = Math.max( 1, validatedQuota.resetAt - Math.floor(Date.now() / 1000), ); headers = { "RateLimit-Limit": "60", "RateLimit-Remaining": String(validatedQuota.remaining), "RateLimit-Reset": String(reset), "X-RateLimit-Reset": String(validatedQuota.resetAt), "X-RateLimit-Owner-Limit": "180", "X-RateLimit-Owner-Remaining": String(validatedQuota.ownerRemaining), }; if (!auth.ok) return developerError( "rate_limited", "Request limit reached. Retry after the indicated delay.", 429, { ...headers, "Retry-After": String(reset) }, ); const joined = path.join("/"); const scope = path[0] === "maps" ? "maps:read" : joined === "me" ? "account:read" : joined === "me/maps" ? "account:maps:read" : joined === "me/games" ? "account:games:read" : null; if (!scope) return developerError("not_found", "Endpoint not found.", 404, headers); if (!auth.scopes.includes(scope)) return developerError( "insufficient_scope", `This key requires the ${scope} permission.`, 403, headers, ); const url = new URL(request.url); let page: DeveloperPagination; try { page = developerPagination( url, joined === "maps" || joined === "me/maps", ); } catch (error) { return developerError( "invalid_request", (error as Error).message, 400, headers, ); } if (joined === "maps") return developerJson(await data.maps(page), 200, headers); if (path[0] === "maps" && path.length === 2) { if (url.search || !z.string().uuid().safeParse(path[1]).success) return developerError( "invalid_request", "A valid map UUID and no query parameters are required.", 400, headers, ); const map = await data.map(path[1]); return map ? developerJson({ map }, 200, headers) : developerError("not_found", "Published map not found.", 404, headers); } if (joined === "me") { if (url.search) return developerError( "invalid_request", "This endpoint has no query parameters.", 400, headers, ); const account = await data.account(auth.ownerId); return account ? developerJson({ account }, 200, headers) : developerError("not_found", "Account not found.", 404, headers); } if (joined === "me/maps") return developerJson(await data.maps(page, auth.ownerId), 200, headers); if (joined === "me/games") return developerJson(await data.games(auth.ownerId, page), 200, headers); return developerError("not_found", "Endpoint not found.", 404, headers); } catch { return developerError( "service_unavailable", "The developer API is temporarily unavailable.", 503, headers, ); } }