Skip to content

v0.3.0: typed errors, cookie auth, full v1/v2 endpoint coverage#2

Open
nyo16 wants to merge 10 commits into
masterfrom
feat/v0.3.0
Open

v0.3.0: typed errors, cookie auth, full v1/v2 endpoint coverage#2
nyo16 wants to merge 10 commits into
masterfrom
feat/v0.3.0

Conversation

@nyo16
Copy link
Copy Markdown
Owner

@nyo16 nyo16 commented May 2, 2026

Summary

  • Bumps to v0.3.0 with typed RateLimitError / AuthError (breaking — see UPGRADING.md), SHA-256 cert pinning, cookie + CSRF auth, and UnifiApi.detect/1 controller probe.
  • Adds 18 new operational endpoint modules (events, alarms, anomalies, IDS, rogue APs, rich live wireless stats, history, DPI, traffic, WAN, system log, topology, dashboard, port-forward CRUD, UPS, port
    anomalies, active leases, plus Protect events with thumbnails) — feature parity with unpoller/unpoller's read-mostly surface.
  • Ships UnifiApi.Auth.Session, a supervised GenServer that auto-rotates CSRF from response headers, plus Formatter shortcuts, two new runnable examples, multi-controller recipe, and a Phoenix-style migration
    guide.

What's new

Area Modules / changes
Errors UnifiApi.RateLimitError (parsed Retry-After, clamped 1..300s), UnifiApi.AuthError
TLS cert_fingerprints: SHA-256 pinning, expanded "Self-Signed Certificates" docs
Auth UnifiApi.Auth.Cookie.{login,logout,refresh_csrf,csrf_token}/N (stateless), UnifiApi.Auth.Session (supervised, auto-rotating)
Discovery UnifiApi.detect/1 (UDM vs Cloud Key probe), Client.v1_prefix/0 + :v1_path config
Network (v1 / v2) Events, Alarms, Anomalies, IDS, RogueAP, ClientsLive, ClientsHistory, DPI, Traffic, SystemLog, ActiveLeases, WAN, PortAnomalies, UPS, PortForward (CRUD),
Dashboard, Topology
Protect New Protect.Events (motion/ring/smartDetect with binary thumbnail support)
Client get_v1/3 envelope unwrapper, :params passthrough on every verb
Formatter events/1, alarms/1, clients_live/1, anomalies/1 + :subsystem and :severity colour rules
Docs README badges, "Multiple Controllers" recipe, operational-data coverage table, CHANGELOG.md, UPGRADING.md
Examples operational.exs, protect_events.exs joining quickstart/dashboard/snapshots

Breaking change

401, 403, and 429 responses now return %UnifiApi.AuthError{} and %UnifiApi.RateLimitError{} structs instead of {:error, {status, body}} tuples. Catch-all {:error, _} matches are unaffected; only callers
pattern-matching {:error, {401, _}} etc. need to update. See UPGRADING.md for before/after examples and a search-and-replace cheat sheet.

Validation status

The integration-API surface (Sites, Devices, Clients, Protect cameras, etc.) is unchanged.

The new cookie auth flow, detect/1 heuristic, and all 18 v1/v2 endpoint shapes are sourced from unpoller/unpoller and community documentation. Every module ships with Req.Test plug-based unit tests but
has not been exercised end-to-end against live UDM Pro or Cloud Key hardware. Each module is thin (~30–80 LOC), so any field-name or query-param mismatches encountered in practice will be one-line fixes.

Test plan

  • mix format --check-formatted
  • mix credo --strict (0 issues)
  • mix dialyzer (0 errors)
  • mix test213 passed (was 133 on master, +80 new), all async: true via Req.Test plugs
  • mix docs builds clean with new groups: Network API (Operational, v1), Protect API (v1), Authentication, Errors
  • Smoke test against a real UDM Pro:
    UNIFI_BASE_URL=https://… UNIFI_USERNAME=… UNIFI_PASSWORD=… \
      elixir examples/operational.exs
  • Smoke test against a Cloud Key (set UNIFI_NETWORK_PATH=/integration and UNIFI_V1_PATH="" first; UnifiApi.detect/1 will report the right values).
  • Verify UnifiApi.Auth.Session survives a CSRF rotation in a real session (poll for ~10 minutes, watch for 403s).

Stats

7 commits
~3500 lines added across lib/ + test/ + docs
+18 modules, +1 GenServer, +5 runnable examples

