209 lines
6.0 KiB
TypeScript
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.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|