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

View File

@@ -0,0 +1,137 @@
import "server-only";
import { createHash, randomBytes } from "node:crypto";
import { z } from "zod";
import { admin, required, user } from "./supabase";
import { accountProfile } from "./profile";
import { browserAccount } from "./request-security";
import { verifyOrigin } from "./request-origin";
import { ServiceError } from "./service-error";
import { developerKeyInput } from "@/shared/developer-api";
import { developerError, developerJson } from "./developer-response";
async function body(request: Request) {
if (!request.headers.get("content-type")?.includes("application/json"))
throw new ServiceError("Use application/json for key management.", 400);
const reader = request.body?.getReader();
if (!reader) throw new ServiceError("JSON body required.", 400);
const chunks: Uint8Array[] = [];
let bytes = 0;
while (true) {
const next = await reader.read();
if (next.done) break;
bytes += next.value.byteLength;
if (bytes > 8192) {
await reader.cancel();
throw new ServiceError("Request too large.", 413);
}
chunks.push(next.value);
}
try {
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
} catch {
throw new ServiceError("Valid JSON required.", 400);
}
}
export async function developerKeys(request: Request) {
try {
if (!["GET", "POST", "DELETE"].includes(request.method))
return developerError(
"method_not_allowed",
"Use GET, POST or DELETE.",
405,
{ Allow: "GET, POST, DELETE" },
);
// API/device/metrics credentials cannot mint keys, even alongside account cookies.
const owner = await browserAccount(request, user);
if (request.headers.get("sec-fetch-site") === "cross-site")
return developerError(
"invalid_origin",
"Use the developer panel on MineGeo.",
403,
);
try {
verifyOrigin(request);
} catch {
return developerError(
"invalid_origin",
"Request origin is not allowed.",
403,
);
}
await accountProfile(owner);
if (request.method === "GET") {
const keys = required(
await admin().rpc("developer_keys_list", { p_owner: owner.id }),
);
return developerJson({ keys });
}
if (request.method === "DELETE") {
const { id } = z
.object({ id: z.string().uuid() })
.strict()
.parse(await body(request));
const ok = required(
await admin().rpc("developer_key_revoke", {
p_owner: owner.id,
p_key: id,
}),
);
return ok
? developerJson({ ok: true })
: developerError("not_found", "API key not found.", 404);
}
const input = developerKeyInput.parse(await body(request));
const allowed = required(
await admin().rpc("check_rate", {
p_key: `developer:create:${owner.id}`,
p_limit: 10,
p_seconds: 3600,
}),
);
if (!allowed)
return developerError(
"rate_limited",
"Too many key creations. Try again later.",
429,
{ "Retry-After": "3600" },
);
const token = `mg_dev_${randomBytes(32).toString("hex")}`;
const result = await admin().rpc("developer_key_create", {
p_owner: owner.id,
p_name: input.name,
p_scopes: input.scopes,
p_token_hash: createHash("sha256").update(token).digest("hex"),
p_prefix: token.slice(0, 17),
p_expires_days: input.expiresDays,
});
if (
result.error?.code === "22023" &&
result.error.message === "Maximum 5 active API keys"
)
return developerError(
"key_limit",
"You can have at most five active API keys. Revoke an unused key first.",
409,
);
const key = required(result);
return developerJson({ key, token }, 201);
} catch (error) {
if (error instanceof z.ZodError)
return developerError(
"invalid_request",
"Check the key name, permissions and expiry.",
400,
);
if (error instanceof ServiceError)
return developerError(
error.status === 401 ? "sign_in_required" : "invalid_request",
error.message,
error.status,
);
return developerError(
"service_unavailable",
"API key management is temporarily unavailable.",
503,
);
}
}