nyo16 added 7 commits May 2, 2026 08:28
Breaking change
- 401, 403, and 429 responses now return %UnifiApi.AuthError{} and
  %UnifiApi.RateLimitError{} structs instead of {:error, {status, body}}
  tuples. RateLimitError carries Retry-After parsed from the response
  header (clamped 1..300s), so pollers can back off without re-parsing.
  Catch-all {:error, _} matches are unaffected; only callers that
  pattern-matched the specific status codes need to update. See
  UPGRADING.md for before/after examples.

Polish
- CHANGELOG.md (Keep-a-Changelog format)
- UPGRADING.md (Phoenix-style migration guide)
- README badges (CI / Hex / HexDocs / License)
- examples/{quickstart,dashboard,snapshots}.exs runnable scripts
- UnifiApi.Network.Devices @moduledoc field reference
- mix.exs maintainers, Changelog/Upgrading links, docs.extras +
  Errors group

Verification
- mix format --check-formatted, mix credo --strict, mix dialyzer all clean
- 143 tests passing (was 133 + 10 new in test/unifi_api/error_test.exs)
Self-signed UDM/Cloud Key controllers previously left users with a
binary choice: verify_ssl: true (rarely usable in practice) or
verify_ssl: false (no authentication of the peer). Pinning a known
SHA-256 fingerprint splits the difference: the connection is rejected
unless the leaf cert matches, without requiring a CA.

- New :cert_fingerprints option on UnifiApi.new/1 and Client.new/1,
  also configurable via Application env. Accepts ssh-keygen-style
  ("sha256:AB:CD:..."), bare colon-separated, or plain 64-char hex.
  Overrides :verify_ssl when set.
- Custom verify_fun pins the leaf certificate's SHA-256; CA validation
  is bypassed (we trust by fingerprint).
- 11 new tests covering parser edge cases and connect_options shape.
- README "Self-Signed Certificates" section now documents all three TLS
  modes with an openssl recipe for extracting fingerprints.

Additive change — no migration required.
Phase 2.3 — UnifiApi.Auth.Cookie
- login/4 supports both :udm (POST /api/auth/login) and :cloud_key
  (POST /api/login) styles. Captures Set-Cookie cookies and X-CSRF-Token
  response header (or csrf_token cookie for Cloud Key) into the returned
  Req.Request. Returned client is stateless — pass to any UnifiApi.Network
  or UnifiApi.Protect module.
- refresh_csrf/2 issues a lightweight GET and updates the token from the
  response, for callers that hit 403 on a write mid-session.
- csrf_token/1 reads the currently installed token.
- logout/2 invalidates the controller-side session.
- Auto-refresh on rotation is a deliberate non-goal in this release;
  documented in the module docs.

Phase 2.4 — UnifiApi.detect/1
- Probes GET / with redirect: false and reports controller style
  (:udm vs :cloud_key) along with the matching network/protect/v1
  prefixes and auth_path. Heuristic from unpoller: 200 → UniFi OS,
  301/302/303 → standalone / Cloud Key.

Phase 3 prep — Client.v1_prefix/0
- New helper that returns the legacy /api/s/{site}/... prefix
  (default /proxy/network for UDM, "" for Cloud Key via :v1_path
  config or UNIFI_V1_PATH env). Foundation for the upcoming v1
  endpoint modules (events, alarms, IDS, anomalies, ClientsLive,
  topology, DPI, traffic, etc.).

mix.exs adds Authentication group to docs.

Tests: 26 new (169 total), all async via Req.Test plugs.
Verification: mix format / credo --strict / dialyzer / test all clean.

Validation status: cookie auth, detect heuristic, and v1_prefix have not
been exercised end-to-end against live UDM Pro / Cloud Key hardware. The
shapes follow unpoller's documented conventions; please file an issue
with controller model + firmware if anything needs adjustment.
The integration API doesn't yet expose the operational data ops users
actually want — events, alarms, rich wireless stats, topology — so this
adds the first batch of legacy v1 / v2 endpoint modules. All require
cookie + CSRF auth via UnifiApi.Auth.Cookie.

Infrastructure
- Client.get_v1/3 unwraps the standard %{"meta" => %{"rc" => "ok"},
  "data" => [...]} envelope, surfacing meta.rc == "error" as
  {:error, {:unifi_error, msg}}.
- Client.{get,post,put,patch,delete}/3 now accept a generic :params
  keyword option for arbitrary query strings (used by v1 modules to
  pass _start, _limit, within without polluting the integration param
  builder).

Modules
- UnifiApi.Network.Events — /api/s/{site}/stat/event, supports
  within_hours, limit, start.
- UnifiApi.Network.Alarms — /api/s/{site}/list/alarm, plus archive/3
  that POSTs cmd: archive-alarm to /cmd/evtmgr.
