@mgcrea/unifi-protect
v0.2.0
Published
A UniFi Protect client for Node: authenticated requests, the realtime update stream, and livestream access
Downloads
125
Maintainers
Readme
@mgcrea/unifi-protect
A client for the UniFi Protect private API: the UniFi OS credential handshake, the bootstrap document, and the realtime update stream — with the console's certificate pinned rather than its verification switched off.
This talks to the console's own private API, the one its web app uses. It is undocumented and Ubiquiti moves endpoints between releases.
nvr.versionis the first thing to check when something that worked stops working.
Why this exists
It was written for @mgcrea/homebridge-unifi-protect, which holds
local-admin credentials for a security system. Owning that path end to end was
the point, so the client is small, has three runtime dependencies, and reads
like something you can audit in an afternoon.
Features
- Certificate pinning that works by IP. UniFi consoles present a self-signed
certificate with no IP SAN, so ordinary verification cannot succeed when you
address the console by address — which is why everything in this space reaches
for
rejectUnauthorized: false. This client captures the certificate once, uses it as its own trust anchor, and replaces the host name check with a SHA-256 fingerprint comparison. See docs/protocol.md. - Session handling that survives a restart, cached owner-only. The password is never logged and never written to disk.
- The realtime update stream, with a pure, tested codec for its binary frame format, a watchdog for the wedged-open socket TCP will not surface, and jittered reconnect backoff.
- A live state store that applies the console's nested partial updates correctly — a temperature delta does not drop your humidity reading.
- Tolerant bootstrap parsing. One device in an unrecognised shape is skipped and named, not allowed to take every other device down with it.
Install
pnpm add @mgcrea/unifi-protectRequires Node 22 or newer.
Usage
import { connectProtect } from "@mgcrea/unifi-protect";
const protect = await connectProtect({
host: "10.0.0.1",
username: "bridge",
password: process.env.PROTECT_PASSWORD!,
stateDir: "./.cache",
store: {
onEvent: (event) => console.log(event.type, event.camera),
onDeviceChanged: (kind, device) => console.log(kind, device.name),
},
});
console.log(`Pinned certificate: ${protect.fingerprint}`);
for (const camera of protect.store.cameras()) console.log(camera.name);
await protect.disconnect();connectProtect resolves once the first bootstrap has landed, so devices can be
enumerated straight away; the update stream is then held open and reconnected
for you.
Options
| Option | Default | Notes |
|---|---|---|
| host | — | Hostname or IP, no scheme. |
| port | 443 | |
| username / password | — | Use a Local Access Only user. A Ubiquiti cloud (SSO) account usually cannot log in locally. |
| totp | — | Of little use to a long-running process; prefer a local user without 2FA. |
| stateDir | — | Where session.json and certificate.json live. Both written 0600. |
| fingerprint | — | A known SHA-256 fingerprint, which skips trust-on-first-use. |
| insecureTls | false | Turns verification off entirely. Logged loudly. Don't. |
Credentials
Create a dedicated local user on the console rather than reusing your own account: Settings → Admins → Add Admin → Local Access Only, with Protect permissions. Give it View access if you only need to read, Admin if you intend to change camera settings.
Certificate pinning
On first connect the client records the console's certificate and prints its fingerprint:
Pinned the certificate for 10.0.0.1: 3F2A…C61D.
Set this as the configured fingerprint to make the trust explicit rather than learned.Trust-on-first-use is only as good as that first moment, so pass the value back
as fingerprint once you have it. If the certificate later changes, connections
fail with a message naming the file to delete if the change was legitimate.
Verifying against your console
UNIFI_PROTECT_HOST=10.0.0.1 \
UNIFI_PROTECT_USERNAME=bridge \
UNIFI_PROTECT_PASSWORD=... \
pnpm probeLogs in, prints the fingerprint, enumerates every device, flags any camera whose smart-detect zones ask for something the device switch does not allow, and then tails the update stream for thirty seconds so you can walk past a camera and watch the events arrive.
Development
pnpm install
pnpm test # lint, typecheck, spec, format:check
pnpm spec # vitest
pnpm builddocs/protocol.md is the annotated companion to src/const.ts — what the wire
actually does, and the traps in it.
Releasing
pnpm release runs the test suite, bumps the version, tags and pushes. The tag
triggers the publish workflow, which uses npm trusted publishing over OIDC —
there is no NPM_TOKEN to store or rotate.
Credits
The wire protocol has been documented by several people over the years, most notably in hjdhjd/unifi-protect. This is an independent implementation; no code was taken from it.
License
MIT
