返回 DeepSeek-Reasonix
admin.ts
1 import { Hono } from "hono";
2 import type { AppEnv } from "../env";
3 import { toPackageDTO } from "../types";
4 import { repos } from "../db";
5 import { requireAdmin } from "../http/auth";
6 import { writeRateLimit } from "../http/ratelimit";
7 import { ApiError } from "../http/errors";
8 import { z } from "zod";
9
10 const admin = new Hono<AppEnv>();
11
12 const now = () => new Date().toISOString();
13
14 const ApprovalRevisionSchema = z.object({
15 expectedVersion: z.string().min(1).max(64),
16 expectedUpdatedAt: z.string().min(1).max(64),
17 expectedStatus: z.enum(["pending", "hidden", "rejected"]),
18 });
19
20 admin.use("*", requireAdmin);
21
22 // Review queue. ?status defaults to pending; also accepts rejected/hidden/active.
23 admin.get("/packages", async (c) => {
24 const status = new URL(c.req.url).searchParams.get("status") || "pending";
25 const rows = await repos(c.env).packages.listByStatus(status, 200);
26 return c.json({ packages: rows.map(toPackageDTO) });
27 });
28
29 // Approve a pending package → live. Its publish event is emitted here (on first
30 // approval), so the activity feed only ever announces public packages.
31 admin.post("/packages/:handle/:name/approve", writeRateLimit, async (c) => {
32 const slug = `${c.req.param("handle")}/${c.req.param("name")}`;
33 const { packages: repo, events } = repos(c.env);
34 const revision = ApprovalRevisionSchema.safeParse(await c.req.json().catch(() => null));
35 if (!revision.success) {
36 throw new ApiError(400, "invalid_review_revision", "Approval requires the reviewed package revision.");
37 }
38 const approvedAt = now();
39 const row = await repo.setStatusIfCurrent(
40 slug,
41 "active",
42 revision.data.expectedVersion,
43 revision.data.expectedUpdatedAt,
44 revision.data.expectedStatus,
45 approvedAt,
46 );
47 if (!row) {
48 const current = await repo.bySlug(slug);
49 if (!current) throw new ApiError(404, "not_found", "No such package.");
50 throw new ApiError(
51 409,
52 "stale_review",
53 "Package changed since it was reviewed. Refresh and review the latest version.",
54 );
55 }
56 await events.log({
57 type: "publish",
58 packageId: row.id,
59 actorHandle: row.scope_handle,
60 summary: `published ${row.slug}@${row.latest_version}`,
61 now: approvedAt,
62 });
63 return c.json({ package: toPackageDTO(row) });
64 });
65
66 admin.post("/packages/:handle/:name/reject", writeRateLimit, async (c) => {
67 const slug = `${c.req.param("handle")}/${c.req.param("name")}`;
68 const row = await repos(c.env).packages.setStatus(slug, "rejected", now());
69 if (!row) throw new ApiError(404, "not_found", "No such package.");
70 return c.json({ package: toPackageDTO(row) });
71 });
72
73 // Take a previously-approved package back down.
74 admin.post("/packages/:handle/:name/hide", writeRateLimit, async (c) => {
75 const slug = `${c.req.param("handle")}/${c.req.param("name")}`;
76 const row = await repos(c.env).packages.setStatus(slug, "hidden", now());
77 if (!row) throw new ApiError(404, "not_found", "No such package.");
78 return c.json({ package: toPackageDTO(row) });
79 });
80
81 // Grant or revoke the verified badge. Body {verified:false} revokes; default grants.
82 admin.post("/packages/:handle/:name/verify", writeRateLimit, async (c) => {
83 const slug = `${c.req.param("handle")}/${c.req.param("name")}`;
84 const body = (await c.req.json().catch(() => ({}))) as { verified?: boolean };
85 const row = await repos(c.env).packages.setVerified(slug, body.verified !== false, now());
86 if (!row) throw new ApiError(404, "not_found", "No such package.");
87 return c.json({ package: toPackageDTO(row) });
88 });
89
90 export default admin;
91
91 lines TYPESCRIPT