Public: anyone can read this; the crew can change it.
Child-forge routes leave api.ts for src/http/api/forges.ts #6955
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/6331-child-forge-routes-leave-api-ts-for-src → main 2 commits fork
2 files
changed +65
−50
src/http/api.ts +2 −50
| @@ -76,11 +76,9 @@ import { emailVerified } from "../core/verify.ts"; | ||
| 76 | 76 | import { readHealth } from "../actions/health.ts"; |
| 77 | 77 | import { isProbe } from "../kernel/probe.ts"; |
| 78 | 78 | import { PRIVATE_ADDRESS_REFUSED } from "../kernel/address_words.ts"; |
| 79 | import { listChildren } from "../actions/spawn.ts"; | |
| 80 | 79 | import { forgeState, PARENT_HEADER } from "../spawn/clients.ts"; |
| 81 | 80 | import { familyView } from "../spawn/family.ts"; |
| 82 | 81 | import { receiveBeat, SIGNATURE_HEADER } from "../spawn/heartbeat.ts"; |
| 83 | import { reissueDoor, spawnDoor } from "../spawn/door.ts"; | |
| 84 | 82 | import { forgeProposals, proposeToParentForge, proposeUpstream } from "../actions/upstream.ts"; |
| 85 | 83 | import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords, takeOrTell } from "../actions/follow.ts"; |
| 86 | 84 | import { relationOf } from "../actions/forks.ts"; |
| @@ -356,8 +354,6 @@ import { repoDir } from "../git/repos.ts"; | ||
| 356 | 354 | import { carriedPulls, pullPath } from "../story/wave_pulls.ts"; |
| 357 | 355 | import { pullRefs, readWave, readWaves } from "../story/waves.ts"; |
| 358 | 356 | import type { Result } from "../kernel/result.ts"; |
| 359 | import { sizesView } from "../spawn/sizes.ts"; | |
| 360 | import { holds, weakEtag } from "./etag.ts"; | |
| 361 | 357 | import { inviteLink, list as listInviteLinks, mint as mintInviteLink, revoke as revokeInviteLink } from "../actions/invites.ts"; |
| 362 | 358 | import { originForRepo, type OriginRepoRow } from "../core/origins.ts"; |
| 363 | 359 | import { subscribe as subscribeJob, type MirrorJob } from "../origins/jobs.ts"; |
| @@ -418,6 +414,7 @@ import { frontInput, frontJson, setFront } from "../actions/front.ts"; | ||
| 418 | 414 | import { COPIES_PER_DAY, makeItYours } from "../actions/make_it_yours.ts"; |
| 419 | 415 | import { passportRoutes } from "./passport_edge.ts"; |
| 420 | 416 | import { egressGrantRoutes } from "./api/egress_grants.ts"; |
| 417 | import { forgeRoutes } from "./api/forges.ts"; | |
| 421 | 418 | import { agentRoutes } from "./api/agents.ts"; |
| 422 | 419 | import { boardRoutes } from "./api/boards.ts"; |
| 423 | 420 | import { labelsRoutes } from "./api/labels.ts"; |
| @@ -5061,52 +5058,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 5061 | 5058 | return timePhase(ctx.request, "render", () => json(view)); |
| 5062 | 5059 | }); |
| 5063 | 5060 | |
| 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); | |
| 5110 | 5062 | |
| 5111 | 5063 | router.add("GET", "/api/v1/apps", (ctx) => { |
| 5112 | 5064 | 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 | } |
new above
offline