Developer

Heron API Contract

Version: 0.4.0 Date: 2026-06-22 Status: Current. CLI tool added, /debug and /token documented

Overview

Heron has two integration surfaces, used by three clients:

SurfaceUsed byPurpose
USB WebSerialheron-onboard PWA onlyFirst-time setup over USB (identify, challenge, provision)
Local HTTP :8080heron-dashboard + heron CLIOngoing monitoring on LAN

Important: The local dashboard and CLI do not use WebSerial and do not authenticate with device identity. They find the Pico on the network and read the HTTP API.

Passive monitor: observe only

Heron is a passive DNS observer. It runs a local DNS server that forwards every query transparently to upstream (1.1.1.1 / 8.8.8.8) and logs it. It does not block, redirect, or filter. The buzzer on the board is an alert, not enforcement.


USB WebSerial Provisioning Protocol

Apps: heron-onboard ↔ Pico firmware (lib/provisioning.py)

Transport: USB CDC/ACM, 115200 baud, newline-delimited JSON

Browser: Chrome/Edge only (WebSerial API)

Opening the serial port resets the Pico. Unprovisioned devices enter the provisioning loop immediately (before I2C/OLED init).

Boot signals

While waiting for commands, the Pico emits a ready beacon:

{"status": "ready"}

Repeated every ~2 seconds. The PWA waits for this before sending commands.

identify

Read device identity from key storage.

Send:

{"cmd":"identify"}

Response:

{"status":"ok","serial":"75e409665340a674","has_keys":true}

serial is an 8-byte hex identifier. has_keys is true when cryptographic keys are present.

challenge

Cryptographic device attestation. The PWA sends a random 32-byte nonce; the device signs it with its private key and returns the signature plus its certificate.

Send:

{"cmd":"challenge","nonce":"<64 hex chars>"}

Response:

{"status":"ok","serial":"75e409665340a674","cert":"<hex>","signature":"<hex>"}

Errors: missing_nonce, bad_nonce, bad_nonce_length, no_keys, no_cert, sign_failed:*

The PWA verifies the certificate against the embedded Northsline root public key, then verifies the nonce signature against the device public key extracted from the cert. Both checks use Web Crypto (SubtleCrypto, ECDSA P-256). No network call.

scan

Return available WiFi networks as JSON.

Send:

{"cmd":"scan"}

Response:

{
  "status": "ok",
  "networks": [
    {"ssid": "HomeWiFi", "bssid": "aa:bb:cc:dd:ee:ff", "channel": 6, "rssi": -45, "hidden": false}
  ]
}

Capped at 15 networks.

router_info

Return the connected AP's BSSID and the Pico's IP address.

Send:

{"cmd":"router_info"}

Response:

{"status":"ok","bssid":"aa:bb:cc:dd:ee:ff","ip":"192.168.1.42"}

Errors: {"status":"error","reason":"not_connected"} if WiFi is not connected.

provision

Write Wi-Fi credentials to /config.json. No sticker code needed. Device identity was already verified via challenge.

Send:

{"cmd":"provision","ssid":"HomeWiFi","pass":"hunter2"}

Response: {"status":"ok"} or {"status":"error","reason":"..."}

ReasonMeaning
bad_jsonLine could not be parsed
unknown_cmdMissing or invalid cmd
missing_wifiSSID or password missing
write_failed:*Flash write error

After success, device reboots, joins Wi-Fi, starts DNS + HTTP.

Onboarding PWA flow (UI order)

1. Connect     WebSerial open → wait for ready → identify
2. Verify      Cryptographic challenge-response (WebSerial + Web Crypto)
3. Wifi        WebSerial scan networks → provision with SSID/pass
4. Router      WebSerial router_info → OUI lookup → router DNS instructions
5. Done        User unplugs → wall power

Source: heron-onboard/src/lib/components/StickerGate.svelte, heron-onboard/src/lib/serial.ts, heron-onboard/src/lib/crypto.ts


Device Identity (crypto attestation)

Device verification is fully offline. No cloud, no server, no account.

Certificate format (binary)

[0:1]       signature length (1 byte)
[1:1+sigLen] DER-encoded ECDSA signature (root signs serial || pubKey)
[1+sigLen:1+sigLen+8]   serial number (8 bytes)
[1+sigLen+8:1+sigLen+8+65] public key (65 bytes, uncompressed)

Crypto primitives

  • Curve: NIST P-256 (secp256r1)
  • Signature: ECDSA with SHA-256
  • Browser verification: Web Crypto SubtleCrypto (native, no dependencies)
  • Device signing: Pure Python ECDSA (lib/ecdsa.py, ~3s on RP2350)

Serial number

8-byte hex (e.g., 75E409665340A674). Printed on sticker for reference. No security role. The cryptographic identity is what proves the device is genuine.


Local HTTP API

App: heron-dashboard and heron CLI

Base URL:

  • Primary: http://heron.local:8080 (mDNS)
  • Fallback: http://<PICO_IP>:8080

Discovery (heron-dashboard/src/lib/api/discovery.ts, heron-cli/heron_cli/cli.py):

  1. Probe heron.local:8080/health (2s timeout)
  2. Probe saved IP (localStorage / ~/.heron-host)
  3. Manual IP entry (banner / --host flag)

No authentication. LAN access only.

GET /health

{"status": "ok"}

GET /audit/weekly

Query params: since (epoch seconds), limit (default 50, max 150)

Response (MVP):

[
  {
    "source": "192.168.1.26",
    "domain": "telemetry.example.com",
    "timestamp": 1717123456.789,
    "flagged": false
  }
]

timestamp is seconds (float). flagged always false in MVP.

GET /devices

[
  {
    "id": "187",
    "ip": "192.168.1.26",
    "name": "Device at 192.168.1.26",
    "first_seen": 1717120000,
    "last_seen": 1717123456,
    "trust_level": "unknown",
    "query_count": 12,
    "flagged_count": 0
  }
]

GET /stats

{
  "total_queries": 1240,
  "unique_domains": 45,
  "flagged_count": 3,
  "device_count": 7,
  "period_start": 1717120000
}

GET /allowlist

[
  {
    "id": "1717123456789012345",
    "pattern": "*.telemetry.microsoft.com",
    "created_at": 1717123456
  }
]

PUT /allowlist

Request: { "pattern": "*.example.com" }

Response: 201 { "status": "ok", "id": "..." }

DELETE /allowlist/<id>

200 {"status":"ok"} or 404

GET /debug

Raw internal state. Useful for diagnosing empty-log problems.

{
  "dns_requests": {"count": 42, "last_5": [...]},
  "device_tracker": {"device_count": 5, "devices": [...]},
  "dns_last_error": null,
  "dns_sock_bound": true
}

GET /token

Truncated device token hint. Full token never exposed publicly.

{"status": "ok", "token_hint": "a1b2c3d4"}

Returns 404 if no token exists.

Not implemented (MVP)

  • GET /config, POST /config. Wi-Fi changes via USB reprovision only
  • GET /devices/<id>

CLI Tool

A terminal client (heron) lives in heron/backend/heron-cli/. It talks to

the same HTTP API as the dashboard and adds capabilities the web UI can't

offer: per-device live streams, snapshot-based change detection, exit-code

monitoring for cron jobs, and JSON output for piping to other tools.

Install: pip install ./heron-cli (from the backend/ directory)

Commands:

CommandDescription
heronPretty summary (stats, devices, activity)
heron report --tableCompact table format
heron report --jsonRaw JSON output (pipeable)
heron report --compactOne-line status
heron watchLive refresh every 5s
heron follow <ip>Live DNS query stream from one device
heron diffWhat changed since last run
heron monitorSilent check: exit 0 if OK, exit 1 if alert
heron allow add/rm/lsAllowlist management
heron debugRaw /debug dump
heron --host <ip>Manual device IP

Source: heron/backend/heron-cli/heron_cli/cli.py

Dependencies: Python 3.8+ stdlib only. Zero external packages.


Data Adaptation Layer

The official dashboard (heron-dashboard/src/lib/api/adapters.ts) transforms firmware MVP responses into rich UI types:

  • Seconds → milliseconds (timestamp * 1000)
  • source IP → deviceId via /devices cache
  • Generated event IDs from source + domain + timestamp
  • Defaults for severity, kind, vendor, bytes, etc.

Optional for third-party clients; required for the official dashboard UI.


Error Responses (HTTP)

  • 200 OK
  • 201 Created (allowlist)
  • 204 CORS preflight
  • 400 Bad request
  • 404 Not found
  • 503 MemoryError during serialization

Changelog

DateVersionChange
2026-05-31v0.1.0Initial draft
2026-06-01v0.2.0Heron B: WebSerial + /activate + weekly audit
2026-06-07v0.2.1Data Adaptation Layer section
2026-06-07v0.2.2App separation, ready beacon, onboarding flow, dashboard discovery
2026-06-20v0.3.0Cryptographic device attestation replaces cloud activation. challenge command added. provision no longer needs sticker code. Cloud /activate removed. Fully offline.
2026-06-22v0.4.0heron CLI tool added (heron-cli/heron_cli/cli.py). /debug and /token endpoints documented. CLI adds: report (table/json/compact), watch, follow, diff, monitor, allow, debug.