Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: drop hello from garden client, allow boruta channel join

Garden client no longer sends garden:hello over websocket. After HTTP
registration it joins the private channel directly with stored
garden_sid.

Server channel join now dispatches on access_token type: boruta-
authenticated gardens are authorized by matching garden_id from the
OAuth context, while legacy registration-token gardens still use
local_sid matching.

sow-149

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+27 -107
+5 -73
apps/garden/lib/garden/socket.ex
··· 299 299 300 300 @impl Slipstream 301 301 def handle_join(@lobby_topic, %{"conn_sid" => conn_sid}, socket) do 302 - Logger.info(msg: "Joined channel topic", topic: @lobby_topic, conn_sid: conn_sid) 303 - 304 302 storage = Garden.Storage.read() 305 - {public_key_pem, storage} = Garden.Auth.ensure_keypair(storage) 303 + garden_sid = storage.garden_sid 306 304 307 - {:ok, hello_ref} = 308 - push_message( 309 - socket, 310 - SowerClient.GardenHello.cast!(%{ 311 - name: Garden.Config.get().name, 312 - local_sid: storage.local_sid, 313 - garden_sid: storage.garden_sid, 314 - public_key: public_key_pem 315 - }) 316 - ) 305 + Logger.info(msg: "Joined lobby, joining private channel", garden_sid: garden_sid) 317 306 318 307 socket = 319 308 socket 320 - |> assign(:hello_ref, hello_ref) 321 309 |> assign(:conn_sid, conn_sid) 310 + |> assign(:garden_sid, garden_sid) 311 + |> maybe_schedule_existing_reauth(storage.oauth_credentials) 312 + |> join("garden:#{garden_sid}", %{local_sid: storage.local_sid}) 322 313 323 314 {:ok, socket} 324 315 end ··· 406 397 end 407 398 408 399 @impl Slipstream 409 - def handle_reply( 410 - ref, 411 - {:ok, %{"sid" => garden_sid} = reply}, 412 - socket 413 - ) 414 - when ref == socket.assigns.hello_ref do 415 - Logger.debug(msg: "Received hello reply", garden: reply) 416 - storage = Garden.Storage.read() 417 - 418 - {:join, garden_sid, persist?: persist?} = 419 - Lifecycle.process_hello_reply(garden_sid, storage.garden_sid) 420 - 421 - storage = if persist?, do: Map.put(storage, :garden_sid, garden_sid), else: storage 422 - 423 - {storage, socket} = 424 - case reply do 425 - %{"oauth_credentials" => %{"client_id" => client_id}} -> 426 - Logger.info(msg: "Received client registration", client_id: client_id) 427 - 428 - oauth_creds = %{client_id: client_id} 429 - 430 - storage = Map.put(storage, :oauth_credentials, oauth_creds) 431 - 432 - case Garden.Auth.request_token(client_id, storage.private_key_pem) do 433 - {:ok, token_response} -> 434 - updated_creds = 435 - Map.merge(oauth_creds, %{ 436 - access_token: token_response.access_token, 437 - expires_in: token_response.expires_in, 438 - token_issued_at: System.system_time(:second) 439 - }) 440 - 441 - {Map.put(storage, :oauth_credentials, updated_creds), 442 - schedule_reauthentication(socket, updated_creds)} 443 - 444 - {:error, _} -> 445 - Logger.warning(msg: "Initial token request failed after registration") 446 - {storage, socket} 447 - end 448 - 449 - _ -> 450 - {storage, maybe_schedule_existing_reauth(socket, storage.oauth_credentials)} 451 - end 452 - 453 - if persist? or Map.has_key?(reply, "oauth_credentials") do 454 - Storage.write(storage) 455 - end 456 - 457 - socket = 458 - socket 459 - |> join("garden:#{garden_sid}", %{local_sid: storage.local_sid}) 460 - |> Map.put( 461 - :assigns, 462 - socket.assigns |> Map.delete(:hello_ref) |> Map.put(:garden_sid, garden_sid) 463 - ) 464 - 465 - {:ok, socket} 466 - end 467 - 468 400 def handle_reply(_ref, :ok, socket) do 469 401 {:noreply, socket} 470 402 end
-4
apps/garden/lib/garden/socket/lifecycle.ex
··· 83 83 end 84 84 end 85 85 86 - def process_hello_reply(garden_sid, stored_garden_sid) do 87 - {:join, garden_sid, persist?: garden_sid != stored_garden_sid} 88 - end 89 - 90 86 def should_reload?(active_deployments, pending_reload) do 91 87 map_size(active_deployments) == 0 and pending_reload 92 88 end
-17
apps/garden/test/garden/socket/lifecycle_test.exs
··· 256 256 refute Lifecycle.should_reload?(%{}, false) 257 257 end 258 258 end 259 - 260 - describe "process_hello_reply/2" do 261 - test "returns persist true when garden_sid differs from stored" do 262 - assert {:join, "garden_new", persist?: true} = 263 - Lifecycle.process_hello_reply("garden_new", "garden_old") 264 - end 265 - 266 - test "returns persist false when garden_sid matches stored" do 267 - assert {:join, "garden_same", persist?: false} = 268 - Lifecycle.process_hello_reply("garden_same", "garden_same") 269 - end 270 - 271 - test "returns persist true when stored is nil" do 272 - assert {:join, "garden_new", persist?: true} = 273 - Lifecycle.process_hello_reply("garden_new", nil) 274 - end 275 - end 276 259 end
+22 -13
apps/sower/lib/sower_web/garden_channel.ex
··· 57 57 defp do_join_private( 58 58 topic_sid, 59 59 topic, 60 - %{"local_sid" => local_sid}, 61 - %{assigns: %{conn_sid: conn_sid}} = socket 60 + params, 61 + %{assigns: %{conn_sid: conn_sid, access_token: access_token}} = socket 62 62 ) do 63 - Sower.Repo.put_org_id(socket.assigns.access_token.org_id) 63 + Sower.Repo.put_org_id(access_token.org_id) 64 64 65 65 Logger.debug( 66 66 msg: "Channel topic joined", 67 67 topic: topic, 68 - local_sid: local_sid, 69 68 conn_sid: conn_sid 70 69 ) 71 70 72 - case Orchestration.get_garden_sid(topic_sid) do 73 - nil -> 74 - {:error, %{reason: "unauthorized"}} 75 - 76 - garden when is_nil(garden.local_sid) -> 77 - {:error, %{reason: "unauthorized"}} 78 - 79 - garden when garden.local_sid == local_sid -> 71 + case authorize_private_join(topic_sid, params, access_token) do 72 + {:ok, garden} -> 80 73 send(self(), :track_presence) 81 74 send(self(), :reconcile_deployments) 82 75 {:ok, %{conn_sid: conn_sid}, assign(socket, :garden, garden)} 83 76 84 - _ -> 77 + :error -> 85 78 {:error, %{reason: "unauthorized"}} 86 79 end 87 80 end 81 + 82 + defp authorize_private_join(topic_sid, _params, %Sower.GardenAuth.Context{} = context) do 83 + case Orchestration.get_garden_sid(topic_sid) do 84 + %{id: id} = garden when id == context.garden_id -> {:ok, garden} 85 + _ -> :error 86 + end 87 + end 88 + 89 + defp authorize_private_join(topic_sid, %{"local_sid" => local_sid}, _access_token) do 90 + case Orchestration.get_garden_sid(topic_sid) do 91 + %{local_sid: ^local_sid} = garden when not is_nil(local_sid) -> {:ok, garden} 92 + _ -> :error 93 + end 94 + end 95 + 96 + defp authorize_private_join(_topic_sid, _params, _access_token), do: :error 88 97 89 98 @impl Phoenix.Channel 90 99 def handle_in("ping", _, socket) do