import { z } from "zod"; export const developerScopes = [ "maps:read", "account:read", "account:maps:read", "account:games:read", ] as const; export type DeveloperScope = (typeof developerScopes)[number]; export type DeveloperKey = { id: string; name: string; prefix: string; scopes: DeveloperScope[]; createdAt: string; expiresAt: string; revokedAt: string | null; lastUsedAt: string | null; }; export const developerKeyInput = z .object({ name: z.string().trim().min(1).max(60), scopes: z .array(z.enum(developerScopes)) .min(1) .max(4) .refine( (values) => new Set(values).size === values.length, "Duplicate permissions", ), expiresDays: z.number().int().min(1).max(365).default(90), }) .strict(); export function developerPagination(url: URL, search = false) { const allowed = new Set( search ? ["limit", "offset", "q"] : ["limit", "offset"], ); for (const key of url.searchParams.keys()) { if (!allowed.has(key) || url.searchParams.getAll(key).length !== 1) throw new Error("Unsupported or duplicate query parameter"); } const integer = (key: string, fallback: number, min: number, max: number) => { const raw = url.searchParams.get(key); if (raw === null) return fallback; if (!/^\d+$/.test(raw)) throw new Error(`Invalid ${key}`); const value = Number(raw); if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`Invalid ${key}`); return value; }; const q = (url.searchParams.get("q") ?? "").trim(); if (q.length > 80) throw new Error("Search must be 80 characters or fewer"); return { limit: integer("limit", 20, 1, 50), offset: integer("offset", 0, 0, 10000), q, }; } export type DeveloperPagination = ReturnType; export function developerPage(total: number, page: DeveloperPagination) { return { limit: page.limit, offset: page.offset, total, nextOffset: page.offset + page.limit < total && page.offset + page.limit <= 10000 ? page.offset + page.limit : null, }; }