Add developer API, Prometheus metrics, and community links

This commit is contained in:
2026-10-12 19:59:37 +05:00
parent 3cc3136762
commit 018be5049e
50 changed files with 4087 additions and 31 deletions

71
shared/developer-api.ts Normal file
View File

@@ -0,0 +1,71 @@
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<typeof developerPagination>;
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,
};
}