Public: anyone can read this; the crew can change it.
The OpenAPI document describes the admin's writes: a capacity setting, a limit and a team #5133
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5116-the-openapi-document-describes-the-admin → main 2 commits fork 1 thread · 1 unresolved
2 files
changed +55
−0
src/contract/openapi.ts +43
| @@ -7,6 +7,9 @@ | ||
| 7 | 7 | * added here, and `test/openapi.test.ts` fails until it is. |
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | import { CAPACITY_KEYS } from "../core/capacity.ts"; | |
| 11 | import { LIMIT_KEYS } from "../settings/limits.ts"; | |
| 12 | ||
| 10 | 13 | interface Route { |
| 11 | 14 | readonly method: "get" | "post" | "put" | "patch" | "delete"; |
| 12 | 15 | readonly path: string; |
| @@ -212,7 +215,19 @@ export const ROUTES: readonly Route[] = [ | ||
| 212 | 215 | "Read the forge's limits (a person admin's token, any scope); answers { items: [{ key, value, isSet, defaultValue, ... }], changes, next_cursor }", |
| 213 | 216 | auth: true, |
| 214 | 217 | refusals: { not_found: "the token is not a person who administers the forge" }, |
| 218 | }, { | |
| 219 | method: "put", | |
| 220 | path: "/api/v1/admin/limits/{key}", | |
| 221 | summary: `Change one limit live, or send value null to take it back to its default (a person admin's token, admin scope); {key} is one of ${LIMIT_KEYS.join(", ")}; answers the changed limit`, | |
| 222 | auth: true, | |
| 223 | body: { value: "integer or null" }, | |
| 224 | refusals: { | |
| 225 | not_found: "the token is not a person who administers the forge (404)", | |
| 226 | no_such_limit: "{key} is not a limit (422)", | |
| 227 | out_of_bounds: "the value is outside the limit's bounds, named in the message (422)", | |
| 228 | }, | |
| 215 | 229 | }, |
| 230 | ||
| 216 | 231 | { |
| 217 | 232 | method: "get", |
| 218 | 233 | path: "/api/v1/admin/capacity", |
| @@ -220,7 +235,20 @@ export const ROUTES: readonly Route[] = [ | ||
| 220 | 235 | "Read the forge's capacity settings (a person admin's token, any scope); answers { items: [{ key, label, help, value, source, default, env, changed? }], changes, next_cursor }", |
| 221 | 236 | auth: true, |
| 222 | 237 | refusals: { not_found: "the token is not a person who administers the forge" }, |
| 238 | }, { | |
| 239 | method: "put", | |
| 240 | path: "/api/v1/admin/capacity/{key}", | |
| 241 | summary: `Change one capacity setting live, or send value null to take it back to its default (a person admin's token, admin scope); {key} is one of ${CAPACITY_KEYS.join(", ")}; answers the changed setting`, | |
| 242 | auth: true, | |
| 243 | body: { value: "string or null" }, | |
| 244 | refusals: { | |
| 245 | not_found: "the token is not a person who administers the forge (404)", | |
| 246 | no_such_setting: "{key} is not a capacity setting (422)", | |
| 247 | invalid_value: "the value is outside what this setting takes (422)", | |
| 248 | no_credential: "the setting needs a credential the forge does not hold (422)", | |
| 249 | }, | |
| 223 | 250 | }, |
| 251 | ||
| 224 | 252 | { |
| 225 | 253 | method: "get", |
| 226 | 254 | path: "/api/v1/admin/teams", |
| @@ -228,7 +256,22 @@ export const ROUTES: readonly Route[] = [ | ||
| 228 | 256 | "Read the crew's teams (a person admin's token, any scope); answers { teams: [{ slug, charter, dial, members, ... }] }", |
| 229 | 257 | auth: true, |
| 230 | 258 | refusals: { not_found: "the token is not a person who administers the forge" }, |
| 259 | }, { | |
| 260 | method: "put", | |
| 261 | path: "/api/v1/admin/teams/{slug}", | |
| 262 | summary: "Change a crew team's charter, dial or members; send only what changes (a person admin's token, admin scope); answers the team", | |
| 263 | auth: true, | |
| 264 | body: { charter: "string", dial: "integer", members: "array of agent ids" }, | |
| 265 | refusals: { | |
| 266 | invalid_body: "charter is not text, dial not a number, or members not a list of agent ids (422)", | |
| 267 | not_found: "no such team, or the token is not a person who administers the forge (404)", | |
| 268 | bad_dial: "dial is not a whole number in range (422)", | |
| 269 | bad_charter: "the charter is too long (422)", | |
| 270 | over_100: "the teams' dials together would pass 100% (422)", | |
| 271 | bad_member: "a member is not an agent on this forge (422)", | |
| 272 | }, | |
| 231 | 273 | }, |
| 274 | ||
| 232 | 275 | { |
| 233 | 276 | method: "get", |
| 234 | 277 | path: "/api/v1/admin/people", |
test/openapi.test.ts +12
| @@ -204,6 +204,18 @@ test("an app's routes are described, and env values are never read back", () => | ||
| 204 | 204 | expect(doc.paths[`${A}/{env}/redeploy`].post.responses["202"]).toBeDefined(); |
| 205 | 205 | }); |
| 206 | 206 | |
| 207 | test("the admin's writes are described: a capacity setting, a limit and a team", () => { | |
| 208 | const doc = openapi("https://f.test", "1") as any; | |
| 209 | for (const path of ["capacity/{key}", "limits/{key}", "teams/{slug}"]) { | |
| 210 | const put = doc.paths[`/api/v1/admin/${path}`]?.put; | |
| 211 | expect(put, path).toBeDefined(); | |
| 212 | expect(put.security).toBeDefined(); | |
| 213 | expect(put.requestBody).toBeDefined(); | |
| 214 | } | |
| 215 | expect(doc.paths["/api/v1/admin/capacity/{key}"].put.summary).toContain("egress"); | |
| 216 | expect(doc.paths["/api/v1/admin/limits/{key}"].put.summary).toContain("api.anonymous_per_source"); | |
| 217 | }); | |
| 218 | ||
| 207 | 219 | test("a person's sign-ins and ending one are described, with no session secret", () => { |
| 208 | 220 | const doc = openapi("https://f.test", "1") as any; |
| 209 | 221 | const list = doc.paths["/api/v1/users/{username}/signins"]?.get; |
new above
offline