pr_01m47d15m3e54sn21z27rpy5n9/apps/api/src/openapi.ts

246 lines8,097 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

docs.g1t.sh, generated OpenAPI with an interactive reference, full footer1import { operationsByName } from "./operations";
2
3/** What the OpenAPI document needs to know about a REST route. */
4export type RouteDoc = {
5 method: "GET" | "POST";
6 path: string;
7 operation: string;
8 tag: string;
9};
10
11const ERROR = { $ref: "#/components/schemas/Error" };
12
13function errorResponse(description: string) {
14 return {
15 description,
16 content: { "application/json": { schema: ERROR } },
17 };
18}
19
20/** A short title from an operation name: `open_intent` is "Open intent". */
21function title(operation: string): string {
22 if (operation === "whoami") return "Get the current user";
23 const words = operation.split("_").join(" ");
24 return words[0].toUpperCase() + words.slice(1);
25}
26
27/** Hono's `:name` path parameters as OpenAPI's `{name}`. */
28function openApiPath(path: string): string {
29 return path.replace(/:([a-z_]+)/g, "{$1}");
30}
31
Device sign-in replaces registering and minting tokens over the API32/** Hand-written entries for device sign-in, which is not an operation. */
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer33const ONBOARDING = {
Device sign-in replaces registering and minting tokens over the API34 "/v1/device/code": {
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer35 post: {
Device sign-in replaces registering and minting tokens over the API36 operationId: "device_code",
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer37 tags: ["Accounts"],
Device sign-in replaces registering and minting tokens over the API38 summary: "Start signing in",
39 description:
40 "Begins a device sign-in. Show the person `verification_uri_complete` and have them open it in a browser, where they sign in or register and approve the code. Then poll `/v1/device/token`.",
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer41 security: [],
42 requestBody: {
43 content: {
44 "application/json": {
45 schema: {
46 type: "object",
47 properties: {
Device sign-in replaces registering and minting tokens over the API48 client_name: {
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer49 type: "string",
Device sign-in replaces registering and minting tokens over the API50 description: "What is asking, shown to the person approving. For example, Claude Code.",
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer51 },
52 },
53 },
54 },
55 },
56 },
57 responses: {
Device sign-in replaces registering and minting tokens over the API58 "200": {
59 description: "The codes for this sign-in.",
60 content: {
61 "application/json": {
62 schema: {
63 type: "object",
64 properties: {
65 device_code: { type: "string", description: "Secret. Send it to /v1/device/token." },
66 user_code: { type: "string", description: "Shown to the person, like WDJB-MJHT." },
67 verification_uri: { type: "string" },
68 verification_uri_complete: {
69 type: "string",
70 description: "The link to give the person; it carries the code.",
71 },
72 expires_in: { type: "integer", description: "Seconds until the codes expire." },
73 interval: { type: "integer", description: "Seconds to wait between polls." },
74 },
75 },
76 },
77 },
78 },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer79 },
80 },
81 },
Device sign-in replaces registering and minting tokens over the API82 "/v1/device/token": {
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer83 post: {
Device sign-in replaces registering and minting tokens over the API84 operationId: "device_token",
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer85 tags: ["Accounts"],
Device sign-in replaces registering and minting tokens over the API86 summary: "Finish signing in",
87 description:
88 "Asks whether the person has approved. Poll no faster than the interval. The token is returned once.",
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer89 security: [],
90 requestBody: {
91 required: true,
92 content: {
93 "application/json": {
94 schema: {
95 type: "object",
Device sign-in replaces registering and minting tokens over the API96 required: ["device_code"],
97 properties: { device_code: { type: "string" } },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer98 },
99 },
100 },
101 },
102 responses: {
Device sign-in replaces registering and minting tokens over the API103 "200": {
104 description: "The state of the sign-in.",
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer105 content: {
106 "application/json": {
107 schema: {
108 type: "object",
Device sign-in replaces registering and minting tokens over the API109 required: ["status"],
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer110 properties: {
Device sign-in replaces registering and minting tokens over the API111 status: { type: "string", enum: ["pending", "approved", "denied", "expired"] },
112 token: { type: "string", description: "Present when approved." },
113 username: { type: "string" },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer114 verified: {
115 type: "boolean",
116 description: "Whether the account's email is confirmed.",
117 },
118 },
119 },
120 },
121 },
122 },
123 },
124 },
125 },
126};
127
128/** The OpenAPI document, generated from the same list the routes are. */
129export function openApiDocument(routes: RouteDoc[]) {
130 const paths: Record<string, Record<string, unknown>> = { ...ONBOARDING };
131 for (const route of routes) {
132 const operation = operationsByName.get(route.operation)!;
133 const pathParams = [...route.path.matchAll(/:([a-z_]+)/g)].map((match) => match[1]);
134 // `owner` and `name` in the path stand for the operation's `repo` input.
135 const covered = new Set([...pathParams, "repo"]);
136 const inputs = Object.entries(operation.input.properties).filter(
137 ([name]) => !covered.has(name),
138 );
139 const required = (operation.input.required ?? []).filter(
140 (name) => !covered.has(name),
141 );
142
143 const parameters: unknown[] = pathParams.map((name) => ({
144 name,
145 in: "path",
146 required: true,
147 schema: { type: name === "number" ? "integer" : "string" },
148 }));
149 let requestBody: unknown;
150 if (route.method === "GET") {
151 for (const [name, schema] of inputs) {
152 parameters.push({
153 // The repository search parameter is `q` on the wire.
154 name: route.operation === "list_repos" && name === "query" ? "q" : name,
155 in: "query",
156 required: false,
157 schema,
158 });
159 }
160 } else if (inputs.length > 0) {
161 requestBody = {
162 required: required.length > 0,
163 content: {
164 "application/json": {
165 schema: {
166 type: "object",
167 properties: Object.fromEntries(inputs),
168 ...(required.length > 0 ? { required } : {}),
169 },
170 },
171 },
172 };
173 }
174
175 const path = openApiPath(route.path);
176 paths[path] ??= {};
177 paths[path][route.method.toLowerCase()] = {
178 operationId: route.operation,
179 tags: [route.tag],
180 summary: title(route.operation),
181 description: operation.description,
182 parameters,
183 ...(requestBody ? { requestBody } : {}),
184 responses: {
185 "200": {
186 description: "Success.",
187 content: { "application/json": { schema: {} } },
188 },
189 "401": errorResponse("A token is required, or the one sent is not valid."),
190 "403": errorResponse("Signed in, but not allowed to do this."),
191 "404": errorResponse("It does not exist, or you cannot see it."),
192 "409": errorResponse("The request conflicts with the current state."),
193 "422": errorResponse("The input is not valid."),
194 },
195 };
196 }
197
198 return {
199 openapi: "3.1.0",
200 info: {
201 title: "g1t API",
202 version: "1",
203 description:
204 "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh.",
205 license: { name: "MIT", identifier: "MIT" },
206 },
207 servers: [{ url: "https://api.g1t.sh" }],
208 security: [{ token: [] }, {}],
209 tags: [
Device sign-in replaces registering and minting tokens over the API210 { name: "Accounts", description: "Signing in from a tool, and the current user." },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer211 { name: "Repositories" },
212 { name: "Intents", description: "Goals stated against a repository." },
213 { name: "Attempts", description: "An agent's or person's run at an intent." },
214 { name: "Sessions", description: "The record of how an attempt was made." },
215 ],
216 paths,
217 components: {
218 securitySchemes: {
219 token: {
220 type: "http",
221 scheme: "bearer",
222 description: "An access token, `g1t_…`. Public data needs none.",
223 },
224 },
225 schemas: {
226 Error: {
227 type: "object",
228 required: ["error"],
229 properties: {
230 error: {
231 type: "object",
232 required: ["code", "message"],
233 properties: {
234 code: {
235 type: "string",
236 enum: ["unauthenticated", "forbidden", "not_found", "conflict", "invalid"],
237 },
238 message: { type: "string" },
239 },
240 },
241 },
242 },
243 },
244 },
245 };
246}