Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cf501d2
Merge pull request #1225 from ChronoAIProject/sync/post-release-v0.16.1
chronoai-shining Aug 6, 2026
b14d68a
feat(api): add OpenAPI helper layer for the spec rewrite (#1214)
chronoai-shining Aug 7, 2026
d34c9fa
feat(api): document skills CRUD and versioning in the OpenAPI spec (#…
chronoai-shining Aug 7, 2026
06bb714
feat(api): document skillsets in the OpenAPI spec (#1214)
chronoai-shining Aug 7, 2026
ad1740e
feat(api): document search, facets, and format in the OpenAPI spec (#…
chronoai-shining Aug 7, 2026
788ec8a
feat(api): document generation, playground, and assistant (#1214)
chronoai-shining Aug 7, 2026
91f475e
feat(api): document audit and analytics in the OpenAPI spec (#1214)
chronoai-shining Aug 7, 2026
5a5d7e9
feat(api): document the account surface in the OpenAPI spec (#1214)
chronoai-shining Aug 7, 2026
6fdc24e
feat(api): document notifications, announcements, and broadcasts (#1214)
chronoai-shining Aug 7, 2026
785adcf
feat(api): document the admin surface in the OpenAPI spec (#1214)
chronoai-shining Aug 7, 2026
042fe0c
feat(api): document admin quota and redemption codes (#1214)
chronoai-shining Aug 7, 2026
3bd9241
feat(api): document the user directory and GitHub mirror (#1214)
chronoai-shining Aug 7, 2026
8fad07e
feat(api): document the OpenAPI endpoint and the K8s probes (#1214)
chronoai-shining Aug 7, 2026
8258d00
refactor(api): assemble the OpenAPI spec from per-domain modules (#1214)
chronoai-shining Aug 7, 2026
165b7d0
chore(api): delete the hand-mirrored openapi/schemas.ts (#1214)
chronoai-shining Aug 7, 2026
84d7194
chore(api): drop the zod-to-json-schema dependency (#1214)
chronoai-shining Aug 7, 2026
4cc6ca3
docs: record the OpenAPI module layout and enforced invariants (#1214)
chronoai-shining Aug 7, 2026
41a2dad
docs: changeset for #1214
chronoai-shining Aug 7, 2026
a3e0962
perf(api): serialize the OpenAPI document once and make it cacheable …
chronoai-shining Aug 7, 2026
070271e
feat(api): document the admin settings surface in the OpenAPI spec (#…
chronoai-shining Aug 7, 2026
581b951
test(api): exempt openapi/ from the hardcoded-URL sweep (#1214)
chronoai-shining Aug 7, 2026
b32b9c1
docs: release notes for the next release (#1214)
chronoai-shining Aug 7, 2026
f883d21
fix(api): correct six inaccuracies in the admin-settings docs (#1214)
chronoai-shining Aug 7, 2026
6ed5ce4
Merge pull request #1227 from ChronoAIProject/feat/openapi-complete-spec
chronoai-shining Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/openapi-complete-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"ornn-api": minor
---

Make `GET /api/v1/openapi.json` a complete, usable contract (#1214).

Two defects made the published spec unusable for client generation:

- **Every schema was empty.** `toSchema` called `zod-to-json-schema@3`, which only understands zod 3 internals and returns `{}` for a zod 4 schema *without throwing*. The document stayed structurally valid while describing nothing: `GET /skill-search` advertised `parameters: []`, and every request and response body was `schema: {}`. `toSchema` now uses zod 4's built-in `z.toJSONSchema`, so property names, types, descriptions, enums, defaults, and numeric bounds all reach the spec. Request and response schemas are generated in opposite directions, so a `.default()` field is correctly optional on the way in and required on the way out. The dependency is removed.
- **Every error response was described wrong.** Errors were documented as `application/json` wrapping the legacy `{ data, error }` envelope, but the API has emitted RFC 7807 `application/problem+json` with fields at the body root since #456. Generated clients read `error.message` and got `undefined`. All error responses now declare the real problem+json body — `type`, `title`, `status`, `detail`, `instance`, `code`, `requestId`, plus per-field `errors[]` on validation failures.

Coverage goes from 13 documented operations to all 107, including previously undocumented domains: skillsets, versions, dist-tags, closures, diffs, audit, analytics, notifications, announcements, broadcasts, quota, redemption codes, admin settings, LLM providers, `/me/*`, `/users/*`, permissions, ownership transfer, the GitHub mirror, and the K8s probes. Every operation now carries a summary, an integrator-facing description, a unique `operationId`, tags, an explicit security declaration, described parameters with schemas and examples, and its full set of error responses.

The spec is now assembled from one module per domain under `src/openapi/paths/`, deriving schemas from the same Zod definitions the handlers validate against. Contract tests enforce both directions against the booted router — no documented endpoint the API does not serve, and no served endpoint the spec does not document — with no allowlist, so the coverage gap cannot silently reopen.
17 changes: 17 additions & 0 deletions .github/release-notes-20260807.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Fixed

- The API reference published empty schemas for every request and response
- Skill search documented none of its query parameters
- Error responses were described with the wrong body shape
- Skill download docs showed a version-tag form that fails
- Few technical bugs fixed

## New Feature

- The machine-readable API reference now covers every endpoint
- Fetching the API reference supports conditional requests and caching

## Changed

- API reference descriptions rewritten for agent developers
- Technical enhancement
3 changes: 0 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 34 additions & 3 deletions docs/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,43 @@ Every response carries:

## 10. OpenAPI

- `GET /v1/openapi.json` is the source of truth.
- `GET /api/v1/openapi.json` is the source of truth. It is generated at server boot from the same Zod schemas the handlers validate against, so it cannot drift from the running API.
- Every route declares security, request content types, all documented error responses, and at least one example.
- CI contract test asserts every handler in code appears in the spec with complete metadata.
- Error `type` URLs point to live documentation per § 1.6.

### 10.1 Skill manifest JSON Schema
### 10.1 Where the spec lives

```
ornn-api/src/openapi/
├── helpers.ts shared building blocks — the ONLY sanctioned way to
│ declare a response, a parameter, or a request body
├── specBuilder.ts document assembly: info, tags, security schemes
└── paths/<domain>.ts one module per domain, exporting (prefix) => PathMap
```

**Adding a route means adding it to the matching `paths/` module in the same change.** There is no allowlist to opt out of; CI fails otherwise (§ 10.2).

Two rules keep the document honest:

- **Schemas are derived, not transcribed.** Import the domain's Zod schema and pass it to `toSchema` / `jsonBody` / `queryParams` / `jsonResponse`. Hand-written JSON Schema is a last resort, permitted only where no Zod schema exists, and must name the TypeScript interface it mirrors.
- **`toSchema` uses zod 4's built-in `z.toJSONSchema`.** Do **not** reintroduce `zod-to-json-schema`: that package only understands zod 3 internals and returns `{}` for every zod 4 schema *without throwing*. It silently emptied every schema and parameter list in the published document (#1214). `helpers.test.ts` and `tests/contract/openapi.test.ts` both assert schemas are non-empty.

Request and response schemas are generated in different directions. A field with a `.default()` is optional on the way in and guaranteed on the way out, so `jsonBody` / `queryParams` use `io: "input"` and `jsonResponse` uses `io: "output"`. Response schemas are emitted without `additionalProperties: false` so adding a field server-side does not break clients validating against a cached spec.

### 10.2 Enforced invariants

`ornn-api/tests/contract/` asserts, against the **booted router**, in both directions:

| Invariant | Meaning | Test |
|---|---|---|
| documented ⇒ registered | The spec never advertises an endpoint the API does not serve. | `openapiRoutes.test.ts` |
| registered ⇒ documented | The API never serves an endpoint the spec does not describe. | `openapiRoutes.test.ts` |

`openapi.test.ts` additionally requires, per operation: a summary, a description of real substance, a unique `operationId`, at least one declared tag (itself declared at the document root), an explicit `security` declaration, every templated path parameter declared, a description and non-empty schema on every parameter, at least one 2xx with content, at least one error response, and every 4xx/5xx typed as `application/problem+json` with RFC 7807 fields at the body root.

Both directions matter. Before #1214 only the first was enforced, and the document decayed to describing 13 of 104 routes while still passing CI.

### 10.3 Skill manifest JSON Schema

The canonical JSON Schema for `SKILL.md` YAML frontmatter is published at:

Expand Down
3 changes: 1 addition & 2 deletions ornn-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"pino-pretty": "^13.1.3",
"posthog-node": "^5.38.6",
"yaml": "^2.9.0",
"zod": "^4.4.3",
"zod-to-json-schema": "^3.25.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@types/bun": "latest",
Expand Down
19 changes: 18 additions & 1 deletion ornn-api/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ContentfulStatusCode } from "hono/utils/http-status";
import { cors } from "hono/cors";
import { join } from "node:path";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import pino from "pino";
import { type SkillConfig } from "./infra/config";

Expand Down Expand Up @@ -1127,7 +1128,23 @@ export async function bootstrap(
serverUrl: config.ornnApiBaseUrl,
version: pkg.version,
});
app.get("/api/v1/openapi.json", (c) => c.json(spec));
// Serialize once at boot. The document is static for the lifetime of the
// process, and now that it describes all 107 operations it is ~1 MB — so
// `c.json(spec)` would re-run JSON.stringify over the whole thing on every
// request. `ETag` + `Cache-Control` per CONVENTIONS.md §8, which lists this
// endpoint as public and cacheable; a conditional request costs a hash
// comparison instead of a megabyte of transfer.
const specJson = JSON.stringify(spec);
const specEtag = `"${createHash("sha256").update(specJson).digest("hex").slice(0, 32)}"`;
logger.info({ bytes: specJson.length, etag: specEtag }, "OpenAPI spec built");
app.get("/api/v1/openapi.json", (c) => {
if (c.req.header("if-none-match") === specEtag) return c.body(null, 304);
return c.body(specJson, 200, {
"Content-Type": "application/json; charset=UTF-8",
"Cache-Control": "public, max-age=300",
ETag: specEtag,
});
});

// Kubernetes liveness probe — process is alive. No dependency checks.
// `/health` kept as an alias for backward compatibility; K8s manifests
Expand Down
Loading
Loading