- UnifiApi.Network.ClientsLive — /api/s/{site}/stat/sta with RSSI,
  signal, noise, CCQ, satisfaction, MCS, etc. Plus list_all/3 for
  /stat/alluser (online + offline history with last_seen).
- UnifiApi.Network.Topology — /v2/api/site/{site}/topology graph.

mix.exs adds a "Network API (Operational, v1)" docs group. README has
a new "Operational Data" section showing the cookie-auth + v1 endpoint
flow, with a UnifiApi.detect/1 probe for unknown controller styles.

178 tests pass (was 169, +9 new). Format / credo --strict / dialyzer
all clean.

Validation status: like the cookie auth and detect heuristic in the
previous commit, the v1 endpoint shapes are sourced from community
documentation (primarily unpoller/unpoller) and not yet exercised
end-to-end. File issues with controller model + firmware if anything
needs adjustment.
Adds the remaining 14 modules to round out the unpoller-equivalent
read-mostly surface, all behind cookie + CSRF auth.

Network operational (legacy v1):
- Anomalies — /stat/anomalies
- IDS — /stat/ips/event (Suricata IDS / IPS detections)
- RogueAP — /stat/rogueap and /rest/rogueknown
- DPI — /stat/sitedpi (by-site) and /stat/stadpi (per-client)
- UPS — /stat/ups-devices
- PortForward — full CRUD on /rest/portforward (the one mutating module
  in this batch)

Network v2 paths:
- ClientsHistory — /v2/.../clients/history
- Traffic — /v2/.../traffic and /v2/.../country-traffic
- SystemLog — /v2/.../system-log/all
- ActiveLeases — /v2/.../active-leases
- WAN — enriched-configuration, isp-status, load-balancing, wan-slas
- PortAnomalies — /v2/.../ports/port-anomalies
- Dashboard — /v2/.../aggregated-dashboard?historySeconds=N

Protect:
- Protect.Events — /proxy/protect/api/events with start/end/types/cameras
  filters, /api/events/{id} for one event, /api/events/{id}/thumbnail
  returning binary JPEG via Client.get_raw, and /api/events/system-logs.

mix.exs — modules grouped under "Network API (Operational, v1)" and a
new "Protect API (v1)" group.

README — replaces the four-module operational example with a full
coverage table covering all 18 modules and a Protect-events thumbnail
recipe.

204 tests pass (was 178, +26 new). mix format / credo --strict /
dialyzer all clean.

Validation status (unchanged): all v1/v2 endpoint shapes come from
community documentation, primarily unpoller/unpoller. Not yet
exercised end-to-end against live UDM Pro or Cloud Key hardware.
File issues with controller model + firmware if anything is shaped
differently; the modules are thin enough that fixes are typically
one-line tweaks to query-param keys or response unwrapping.
Wraps the v0.3.0 work with the user-facing ergonomics that close out
the original plan.

Formatter
- events/1, alarms/1, clients_live/1, anomalies/1 — colored-table
  shortcuts matching the existing devices/clients/cameras pattern.
- New :subsystem (wlan/lan/wan/vpn/ips/system colour-coded) and
  :severity (critical/warn/info) colour rules on table/3.

Examples
- examples/operational.exs — full cookie-auth flow with detect/1,
  recent events, active alarms, worst-RSSI live clients via the
  new Formatter shortcuts.
- examples/protect_events.exs — last-hour Protect events + thumbnail
  saving via Protect.Events.thumbnail/3.

Docs
- README "Multiple Controllers" section with a Task.async_stream
  parallel-poll pattern and a per-controller path-config recipe for
  fleets that mix UDM and Cloud Key gear.

Tests
- Auth.Cookie.logout/2 covered for both :udm and :cloud_key styles.

206 tests passing (was 204). mix format / credo --strict / dialyzer
all clean.
Closes the deferred auto-refresh story in UnifiApi.Auth.Cookie. The
stateless login/4 + refresh_csrf/2 flow is fine for one-shot scripts,
but long-running pollers need the controller's mid-session CSRF
rotations to be handled automatically.

Session is a GenServer that:
- Logs in synchronously during init/1 (so a bad credential surfaces
  as a supervision-time error, not a runtime surprise).
- Returns a Req.Request whose request steps pull the current cookies +
  CSRF from the GenServer at send time, and whose response steps
  capture any rotated x-csrf-token header back into the GenServer
  via cast.
- Exposes refresh/1 (probe-and-update without re-login) and relogin/1
  (full re-login) as explicit escape hatches.

