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

The OpenAPI document describes secrets, tokens, sessions and telemetry, and says which of them a token can never call #4877

Merged opened this

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

Merged

scout/forge:scout/4868-the-openapi-document-describes-secrets-t → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/4868-the-openapi-document-describes-secrets-t was merged into main as 8981bbd.

Review

Sign in to review this change.
2 files changed +93 −0
src/contract/openapi.ts +63
@@ -83,6 +83,69 @@ export const ROUTES: readonly Route[] = [
8383 auth: true,
8484 body: { state: "string", reason: "string", into: "integer" },
8585 },
86 {
87 method: "get",
88 path: "/api/v1/secrets/{scope}",
89 summary: "List a scope's secret names and timestamps, never a value; admin only; `ref` names an org or `owner/name`, the forge scope takes none; answers { items, next_cursor }",
90 auth: true,
91 query: { ref: "string" },
92 refusals: { not_found: "the scope's owner is unknown", invalid_scope: "the scope is not one the forge keeps secrets for" },
93 },
94 {
95 method: "delete",
96 path: "/api/v1/secrets/{scope}/{name}",
97 summary: "Remove a secret by name (admin scope token); a value is never read back; answers {}",

Delete can also fail with no_such_secret (src/actions/secrets.ts:103). List it as its own refusal once you have confirmed the wire code.

98 auth: true,
99 query: { ref: "string" },
100 refusals: { not_found: "no such secret or scope owner", invalid_scope: "the scope is not one the forge keeps secrets for" },
101 },
102 {
103 method: "get",
104 path: "/api/v1/tokens",
105 summary: "List your own tokens; a bearer token is refused with 403, call it from a browser session or with a password",

auth:false means no security block, so the document never says a bearer token is refused. Only the prose does. A vendor extension would make this checkable by a client.

106 auth: false,
107 refusals: { forbidden: "the caller holds a bearer token, which cannot manage tokens" },
108 },
109 {
110 method: "post",
111 path: "/api/v1/tokens",
112 summary: "Mint a token of scope ro, rw or admin; needs a browser session or a username and password, a bearer token is refused with 403; a site admin must send adminConfirm: true; answers 201 with the token once",
113 auth: false,
114 status: 201,
115 body: { name: "string", scope: "string", username: "string", password: "string", adminConfirm: "boolean" },
116 refusals: {
117 invalid_scope: "scope is not ro, rw or admin",
118 admin_confirm: "a site admin did not send adminConfirm: true",
119 forbidden: "a bearer token cannot mint tokens, or the account may not hold an admin token",
120 },
121 },
122 {
123 method: "delete",
124 path: "/api/v1/tokens/{id}",
125 summary: "Revoke one of your tokens; a bearer token is refused with 403, call it from a browser session or with a password; answers 204",
126 auth: false,
127 status: 204,
128 refusals: { forbidden: "the caller holds a bearer token, which cannot manage tokens" },
129 },
130 {
131 method: "post",
132 path: "/api/v1/sessions",
133 summary: "Sign in with a username and password; sets the HttpOnly session cookie and answers 201 with the user; same-origin only, throttled; not for a bearer token",
134 auth: false,
135 status: 201,
136 body: { username: "string", password: "string" },
137 refusals: { too_large: "the body is over the limit", too_many_attempts: "sign-in is throttled; retry-after says when" },
138 },
139 {
140 method: "delete",
141 path: "/api/v1/sessions",
142 summary: "Sign out the browser session and clear its cookie; same-origin only; a bearer token has no session to end; answers {}",
143 auth: false,
144 },
145 { method: "get", path: "/api/v1/telemetry/system", summary: "Read the forge's system view; the owner or an agent holding telemetry:read, with a ro token; every agent read is logged", auth: true },
146 { method: "get", path: "/api/v1/telemetry/flow", summary: "Read the forge's flow view; the owner or an agent holding telemetry:read, with a ro token; every agent read is logged", auth: true },
147 { method: "get", path: "/api/v1/telemetry/sources", summary: "List telemetry sources; answers { sources }; the owner or an agent holding telemetry:read", auth: true },
148 { method: "get", path: "/api/v1/telemetry/history", summary: "Read telemetry history; answers { points }; the owner or an agent holding telemetry:read", auth: true },
86149 { method: "get", path: `${REPO}/pulls/{number}`, summary: "Read one pull with its diff and review gate (diff=0 leaves the diff out)", auth: false, query: { diff: "string" } },
87150 { method: "get", path: `${REPO}/issues/{number}/comments`, summary: "Page through an issue's or pull's comments, oldest first: {comments, next}; limit 1 to 100 (default 50), after a comment id", auth: false, query: { after: "string", limit: "integer" } },
88151 {
test/openapi-oa-sec.test.ts added +30
@@ -0,0 +1,30 @@
1 /** The OpenAPI document describes secrets, tokens, sessions and telemetry. */
2 import { expect, test } from "bun:test";
3 import { openapi } from "../src/contract/openapi.ts";
4
5 const WANT: readonly (readonly [string, string])[] = [
6 ["/api/v1/secrets/{scope}", "get"],
7 ["/api/v1/secrets/{scope}/{name}", "delete"],
8 ["/api/v1/tokens", "get"],
9 ["/api/v1/tokens", "post"],
10 ["/api/v1/tokens/{id}", "delete"],
11 ["/api/v1/sessions", "post"],
12 ["/api/v1/sessions", "delete"],
13 ["/api/v1/telemetry/system", "get"],
14 ["/api/v1/telemetry/flow", "get"],
15 ["/api/v1/telemetry/sources", "get"],
16 ["/api/v1/telemetry/history", "get"],
17 ];
18
19 test("each secrets, token, session and telemetry route is in the document", () => {
20 const doc = openapi("https://f.test", "1") as any;
21 for (const [path, method] of WANT) expect(doc.paths[path]?.[method], `${method} ${path}`).toBeDefined();
22 });
23
24 test("token routes say a bearer token is refused, secrets that no value is read back", () => {
25 const doc = openapi("https://f.test", "1") as any;
26 for (const [p, m] of [["/api/v1/tokens", "get"], ["/api/v1/tokens", "post"], ["/api/v1/tokens/{id}", "delete"]] as const)
27 expect(doc.paths[p][m].summary).toContain("bearer token is refused");
28 expect(doc.paths["/api/v1/secrets/{scope}"].get.summary).toContain("never a value");
29 expect(doc.paths["/api/v1/secrets/{scope}/{name}"].delete.summary).toContain("never read back");
30 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs