# 3pm-backend route `approveNode`

Paths are relative to the backend's origin (https://fleet.edgecr4ft.net, or your own 3pm-backend), not to this site. Index: https://3pm.edgecr4ft.net/docs/backend/api.md

---

tokens:
  claude: 1827
  o200k: 1121
# POST /api/buckets/{clusterId}/nodes/{nodeId}/approve (approveNode)

Admit a pending node. Idempotent.

- auth: an account — Authorization: Bearer 3pmu_<token>, or the dashboard's session cookie
- answer: NodeView (JSON)

## Types

```ts
interface NodeView {
  node_id: NodeId;
  /** What to display: enrollment first, presence for an active node. */
  state: DisplayState;
  /** The database state alone. */
  enrollment: NodeState;
  hostname: string;
  os: string;
  arch: string;
  agent_version: string;
  /** Which token admitted the host. */
  token_id?: string;
  /**
   * What the host owner lets be asked of this node, exactly as its last
   * `hello` declared it (`link.toml` `caps`). Empty means the node
   * accepts nothing — or, for a node that has not linked since it
   * enrolled, that it has not said yet.
   */
  caps: Capability[];
  /** What this node may ask of the bucket with its own bearer; empty by default. */
  scopes: NodeScope[];
  /** Who may place a call on this node: empty, every node holding `fleet:invoke`; with rules, what a rule admits. */
  callers: CallerRule[];
  /** What the node lets go agent to agent with a LAN neighbour: nothing, a call's files (the default), or its live legs too. */
  direct: DirectMode;
  enrolled_at: EpochMs;
  approved_at?: EpochMs;
  revoked_at?: EpochMs;
  last_seen_at?: EpochMs;
  last_auth_error?: AuthError;
  /** Bytes received over the node's link per second, averaged over the last 5s. */
  rx_bps: number;
  /** Bytes received since the backend started. */
  rx_bytes: number;
  /** Where the node's last link came from, as this backend saw it (behind `--trust-proxy`, the forwarded client); absent until the node linked since the backend started. */
  peer_ip?: string;
  /** The private addresses of the node's interfaces (RFC 1918, CGNAT, IPv6 ULA; container bridges left out), from its last complete host snapshot. */
  private_ips?: string[];
  /** The account the node's daemon runs as, from its last complete host snapshot; absent until it sent one. */
  user?: string;
  /** The accounts with an open login session on the node, from its last complete host snapshot. Sent to the bucket's owner and to a node holding `fleet:read`; absent otherwise, which is not the same as nobody being logged in. */
  sessions?: string[];
}

/** `3pmn_…` — a node. */
type NodeId = string;

/** `NodeView.state`: the enrollment state, or the presence of an active node. */
type DisplayState = NodeState | "online" | "stale" | "offline";

/** Enrollment state in the database, presence aside. */
type NodeState = "active" | "pending" | "revoked" | "unknown";

/** What the host owner lets the backend ask of a node, as its `hello` declared it. */
type Capability = "metrics:read" | "proc:restart" | "proc:stop" | "unknown";

/**
 * What a node may ask of its bucket with its own bearer, granted by the
 * owner (`setNodeAccess`): `fleet:list` the fleet as a list, `fleet:read`
 * its metrics, hosts, trees and action catalogue, `fleet:invoke` placing
 * calls. Nothing by default.
 */
type NodeScope = "fleet:list" | "fleet:read" | "fleet:invoke";

/**
 * One line of a node's `callers`: who may place a call on it once the
 * list holds a rule (an empty list accepts every node holding
 * `fleet:invoke`). `"*"` is the wildcard, and a missing `from_app` /
 * `app` / `action` means `"*"` — except an action of the `3pm:` prefix,
 * which a wildcard never reaches.
 *
 * `from_app` is an operational guard rail, not a boundary: the calling
 * application's name is declared by the calling daemon and the backend
 * cannot check it. The trust boundary is the node.
 */
interface CallerRule {
  /** The calling node: a `3pmn_…` id, or `"*"`. */
  from: string;
  from_app?: string;
  app?: string;
  action?: string;
}

/** `off`, `files` (a call's files go direct between LAN neighbours) or `all` (the default: its live legs too). */
type DirectMode = "off" | "files" | "all" | (string & {});

/** Milliseconds since the Unix epoch. */
type EpochMs = number;

/** The last time a node was turned away at `/api/v1/link`. */
interface AuthError {
  /** `ERR_NODE_PENDING`, `ERR_NODE_REVOKED`, … */
  code: string;
  at: EpochMs;
}
```
