Skip to content

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.securitySchemes is empty — there is no declared X-API-KEY auth.
  • servers is only the relative /integration, with no notion of local vs remote.
  • The Protect spec has no tags and info.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):

  1. 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
    
  2. Downloads each pinned upstream spec from the mirror into specs/.cache/ (gitignored).
  3. Applies the OpenAPI Overlay documents in specs/overlays/.
  4. Validates the result with kin-openapi and writes it to specs/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/&lt;app&gt;/&lt;ver&gt;/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 (including mirror-commit) + overlays ⇒ byte-identical specs/build/**.
  • CI re-runs just sync && just gen and fails if committed specs or lib/** 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-sync workflow advances mirror-commit to 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 to versions.yaml and moves default. 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.