// The CLI's doors onto the platform STUN/TURN relay (relay spec §5): // `carlos relay enable|status|disable|adopted` for a member, and // `carlos relay rotate` for a deployment operator. Every write here is a // thin shell over internal/console/relayops.go's operations — the record, // the lease, the walk and the reconcile all live there; this file's whole // job is the door: gate the caller, decode the request, translate the // operation's sentinel errors into HTTP, and answer with the ONE reply // shape every member-facing door shares (storeReply's pattern, apistore.go). // // FOUR DOORS ARE APP-SCOPED, ONE IS NOT. enable, status, disable and adopted // sit under .../apps/{account}/{app}/relay, behind apiAccountApp exactly // like the store doors: an unknown account, someone else's account and an // app that was never claimed are one byte-identical 404 (apiAccountApp's own // doc comment). rotate has no account or app in its path at all — it moves a // REGION's issuer pointer and re-issues every app enabled there, so its gate // is apiRelayOperator, not apiAccountApp, and its refusal is a byte-identical // 404 rather than the 403 the store's grant door gives a non-operator MEMBER // (apistore.go's handleAPIStoreGrant): that 403 is safe to speak only because // apiAccountApp has already proven the caller belongs to the account it // named, so it teaches them only what THEY lack. rotate proves no membership // at all before answering, so a 403 here would confirm to any valid token // that this deployment runs a relay a caller might rotate — the same // reasoning apiSystemRoller's family already applies to `carlos system`. // // DELIVERY READS THE PER-ROUTE CONFIG-ACK. Spec §5 describes five delivery // states a route can be in; most of them come from a per-route config-ack // (`control/config-ack//.json`, internal/appconfig/ // configack.go) config sync writes after it converges a route. There is no // per-host box list to narrow that read by (relayHeartbeats' own doc // comment says the same for control/relay-ack/.json), so relayReply // reads every ack that exists (relayConfigAcks, relayops.go) and matches by // Host. An ack counts only when its RelaySuffixes equal exactly what the // record currently has issued — the same freshness rule relayCutoverFor's // maps.Equal already applies to a member's `carlos relay adopted`: an ack // that named an OLDER incarnation's identities must not read as "this route // runs what was just issued", so a stale ack falls back to "written, // awaiting delivery" rather than reporting an adoption that never happened // under the current keys. // // syncExecHost's ack (the socket-exec and tcp-exec-file shapes) always // leaves Restarted false and RestartError empty — its bounce is // best-effort and fire-and-forget, and a tcp-exec-file route's process is // never restarted by this platform at all — so "adopted at