Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

feat: per-garden OAuth token lifecycle via Boruta

Add Boruta OAuth library for per-garden token management. Each garden
gets its own OAuth client at registration time with short-lived access
tokens (15 min) and refresh tokens (30 days), replacing reliance on
the static registration token for ongoing auth.

Server-side:
- Sower.GardenAuth module wraps Boruta's token pipeline for
issue (client_credentials) and refresh (refresh_token) grants
- GardenSocket.connect supports dual auth: boruta:<token> prefix for
OAuth tokens, existing base64 for registration tokens
- GardenChannel returns oauth_credentials in hello reply for new
registrations, adds token:refresh handler for mid-session rotation
- POST /api/oauth/token endpoint for HTTP-based refresh before connect
- Boruta tables exempted from org_id enforcement in Repo
- GardenAuth.Context + permissions for OAuth-authenticated gardens

Garden-side:
- Storage gains oauth_credentials field (ETF-persisted)
- Boot sequence: try stored access token → HTTP refresh → registration
token fallback
- Token refresh scheduled at 80% of TTL via channel, with retry

fix: handle storage schema evolution for oauth_credentials field

Old storage.etf files deserialized via binary_to_term produce structs
missing the new oauth_credentials key. Add ensure_fields/1 migration
step that re-structs to fill in missing fields with defaults.

sow-105

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

