Add developer API, Prometheus metrics, and community links
This commit is contained in:
137
lib/developer-key-management.ts
Normal file
137
lib/developer-key-management.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user