v0.3.0: typed errors, cookie auth, full v1/v2 endpoint coverage#2
Open
nyo16 wants to merge 10 commits into
Open
v0.3.0: typed errors, cookie auth, full v1/v2 endpoint coverage#2nyo16 wants to merge 10 commits into
nyo16 wants to merge 10 commits into
Conversation
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RateLimitError/AuthError(breaking — see UPGRADING.md), SHA-256 cert pinning, cookie + CSRF auth, andUnifiApi.detect/1controller probe.anomalies, active leases, plus Protect events with thumbnails) — feature parity with
unpoller/unpoller's read-mostly surface.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 migrationguide.
What's new
UnifiApi.RateLimitError(parsedRetry-After, clamped 1..300s),UnifiApi.AuthErrorcert_fingerprints:SHA-256 pinning, expanded "Self-Signed Certificates" docsUnifiApi.Auth.Cookie.{login,logout,refresh_csrf,csrf_token}/N(stateless),UnifiApi.Auth.Session(supervised, auto-rotating)UnifiApi.detect/1(UDM vs Cloud Key probe),Client.v1_prefix/0+:v1_pathconfigEvents,Alarms,Anomalies,IDS,RogueAP,ClientsLive,ClientsHistory,DPI,Traffic,SystemLog,ActiveLeases,WAN,PortAnomalies,UPS,PortForward(CRUD),Dashboard,TopologyProtect.Events(motion/ring/smartDetect with binary thumbnail support)get_v1/3envelope unwrapper,:paramspassthrough on every verbevents/1,alarms/1,clients_live/1,anomalies/1+:subsystemand:severitycolour rulesCHANGELOG.md,UPGRADING.mdoperational.exs,protect_events.exsjoiningquickstart/dashboard/snapshotsBreaking 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 callerspattern-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/1heuristic, and all 18 v1/v2 endpoint shapes are sourced fromunpoller/unpollerand community documentation. Every module ships withReq.Testplug-based unit tests buthas 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-formattedmix credo --strict(0 issues)mix dialyzer(0 errors)mix test— 213 passed (was 133 on master, +80 new), allasync: trueviaReq.Testplugsmix docsbuilds clean with new groups: Network API (Operational, v1), Protect API (v1), Authentication, ErrorsUNIFI_NETWORK_PATH=/integrationandUNIFI_V1_PATH=""first;UnifiApi.detect/1will report the right values).UnifiApi.Auth.Sessionsurvives a CSRF rotation in a real session (poll for ~10 minutes, watch for 403s).Stats