138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|