openvibers/openvibe.games Commits

/openapi.json: the game server's public API as OpenAPI 3.1 (SEO-3) (#275)

OP OpenVibers committed 10 days ago · 6442abb64b · 2 files, +56 −0

It answered 404. The discovery handler serves the contracts' generated document for
games (openvibe-contracts), without the operations they mark internal-only.

Changes

ddiff --git a/apps/server/src/net/discovery.test.ts b/apps/server/src/net/discovery.test.ts
iindex 232c7b4..ee01952 100644
---- a/apps/server/src/net/discovery.test.ts
++++ b/apps/server/src/net/discovery.test.ts
@@@ -190,6 +190,21 @@ describe('crawl artifacts (plan T11)', () => {
expect((await send('/app-icons/..%2F..%2Fpackage.json')).status).not.toBe(200)
})
++ it('serves /openapi.json (SEO-3): the contracts\' OpenAPI 3.1 document, without internal-only operations', async () => {
++ const r = await send('/openapi.json')
++ expect(r.status).toBe(200)
++ expect(r.headers['content-type']).toBe('application/json; charset=utf-8')
++ expect(r.headers['access-control-allow-origin']).toBe('*')
++ const doc = JSON.parse(r.body) as { openapi: string; paths: Record<string, Record<string, { 'x-openvibe-visibility'?: string | string[] }>> }
++ expect(doc.openapi).toMatch(/^3\.1/)
++ const ops = Object.values(doc.paths).flatMap((item) => Object.values(item))
++ expect(ops.length).toBeGreaterThan(0)
++ for (const op of ops) {
++ const vis = ([] as string[]).concat(op['x-openvibe-visibility'] ?? [])
++ expect(vis.length > 0 && vis.every((v) => v === 'internal')).toBe(false)
++ }
++ })
++
it('serves /llms-full.txt: text/plain, the home page, and nothing robots keeps out', async () => {
const r = await send('/llms-full.txt')
expect(r.status).toBe(200)
ddiff --git a/apps/server/src/net/discovery.ts b/apps/server/src/net/discovery.ts
iindex 4d535a6..294f536 100644
---- a/apps/server/src/net/discovery.ts
++++ b/apps/server/src/net/discovery.ts
@@@ -335,6 +335,35 @@ export function homeJsonLdTags(): string {
.join('\n')
}
++type OpenApiDoc = { openapi?: string; paths?: Record<string, Record<string, unknown>> }
++const contractsOpenApi = createRequire(import.meta.url)('openvibe-contracts').openapi as { document(service: string): OpenApiDoc | null }
++const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'patch', 'head', 'options'])
++
++/**
++ * GET /openapi.json (SEO-3): the game server's public API as OpenAPI 3.1, generated from the contracts
++ * (openvibe-contracts), without the operations they mark internal-only.
++ */
++export function openApiBody(): string | null {
++ const doc = contractsOpenApi.document('games')
++ if (!doc) return null
++ const paths: Record<string, Record<string, unknown>> = {}
++ for (const [p, item] of Object.entries(doc.paths ?? {})) {
++ const kept: Record<string, unknown> = {}
++ for (const [k, op] of Object.entries(item ?? {})) {
++ if (!HTTP_METHODS.has(k)) {
++ kept[k] = op
++ continue
++ }
++ const raw = (op as { 'x-openvibe-visibility'?: string | string[] } | null)?.['x-openvibe-visibility']
++ const vis = raw === undefined ? [] : ([] as string[]).concat(raw)
++ if (vis.length && vis.every((v) => v === 'internal')) continue
++ kept[k] = op
++ }
++ if (Object.keys(kept).some((k) => HTTP_METHODS.has(k))) paths[p] = kept
++ }
++ return JSON.stringify({ ...doc, paths })
++}
++
function send(res: ServerResponse, type: string, body: string): void {
res.writeHead(200, {
'content-type': `${type}; charset=utf-8`,
@@@ -356,6 +385,7 @@ export function discoveryHandler(rq: IncomingMessage, res: ServerResponse): bool
url !== '/sitemap.xml' &&
url !== '/llms.txt' &&
url !== '/llms-full.txt' &&
++ url !== '/openapi.json' &&
url !== '/manifest.webmanifest' &&
!icon
) {
@@@ -366,6 +396,17 @@ export function discoveryHandler(rq: IncomingMessage, res: ServerResponse): bool
res.end()
return true
}
++ if (url === '/openapi.json') {
++ const body = openApiBody()
++ if (!body) {
++ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
++ res.end('No API document.')
++ return true
++ }
++ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' })
++ res.end(rq.method === 'HEAD' ? undefined : body)
++ return true
++ }
if (url === '/manifest.webmanifest') {
send(res, 'application/manifest+json', manifestBody())
return true