Spec Augmentation — UniFi¶
Problem¶
The official UniFi OpenAPI documents (served by developer.ui.com, mirrored daily by
opastorello/unifi-api-docs) are not directly
generatable:
components.securitySchemesis empty — there is no declaredX-API-KEYauth.serversis only the relative/integration, with no notion of local vs remote.- The Protect spec has no
tagsandinfo.version: 0.0.0, so generated operations would not group into meaningful sets and the version is meaningless.
We therefore transform upstream into an SDK-ready spec deterministically.
Approach: pull + overlay (not vendored)¶
We do not commit raw upstream JSON. Instead tools/specgen (invoked by just sync and in CI):
- Reads
specs/versions.yaml— the source of truth for pinned versions:mirror: https://raw.githubusercontent.com/opastorello/unifi-api-docs mirror-commit: 6425bbc2… # immutable; full 40-char SHA enforced by specgen network: default: v10.3.58 versions: [v10.3.58] protect: default: v7.1.46 versions: [v7.1.46] retain: all # coexisting versions are kept; pruning is an explicit decision - Downloads each pinned upstream spec from the mirror into
specs/.cache/(gitignored). - Applies the OpenAPI Overlay documents in
specs/overlays/. - Validates the result with
kin-openapiand writes it tospecs/build/<app>/<appversion>/openapi.json(committed; consumed by codegen and the docs viewer).
flowchart LR
versions["specs/versions.yaml"] --> pull["pull (mirror)"]
pull --> cache["specs/.cache/*.json"]
cache --> overlays["apply overlays"]
overlays --> validate["validate (kin-openapi)"]
validate --> build["specs/build/<app>/<ver>/openapi.json"]
Overlays¶
| File | Responsibility |
|---|---|
common.overlay.yaml |
Add securitySchemes.ApiKeyAuth (type: apiKey, in: header, name: X-API-KEY); set global security; define parameterized local + remote servers with variables (host, consoleId). |
network.overlay.yaml |
Network-specific server defaults; pin info.version to the app version. |
protect.overlay.yaml |
Synthesize tags from path segments (e.g. /v1/cameras/* → Cameras, /v1/viewers/* → Viewers); pin info.version. |
Overlays are the only place corrections are made — lib/ is never hand-edited. If upstream adds
an endpoint that needs special handling, the fix is an overlay action, keeping regeneration
deterministic.
Server augmentation¶
The injected servers express both transports so the generated base URL and docs viewer are correct:
servers:
- url: "https://{host}/proxy/{app}/integration"
description: Local console
variables:
host: { default: "unifi.local" }
app: { enum: [network, protect], default: network }
- url: "https://api.ui.com/v1/connector/consoles/{consoleId}/{app}/integration"
description: Remote cloud connector
variables:
consoleId: { default: "" }
app: { enum: [network, protect], default: network }
At runtime the root unifi.Conn constructs the concrete base URL; the server block primarily drives
documentation and validation.
Determinism & drift¶
Two things have to be pinned, not one. versions pins the UniFi API version;
mirror-commit pins the bytes. The mirror re-scrapes developer.ui.com on a schedule and
rewrites files in place under an unchanged version directory — and some of what it emits (e.g.
unresolved "$1e"-style description placeholders) is position-dependent, so it churns even when the
API itself has not moved. A version path therefore does not identify a fixed document.
mirror-commit was the ref HEAD until 2026-07-31. The consequence was that just sync returned
different bytes over time for an unchanged tree, so the drift guard below failed on upstream
re-scrapes rather than on real drift — CI went red with nothing in this repo having changed.
Config.Validate now rejects anything that is not a full 40-character SHA.
- Same
versions.yaml(includingmirror-commit) + overlays ⇒ byte-identicalspecs/build/**. - CI re-runs
just sync && just genand fails if committed specs orlib/**differ (drift guard). With the pin immutable, this catches drift in this repo — an overlay or fixup edited without regenerating — which is what it was always meant to catch. - The scheduled
spec-syncworkflow advancesmirror-committo the mirror's current HEAD, re-syncs, and opens a PR with whatever that changed; and a separate PR that appends a newly available version toversions.yamland movesdefault. Upstream churn arrives as a reviewable PR instead of as a red build.
Protect v7.1.83¶
The requested Protect v7.1.83 is newer than the mirror's latest and is not cleanly extractable from
the developer.ui.com SPA. We pin v7.1.46 now; when the mirror publishes v7.1.83 the version-bump
PR adds it as a coexisting package and promotes it to default. See
ADR-0007.