Files
MineGeo/shared/developer-openapi.ts

209 lines
6.0 KiB
TypeScript

const string = { type: "string" };
const date = { type: "string", format: "date-time" };
const id = { type: "string", format: "uuid" };
const ref = (name: string) => ({ $ref: `#/components/schemas/${name}` });
const pagination = [
{
name: "limit",
in: "query",
schema: { type: "integer", minimum: 1, maximum: 50, default: 20 },
},
{
name: "offset",
in: "query",
schema: { type: "integer", minimum: 0, maximum: 10000, default: 0 },
},
];
const success = (schema: unknown) => ({
description: "Success",
content: { "application/json": { schema } },
});
const errors = Object.fromEntries(
[
[400, "Invalid query or map ID"],
[401, "Missing, invalid, expired or revoked key"],
[403, "Required permission missing"],
[404, "Resource or endpoint not found"],
[429, "Quota exceeded; wait Retry-After seconds"],
[503, "Temporarily unavailable"],
].map(([status, description]) => [
status,
{ description, content: { "application/json": { schema: ref("Error") } } },
]),
);
const list = (name: string, item: string) => ({
type: "object",
required: [name, "pagination"],
properties: {
[name]: { type: "array", items: ref(item) },
pagination: ref("Pagination"),
},
});
const operation = (
summary: string,
scope: string,
schema: unknown,
parameters: unknown[] = [],
) => ({
summary,
description: `Requires the ${scope} key permission.`,
"x-required-scope": scope,
security: [{ DeveloperKey: [] }],
parameters,
responses: { 200: success(schema), ...errors },
});
export const developerOpenApi = {
openapi: "3.1.0",
info: {
title: "MineGeo Developer API",
version: "1.0.0",
description:
"Read-only HTTPS API. Bearer keys are managed at /dev/api. 60 requests per key per fixed minute; 180 requests per account per minute shared across keys. All authenticated requests, including validation/permission errors, consume quota. A 429 response includes Retry-After; successful authorized responses include RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until reset). X-RateLimit-Reset is a Unix timestamp. Requests with invalid keys cannot access account data. No upload, publication, gameplay writes or moderator access is granted.",
},
servers: [{ url: "/api/dev/v1" }],
paths: {
"/maps": {
get: operation("List published maps", "maps:read", list("maps", "Map"), [
...pagination,
{
name: "q",
in: "query",
schema: { type: "string", maxLength: 80 },
description: "Case-insensitive map-name search",
},
]),
},
"/maps/{id}": {
get: operation(
"Get a published map",
"maps:read",
{
type: "object",
required: ["map"],
properties: { map: ref("Map") },
},
[{ name: "id", in: "path", required: true, schema: id }],
),
},
"/me": {
get: operation("Get the key owner's profile", "account:read", {
type: "object",
required: ["account"],
properties: { account: ref("Account") },
}),
},
"/me/maps": {
get: operation(
"List the owner's maps, including private maps",
"account:maps:read",
list("maps", "Map"),
[
...pagination,
{ name: "q", in: "query", schema: { type: "string", maxLength: 80 } },
],
),
},
"/me/games": {
get: operation(
"List the owner's solo-game history",
"account:games:read",
list("games", "Game"),
pagination,
),
},
},
components: {
securitySchemes: {
DeveloperKey: {
type: "http",
scheme: "bearer",
bearerFormat: "mg_dev_<64 hex characters>",
},
},
schemas: {
Error: {
type: "object",
required: ["error"],
properties: {
error: {
type: "object",
required: ["code", "message"],
properties: { code: string, message: string },
},
},
},
Pagination: {
type: "object",
properties: {
limit: { type: "integer" },
offset: { type: "integer" },
total: { type: "integer" },
nextOffset: { type: ["integer", "null"] },
},
},
Map: {
type: "object",
properties: {
id,
name: string,
description: string,
source: string,
state: string,
bounds: {
type: "object",
properties: {
minX: { type: "number" },
minZ: { type: "number" },
maxX: { type: "number" },
maxZ: { type: "number" },
},
},
panoramaCount: { type: "integer" },
createdAt: date,
updatedAt: date,
url: {
type: "string",
description:
"Relative MineGeo map page path; private maps still require their owner's browser login.",
},
},
},
Account: {
type: "object",
properties: {
id,
displayName: string,
createdAt: date,
appearance: {
type: "object",
properties: {
nameGlow: { type: "boolean" },
glowColor: string,
bugFinder: { type: "boolean" },
customPrefix: { type: ["string", "null"] },
customPrefixIcon: string,
},
},
},
},
Game: {
type: "object",
properties: {
id,
difficulty: { type: "string", enum: ["easy", "normal", "hard"] },
createdAt: date,
currentRound: { type: "integer", minimum: 0, maximum: 5 },
complete: { type: "boolean" },
score: {
type: ["integer", "null"],
minimum: 0,
maximum: 25000,
description:
"Final score only; null until a complete five-round result is available.",
},
},
},
},
},
};