+724 -30
+142 -6
apps/garden/lib/garden/socket.ex
··· 101 101 102 102 defp do_connect() do 103 103 config = Application.get_all_env(__MODULE__) 104 + token_param = resolve_connect_token() 104 105 105 106 uri = 106 107 config 107 108 |> Keyword.get(:uri) 108 - |> Map.put( 109 - :query, 110 - "token=#{Base.encode64(Application.fetch_env!(:garden, :config).access_token)}" 111 - ) 109 + |> Map.put(:query, "token=#{token_param}") 112 110 |> URI.to_string() 113 111 114 112 config = Keyword.put(config, :uri, uri) ··· 128 126 end 129 127 end 130 128 129 + defp resolve_connect_token do 130 + storage = Storage.read() 131 + 132 + case storage.oauth_credentials do 133 + %{access_token: token, token_issued_at: issued_at, expires_in: expires_in} 134 + when is_binary(token) -> 135 + if token_expired?(issued_at, expires_in) do 136 + try_http_refresh(storage) || registration_token() 137 + else 138 + Logger.debug(msg: "Using stored Boruta access token") 139 + "boruta:#{token}" 140 + end 141 + 142 + _ -> 143 + registration_token() 144 + end 145 + end 146 + 147 + defp token_expired?(issued_at, expires_in) 148 + when is_integer(issued_at) and is_integer(expires_in) do 149 + System.system_time(:second) >= issued_at + expires_in 150 + end 151 + 152 + defp token_expired?(_, _), do: true 153 + 154 + defp try_http_refresh(%{oauth_credentials: %{refresh_token: refresh_token}} = storage) 155 + when is_binary(refresh_token) do 156 + endpoint = Application.fetch_env!(:garden, :config).endpoint 157 + 158 + case Req.post("#{endpoint}/api/oauth/token", 159 + json: %{grant_type: "refresh_token", refresh_token: refresh_token} 160 + ) do 161 + {:ok, %{status: 200, body: body}} -> 162 + updated_creds = 163 + storage.oauth_credentials 164 + |> Map.merge(%{ 165 + access_token: body["access_token"], 166 + refresh_token: body["refresh_token"], 167 + expires_in: body["expires_in"], 168 + token_issued_at: System.system_time(:second) 169 + }) 170 + 171 + storage |> Map.put(:oauth_credentials, updated_creds) |> Storage.write() 172 + Logger.info(msg: "Refreshed OAuth token via HTTP") 173 + "boruta:#{body["access_token"]}" 174 + 175 + {:ok, %{status: status}} -> 176 + Logger.warning(msg: "HTTP token refresh failed", status: status) 177 + nil 178 + 179 + {:error, error} -> 180 + Logger.warning(msg: "HTTP token refresh error", error: inspect(error)) 181 + nil 182 + end 183 + end 184 + 185 + defp try_http_refresh(_), do: nil 186 + 187 + defp registration_token do 188 + Logger.debug(msg: "Using registration token") 189 + Base.encode64(Application.fetch_env!(:garden, :config).access_token) 190 + end 191 + 192 + defp schedule_token_refresh(socket, %{expires_in: expires_in}) when is_integer(expires_in) do 193 + # Refresh at 80% of TTL 194 + refresh_ms = trunc(expires_in * 0.8 * 1000) 195 + timer_ref = Process.send_after(self(), :refresh_token, refresh_ms) 196 + 197 + Logger.debug(msg: "Scheduled token refresh", refresh_in_seconds: div(refresh_ms, 1000)) 198 + assign(socket, :refresh_timer, timer_ref) 199 + end 200 + 201 + defp schedule_token_refresh(socket, _), do: socket 202 + 203 + defp maybe_schedule_existing_refresh(socket, %{expires_in: _} = creds) do 204 + schedule_token_refresh(socket, creds) 205 + end 206 + 207 + defp maybe_schedule_existing_refresh(socket, _), do: socket 208 + 131 209 @impl Slipstream 132 210 def handle_join(@lobby_topic, %{"conn_sid" => conn_sid}, socket) do 133 211 Logger.info(msg: "Joined channel topic", topic: @lobby_topic, conn_sid: conn_sid) ··· 245 323 {:join, garden_sid, persist?: persist?} = 246 324 Lifecycle.process_hello_reply(garden_sid, storage.garden_sid) 247 325 248 - if persist? do 249 - storage |> Map.put(:garden_sid, garden_sid) |> Garden.Storage.write() 326 + storage = if persist?, do: Map.put(storage, :garden_sid, garden_sid), else: storage 327 + 328 + {storage, socket} = 329 + case reply do 330 + %{"oauth_credentials" => creds} when is_map(creds) -> 331 + oauth_creds = %{ 332 + client_id: creds["client_id"], 333 + client_secret: creds["client_secret"], 334 + access_token: creds["access_token"], 335 + refresh_token: creds["refresh_token"], 336 + expires_in: creds["expires_in"], 337 + token_type: creds["token_type"] || "bearer", 338 + token_issued_at: System.system_time(:second) 339 + } 340 + 341 + Logger.info(msg: "Received OAuth credentials from registration") 342 + 343 + {Map.put(storage, :oauth_credentials, oauth_creds), 344 + schedule_token_refresh(socket, oauth_creds)} 345 + 346 + _ -> 347 + {storage, maybe_schedule_existing_refresh(socket, storage.oauth_credentials)} 348 + end 349 + 350 + if persist? or Map.has_key?(reply, "oauth_credentials") do 351 + Storage.write(storage) 250 352 end 251 353 252 354 socket = ··· 331 433 send(self(), :check_pending_reload) 332 434 333 435 {:noreply, %{socket | active_deployments: active_deployments}} 436 + end 437 + end 438 + 439 + def handle_info(:refresh_token, socket) do 440 + storage = Storage.read() 441 + 442 + case storage.oauth_credentials do 443 + %{refresh_token: rt} when is_binary(rt) -> 444 + topic = private_channel(socket) 445 + {:ok, ref} = push(socket, topic, "token:refresh", %{refresh_token: rt}) 446 + 447 + case await_reply(ref) do 448 + {:ok, new_tokens} -> 449 + updated_creds = 450 + storage.oauth_credentials 451 + |> Map.merge(%{ 452 + access_token: new_tokens["access_token"], 453 + refresh_token: new_tokens["refresh_token"], 454 + expires_in: new_tokens["expires_in"], 455 + token_issued_at: System.system_time(:second) 456 + }) 457 + 458 + storage |> Map.put(:oauth_credentials, updated_creds) |> Storage.write() 459 + Logger.info(msg: "Token refreshed via channel") 460 + {:noreply, schedule_token_refresh(socket, updated_creds)} 461 + 462 + {:error, error} -> 463 + Logger.warning(msg: "Channel token refresh failed, retrying in 60s", error: error) 464 + Process.send_after(self(), :refresh_token, 60_000) 465 + {:noreply, socket} 466 + end 467 + 468 + _ -> 469 + {:noreply, socket} 334 470 end 335 471 end 336 472
+17 -3
apps/garden/lib/garden/storage.ex
··· 9 9 typedstruct do 10 10 field :local_sid, String.t() 11 11 field :garden_sid, String.t() 12 - 13 12 field :subscriptions, list(SowerClient.Orchestration.Subscription) 13 + field :oauth_credentials, map() 14 14 end 15 15 16 16 @cooldown_seconds 60 ··· 64 64 {:ok, bin} = File.read(file) 65 65 66 66 raw = :erlang.binary_to_term(bin) 67 - data = migrate_agent_sid(raw) 67 + 68 + data = 69 + raw 70 + |> migrate_agent_sid() 71 + |> ensure_fields() 68 72 69 73 if data != raw do 70 74 File.write!(file, :erlang.term_to_binary(data)) ··· 115 119 data 116 120 |> Map.delete(:agent_sid) 117 121 |> Map.put(:garden_sid, sid) 118 - |> then(&struct!(__MODULE__, Map.take(&1, [:local_sid, :garden_sid, :subscriptions]))) 122 + |> then( 123 + &struct!( 124 + __MODULE__, 125 + Map.take(&1, [:local_sid, :garden_sid, :subscriptions, :oauth_credentials]) 126 + ) 127 + ) 119 128 end 120 129 121 130 defp migrate_agent_sid(%__MODULE__{} = data), do: data 131 + 132 + # Ensure deserialized structs have all current fields (handles schema evolution) 133 + defp ensure_fields(%{__struct__: __MODULE__} = data) do 134 + struct(__MODULE__, Map.from_struct(data)) 135 + end 122 136 123 137 defp default() do 124 138 %__MODULE__{
+17
apps/sower/lib/sower/authorization/permissions.ex
··· 6 6 |> map_token_permissions(token |> Sower.Repo.preload(:user)) 7 7 end 8 8 9 + def can(%Sower.GardenAuth.Context{} = context) do 10 + permit() 11 + |> map_garden_auth_permissions(context) 12 + end 13 + 9 14 # block by default 10 15 def can(_), do: permit() 16 + 17 + defp map_garden_auth_permissions(%Permit.Permissions{} = permit, %Sower.GardenAuth.Context{ 18 + org_id: org_id, 19 + scope: scope 20 + }) do 21 + if String.contains?(scope, "garden:agent") do 22 + permit 23 + |> read(Sower.Orchestration.Garden, org_id: org_id) 24 + else 25 + permit 26 + end 27 + end 11 28 12 29 defp map_token_permissions( 13 30 %Permit.Permissions{} = permit,
+82
apps/sower/lib/sower/garden_auth.ex
··· 1 + defmodule Sower.GardenAuth do 2 + use TypedStruct 3 + 4 + require Logger 5 + 6 + alias Boruta.Oauth.Authorization 7 + alias Boruta.Oauth.Error 8 + alias Boruta.Oauth.Request 9 + alias Boruta.Oauth.TokenResponse 10 + 11 + @access_token_ttl 900 12 + @refresh_token_ttl 2_592_000 13 + 14 + typedstruct module: Context do 15 + field :org_id, String.t(), enforce: true 16 + field :garden_id, integer(), enforce: true 17 + field :scope, String.t(), enforce: true 18 + end 19 + 20 + def create_client(garden_name) do 21 + Boruta.Ecto.Admin.create_client(%{ 22 + name: "garden:#{garden_name}", 23 + redirect_uris: ["https://localhost"], 24 + supported_grant_types: ["client_credentials", "refresh_token"], 25 + access_token_ttl: @access_token_ttl, 26 + refresh_token_ttl: @refresh_token_ttl, 27 + authorize_scope: true, 28 + authorized_scopes: [%{name: "garden:agent"}], 29 + confidential: true 30 + }) 31 + end 32 + 33 + def delete_client(boruta_client_id) do 34 + client = Boruta.Ecto.Admin.get_client!(boruta_client_id) 35 + Boruta.Ecto.Admin.delete_client(client) 36 + end 37 + 38 + def issue(client_id, client_secret) do 39 + token_request(%{ 40 + "grant_type" => "client_credentials", 41 + "client_id" => client_id, 42 + "client_secret" => client_secret, 43 + "scope" => "garden:agent" 44 + }) 45 + end 46 + 47 + def refresh(refresh_token_value) do 48 + token_request(%{ 49 + "grant_type" => "refresh_token", 50 + "refresh_token" => refresh_token_value 51 + }) 52 + end 53 + 54 + defp token_request(body_params) do 55 + request = %{body_params: body_params, req_headers: []} 56 + 57 + with {:ok, token_request} <- Request.token_request(request), 58 + {:ok, tokens} <- Authorization.token(token_request), 59 + %TokenResponse{} = response <- TokenResponse.from_token(tokens) do 60 + {:ok, 61 + %{ 62 + access_token: response.access_token, 63 + refresh_token: response.refresh_token, 64 + expires_in: response.expires_in, 65 + token_type: response.token_type 66 + }} 67 + else 68 + {:error, %Error{} = error} -> 69 + Logger.error( 70 + msg: "OAuth token request failed", 71 + error: error.error, 72 + error_description: error.error_description 73 + ) 74 + 75 + {:error, error} 76 + 77 + {:error, reason} -> 78 + Logger.error(msg: "OAuth token request failed", error: inspect(reason)) 79 + {:error, reason} 80 + end 81 + end 82 + end
+35 -3
apps/sower/lib/sower/orchestration/garden.ex
··· 28 28 field :name, :string 29 29 field :local_sid, :string 30 30 field :org_id, Ecto.UUID 31 + field :boruta_client_id, :string 31 32 32 33 has_many :subscriptions, Sower.Orchestration.Subscription 33 34 has_many :deployments, Sower.Orchestration.Deployment ··· 41 42 @doc false 42 43 def changeset(garden, attrs) do 43 44 garden 44 - |> cast(attrs, [:name, :org_id, :local_sid]) 45 + |> cast(attrs, [:name, :org_id, :local_sid, :boruta_client_id]) 45 46 |> validate_required([:name]) 46 47 end 47 48 ··· 98 99 ) 99 100 100 101 if socket.assigns.access_token |> can() |> create?(__MODULE__) do 101 - create_garden(%{name: name, local_sid: local_sid}) 102 + register_new_garden(%{name: name, local_sid: local_sid}) 102 103 else 103 104 {:error, :unauthorized} 104 105 end ··· 129 130 ) 130 131 131 132 if socket.assigns.access_token |> can() |> create?(__MODULE__) do 132 - create_garden(%{name: name, local_sid: local_sid}) 133 + register_new_garden(%{name: name, local_sid: local_sid}) 133 134 else 134 135 {:error, :unauthorized} 135 136 end ··· 195 196 def get_garden_sid!(sid), do: Repo.get_by!(__MODULE__, sid: sid) 196 197 197 198 def get_garden_sid(sid), do: Repo.get_by(__MODULE__, sid: sid) 199 + 200 + def get_by_boruta_client_id(client_id), 201 + do: Repo.get_by(__MODULE__, [boruta_client_id: client_id], skip_org_id: true) 198 202 199 203 def get_garden_local_sid(local_sid), do: Repo.get_by(__MODULE__, local_sid: local_sid) 200 204 201 205 def get_garden_local_sid!(local_sid), do: Repo.get_by!(__MODULE__, local_sid: local_sid) 202 206 207 + def register_new_garden(attrs) do 208 + with {:ok, garden} <- create_garden(attrs), 209 + {:ok, client} <- Sower.GardenAuth.create_client(garden.sid), 210 + {:ok, garden} <- update_garden(garden, %{boruta_client_id: client.id}), 211 + {:ok, token_response} <- Sower.GardenAuth.issue(client.id, client.secret) do 212 + oauth_credentials = 213 + Map.merge(token_response, %{client_id: client.id, client_secret: client.secret}) 214 + 215 + {:ok, garden, oauth_credentials} 216 + else 217 + {:error, reason} -> 218 + Logger.error(msg: "Failed to register new garden with OAuth", error: inspect(reason)) 219 + {:error, reason} 220 + end 221 + end 222 + 203 223 def create_garden(attrs \\ %{}) do 204 224 %__MODULE__{ 205 225 org_id: Sower.Repo.get_org_id(), ··· 216 236 end 217 237 218 238 def delete_garden(%__MODULE__{} = garden) do 239 + if garden.boruta_client_id do 240 + try do 241 + Sower.GardenAuth.delete_client(garden.boruta_client_id) 242 + rescue 243 + Ecto.NoResultsError -> 244 + Logger.warning( 245 + msg: "Boruta client not found during garden deletion", 246 + boruta_client_id: garden.boruta_client_id 247 + ) 248 + end 249 + end 250 + 219 251 Repo.delete(garden) 220 252 end 221 253
+7 -1
apps/sower/lib/sower/repo.ex
··· 25 25 @doc """ 26 26 Enable foreign key multitenancy and require :org_id unless :skip_org_id is passed 27 27 """ 28 + @boruta_tables ["oauth_clients", "oauth_tokens", "oauth_scopes", "oauth_clients_scopes"] 29 + 28 30 @impl Ecto.Repo 29 31 def prepare_query(_operation, query, opts) do 30 32 cond do 31 33 opts[:skip_org_id] || opts[:ecto_query] in [:schema_migration, :preload] || 32 - opts[:schema_migration] || get_in(query.from, [Access.key(:prefix)]) == "durable" -> 34 + opts[:schema_migration] || get_in(query.from, [Access.key(:prefix)]) == "durable" || 35 + boruta_table?(query) -> 33 36 {query, opts} 34 37 35 38 org_id = opts[:org_id] -> ··· 39 42 raise "expected org_id or skip_org_id to be set" 40 43 end 41 44 end 45 + 46 + defp boruta_table?(%{from: %{source: {table, _}}}) when table in @boruta_tables, do: true 47 + defp boruta_table?(_), do: false 42 48 43 49 def put_org_id(org_id) do 44 50 Process.put(@tenant_key, org_id)
+29
apps/sower/lib/sower_web/controllers/oauth/token_controller.ex
··· 1 + defmodule SowerWeb.OAuth.TokenController do 2 + use SowerWeb, :controller 3 + 4 + require Logger 5 + 6 + def create(conn, %{"grant_type" => "refresh_token", "refresh_token" => refresh_token}) do 7 + case Sower.GardenAuth.refresh(refresh_token) do 8 + {:ok, token_response} -> 9 + json(conn, token_response) 10 + 11 + {:error, _} -> 12 + conn 13 + |> put_status(:bad_request) 14 + |> json(%{ 15 + error: "invalid_grant", 16 + error_description: "Refresh token is invalid or expired" 17 + }) 18 + end 19 + end 20 + 21 + def create(conn, _params) do 22 + conn 23 + |> put_status(:bad_request) 24 + |> json(%{ 25 + error: "unsupported_grant_type", 26 + error_description: "Only refresh_token grant is supported" 27 + }) 28 + end 29 + end
+20
apps/sower/lib/sower_web/garden_channel.ex
··· 96 96 {:reply, :ok, socket} 97 97 end 98 98 99 + def handle_in("token:refresh", %{"refresh_token" => refresh_token}, socket) do 100 + case Sower.GardenAuth.refresh(refresh_token) do 101 + {:ok, token_response} -> 102 + {:reply, {:ok, token_response}, socket} 103 + 104 + {:error, _error} -> 105 + {:reply, {:error, %{reason: "token_refresh_failed"}}, socket} 106 + end 107 + end 108 + 99 109 # Accept both "garden:hello" and "agent:hello" 100 110 def handle_in("garden:hello", payload, socket), do: do_handle_hello(payload, socket) 101 111 def handle_in("agent:hello", payload, socket), do: do_handle_hello(payload, socket) ··· 105 115 |> normalize_hello_payload() 106 116 |> SowerClient.GardenHello.cast!() 107 117 |> Sower.Orchestration.get_garden(socket) do 118 + {:ok, garden, oauth_credentials} -> 119 + reply = %{ 120 + sid: garden.sid, 121 + local_sid: garden.local_sid, 122 + oauth_credentials: oauth_credentials 123 + } 124 + 125 + Logger.debug(msg: "Replying to hello with oauth credentials", garden_sid: garden.sid) 126 + {:reply, {:ok, reply}, assign(socket, :garden_sid, garden.sid)} 127 + 108 128 {:ok, garden} -> 109 129 Logger.debug(msg: "Replying to hello", garden: garden) 110 130 {:reply, {:ok, garden}, assign(socket, :garden_sid, garden.sid)}
+52 -16
apps/sower/lib/sower_web/garden_socket.ex
··· 8 8 9 9 @impl Phoenix.Socket 10 10 def connect(%{"token" => token}, socket, _connect_info) do 11 - case token |> Base.decode64!() |> Sower.Accounts.AccessToken.authenticate() do 11 + case authenticate_token(token) do 12 12 {:ok, access_token} -> 13 - if access_token |> can() |> read?(Sower.Orchestration.Garden) do 14 - socket = 15 - socket 16 - |> assign(:access_token, access_token) 17 - |> assign(:conn_sid, SowerClient.Sid.generate("conn")) 18 - 19 - {:ok, socket} 20 - else 21 - Logger.error( 22 - msg: "Access token is not authorized to be a garden", 23 - access_token_sid: access_token.sid 24 - ) 13 + socket = 14 + socket 15 + |> assign(:access_token, access_token) 16 + |> assign(:conn_sid, SowerClient.Sid.generate("conn")) 25 17 26 - {:error, :unauthorized} 27 - end 18 + {:ok, socket} 28 19 29 20 {:error, error} -> 30 - Logger.error(msg: "Invalid authentication", error: error) 21 + Logger.error(msg: "Authentication failed", error: error) 31 22 {:error, :unauthorized} 32 23 end 33 24 end ··· 39 30 40 31 @impl Phoenix.Socket 41 32 def id(_socket), do: nil 33 + 34 + defp authenticate_token("boruta:" <> boruta_token) do 35 + case Boruta.Oauth.Authorization.AccessToken.authorize(value: boruta_token) do 36 + {:ok, oauth_token} -> 37 + case Sower.Orchestration.Garden.get_by_boruta_client_id(oauth_token.client.id) do 38 + nil -> 39 + Logger.error( 40 + msg: "No garden found for Boruta client", 41 + boruta_client_id: oauth_token.client.id 42 + ) 43 + 44 + {:error, :unknown_client} 45 + 46 + garden -> 47 + {:ok, 48 + %Sower.GardenAuth.Context{ 49 + org_id: garden.org_id, 50 + garden_id: garden.id, 51 + scope: oauth_token.scope 52 + }} 53 + end 54 + 55 + {:error, _} -> 56 + {:error, :invalid_boruta_token} 57 + end 58 + end 59 + 60 + defp authenticate_token(base64_token) do 61 + case base64_token |> Base.decode64!() |> Sower.Accounts.AccessToken.authenticate() do 62 + {:ok, access_token} -> 63 + if access_token |> can() |> read?(Sower.Orchestration.Garden) do 64 + {:ok, access_token} 65 + else 66 + Logger.error( 67 + msg: "Access token is not authorized to be a garden", 68 + access_token_sid: access_token.sid 69 + ) 70 + 71 + {:error, :unauthorized} 72 + end 73 + 74 + {:error, error} -> 75 + {:error, error} 76 + end 77 + end 42 78 end
+5
apps/sower/lib/sower_web/router.ex
··· 102 102 get "/openapi", OpenApiSpex.Plug.RenderSpec, [] 103 103 end 104 104 105 + scope "/api/oauth", SowerWeb.OAuth do 106 + pipe_through :api 107 + post "/token", TokenController, :create 108 + end 109 + 105 110 scope "/api/v1", SowerWeb.Api do 106 111 pipe_through [:api, :ensure_token_authenticated] 107 112
+1
apps/sower/mix.exs
··· 29 29 [ 30 30 {:argon2id_elixir, "~> 1.1"}, 31 31 {:bandit, "~> 1.0"}, 32 + {:boruta, "~> 2.3"}, 32 33 {:cloak_ecto, "~> 1.3.0"}, 33 34 {:cuid2_ex, "~> 0.2.0"}, 34 35 {:durable, github: "wavezync/durable"},
+5
apps/sower/priv/repo/migrations/20260401003800_create_boruta.exs
··· 1 + defmodule Sower.Repo.Migrations.CreateBoruta do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.CreateBoruta 5 + end
+5
apps/sower/priv/repo/migrations/20260401003801_openid_connect.exs
··· 1 + defmodule Sower.Repo.Migrations.OpenidConnect do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.OpenidConnect 5 + end
+5
apps/sower/priv/repo/migrations/20260401003802_clients_refresh_tokens.exs
··· 1 + defmodule Sower.Repo.Migrations.ClientsRefreshTokens do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.ClientsRefreshTokens 5 + end
+5
apps/sower/priv/repo/migrations/20260401003803_clients_public_revoke.exs
··· 1 + defmodule Sower.Repo.Migrations.ClientsPublicRevoke do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.ClientsPublicRevoke 5 + end
+5
apps/sower/priv/repo/migrations/20260401003804_store_previous_token.exs
··· 1 + defmodule Sower.Repo.Migrations.StorePreviousToken do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.StorePreviousToken 5 + end
+5
apps/sower/priv/repo/migrations/20260401003805_id_token_signature_alg_configuration.exs
··· 1 + defmodule Sower.Repo.Migrations.IdTokenSignatureAlgConfiguration do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.IdTokenSignatureAlgConfiguration 5 + end
+5
apps/sower/priv/repo/migrations/20260401003806_confidential_clients.exs
··· 1 + defmodule Sower.Repo.Migrations.ConfidentialClients do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.ConfidentialClients 5 + end
+5
apps/sower/priv/repo/migrations/20260401003807_refresh_token_rotation.exs
··· 1 + defmodule Sower.Repo.Migrations.RefreshTokenRotation do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.RefreshTokenRotation 5 + end
+5
apps/sower/priv/repo/migrations/20260401003808_authorization_code_chains.exs
··· 1 + defmodule Sower.Repo.Migrations.AuthorizationCodeChains do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.AuthorizationCodeChains 5 + end
+5
apps/sower/priv/repo/migrations/20260401003809_client_authentication_methods.exs
··· 1 + defmodule Sower.Repo.Migrations.ClientAuthenticationMethods do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.ClientAuthenticationMethods 5 + end
+5
apps/sower/priv/repo/migrations/20260401003810_signed_userinfo_response.exs
··· 1 + defmodule Sower.Repo.Migrations.SignedUserinfoResponse do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.SignedUserinfoResponse 5 + end
+5
apps/sower/priv/repo/migrations/20260401003811_optional_public_key_for_oauth_clients.exs
··· 1 + defmodule Sower.Repo.Migrations.OptionalPublicKeyForOauthClients do 2 + use Ecto.Migration 3 + 4 + use Boruta.Migrations.OptionalPublicKeyForOauthClients 5 + end
+11
apps/sower/priv/repo/migrations/20260401010000_add_boruta_client_id_to_gardens.exs
··· 1 + defmodule Sower.Repo.Migrations.AddBorutaClientIdToGardens do 2 + use Ecto.Migration 3 + 4 + def change do 5 + alter table(:gardens) do 6 + add :boruta_client_id, :string 7 + end 8 + 9 + create index(:gardens, [:boruta_client_id], unique: true) 10 + end 11 + end
+8 -1
apps/sower/test/sower_web/channels/garden_channel_handle_in_test.exs
··· 36 36 "name" => "new-garden" 37 37 }) 38 38 39 - assert_reply ref, :ok, reply 39 + # Registration now includes Boruta client creation (RSA keygen) + token issuance 40 + assert_reply ref, :ok, reply, 5_000 40 41 assert is_binary(reply.sid) 42 + assert is_map(reply.oauth_credentials) 43 + assert is_binary(reply.oauth_credentials.access_token) 44 + assert is_binary(reply.oauth_credentials.refresh_token) 45 + assert is_binary(reply.oauth_credentials.client_id) 46 + assert is_binary(reply.oauth_credentials.client_secret) 47 + assert is_integer(reply.oauth_credentials.expires_in) 41 48 end 42 49 43 50 test "accepts legacy agent:hello event" do
+2
apps/sower_client/lib/sower_client.ex
··· 4 4 # Used by contract evolution tests and baseline generation. 5 5 @server_pushed_schema_titles [ 6 6 "Deployment", 7 + "OAuthCredentials", 7 8 "SeedDeployment", 8 9 "Seed", 9 10 "SeedTag", ··· 24 25 |> OpenApiSpex.add_schemas([ 25 26 SowerClient.GardenHello, 26 27 SowerClient.AgentHello, 28 + SowerClient.Auth.OAuthCredentials, 27 29 SowerClient.Auth.TokenInfo, 28 30 SowerClient.Orchestration.GardenSeedGeneration, 29 31 SowerClient.Orchestration.GardenSeedProfile,
+38
apps/sower_client/lib/sower_client/auth/oauth_credentials.ex
··· 1 + defmodule SowerClient.Auth.OAuthCredentials do 2 + use SowerClient.Schema 3 + 4 + alias OpenApiSpex.Schema 5 + 6 + OpenApiSpex.schema(%{ 7 + title: "OAuthCredentials", 8 + description: "OAuth credentials issued to a garden after registration", 9 + type: :object, 10 + properties: %{ 11 + client_id: %Schema{ 12 + type: :string, 13 + description: "Boruta OAuth client ID" 14 + }, 15 + client_secret: %Schema{ 16 + type: :string, 17 + description: "Boruta OAuth client secret" 18 + }, 19 + access_token: %Schema{ 20 + type: :string, 21 + description: "OAuth access token" 22 + }, 23 + refresh_token: %Schema{ 24 + type: :string, 25 + description: "OAuth refresh token for obtaining new access tokens" 26 + }, 27 + expires_in: %Schema{ 28 + type: :integer, 29 + description: "Access token TTL in seconds" 30 + }, 31 + token_type: %Schema{ 32 + type: :string, 33 + description: "Token type, typically bearer" 34 + } 35 + }, 36 + required: [:client_id, :client_secret, :access_token, :refresh_token, :expires_in] 37 + }) 38 + end
+29
apps/sower_client/test/fixtures/contract_baseline.json
··· 22 22 }, 23 23 "required": [] 24 24 }, 25 + "OAuthCredentials": { 26 + "properties": { 27 + "access_token": { 28 + "type": "string" 29 + }, 30 + "client_id": { 31 + "type": "string" 32 + }, 33 + "client_secret": { 34 + "type": "string" 35 + }, 36 + "expires_in": { 37 + "type": "integer" 38 + }, 39 + "refresh_token": { 40 + "type": "string" 41 + }, 42 + "token_type": { 43 + "type": "string" 44 + } 45 + }, 46 + "required": [ 47 + "access_token", 48 + "client_id", 49 + "client_secret", 50 + "expires_in", 51 + "refresh_token" 52 + ] 53 + }, 25 54 "PresignedUploadReply": { 26 55 "properties": { 27 56 "headers": {
+17
config/config.exs
··· 24 24 pubsub_server: Sower.PubSub, 25 25 live_view: [signing_salt: "nrwHFIM7"] 26 26 27 + config :boruta, Boruta.Oauth, 28 + repo: Sower.Repo, 29 + issuer: "sower", 30 + contexts: [ 31 + access_tokens: Boruta.Ecto.AccessTokens, 32 + clients: Boruta.Ecto.Clients, 33 + codes: Boruta.Ecto.Codes, 34 + scopes: Boruta.Ecto.Scopes 35 + ], 36 + max_ttl: [ 37 + access_token: 2_592_000, 38 + authorization_code: 60, 39 + id_token: 86_400, 40 + refresh_token: 2_592_000 41 + ], 42 + token_generator: Boruta.TokenGenerator 43 + 27 44 config :flop, repo: Sower.Repo 28 45 29 46 config :sower, Sower.Orchestration,
+7
mix.lock
··· 1 1 %{ 2 2 "argon2id_elixir": {:hex, :argon2id_elixir, "1.1.3", "d3c77f3a5241f6ea700c2cbc633ad0f00fa99082382b07a71217e83c93700d8c", [:mix], [{:rustler, "~> 0.36.2", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "87ba6dec1580c0d4209741724f2821e330a8514ccfb9ed67564113f0382267de"}, 3 3 "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"}, 4 + "boruta": {:hex, :boruta, "2.3.1", "d33535cd84fb6516b67a04b12fa6af16c3480a059b3d7bf38f988410dff8049a", [:mix], [{:ecto_sql, ">= 3.5.2", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ex_json_schema, "~> 0.6", [hex: :ex_json_schema, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:nebulex, "~> 2.0", [hex: :nebulex, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:puid, "~> 1.0", [hex: :puid, repo: "hexpm", optional: false]}, {:secure_random, "~> 0.5", [hex: :secure_random, repo: "hexpm", optional: false]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm", "ae06432f70ab8447afc0d64bd404594c0b1452633458ae2377de250ead7bf0d9"}, 4 5 "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, 5 6 "circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"}, 6 7 "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, 7 8 "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, 8 9 "crontab": {:hex, :crontab, "1.2.0", "503611820257939d5d0fd272eb2b454f48a470435a809479ddc2c40bb515495c", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "ebd7ef4d831e1b20fa4700f0de0284a04cac4347e813337978e25b4cc5cc2207"}, 10 + "crypto_rand": {:hex, :crypto_rand, "1.0.4", "0d32cbbaa8c229a45e79cdaefd7a48cfb9e6ed803a5168731fe27409daa27c6f", [:mix], [], "hexpm", "ad1862fd3e1c938f60982902632474868ea96901d75dd53f0ec32dd55e123549"}, 9 11 "cuid2_ex": {:hex, :cuid2_ex, "0.2.0", "b594696ceef7367f8bee7be0a4b07227755e90a1740d6cc73e7670d70e5454d4", [:mix], [], "hexpm", "49c3b81c1864f146e1cc3674ad3984ec16583c253e08d4d71d69b808e0054ea1"}, 10 12 "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, 11 13 "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, ··· 21 23 "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, 22 24 "ex_aws": {:hex, :ex_aws, "2.6.1", "194582c7b09455de8a5ab18a0182e6dd937d53df82be2e63c619d01bddaccdfa", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67842a08c90a1d9a09dbe4ac05754175c7ca253abe4912987c759395d4bd9d26"}, 23 25 "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, 26 + "ex_json_schema": {:hex, :ex_json_schema, "0.11.2", "8f8200e6afa5473f37dbebd1e72bf97d8d5dd0128125123ed1532611267f0138", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "395f4aaf32ea0a14d861b16695e7bc8a1b5d841e0fd374d25aef9701bf8da825"}, 24 27 "ex_nar": {:hex, :ex_nar, "0.3.0", "3136d019241afc97a5da8e43613a56ac0ae30dad12df5d3bc66547e55e7453e0", [:mix], [], "hexpm", "cbb42d047764feac6c411efddcadc31866e9a998dd6e2bc1eb428cec1c49fdcd"}, 25 28 "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, 26 29 "exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"}, ··· 47 50 "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 48 51 "mint_web_socket": {:hex, :mint_web_socket, "1.0.5", "60354efeb49b1eccf95dfb75f55b08d692e211970fe735a5eb3188b328be2a90", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "04b35663448fc758f3356cce4d6ac067ca418bbafe6972a3805df984b5f12e61"}, 49 52 "mix_test_watch": {:hex, :mix_test_watch, "1.4.0", "d88bcc4fbe3198871266e9d2f00cd8ae350938efbb11d3fa1da091586345adbb", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2b4693e17c8ead2ef56d4f48a0329891e8c2d0d73752c0f09272a2b17dc38d1b"}, 53 + "nebulex": {:hex, :nebulex, "2.6.6", "677e27fcfa89eaa085d9509d5e066f305f98c1b2264ce6676eaca6fb08d4939e", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.1", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "8cbf531af6fe407383b6ba410a43a19319af47804929d8a8d1975a780b9952df"}, 50 54 "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 51 55 "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 52 56 "oidcc": {:hex, :oidcc, "3.7.0", "0aad613c4c44d1e807c3bc0a6e01c353b9627303447f86af429aeac2d08c0a9e", [:mix, :rebar3], [{:igniter, "~> 0.6.3", [hex: :igniter, repo: "hexpm", optional: true]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "a7414b744dcd64d238dc48e93a9f298b7d18e06ae5e699a66c54dd43bd594fae"}, ··· 66 70 "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, 67 71 "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 68 72 "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, 73 + "puid": {:hex, :puid, "1.1.2", "4acf2a576afc5896c393459d259e733ad20dd96c969152fcddb68f35c1f5ba4a", [:mix], [{:crypto_rand, "~> 1.0", [hex: :crypto_rand, repo: "hexpm", optional: false]}], "hexpm", "fbd1691e29e576c4fbf23852f4d256774702ad1f2a91b37e4344f7c278f1ffaa"}, 69 74 "quantum": {:hex, :quantum, "3.5.3", "ee38838a07761663468145f489ad93e16a79440bebd7c0f90dc1ec9850776d99", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "500fd3fa77dcd723ed9f766d4a175b684919ff7b6b8cfd9d7d0564d58eba8734"}, 70 75 "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, 71 76 "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, 72 77 "rexec": {:git, "https://codeberg.org/adamcstephens/rexec.git", "b497afe9684ee58d84194fcf9e2fe9bdad6e0c6b", [branch: "main"]}, 73 78 "rustler": {:hex, :rustler, "0.36.2", "6c2142f912166dfd364017ab2bf61242d4a5a3c88e7b872744642ae004b82501", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "93832a6dbc1166739a19cd0c25e110e4cf891f16795deb9361dfcae95f6c88fe"}, 79 + "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, 80 + "shards": {:hex, :shards, "1.1.1", "8b42323457d185b26b15d05187784ce6c5d1e181b35c46fca36c45f661defe02", [:make, :rebar3], [], "hexpm", "169a045dae6668cda15fbf86d31bf433d0dbbaec42c8c23ca4f8f2d405ea8eda"}, 74 81 "shortuuid": {:hex, :shortuuid, "4.1.0", "6f59470b78169c84a0cedbbe3fd4e83f0837a59877a4e1a7c0709916d54239e2", [:mix], [], "hexpm", "7336719118b3cca1ac73e95810199b0b9b7d00f9d71bd2c2d27fed4c4f74388e"}, 75 82 "slipstream": {:hex, :slipstream, "1.2.2", "6b07124ac5f62a50327aa38c84edd0284920ac8aba548e04738827838f233ed0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 0.2 or ~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.1 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ccb873ddb21aadb37c5c7745014febe6da0aa2cef0c4e73e7d08ce11d18aacd0"}, 76 83 "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
+145
nix/packages/deps.nix
··· 222 222 in 223 223 drv; 224 224 225 + boruta = 226 + let 227 + version = "2.3.1"; 228 + drv = buildMix { 229 + inherit version; 230 + name = "boruta"; 231 + appConfigPath = ../../config; 232 + 233 + src = fetchHex { 234 + inherit version; 235 + pkg = "boruta"; 236 + sha256 = "ae06432f70ab8447afc0d64bd404594c0b1452633458ae2377de250ead7bf0d9"; 237 + }; 238 + 239 + beamDeps = [ 240 + ecto_sql 241 + ex_json_schema 242 + joken 243 + jose 244 + nebulex 245 + phoenix 246 + plug 247 + postgrex 248 + puid 249 + secure_random 250 + shards 251 + ]; 252 + }; 253 + in 254 + drv; 255 + 225 256 cloak = 226 257 let 227 258 version = "1.1.4"; ··· 286 317 in 287 318 drv; 288 319 320 + crypto_rand = 321 + let 322 + version = "1.0.4"; 323 + drv = buildMix { 324 + inherit version; 325 + name = "crypto_rand"; 326 + appConfigPath = ../../config; 327 + 328 + src = fetchHex { 329 + inherit version; 330 + pkg = "crypto_rand"; 331 + sha256 = "ad1862fd3e1c938f60982902632474868ea96901d75dd53f0ec32dd55e123549"; 332 + }; 333 + }; 334 + in 335 + drv; 336 + 289 337 cuid2_ex = 290 338 let 291 339 version = "0.2.0"; ··· 516 564 beamDeps = [ 517 565 ex_aws 518 566 sweet_xml 567 + ]; 568 + }; 569 + in 570 + drv; 571 + 572 + ex_json_schema = 573 + let 574 + version = "0.11.2"; 575 + drv = buildMix { 576 + inherit version; 577 + name = "ex_json_schema"; 578 + appConfigPath = ../../config; 579 + 580 + src = fetchHex { 581 + inherit version; 582 + pkg = "ex_json_schema"; 583 + sha256 = "395f4aaf32ea0a14d861b16695e7bc8a1b5d841e0fd374d25aef9701bf8da825"; 584 + }; 585 + 586 + beamDeps = [ 587 + decimal 519 588 ]; 520 589 }; 521 590 in ··· 879 948 in 880 949 drv; 881 950 951 + nebulex = 952 + let 953 + version = "2.6.6"; 954 + drv = buildMix { 955 + inherit version; 956 + name = "nebulex"; 957 + appConfigPath = ../../config; 958 + 959 + src = fetchHex { 960 + inherit version; 961 + pkg = "nebulex"; 962 + sha256 = "8cbf531af6fe407383b6ba410a43a19319af47804929d8a8d1975a780b9952df"; 963 + }; 964 + 965 + beamDeps = [ 966 + shards 967 + telemetry 968 + ]; 969 + }; 970 + in 971 + drv; 972 + 882 973 nimble_options = 883 974 let 884 975 version = "1.1.1"; ··· 1253 1344 in 1254 1345 drv; 1255 1346 1347 + puid = 1348 + let 1349 + version = "1.1.2"; 1350 + drv = buildMix { 1351 + inherit version; 1352 + name = "puid"; 1353 + appConfigPath = ../../config; 1354 + 1355 + src = fetchHex { 1356 + inherit version; 1357 + pkg = "puid"; 1358 + sha256 = "fbd1691e29e576c4fbf23852f4d256774702ad1f2a91b37e4344f7c278f1ffaa"; 1359 + }; 1360 + 1361 + beamDeps = [ 1362 + crypto_rand 1363 + ]; 1364 + }; 1365 + in 1366 + drv; 1367 + 1256 1368 quantum = 1257 1369 let 1258 1370 version = "3.5.3"; ··· 1336 1448 jason 1337 1449 toml 1338 1450 ]; 1451 + }; 1452 + in 1453 + drv; 1454 + 1455 + secure_random = 1456 + let 1457 + version = "0.5.1"; 1458 + drv = buildMix { 1459 + inherit version; 1460 + name = "secure_random"; 1461 + appConfigPath = ../../config; 1462 + 1463 + src = fetchHex { 1464 + inherit version; 1465 + pkg = "secure_random"; 1466 + sha256 = "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"; 1467 + }; 1468 + }; 1469 + in 1470 + drv; 1471 + 1472 + shards = 1473 + let 1474 + version = "1.1.1"; 1475 + drv = buildRebar3 { 1476 + inherit version; 1477 + name = "shards"; 1478 + 1479 + src = fetchHex { 1480 + inherit version; 1481 + pkg = "shards"; 1482 + sha256 = "169a045dae6668cda15fbf86d31bf433d0dbbaec42c8c23ca4f8f2d405ea8eda"; 1483 + }; 1339 1484 }; 1340 1485 in 1341 1486 drv;