# NetworkManager API Documentation

## Overview

The network manager API provides methods for managing WiFi connections, monitoring network state, and handling credential prompts through NetworkManager or iwd (and systemd-networkd for ethernet only). Communication occurs over a message-based protocol (websocket, IPC, etc.) with event subscriptions for state updates.

## API Methods

### network.hotspot.configure

Create or update the DMS-managed hotspot profile. Hotspot support is capability-gated: clients should require API v28+, `hotspotSupported: true`, and `hotspotAvailable: true` from network state before showing hotspot controls.

For this implementation, only hotspot-capable backends such as NetworkManager should accept this action. Unsupported backends return an error such as `hotspot not supported by active network backend`.

Configuration changes are rejected while the DMS hotspot is active or activating; stop it before updating the profile.

**Request:**
```json
{
  "method": "network.hotspot.configure",
  "params": {
    "ssid": "Dank Hotspot",
    "password": "optional-password",
    "device": "wlan0",
    "band": "bg"
  }
}
```

**Parameters:**
- `ssid` (string, required): Hotspot SSID to advertise.
- `password` (string, optional): WPA-PSK password. Omit for an open hotspot when the backend allows it.
- `device` (string, optional): Wi-Fi interface name to use, for example `wlan0`. When omitted, the backend picks an AP-capable radio at start time, preferring one that is already hosting the hotspot, then an idle radio, and only as a last resort a radio carrying an active connection (which NetworkManager will disconnect). Network state exposes `apCapable` on each `wifiDevices` entry so clients can predict this choice.
- `band` (string, optional): Requested NetworkManager band: `bg` for 2.4GHz or `a` for 5GHz.

**Response:**
```json
{
  "success": true,
  "message": "hotspot configured"
}
```

### network.hotspot.start

Start the previously configured DMS-managed hotspot profile. This action does not accept or require SSID/password parameters; call `network.hotspot.configure` first when changing hotspot settings.

