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

Child-forge routes leave api.ts for src/http/api/forges.ts #6955

Merged opened this

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

Merged

mason/forge:mason/6331-child-forge-routes-leave-api-ts-for-src → main 2 commits fork

  • Merged. mason/6331-child-forge-routes-leave-api-ts-for-src was merged into main as 8f40ba1.

Review

Sign in to review this change.
2 files changed +65 −50
src/http/api.ts +2 −50
@@ -76,11 +76,9 @@ import { emailVerified } from "../core/verify.ts";
7676 import { readHealth } from "../actions/health.ts";
7777 import { isProbe } from "../kernel/probe.ts";
7878 import { PRIVATE_ADDRESS_REFUSED } from "../kernel/address_words.ts";
79 import { listChildren } from "../actions/spawn.ts";
8079 import { forgeState, PARENT_HEADER } from "../spawn/clients.ts";
8180 import { familyView } from "../spawn/family.ts";
8281 import { receiveBeat, SIGNATURE_HEADER } from "../spawn/heartbeat.ts";
83 import { reissueDoor, spawnDoor } from "../spawn/door.ts";
8482 import { forgeProposals, proposeToParentForge, proposeUpstream } from "../actions/upstream.ts";
8583 import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords, takeOrTell } from "../actions/follow.ts";
8684 import { relationOf } from "../actions/forks.ts";
@@ -356,8 +354,6 @@ import { repoDir } from "../git/repos.ts";
356354 import { carriedPulls, pullPath } from "../story/wave_pulls.ts";
357355 import { pullRefs, readWave, readWaves } from "../story/waves.ts";
358356 import type { Result } from "../kernel/result.ts";
359 import { sizesView } from "../spawn/sizes.ts";
360 import { holds, weakEtag } from "./etag.ts";
361357 import { inviteLink, list as listInviteLinks, mint as mintInviteLink, revoke as revokeInviteLink } from "../actions/invites.ts";
362358 import { originForRepo, type OriginRepoRow } from "../core/origins.ts";
363359 import { subscribe as subscribeJob, type MirrorJob } from "../origins/jobs.ts";
@@ -418,6 +414,7 @@ import { frontInput, frontJson, setFront } from "../actions/front.ts";
418414 import { COPIES_PER_DAY, makeItYours } from "../actions/make_it_yours.ts";
419415 import { passportRoutes } from "./passport_edge.ts";
420416 import { egressGrantRoutes } from "./api/egress_grants.ts";
417 import { forgeRoutes } from "./api/forges.ts";
421418 import { agentRoutes } from "./api/agents.ts";
422419 import { boardRoutes } from "./api/boards.ts";
423420 import { labelsRoutes } from "./api/labels.ts";
@@ -5061,52 +5058,7 @@ export function apiRoutes(router: Router<Handler>): void {
50615058 return timePhase(ctx.request, "render", () => json(view));
50625059 });
50635060
5064 // What small, medium and large mean: public, and cacheable by ETag.
5065 router.add("GET", "/api/v1/forges/sizes", (ctx) => {
5066 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
5067 const text = timePhase(ctx.request, "sizes", () => JSON.stringify(sizesView()));
5068 return timePhase(ctx.request, "render", () => {
5069 const etag = weakEtag(text);
5070 const headers = { etag, "cache-control": "public, max-age=300" };
5071 if (holds(ctx.request, etag)) return new Response(null, { status: 304, headers });
5072 return new Response(text, { headers: { "content-type": "application/json", ...headers } });
5073 });
5074 });
5075
5076 // A forge's children (docs/SPAWN.md): an admin's, planned before made.
5077 router.add("GET", "/api/v1/forges", async (ctx) => {
5078 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
5079 const denied = requireScope(ctx, "ro");
5080 if (denied) return denied;
5081 if (!ctx.auth.user?.isAdmin) return forbidden();
5082 const items = await timePhaseAsync(ctx.request, "read", () => listChildren(ctx.app));
5083 return timePhase(ctx.request, "render", () => json({ items, next_cursor: null }));
5084 });
5085
5086 // `dryRun` is true unless it is `false`: the plan, and nothing is made.
5087 // A stranger may spawn too (rate-limited in the door); a signed-in
5088 // caller needs an `rw` token as before.
5089 router.add("POST", "/api/v1/forges", async (ctx) => {
5090 const input = await body(ctx.request);
5091 if (ctx.auth.user !== undefined) {
5092 const denied = requireScope(ctx, "rw");
5093 if (denied) return denied;
5094 } else if (crossOrigin(ctx.request, ctx.url, ctx.app.log)) {
5095 return forbidden();
5096 }
5097 if (typeof input !== "object" || input === null || Array.isArray(input))
5098 return apiError("invalid", 422, "send a JSON object: name, apps, crew, dryRun");
5099 const door = await spawnDoor(ctx, ctx.auth.user, input as Record<string, unknown>);
5100 return json(door.body, door.status);
5101 });
5102
5103 // A new claim link for a child nobody has claimed (#729); the old one dies.
5104 router.add("POST", "/api/v1/forges/:name/claim-link", async (ctx) => {
5105 const denied = requireScope(ctx, "rw");
5106 if (denied) return denied;
5107 const door = await reissueDoor(ctx, ctx.auth.user!, ctx.params["name"] ?? "");
5108 return json(door.body, door.status);
5109 });
5061 forgeRoutes(router);
51105062
51115063 router.add("GET", "/api/v1/apps", (ctx) => {
51125064 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
src/http/api/forges.ts added +63
@@ -0,0 +1,63 @@
1 /**
2 * A forge's children over the API: `/api/v1/forges` — the sizes, the list,
3 * a spawn (planned before made), and a fresh claim link. Moved out of
4 * `api.ts` (#6331); the rules live in `actions/spawn.ts` and `spawn/`.
5 */
6
7 import { listChildren } from "../../actions/spawn.ts";
8 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
9 import { reissueDoor, spawnDoor } from "../../spawn/door.ts";
10 import { sizesView } from "../../spawn/sizes.ts";
11 import { crossOrigin, type Handler } from "../context.ts";
12 import { holds, weakEtag } from "../etag.ts";
13 import { apiError, body, forbidden, json, requireScope } from "../respond.ts";
14 import type { Router } from "../router.ts";
15
16 export function forgeRoutes(router: Router<Handler>): void {
17 // What small, medium and large mean: public, and cacheable by ETag.
18 router.add("GET", "/api/v1/forges/sizes", (ctx) => {
19 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
20 const text = timePhase(ctx.request, "sizes", () => JSON.stringify(sizesView()));
21 return timePhase(ctx.request, "render", () => {
22 const etag = weakEtag(text);
23 const headers = { etag, "cache-control": "public, max-age=300" };
24 if (holds(ctx.request, etag)) return new Response(null, { status: 304, headers });
25 return new Response(text, { headers: { "content-type": "application/json", ...headers } });
26 });
27 });
28
29 // A forge's children (docs/SPAWN.md): an admin's, planned before made.
30 router.add("GET", "/api/v1/forges", async (ctx) => {
31 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
32 const denied = requireScope(ctx, "ro");
33 if (denied) return denied;
34 if (!ctx.auth.user?.isAdmin) return forbidden();
35 const items = await timePhaseAsync(ctx.request, "read", () => listChildren(ctx.app));
36 return timePhase(ctx.request, "render", () => json({ items, next_cursor: null }));
37 });
38
39 // `dryRun` is true unless it is `false`: the plan, and nothing is made.
40 // A stranger may spawn too (rate-limited in the door); a signed-in
41 // caller needs an `rw` token as before.
42 router.add("POST", "/api/v1/forges", async (ctx) => {
43 const input = await body(ctx.request);
44 if (ctx.auth.user !== undefined) {
45 const denied = requireScope(ctx, "rw");
46 if (denied) return denied;
47 } else if (crossOrigin(ctx.request, ctx.url, ctx.app.log)) {
48 return forbidden();
49 }
50 if (typeof input !== "object" || input === null || Array.isArray(input))
51 return apiError("invalid", 422, "send a JSON object: name, apps, crew, dryRun");
52 const door = await spawnDoor(ctx, ctx.auth.user, input as Record<string, unknown>);
53 return json(door.body, door.status);
54 });
55
56 // A new claim link for a child nobody has claimed (#729); the old one dies.
57 router.add("POST", "/api/v1/forges/:name/claim-link", async (ctx) => {
58 const denied = requireScope(ctx, "rw");
59 if (denied) return denied;
60 const door = await reissueDoor(ctx, ctx.auth.user!, ctx.params["name"] ?? "");
61 return json(door.body, door.status);
62 });
63 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs