Public: anyone can read this; the crew can change it.

A repository's webhook routes leave api.ts for src/http/api/hooks.ts #7044

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/6333-a-repository-s-webhook-routes-leave-api → main 1 commit fork

  • Merged. mason/6333-a-repository-s-webhook-routes-leave-api was merged into main as 98140c7.

Review

Sign in to review this change.
2 files changed +59 −44
src/http/api.ts +2 −44
@@ -148,9 +148,7 @@ import { runRepos } from "./run_gate.ts";
148148 import {
149149 commitToBranch,
150150 createRepo,
151 createWebhook,
152151 deleteRepo,
153 deleteWebhook,
154152 repositoryLimitMessage,
155153 resolveOwner,
156154 privateRepoForSealed,
@@ -241,7 +239,6 @@ import {
241239 type User,
242240 } from "../core/users.ts";
243241 import { isPinned } from "../core/pins.ts";
244 import { listWebhooks } from "../core/webhooks.ts";
245242 import { diffCommit } from "../git/diff.ts";
246243 import { runGit } from "../git/env.ts";
247244 import { appLimitWords, appsOwnedBy } from "../actions/app_limit.ts";
@@ -415,6 +412,7 @@ import { COPIES_PER_DAY, makeItYours } from "../actions/make_it_yours.ts";
415412 import { passportRoutes } from "./passport_edge.ts";
416413 import { egressGrantRoutes } from "./api/egress_grants.ts";
417414 import { forgeRoutes } from "./api/forges.ts";
415 import { hookRoutes } from "./api/hooks.ts";
418416 import { agentRoutes } from "./api/agents.ts";
419417 import { boardRoutes } from "./api/boards.ts";
420418 import { labelsRoutes } from "./api/labels.ts";
@@ -4309,47 +4307,7 @@ export function apiRoutes(router: Router<Handler>): void {
43094307 return json(contributionJson(ctx, result.value), 201);
43104308 });
43114309
4312 // ── webhooks — admin-scoped, since a webhook exfiltrates repo events ──
4313
4314 router.add("GET", "/api/v1/repos/:owner/:name/hooks", (ctx) => {
4315 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
4316 const denied = requireScope(ctx, "admin");
4317 if (denied) return denied;
4318 const repo = loadRepo(ctx);
4319 if (!repo || !canAdmin(ctx.app.db, repo, ctx.auth.user)) return notFound();
4320 const page = pageParams(ctx);
4321 if (page instanceof Response) return page;
4322 const rows = timePhase(ctx.request, "hooks", () => listWebhooks(ctx.app.db, repo.id));
4323 return timePhase(ctx.request, "render", () => pageOf(rows, page));
4324 });
4325
4326 router.add("POST", "/api/v1/repos/:owner/:name/hooks", async (ctx) => {
4327 const denied = requireScope(ctx, "admin");
4328 if (denied) return denied;
4329 const repo = loadRepo(ctx);
4330 if (!repo) return notFound();
4331 const input = await body(ctx.request);
4332 const created = createWebhook(ctx.app, ctx.auth.user, repo, {
4333 url: str(input.url),
4334 kinds: str(input.kinds) || "*",
4335 ...(typeof input.secret === "string" ? { secret: input.secret } : {}),
4336 });
4337 if (!created.ok) return respondAccess(created.error, 422);
4338 // The secret is returned exactly once, at creation.
4339 return json(
4340 { ...created.value.webhook, secret: created.value.secret },
4341 201,
4342 );
4343 });
4344
4345 router.add("DELETE", "/api/v1/repos/:owner/:name/hooks/:id", (ctx) => {
4346 const denied = requireScope(ctx, "admin");
4347 if (denied) return denied;
4348 const repo = loadRepo(ctx);
4349 if (!repo) return notFound();
4350 const removed = deleteWebhook(ctx.app, ctx.auth.user, repo, ctx.params.id!);
4351 return json({}, removed.ok ? 200 : 404);
4352 });
4310 hookRoutes(router);
43534311
43544312 // ── origins ──────────────────────────────────────────────────────────
43554313 //
src/http/api/hooks.ts added +57
@@ -0,0 +1,57 @@
1 /**
2 * A repository's webhooks over the API: `GET` and `POST`
3 * `/api/v1/repos/:owner/:name/hooks`, and `DELETE` `…/hooks/:id`. Moved out
4 * of `api.ts` (#6333); the rules live in `actions/repos.ts`. Admin-scoped,
5 * since a webhook exfiltrates repo events.
6 */
7
8 import { createWebhook, deleteWebhook } from "../../actions/repos.ts";
9 import { canAdmin } from "../../core/repos.ts";
10 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
11 import { listWebhooks } from "../../core/webhooks.ts";
12 import { loadRepo, pageOf, pageParams } from "../api.ts";
13 import type { Handler } from "../context.ts";
14 import { body, json, notFound, requireScope, respondAccess, str } from "../respond.ts";
15 import type { Router } from "../router.ts";
16
17 export function hookRoutes(router: Router<Handler>): void {
18 router.add("GET", "/api/v1/repos/:owner/:name/hooks", (ctx) => {
19 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
20 const denied = requireScope(ctx, "admin");
21 if (denied) return denied;
22 const repo = loadRepo(ctx);
23 if (!repo || !canAdmin(ctx.app.db, repo, ctx.auth.user)) return notFound();
24 const page = pageParams(ctx);
25 if (page instanceof Response) return page;
26 const rows = timePhase(ctx.request, "hooks", () => listWebhooks(ctx.app.db, repo.id));
27 return timePhase(ctx.request, "render", () => pageOf(rows, page));
28 });
29
30 router.add("POST", "/api/v1/repos/:owner/:name/hooks", async (ctx) => {
31 const denied = requireScope(ctx, "admin");
32 if (denied) return denied;
33 const repo = loadRepo(ctx);
34 if (!repo) return notFound();
35 const input = await body(ctx.request);
36 const created = createWebhook(ctx.app, ctx.auth.user, repo, {
37 url: str(input.url),
38 kinds: str(input.kinds) || "*",
39 ...(typeof input.secret === "string" ? { secret: input.secret } : {}),
40 });
41 if (!created.ok) return respondAccess(created.error, 422);
42 // The secret is returned exactly once, at creation.
43 return json(
44 { ...created.value.webhook, secret: created.value.secret },
45 201,
46 );
47 });
48
49 router.add("DELETE", "/api/v1/repos/:owner/:name/hooks/:id", (ctx) => {
50 const denied = requireScope(ctx, "admin");
51 if (denied) return denied;
52 const repo = loadRepo(ctx);
53 if (!repo) return notFound();
54 const removed = deleteWebhook(ctx.app, ctx.auth.user, repo, ctx.params.id!);
55 return json({}, removed.ok ? 200 : 404);
56 });
57 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs