access/README.md
Patrick Mulligan 40564f06ec feat: access extension — NFC door access via boltcards SUN + Home Assistant
Promoted from the door-portal scratch repo to its own repo for install via the
aiolabs catalog. Authenticates a tapped Bolt Card via boltcards /verify,
authorizes against per-door grants, and fires the door's local Home Assistant
webhook to unlock a Z-Wave lock. Fails closed; logs every attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 22:47:41 +02:00

70 lines
2.8 KiB
Markdown

# Access — LNbits extension
NFC **door access** for LNbits. A tapped Bolt Card (NTAG424) is authenticated
via the `boltcards` SUN check, authorized against per-door **grants**, and — if
allowed — the door's local **Home Assistant** webhook is fired to unlock a
Z-Wave lock. Every tap is logged.
Runs on the **on-prem LNbits** on the door's LAN, so the unlock path stays local
and the door does not depend on the internet.
## Flow
```text
Pi + PN532 reader
→ POST /access/api/v1/check (X-Controller-Token: <door token>)
{ "doorId": "...", "external_id": "...", "p": "...", "c": "..." }
1. authenticate → boltcards /verify (valid, non-replayed SUN?)
2. authorize → active grant for this external_id on this door?
3. actuate → POST door.ha_webhook_url (→ lock.unlock → Z-Wave)
4. log → access.logs (allow/deny + reason)
→ { "allow": true|false, "reason": "..." }
```
The reader never unlocks directly; this extension owns the allow/deny decision
and the Home Assistant call. Fails **closed** at every step.
## Data model
- **doors** — a resource: `name`, `controller_token` (reader secret),
`ha_webhook_url` (local HA webhook), `boltcards_base_url` (blank = this
instance), `unlock_timeout_ms`, `enabled`.
- **grants** — `external_id → door` permission, `enabled`/`expires_at`.
- **logs** — every allow/deny with a reason.
## Admin API (wallet admin key)
- `GET/POST /access/api/v1/doors`, `PUT/DELETE /access/api/v1/doors/{id}`
- `GET/POST /access/api/v1/grants`, `DELETE /access/api/v1/grants/{id}`
- `GET /access/api/v1/logs`
## Reader API (controller token)
```http
POST /access/api/v1/check
X-Controller-Token: <door.controller_token>
Content-Type: application/json
{ "doorId": "front-door", "external_id": "abc123", "p": "<32-hex>", "c": "<16-hex>" }
```
## Setup
1. Install on the on-prem LNbits (same instance as `boltcards`).
2. Create a **door**, set its Home Assistant webhook URL, copy its
**controller token** to the reader config.
3. Issue Bolt Cards in `boltcards` as usual; add a **grant** (the card's
`external_id` → the door).
4. Point the door reader (Pi) at `/access/api/v1/check` with the token.
## Notes
- Card authentication delegates to `boltcards` over loopback HTTP via its
**`/api/v1/verify/{external_id}`** endpoint (aiolabs fork ≥ **1.1.1-aio.2**) —
a side-effect-light SUN check that returns `{"authenticated": true, …}`
without `/scan`'s spend semantics (no withdrawRequest, no daily-limit, no
"hit"), while still advancing the SUN counter for replay protection.
- Requires boltcards **1.1.1-aio.2+** installed on the same LNbits.
- Empty `ha_webhook_url` = authorize + log only (bring-up mode; no unlock).
- Keep this on the LAN; use a strong per-door controller token; never expose
Home Assistant publicly.