feat(api): add interactive OpenAPI documentation (#264)

Co-authored-by: jarvis <jarvis@agents.realmroot.dev>
This commit is contained in:
realmroot[bot]
2026-09-04 17:27:53 +00:00
committed by GitHub
parent fca1cf21e8
commit d16fdd860f
4 changed files with 114 additions and 0 deletions
+1
View File
@@ -44,6 +44,7 @@
"@base-ui/react": "^1.3.0",
"@fontsource-variable/geist": "^5.2.8",
"@realmroot/enbor-sdk": "0.3.0",
"@scalar/hono-api-reference": "^0.12.0",
"@tanstack/react-query": "^5.100.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+70
View File
@@ -26,6 +26,9 @@ importers:
'@realmroot/enbor-sdk':
specifier: 0.3.0
version: 0.3.0
'@scalar/hono-api-reference':
specifier: ^0.12.0
version: 0.12.0(hono@4.13.5)
'@tanstack/react-query':
specifier: ^5.100.9
version: 5.102.8(react@19.2.8)
@@ -2066,6 +2069,32 @@ packages:
cpu: [x64]
os: [win32]
'@scalar/client-side-rendering@0.3.10':
resolution: {integrity: sha512-FlUByzYhsas54zIvsiTJ7UgAZiVUQmErWvrQm+CKTBnIvSmIb5eWowJ4/O0bbkUa8rsOIdmCcX+HtX+EDdf0CA==}
engines: {node: '>=22'}
'@scalar/helpers@0.11.2':
resolution: {integrity: sha512-IgHW/hIj1XAvsPIBKiOgmK87qbyDjaQQg9S/auXU4MUtvnwKKpOeIOJc0NC71FkMdr4Mok0SSVI748cmlptoXQ==}
engines: {node: '>=22'}
'@scalar/hono-api-reference@0.12.0':
resolution: {integrity: sha512-K8WA0NTDnwCmTvh33AmkHuoolDuxEW7Jst/Am7+tcijPgFcsGE0l3O7Vk9RkNMMQT643Vy9nJDyhfnkMIWA5vQ==}
engines: {node: '>=22'}
peerDependencies:
hono: ^4.12.5
'@scalar/schemas@0.8.4':
resolution: {integrity: sha512-gbx/UjAswjJc+OZp/vcVRIopjqoJ62pOkrKLvVHa1MuMRoVxcPyNzW2/SQmnvdJku7C7a6PsKaPUQ6gZXGA4eg==}
engines: {node: '>=22'}
'@scalar/types@0.18.3':
resolution: {integrity: sha512-hPLLxVt/ah4RpCVp5UrLEpnBEgTwf+RvPtRiSEty3QqfBJUfADMXHz+oWK6UAa/vALg2AWuJLCD8hRlyQ2ET9w==}
engines: {node: '>=22'}
'@scalar/validation@0.6.3':
resolution: {integrity: sha512-j3s9XPv8Wo1EG5/naBi4XGF0uXYQ2A3QZNI0NKWgCMxRHVrDJGlZEYymcHDHY7fquRm73P8U3c8xqJqB5yad6w==}
engines: {node: '>=20'}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -4300,6 +4329,10 @@ packages:
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tailwind-merge@3.6.0:
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
@@ -4380,6 +4413,10 @@ packages:
peerDependencies:
tailwindcss: '>=4.0.0-0'
type-fest@5.9.0:
resolution: {integrity: sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==}
engines: {node: '>=20'}
type-is@2.1.0:
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
engines: {node: '>= 18'}
@@ -6462,6 +6499,33 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.63.1':
optional: true
'@scalar/client-side-rendering@0.3.10':
dependencies:
'@scalar/schemas': 0.8.4
'@scalar/types': 0.18.3
'@scalar/validation': 0.6.3
'@scalar/helpers@0.11.2': {}
'@scalar/hono-api-reference@0.12.0(hono@4.13.5)':
dependencies:
'@scalar/client-side-rendering': 0.3.10
hono: 4.13.5
'@scalar/schemas@0.8.4':
dependencies:
'@scalar/helpers': 0.11.2
'@scalar/validation': 0.6.3
'@scalar/types@0.18.3':
dependencies:
'@scalar/helpers': 0.11.2
nanoid: 5.1.16
type-fest: 5.9.0
zod: 4.5.4
'@scalar/validation@0.6.3': {}
'@sec-ant/readable-stream@0.4.1': {}
'@sindresorhus/is@7.2.0': {}
@@ -8951,6 +9015,8 @@ snapshots:
systeminformation@5.33.6: {}
tagged-tag@1.0.0: {}
tailwind-merge@3.6.0: {}
tailwindcss@4.3.3: {}
@@ -9019,6 +9085,10 @@ snapshots:
dependencies:
tailwindcss: 4.3.3
type-fest@5.9.0:
dependencies:
tagged-tag: 1.0.0
type-is@2.1.0:
dependencies:
content-type: 2.1.0
+25
View File
@@ -1,3 +1,4 @@
import { Scalar } from "@scalar/hono-api-reference";
import { RESOURCE_SCOPES } from "@server/auth/realmroot";
import { akPublicUrl, akResource } from "@server/config/serviceUrls";
import type { Env } from "@server/env";
@@ -27,6 +28,30 @@ export function registerResourceServerRoutes(app: Api): void {
});
});
app.get("/api/openapi.json", (c) => c.json(toolboxDocument(c.env)));
app.get(
"/api/docs",
Scalar({
url: "/api/openapi.json",
pageTitle: "Agent Kanban API Docs",
theme: "default",
darkMode: true,
customCss: `
:root {
--scalar-font: "Geist", ui-sans-serif, system-ui, sans-serif;
--scalar-font-code: "Geist Mono", ui-monospace, monospace;
--scalar-color-accent: #0891b2;
--scalar-background-accent: rgba(8, 145, 178, 0.1);
}
.dark-mode {
--scalar-color-accent: #22d3ee;
--scalar-background-1: #09090b;
--scalar-background-2: #18181b;
--scalar-background-3: #27272a;
--scalar-background-accent: rgba(34, 211, 238, 0.1);
}
`,
}),
);
app.get("/api/toolbox/openapi.json", (c) => c.notFound());
}
@@ -11,6 +11,24 @@ const env = {
const resource = `${env.AK_PUBLIC_ORIGIN}/api`;
describe("Resource Server HTTP capabilities", () => {
it("serves interactive API documentation for the canonical OpenAPI document", async () => {
const docs = await api.request("/api/docs", {}, env);
const openapi = await api.request("/api/openapi.json", {}, env);
expect(docs.status).toBe(200);
expect(docs.headers.get("content-type")).toContain("text/html");
const html = await docs.text();
expect(html).toContain("Agent Kanban API Docs");
expect(html).toContain("/api/openapi.json");
expect(openapi.status).toBe(200);
expect(openapi.headers.get("content-type")).toContain("application/json");
await expect(openapi.json()).resolves.toMatchObject({
openapi: "3.1.0",
info: { title: "Agent Kanban API" },
});
});
it("[spec: resource-server/discovery] publishes protected-resource and Toolbox discovery over HTTP", async () => {
const metadata = await api.request("/.well-known/oauth-protected-resource/api", {}, env);
const service = await api.request("/api", {}, env);