A successful response only means the activation was requested; the outcome is reported asynchronously through network state updates. While activation is in flight, `hotspotActivating` is `true`; on success `hotspotEnabled` becomes `true`; on failure `hotspotActivating` returns to `false` and `hotspotLastError` carries one of `hotspot-ip-config-failed` (IP sharing setup failed, commonly a missing `dnsmasq`, which NetworkManager's shared IPv4 method requires), `hotspot-supplicant-failed` (the Wi-Fi driver could not start AP mode), or `hotspot-failed`. `hotspotLastError` is cleared on the next successful start.

**Request:**
```json
{
  "method": "network.hotspot.start"
}
```

**Response:**
```json
{
  "success": true,
  "message": "hotspot started"
}
```

### network.hotspot.stop

Stop the active DMS-managed hotspot connection. It must not stop arbitrary user-created hotspot profiles.

**Request:**
```json
{
  "method": "network.hotspot.stop"
}
```

**Response:**
```json
{
  "success": true,
  "message": "hotspot stopped"
}
```

### network.hotspot.getSecrets

Retrieve the stored password of the DMS-managed hotspot profile, for prefilling edit forms. Returns an empty string for an open hotspot. Network state exposes `hotspotSecured` so clients can tell an open hotspot apart from a secured one without fetching the secret.

**Request:**
```json
{
  "method": "network.hotspot.getSecrets"
}
```

**Response:**
```json
{
  "password": "the-stored-psk"
}
```

### network.wifi.connect

Initiate a WiFi connection.

**Request:**
```json
{
  "method": "network.wifi.connect",
  "params": {
    "ssid": "NetworkName",
    "password": "optional-password",
    "interactive": true
  }
}
```

**Parameters:**
- `ssid` (string, required): Network SSID
- `password` (string, optional): Pre-shared key for WPA/WPA2/WPA3 networks
- `interactive` (boolean, optional): Enable credential prompting if authentication fails or password is missing. Automatically set to `true` when connecting to secured networks without providing a password.

**Response:**
```json
{
  "success": true,
  "message": "connecting"
}
```

**Behavior:**
- Returns immediately; connection happens asynchronously
- State updates delivered via `network` service subscription
- Credential prompts delivered via `network.credentials` service subscription

### network.credentials.submit

Submit credentials in response to a prompt.

**Request:**
```json
{
  "method": "network.credentials.submit",
  "params": {
    "token": "correlation-token",
    "secrets": {
      "psk": "password"
    },
    "save": true
  }
}
```

**Parameters:**
- `token` (string, required): Token from credential prompt
- `secrets` (object, required): Key-value map of credential fields
- `save` (boolean, optional): Whether to persist credentials (default: false)

**Common secret fields:**
- `psk`: Pre-shared key for WPA2/WPA3 personal networks
- `identity`: Username for 802.1X enterprise networks
- `password`: Password for 802.1X enterprise networks

### network.credentials.cancel

Cancel a credential prompt.

**Request:**
```json
{
  "method": "network.credentials.cancel",
  "params": {
    "token": "correlation-token"
  }
}
```

## Event Subscriptions

### Subscribing to Events

Subscribe to receive network state updates and credential prompts:

```json
{
  "method": "subscribe",
  "params": {
    "services": ["network", "network.credentials"]
  }
}
```

Both services are required for full connection handling. Missing `network.credentials` means credential prompts won't be received.

### network Service Events

State updates are sent whenever network configuration changes:

```json
{
  "service": "network",
  "data": {
    "networkStatus": "wifi",
    "isConnecting": false,
    "connectingSSID": "",
    "wifiConnected": true,
    "wifiSSID": "MyNetwork",
    "wifiIP": "192.168.1.100",
    "lastError": ""
  }
}
```

**State fields:**
- `networkStatus`: Current connection type (`wifi`, `ethernet`, `disconnected`)
- `isConnecting`: Whether a connection attempt is in progress
- `connectingSSID`: SSID being connected to (empty when idle)
- `wifiConnected`: Whether associated with an access point
- `wifiSSID`: Currently connected network name
- `wifiIP`: Assigned IP address (empty until DHCP completes)
- `savedWifiNetworks` (API v26+): Saved WiFi profiles exposed at SSID granularity. If a backend has multiple profiles for the same SSID, DMS merges them into one SSID-level entry. Clients talking to older servers should derive saved visible networks from `wifiNetworks` entries where `saved` is true.
- `savedWifiNetworks[].outOfRange` (API v26+): Whether the saved profile is not currently visible in scan results. Fallback entries derived from `wifiNetworks` should be treated as visible (`outOfRange: false`).
- `hotspotSupported` (API v28+): Whether the active backend implements hotspot actions.
- `hotspotAvailable` (API v28+): Whether hotspot support is usable on this backend/device set. For NetworkManager this means at least one AP-capable managed Wi-Fi device exists, independent of Wi-Fi radio enabled state.
- `hotspotConfigured` (API v28+): Whether the DMS-managed hotspot profile exists.
- `hotspotEnabled` (API v28+): Whether the DMS-managed hotspot profile is currently active.
- `hotspotActivating` (API v28+): Whether hotspot activation is currently in progress.
- `hotspotSecured` (API v28+): Whether the configured hotspot uses password-based security.
- `hotspotSSID` (API v28+): Configured DMS hotspot SSID.
- `hotspotDevice` (API v28+): Optional configured Wi-Fi device for the DMS hotspot.
- `hotspotBand` (API v28+): Optional configured hotspot band (`bg` or `a`).
- `hotspotLastError` (API v28+): Machine-readable error from the most recent failed hotspot activation. Cleared when the next start succeeds.
- `lastError`: Error message from last failed connection attempt

### network.credentials Service Events

Credential prompts are sent when authentication is required:

```json
{
  "service": "network.credentials",
  "data": {
    "token": "unique-prompt-id",
    "ssid": "NetworkName",
    "setting": "802-11-wireless-security",
    "fields": ["psk"],
    "hints": ["wpa3", "sae"],
    "reason": "Credentials required"
  }
}
```

**Prompt fields:**
- `token`: Unique identifier for this prompt (use in submit/cancel)
- `ssid`: Network requesting credentials
- `setting`: Authentication type (`802-11-wireless-security` for personal WiFi, `802-1x` for enterprise)
- `fields`: Array of required credential field names
- `hints`: Additional context about the network type
- `reason`: Human-readable explanation (e.g., "Previous password was incorrect")

## Connection Flow

### Typical Timeline

```
T+0ms     Call network.wifi.connect
T+10ms    Receive {"success": true, "message": "connecting"}
T+100ms   State update: isConnecting=true, connectingSSID="Network"
T+500ms   Credential prompt (if needed)
T+1000ms  Submit credentials
T+3000ms  State update: wifiConnected=true, wifiIP="192.168.x.x"
```

### State Machine

```
IDLE
  |
  | network.wifi.connect
  v
CONNECTING (isConnecting=true, connectingSSID set)
  |
  +-- Needs credentials
  |     |
  |     v
  |   PROMPTING (credential prompt event)
  |     |
  |     | network.credentials.submit
  |     v
  |   back to CONNECTING
  |
  +-- Success
  |     |
  |     v
  |   CONNECTED (wifiConnected=true, wifiIP set, isConnecting=false)
  |
  +-- Failure
        |
        v
      ERROR (isConnecting=false, !wifiConnected, lastError set)
```

## Connection Success Detection

A connection is successful when all of the following are true:

1. `wifiConnected` is `true`
2. `wifiIP` is set and non-empty
3. `wifiSSID` matches the target network
4. `isConnecting` is `false`

Do not rely on `wifiConnected` alone - the device may be associated with an access point but not have an IP address yet.

**Example:**
```javascript
function isConnectionComplete(state, targetSSID) {
    return state.wifiConnected &&
           state.wifiIP &&
           state.wifiIP !== "" &&
           state.wifiSSID === targetSSID &&
           !state.isConnecting;
}
```

## Error Handling

### Error Detection

Errors occur when a connection attempt stops without success:

```javascript
function checkForFailure(state, wasConnecting, targetSSID) {
    // Was connecting, now idle, but not connected
    if (wasConnecting &&
        !state.isConnecting &&
        state.connectingSSID === "" &&
        !state.wifiConnected) {
        return state.lastError || "Connection failed";
    }
    return null;
}
```

### Common Error Scenarios

#### Wrong Password

**Detection methods:**

1. Quick failure (< 3 seconds from start)
2. `lastError` contains "password", "auth", or "secrets"
3. Second credential prompt with `reason: "Previous password was incorrect"`

**Handling:**
```javascript
if (prompt.reason === "Previous password was incorrect") {
    // Show error, clear password field, re-focus input
}
```

#### Network Out of Range

**Detection:**
- `lastError` contains "not-found" or "connection-attempt-failed"

#### Connection Timeout

**Detection:**
- `isConnecting` remains true for > 30 seconds

**Implementation:**
```javascript
let timeout = setTimeout(() => {
    if (currentState.isConnecting) {
        handleTimeout();
    }
}, 30000);
```

#### DHCP Failure

**Detection:**
- `wifiConnected` is true
- `wifiIP` is empty after 15+ seconds

### Error Message Translation

Map technical errors to user-friendly messages:

| lastError value | Meaning | User message |
|----------------|---------|--------------|
| `secrets-required` | Password needed | "Please enter password" |
| `authentication-failed` | Wrong password | "Incorrect password" |
| `connection-removed` | Profile deleted | "Network configuration removed" |
| `connection-attempt-failed` | Generic failure | "Failed to connect" |
| `network-not-found` | Out of range | "Network not found" |
| `(timeout)` | Timeout | "Connection timed out" |

## Credential Handling

### Secret Agent Architecture

The credential system uses a broker pattern:

```
NetworkManager -> SecretAgent -> PromptBroker -> UI -> User
                                       ^
                                       |
                                  User Response
                                       |
NetworkManager <- SecretAgent <- PromptBroker <- UI
```

### Implementing a Broker

```go
type CustomBroker struct {
    ui       UIInterface
    pending  map[string]chan network.PromptReply
}

func (b *CustomBroker) Ask(ctx context.Context, req network.PromptRequest) (string, error) {
    token := generateToken()
    b.pending[token] = make(chan network.PromptReply, 1)

    // Send to UI
    b.ui.ShowCredentialPrompt(token, req)

    return token, nil
}

func (b *CustomBroker) Wait(ctx context.Context, token string) (network.PromptReply, error) {
    select {
    case <-ctx.Done():
        return network.PromptReply{}, errors.New("timeout")
    case reply := <-b.pending[token]:
        return reply, nil
    }
}

func (b *CustomBroker) Resolve(token string, reply network.PromptReply) error {
    if ch, ok := b.pending[token]; ok {
        ch <- reply
        close(ch)
        delete(b.pending, token)
    }
    return nil
}
```

### Credential Field Types

**Personal WiFi (802-11-wireless-security):**
- Fields: `["psk"]`
- UI: Single password input

**Enterprise WiFi (802-1x):**
- Fields: `["identity", "password"]`
- UI: Username and password inputs

### Building Secrets Object

```javascript
function buildSecrets(setting, fields, formData) {
    let secrets = {};

    if (setting === "802-11-wireless-security") {
        secrets.psk = formData.password;
    } else if (setting === "802-1x") {
        secrets.identity = formData.username;
        secrets.password = formData.password;
    }

    return secrets;
}
```

## Best Practices

### Track Target Network

Always store which network you're connecting to:

```javascript
let targetSSID = null;

function connect(ssid) {
    targetSSID = ssid;
    // send request
}

function onStateUpdate(state) {
    if (!targetSSID) return;

    if (state.wifiSSID === targetSSID && state.wifiConnected && state.wifiIP) {
        // Success for the network we care about
        targetSSID = null;
    }
}
```

### Implement Timeouts

Never wait indefinitely for a connection:

```javascript
const CONNECTION_TIMEOUT = 30000; // 30 seconds
const DHCP_TIMEOUT = 15000;       // 15 seconds

let timer = setTimeout(() => {
    if (stillConnecting) {
        handleTimeout();
    }
}, CONNECTION_TIMEOUT);
```

### Handle Credential Re-prompts

Wrong passwords trigger a second prompt:

```javascript
function onCredentialPrompt(prompt) {
    if (prompt.reason.includes("incorrect")) {
        // Show error, but keep dialog open
        showError("Wrong password");
        clearPasswordField();
    } else {
        // First time prompt
        showDialog(prompt);
    }
}
```

### Clean Up State

Reset tracking variables on success, failure, or cancellation:

```javascript
function cleanup() {
    clearTimeout(timer);
    targetSSID = null;
    closeDialogs();
}
```

### Subscribe to Both Services

Missing `network.credentials` means prompts won't arrive:

```javascript
// Correct
services: ["network", "network.credentials"]

// Wrong - will miss credential prompts
services: ["network"]
```

## Testing

### Connection Test Checklist

- [ ] Connect to open network
- [ ] Connect to WPA2 network with password provided
- [ ] Connect to WPA2 network without password (triggers prompt)
- [ ] Enter wrong password (verify error and re-prompt)
- [ ] Cancel credential prompt
- [ ] Connection timeout after 30 seconds
- [ ] DHCP timeout detection
- [ ] Network out of range
- [ ] Reconnect to already-configured network

### Verifying Secret Agent Setup

Check connection profile flags:
```bash
nmcli connection show "NetworkName" | grep flags
# Should show: 802-11-wireless-security.psk-flags: 1 (agent-owned)
```

Check agent registration in logs:
```
INFO: Registered with NetworkManager as secret agent
```

## Security

- Never log credential values (passwords, PSKs)
- Clear password fields when dialogs close
- Implement prompt timeouts (default: 2 minutes)
- Validate user input before submission
- Use secure channels for credential transmission

## Troubleshooting

### Credential prompt doesn't appear

**Check:**
- Subscribed to both `network` and `network.credentials`
- Connection has `interactive: true`
- Secret flags set to AGENT_OWNED (value: 1)
- Broker registered successfully

### Connection succeeds without prompting

**Cause:** NetworkManager found saved credentials

**Solution:** Delete existing connection first, or use different credentials

### State updates seem delayed

**Expected behavior:** State changes occur in rapid succession during connection

**Solution:** Debounce UI updates; only act on final state

### Multiple rapid credential prompts

**Cause:** Connection profile has incorrect flags or conflicting agents

**Solution:**
- Check only one agent is running
- Verify psk-flags value
- Check NetworkManager logs for agent conflicts

## Data Structures Reference

### PromptRequest
```go
type PromptRequest struct {
    SSID        string   `json:"ssid"`
    SettingName string   `json:"setting"`
    Fields      []string `json:"fields"`
    Hints       []string `json:"hints"`
    Reason      string   `json:"reason"`
}
```

### PromptReply
```go
type PromptReply struct {
    Secrets map[string]string `json:"secrets"`
    Save    bool              `json:"save"`
    Cancel  bool              `json:"cancel"`
}
```

### NetworkState
```go
type NetworkState struct {
    NetworkStatus  string `json:"networkStatus"`
    IsConnecting   bool   `json:"isConnecting"`
    ConnectingSSID string `json:"connectingSSID"`
    WifiConnected  bool   `json:"wifiConnected"`
    WifiSSID       string `json:"wifiSSID"`
    WifiIP         string `json:"wifiIP"`
    LastError      string `json:"lastError"`
}
```