A GenServer is justified per the Elixir Iron Law — the CSRF token
mutates across calls and may be touched concurrently from multiple
consumers. Read-only single-shot usage should stick with
UnifiApi.Auth.Cookie.

mix.exs adds Session to the Authentication docs group; README's
operational-data section now distinguishes one-shot (Cookie) from
long-running (Session) flows; Cookie's @moduledoc points at Session
for the auto-refresh case.

213 tests passing (was 206, +7 new). format / credo --strict /
dialyzer all clean.
@nyo16 nyo16 changed the title Feat/v0.3.0 v0.3.0: typed errors, cookie auth, full v1/v2 endpoint coverage May 2, 2026
nyo16 added 3 commits May 2, 2026 12:22
…matrix

Closes the optional follow-up list from the v0.3.0 PR description.

Streaming
- Client.stream_v1/3: Stream.resource-based paginator using _start /
  _limit (the v1 convention), wrapping Client.get_v1/3.
- Adds stream/3 to Network.Events, Network.Alarms, Network.IDS so
  callers can Enum.to_list / Enum.take across pages without doing
  the bookkeeping by hand.

DPI helpers
- Network.DPI.with_names/2 joins the numeric cat / app IDs returned
  by /stat/sitedpi and /stat/stadpi against the categories and
  applications lists from the integration-API Resources module,
  adding category_name / application_name to every by_cat / by_app
  entry. Unknown IDs map to nil; entries without cat/app keys pass
  through untouched.

Formatter
- New numeric :rssi and :satisfaction colour rules. Signal in dBm
  is bucketed (>=-60 green, -60..-70 yellow, <-70 red); satisfaction
  uses 80/50 thresholds. Wired into the clients_live/1 shortcut.

CI
- test job matrix expanded to three Elixir/OTP combos:
    Elixir 1.18.0 on OTP 26.2.5  (minimum supported, stdlib JSON)
    Elixir 1.18.3 on OTP 27.2    (canonical; matches other jobs)
    Elixir 1.18.4 on OTP 27.2    (latest 1.18 + latest OTP)
  format / credo / dialyzer / publish keep the canonical combo.

220 tests passing (was 213). format / credo --strict / dialyzer all
clean. All four optional follow-ups are additive — nothing existing
breaks.
The previous commit landed Client.stream_v1, the v1 stream/3 variants,
DPI.with_names, and the numeric Formatter colour rules with @doc/inline
docs but didn't update the README.

- "Available stream functions" table split into two: integration API
  (offset/limit) and operational v1 (Events/Alarms/IDS via _start/_limit).
  Plus a short example block and a pointer to Client.stream_v1/3 for
  the rest.
- "Operational Data" section gains a "DPI with names" subsection
  showing the with_names/2 + Resources.list_dpi_* recipe with a
  top-10-apps example.
- "Formatted Output" gains an "Operational shortcuts" block covering
  events/alarms/clients_live/anomalies and a colour-rules table
  documenting :state, :type, :subsystem, :severity, :rssi (numeric),
  :satisfaction (numeric).

mix format clean. mix docs regenerated.
Pagination
- Audit found two endpoints with paging opts but no stream/N: the
  v2 ClientsHistory (pageSize / pageNumber) and SystemLog (same).
- Adds Client.stream_paged/2 — a generic page-number paginator
  (cursor + increment, halts on a short page) so v2 modules don't
  have to roll their own Stream.resource.
- Wires ClientsHistory.stream/3 and SystemLog.stream/3 on top of it.
- Other modules without streams (Anomalies, RogueAP, ClientsLive,
  ActiveLeases, PortAnomalies, UPS, PortForward, DPI, Topology, WAN,
  PortAnomalies, Protect.Events) are unpaginated by design — confirmed
  by the audit.

QoL helpers
- UnifiApi.ping/1: auth-agnostic GET / reachability check, returns
  :ok for any 2xx-3xx, {:error, _} otherwise.
- UnifiApi.Time: now_ms / minutes_ago / hours_ago / days_ago for the
  unix-millisecond params used by Protect.Events.list, Traffic, etc.
  Cuts the System.os_time(:millisecond) - 60 * 60 * 1000 boilerplate.
- Network.Sites.find_by_name/2 and find_by_internal_reference/2:
  resolve a site by display name or controller slug.

Docs
- README gets a "Quality-of-Life Helpers" section under Quick Start
  showing ping, detect, find_by_name, and the Time helpers.
- Streaming table picks up a v2 (pageSize / pageNumber) row for the
  new streams; pointer to Client.stream_paged/2 for ad-hoc cases.

235 tests passing (was 220, +15 new). format / credo --strict /
dialyzer all clean. All additive — nothing existing breaks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant