Agent Provisioning

How an ESP32 running the WakeLink firmware gets its Wi-Fi credentials, its end-to-end EWSP agent token, and its per-agent relay token — using the firmware's built-in AP setup portal. Your master account token never leaves your phone.

Note
The firmware exposes two equivalent surfaces while in provisioning mode: a JSON HTTP API under /api/* consumed by the Android setup wizard (the primary path), and a browser-friendly HTML form on GET / kept as a fallback for desktop users. Both share the same NVS AP password and write to the same wakelink namespace.

Three secrets, three scopes

Provisioning binds three unrelated secrets to the agent. Conflating them is the #1 source of configuration bugs.

EWSP agent token

  • End-to-end secret shared only between your client and your ESP. The relay never sees it.
  • You choose its value during provisioning — anything ≥ 32 random characters. Generate with openssl rand -hex 16.
  • EWSP session keys are HKDF-derived from this token, then used with an XChaCha20-Poly1305 AEAD on every wake packet.
  • Stored in the wakelink NVS namespace as plaintext (never transmitted to server).

Per-agent relay token (wla_…)

  • Generated by the relay when you register an agent via the Android app or CLI. Format: wla_<64 hex chars>.
  • Stored as a SHA-256 hash in the relay's agents table; the ESP32 stores the plaintext in NVS.
  • Used by the firmware to authenticate its WebSocket session to the relay, routing packets to the right agent.
  • Your master account token (wl_…) is never stored on the ESP32. The Android app uses it once to create the agent and receive this wla_ token, then passes it to the agent.
Note
Master account token (wl_…): This is your user-level API token, used by the Android app and CLI to call the relay REST API. It is never provisioned to or stored on the ESP32 hardware. When you create an agent via POST /api/v1/agents/, the relay returns a fresh wla_… agent relay token — that is what the app then sends to the ESP32.

AP portal flow

When the firmware boots without saved credentials it starts a soft-AP and waits for you on a tiny HTML form served on its own subnet.

1 ESP boots into provisioning mode

The agent starts WakeLink-Setup as a soft-AP at 192.168.4.1. On first boot it generates a random 8-character AP password via esp_random() and persists it in NVS (namespace wl_prov, key ap_pass).

2 Retrieve the AP password from NVS

Dump the NVS partition with esptool.py read_flash and parse it with the ESP-IDF nvs_partition_gen.py tool.

3 Join the AP from your phone or laptop

Connect to SSID WakeLink-Setup using the password from NVS. Your client should auto-receive 192.168.4.x via DHCP.

4 Open the form at http://192.168.4.1/

The firmware serves both a CSRF-protected HTML form and a JSON API under /api/*. The Android wizard targets the JSON API.

5 Submit credentials (form) or run the wizard (JSON)

Either fill in the HTML fields and press Save & Connect, or — when using the Android app — let it call the provisioning JSON API endpoints. Both paths end with the agent rebooting into normal mode using the saved credentials.

JSON API (wizard path)

The provisioning JSON API is what the Android setup wizard speaks. Every endpoint accepts and returns application/json. Authentication is a two-step CSRF-token flow rooted in the NVS-stored AP password.

Note
Authentication flow: Call POST /api/login with {"ap_password":"<8 chars>"} to receive a csrf_token. Every subsequent /api/* call must include X-CSRF-Token: <token>.

Endpoints

EndpointDescription
POST /api/loginObtain CSRF token. Returns {ok,csrf_token,agent_info:{chip,mac,fw_version}} + Set-Cookie: wl_verified=1.
GET /api/infoReturns {chip,mac,fw_version,provisioned,agent_id,...}.
GET /api/scanRuns a live Wi-Fi scan. Returns {networks:[{ssid,rssi,encryption,bssid,channel,...}]}.
POST /api/wifiBody: {ssid,password}. Tests connection with a 15 s timeout.
POST /api/saveWrites any subset of NVS fields: ssid, password, server_host, server_port, tls_enabled, agent_id, agent_token, api_token.
POST /api/save-and-restartSame as /api/save plus a reboot 500 ms later.
POST /api/restartReboot in 500 ms.
POST /api/factory-resetWipes both wakelink and wl_prov NVS namespaces, then reboots.
{\`# End-to-end wizard call sequence
curl -X POST http://192.168.4.1/api/login \\
     -H 'Content-Type: application/json' \\
     -d '{"ap_password":"AbCdEfGh"}'\`}

HTML form fallback

The form posts application/x-www-form-urlencoded to POST /save. The CSRF token is embedded in the rendered HTML page.

FieldRequiredDescription
wifi_ssidYesYour Wi-Fi network name.
wifi_passWi-Fi password.
server_hostRelay host, e.g. wakelink-project.org.
server_portDefaults to 443.
tls_enabledCheckbox. Automatically forced on for port 443.
agent_idYesPublic agent identifier, format WL<8 hex>.
agent_tokenYesYour EWSP secret. The relay never sees this.
api_tokenYesYour per-agent relay token (wla_...). Not your master account token.

Rotation & re-provisioning

Reset to AP mode

Hold the firmware's reset button for 10 seconds — saved credentials in the wakelink NVS namespace are wiped.

Rotate EWSP agent token

Reset to AP mode, re-provision with a new agent_token. The relay is not involved — both ends only need to share the same secret.

Rotate per-agent relay token (wla_…)

Call POST /api/v1/agents/{id}/rotate-token from the Android app or CLI. Reset the ESP32 to AP mode and re-provision with the new value.

Rotate master account token (wl_…)

Regenerate it in the dashboard via POST /api/v1/auth/regenerate-token. The ESP32 does not need re-provisioning — it stores wld, not your master token.

Why this is safe

End-to-end encryption

Every wake payload is sealed by the EWSP layer derived from agent_token before it ever leaves your client.

Blind relay

The server only forwards opaque ciphertext. It never holds the EWSP agent token and cannot decrypt payloads.

Master token isolation

Your wl_… master account token never reaches the ESP32 hardware.

Local-only AP setup

The setup portal is reachable only from the ESP's own subnet (~10 m range). It shuts down the moment a config is saved.