Elixir SDK for Pocketenv
1
fork

Configure Feed

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

Add secrets, SSH keys, and Tailscale support

Introduce secrets, SSH keys and Tailscale auth key APIs and types.
Implement client-side sealed-box encryption (Pocketenv.Crypto) using
X25519 + NaCl (kcl) with base64url output. Add redaction for private
keys and auth keys and validate Tailscale key prefix. Add kcl
dependency.

+546 -2
+116
lib/pocketenv.ex
··· 143 143 @spec get_profile(String.t(), keyword()) :: 144 144 {:ok, Sandbox.Types.Profile.t()} | {:error, term()} 145 145 defdelegate get_profile(did, opts \\ []), to: API 146 + 147 + # --------------------------------------------------------------------------- 148 + # Secrets 149 + # --------------------------------------------------------------------------- 150 + 151 + @doc """ 152 + Lists all secrets for a sandbox. 153 + 154 + ## Options 155 + 156 + - `:limit` — max results (default: `100`). 157 + - `:offset` — pagination offset (default: `0`). 158 + - `:token` — bearer token override. 159 + 160 + ## Example 161 + 162 + {:ok, secrets} = Pocketenv.list_secrets(sandbox.id) 163 + """ 164 + @spec list_secrets(String.t(), keyword()) :: 165 + {:ok, [Sandbox.Types.Secret.t()]} | {:error, term()} 166 + defdelegate list_secrets(sandbox_id, opts \\ []), to: API 167 + 168 + @doc """ 169 + Adds an encrypted secret to a sandbox. 170 + 171 + The `value` is encrypted client-side using the server's public key 172 + (configured via `:public_key` app config or `POCKETENV_PUBLIC_KEY` env var) 173 + before being sent to the API. 174 + 175 + ## Example 176 + 177 + {:ok, _} = Pocketenv.add_secret(sandbox.id, "DATABASE_URL", "postgres://...") 178 + """ 179 + @spec add_secret(String.t(), String.t(), String.t(), keyword()) :: 180 + {:ok, map()} | {:error, term()} 181 + defdelegate add_secret(sandbox_id, name, value, opts \\ []), to: API 182 + 183 + @doc """ 184 + Deletes a secret by its id. 185 + 186 + ## Example 187 + 188 + {:ok, _} = Pocketenv.delete_secret("secret-id") 189 + """ 190 + @spec delete_secret(String.t(), keyword()) :: {:ok, map()} | {:error, term()} 191 + defdelegate delete_secret(id, opts \\ []), to: API 192 + 193 + # --------------------------------------------------------------------------- 194 + # SSH Keys 195 + # --------------------------------------------------------------------------- 196 + 197 + @doc """ 198 + Fetches the SSH key pair for a sandbox. 199 + 200 + The returned `private_key` field contains the redacted (server-side) value. 201 + 202 + ## Example 203 + 204 + {:ok, ssh_key} = Pocketenv.get_ssh_keys(sandbox.id) 205 + IO.puts(ssh_key.public_key) 206 + """ 207 + @spec get_ssh_keys(String.t(), keyword()) :: 208 + {:ok, Sandbox.Types.SshKey.t()} | {:error, term()} 209 + defdelegate get_ssh_keys(sandbox_id, opts \\ []), to: API 210 + 211 + @doc """ 212 + Stores an SSH key pair for a sandbox. 213 + 214 + The `private_key` is encrypted client-side using the server's public key 215 + before transmission. A redacted version is stored alongside it. 216 + 217 + ## Parameters 218 + 219 + - `sandbox_id` — sandbox ID. 220 + - `private_key` — PEM-encoded OpenSSH private key string. 221 + - `public_key` — OpenSSH public key string (`ssh-ed25519 AAAA...`). 222 + 223 + ## Example 224 + 225 + {:ok, _} = Pocketenv.put_ssh_keys(sandbox.id, private_pem, public_key) 226 + """ 227 + @spec put_ssh_keys(String.t(), String.t(), String.t(), keyword()) :: 228 + {:ok, map()} | {:error, term()} 229 + defdelegate put_ssh_keys(sandbox_id, private_key, public_key, opts \\ []), to: API 230 + 231 + # --------------------------------------------------------------------------- 232 + # Tailscale 233 + # --------------------------------------------------------------------------- 234 + 235 + @doc """ 236 + Fetches the Tailscale auth key for a sandbox. 237 + 238 + The returned `auth_key` is the redacted value stored on the server. 239 + 240 + ## Example 241 + 242 + {:ok, ts} = Pocketenv.get_tailscale_auth_key(sandbox.id) 243 + IO.puts(ts.auth_key) 244 + """ 245 + @spec get_tailscale_auth_key(String.t(), keyword()) :: 246 + {:ok, Sandbox.Types.TailscaleAuthKey.t()} | {:error, term()} 247 + defdelegate get_tailscale_auth_key(sandbox_id, opts \\ []), to: API 248 + 249 + @doc """ 250 + Stores a Tailscale auth key for a sandbox. 251 + 252 + The `auth_key` must start with `"tskey-auth-"`. It is encrypted client-side 253 + using the server's public key before transmission. 254 + 255 + ## Example 256 + 257 + {:ok, _} = Pocketenv.put_tailscale_auth_key(sandbox.id, "tskey-auth-xxxx") 258 + """ 259 + @spec put_tailscale_auth_key(String.t(), String.t(), keyword()) :: 260 + {:ok, map()} | {:error, term()} 261 + defdelegate put_tailscale_auth_key(sandbox_id, auth_key, opts \\ []), to: API 146 262 end
+148 -1
lib/pocketenv/api.ex
··· 4 4 # pipe on `%Sandbox{}` structs. This module is not part of the public API. 5 5 6 6 alias Pocketenv.Client 7 - alias Sandbox.Types.{ExecResult, Port, Profile} 7 + alias Pocketenv.Crypto 8 + alias Sandbox.Types.{ExecResult, Port, Profile, Secret, SshKey, TailscaleAuthKey} 8 9 9 10 @default_base "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw" 10 11 ··· 210 211 end 211 212 212 213 # --------------------------------------------------------------------------- 214 + # Secrets 215 + # --------------------------------------------------------------------------- 216 + 217 + def list_secrets(sandbox_id, opts \\ []) do 218 + params = %{ 219 + "sandboxId" => sandbox_id, 220 + "offset" => Keyword.get(opts, :offset, 0), 221 + "limit" => Keyword.get(opts, :limit, 100) 222 + } 223 + 224 + case Client.get( 225 + "/xrpc/io.pocketenv.secret.getSecrets", 226 + take_token(opts) ++ [params: params] 227 + ) do 228 + {:ok, %{"secrets" => items}} -> {:ok, Enum.map(items, &Secret.from_map/1)} 229 + {:error, _} = err -> err 230 + end 231 + end 232 + 233 + def add_secret(sandbox_id, name, value, opts \\ []) do 234 + {:ok, encrypted} = Crypto.encrypt(value) 235 + 236 + Client.post( 237 + "/xrpc/io.pocketenv.secret.addSecret", 238 + %{"secret" => %{"sandboxId" => sandbox_id, "name" => name, "value" => encrypted}}, 239 + take_token(opts) 240 + ) 241 + end 242 + 243 + def delete_secret(id, opts \\ []) do 244 + Client.post( 245 + "/xrpc/io.pocketenv.secret.deleteSecret", 246 + nil, 247 + take_token(opts) ++ [params: %{"id" => id}] 248 + ) 249 + end 250 + 251 + # --------------------------------------------------------------------------- 252 + # SSH Keys 253 + # --------------------------------------------------------------------------- 254 + 255 + def get_ssh_keys(sandbox_id, opts \\ []) do 256 + case Client.get( 257 + "/xrpc/io.pocketenv.sandbox.getSshKeys", 258 + take_token(opts) ++ [params: %{"id" => sandbox_id}] 259 + ) do 260 + {:ok, data} -> {:ok, SshKey.from_map(data)} 261 + {:error, _} = err -> err 262 + end 263 + end 264 + 265 + def put_ssh_keys(sandbox_id, private_key, public_key, opts \\ []) do 266 + {:ok, encrypted_private_key} = Crypto.encrypt(private_key) 267 + redacted = redact_ssh_private_key(private_key) 268 + 269 + Client.post( 270 + "/xrpc/io.pocketenv.sandbox.putSshKeys", 271 + %{ 272 + "id" => sandbox_id, 273 + "privateKey" => encrypted_private_key, 274 + "publicKey" => public_key, 275 + "redacted" => redacted 276 + }, 277 + take_token(opts) 278 + ) 279 + end 280 + 281 + # --------------------------------------------------------------------------- 282 + # Tailscale 283 + # --------------------------------------------------------------------------- 284 + 285 + def get_tailscale_auth_key(sandbox_id, opts \\ []) do 286 + case Client.get( 287 + "/xrpc/io.pocketenv.sandbox.getTailscaleAuthKey", 288 + take_token(opts) ++ [params: %{"id" => sandbox_id}] 289 + ) do 290 + {:ok, data} -> {:ok, TailscaleAuthKey.from_map(data)} 291 + {:error, _} = err -> err 292 + end 293 + end 294 + 295 + def put_tailscale_auth_key(sandbox_id, auth_key, opts \\ []) do 296 + unless String.starts_with?(auth_key, "tskey-auth-") do 297 + raise ArgumentError, "Tailscale auth key must start with \"tskey-auth-\"" 298 + end 299 + 300 + {:ok, encrypted} = Crypto.encrypt(auth_key) 301 + redacted = redact_tailscale_key(auth_key) 302 + 303 + Client.post( 304 + "/xrpc/io.pocketenv.sandbox.putTailscaleAuthKey", 305 + %{"id" => sandbox_id, "authKey" => encrypted, "redacted" => redacted}, 306 + take_token(opts) 307 + ) 308 + end 309 + 310 + # --------------------------------------------------------------------------- 213 311 # Private helpers 214 312 # --------------------------------------------------------------------------- 313 + 314 + defp redact_ssh_private_key(private_key) do 315 + header = "-----BEGIN OPENSSH PRIVATE KEY-----" 316 + footer = "-----END OPENSSH PRIVATE KEY-----" 317 + 318 + case {String.contains?(private_key, header), String.contains?(private_key, footer)} do 319 + {true, true} -> 320 + header_end = :binary.match(private_key, header) |> elem(0) 321 + body_start = header_end + byte_size(header) 322 + footer_start = :binary.match(private_key, footer) |> elem(0) 323 + body = binary_part(private_key, body_start, footer_start - body_start) 324 + 325 + chars = String.graphemes(body) 326 + 327 + non_newline_indices = 328 + chars 329 + |> Enum.with_index() 330 + |> Enum.filter(fn {c, _i} -> c != "\n" end) 331 + |> Enum.map(fn {_c, i} -> i end) 332 + 333 + masked_chars = 334 + if length(non_newline_indices) > 15 do 335 + middle_indices = Enum.slice(non_newline_indices, 10, length(non_newline_indices) - 15) 336 + mask_set = MapSet.new(middle_indices) 337 + 338 + chars 339 + |> Enum.with_index() 340 + |> Enum.map(fn {c, i} -> if MapSet.member?(mask_set, i), do: "*", else: c end) 341 + else 342 + chars 343 + end 344 + 345 + masked_body = Enum.join(masked_chars) 346 + 347 + "#{header}#{masked_body}#{footer}" 348 + |> String.replace("\n", "\\n") 349 + 350 + _ -> 351 + String.replace(private_key, "\n", "\\n") 352 + end 353 + end 354 + 355 + defp redact_tailscale_key(auth_key) when byte_size(auth_key) > 14 do 356 + String.slice(auth_key, 0, 11) <> 357 + String.duplicate("*", byte_size(auth_key) - 14) <> 358 + String.slice(auth_key, -3, 3) 359 + end 360 + 361 + defp redact_tailscale_key(auth_key), do: auth_key 215 362 216 363 defp do_wait(id, opts, deadline, interval_ms) do 217 364 if System.monotonic_time(:millisecond) >= deadline do
+64
lib/pocketenv/crypto.ex
··· 1 + defmodule Pocketenv.Crypto do 2 + @moduledoc false 3 + # Client-side encryption matching libsodium's crypto_box_seal (anonymous sealed box). 4 + # 5 + # Algorithm: 6 + # 1. Generate ephemeral Curve25519 (X25519) keypair via :crypto 7 + # 2. Derive nonce = first 24 bytes of BLAKE2b(eph_pk || recipient_pk) 8 + # 3. Encrypt with NaCl crypto_box(message, nonce, eph_sk, recipient_pk) [Kcl] 9 + # 4. Output = eph_pk (32 bytes) || ciphertext 10 + # 5. Base64url-encode without padding (matches TypeScript sodium implementation) 11 + # 12 + # The server's public key is resolved in order from: 13 + # 1. :public_key in the :pocketenv_ex application config 14 + # 2. POCKETENV_PUBLIC_KEY environment variable 15 + # 3. The default production key below 16 + 17 + @default_public_key "2bf96e12d109e6948046a7803ef1696e12c11f04f20a6ce64dbd4bcd93db9341" 18 + 19 + @doc """ 20 + Encrypts `plaintext` using the server's public key via a sealed box. 21 + 22 + Returns `{:ok, base64url_ciphertext}`. 23 + """ 24 + @spec encrypt(String.t()) :: {:ok, String.t()} 25 + def encrypt(plaintext) when is_binary(plaintext) do 26 + sealed = box_seal(plaintext, public_key()) 27 + {:ok, Base.url_encode64(sealed, padding: false)} 28 + end 29 + 30 + @doc """ 31 + Same as `encrypt/1` but returns the ciphertext directly. 32 + """ 33 + @spec encrypt!(String.t()) :: String.t() 34 + def encrypt!(plaintext) do 35 + {:ok, ciphertext} = encrypt(plaintext) 36 + ciphertext 37 + end 38 + 39 + # --------------------------------------------------------------------------- 40 + # Private 41 + # --------------------------------------------------------------------------- 42 + 43 + # Implements libsodium crypto_box_seal using Kcl (NaCl crypto_box) + 44 + # Erlang :crypto for X25519 key generation and BLAKE2b nonce derivation. 45 + defp box_seal(message, recipient_pk) do 46 + {eph_pk, eph_sk} = :crypto.generate_key(:ecdh, :x25519) 47 + 48 + # nonce = BLAKE2b(eph_pk || recipient_pk), take first 24 bytes 49 + nonce = :crypto.hash(:blake2b, eph_pk <> recipient_pk) |> binary_part(0, 24) 50 + 51 + {ciphertext, _state} = Kcl.box(message, nonce, eph_sk, recipient_pk) 52 + 53 + eph_pk <> ciphertext 54 + end 55 + 56 + defp public_key do 57 + hex = 58 + Application.get_env(:pocketenv_ex, :public_key) || 59 + System.get_env("POCKETENV_PUBLIC_KEY") || 60 + @default_public_key 61 + 62 + Base.decode16!(hex, case: :mixed) 63 + end 64 + end
+144 -1
lib/sandbox.ex
··· 31 31 """ 32 32 33 33 alias Pocketenv.API 34 - alias Sandbox.Types.{ExecResult, Port, Profile} 34 + alias Sandbox.Types.{ExecResult, Port, Profile, Secret, SshKey, TailscaleAuthKey} 35 35 36 36 @type status :: :running | :stopped | :unknown 37 37 ··· 367 367 368 368 def vscode(%__MODULE__{} = sandbox, opts) do 369 369 API.expose_vscode(sandbox.name, opts) 370 + end 371 + 372 + # --------------------------------------------------------------------------- 373 + # Secrets 374 + # --------------------------------------------------------------------------- 375 + 376 + @doc """ 377 + Lists all secrets for the sandbox. 378 + 379 + ## Options 380 + 381 + - `:limit` — max results (default: `100`). 382 + - `:offset` — pagination offset (default: `0`). 383 + - `:token` — bearer token override. 384 + 385 + ## Example 386 + 387 + {:ok, secrets} = sandbox |> Sandbox.list_secrets() 388 + """ 389 + @spec list_secrets(t() | {:ok, t()}, keyword()) :: 390 + {:ok, [Secret.t()]} | {:error, term()} 391 + def list_secrets(sandbox_or_result, opts \\ []) 392 + def list_secrets({:ok, %__MODULE__{} = sandbox}, opts), do: list_secrets(sandbox, opts) 393 + 394 + def list_secrets(%__MODULE__{} = sandbox, opts) do 395 + API.list_secrets(sandbox.id, opts) 396 + end 397 + 398 + @doc """ 399 + Adds an encrypted secret to the sandbox. 400 + 401 + ## Example 402 + 403 + sandbox |> Sandbox.set_secret("DATABASE_URL", "postgres://...") 404 + """ 405 + @spec set_secret(t() | {:ok, t()}, String.t(), String.t(), keyword()) :: 406 + {:ok, map()} | {:error, term()} 407 + def set_secret(sandbox_or_result, name, value, opts \\ []) 408 + 409 + def set_secret({:ok, %__MODULE__{} = sandbox}, name, value, opts), 410 + do: set_secret(sandbox, name, value, opts) 411 + 412 + def set_secret(%__MODULE__{} = sandbox, name, value, opts) do 413 + API.add_secret(sandbox.id, name, value, opts) 414 + end 415 + 416 + @doc """ 417 + Deletes a secret by its id. 418 + 419 + ## Example 420 + 421 + sandbox |> Sandbox.delete_secret("secret-id") 422 + """ 423 + @spec delete_secret(t() | {:ok, t()}, String.t(), keyword()) :: 424 + {:ok, map()} | {:error, term()} 425 + def delete_secret(sandbox_or_result, id, opts \\ []) 426 + 427 + def delete_secret({:ok, %__MODULE__{} = sandbox}, id, opts), 428 + do: delete_secret(sandbox, id, opts) 429 + 430 + def delete_secret(%__MODULE__{}, id, opts) do 431 + API.delete_secret(id, opts) 432 + end 433 + 434 + # --------------------------------------------------------------------------- 435 + # SSH Keys 436 + # --------------------------------------------------------------------------- 437 + 438 + @doc """ 439 + Fetches the SSH key pair for the sandbox. 440 + 441 + ## Example 442 + 443 + {:ok, ssh_key} = sandbox |> Sandbox.get_ssh_keys() 444 + IO.puts(ssh_key.public_key) 445 + """ 446 + @spec get_ssh_keys(t() | {:ok, t()}, keyword()) :: 447 + {:ok, SshKey.t()} | {:error, term()} 448 + def get_ssh_keys(sandbox_or_result, opts \\ []) 449 + def get_ssh_keys({:ok, %__MODULE__{} = sandbox}, opts), do: get_ssh_keys(sandbox, opts) 450 + 451 + def get_ssh_keys(%__MODULE__{} = sandbox, opts) do 452 + API.get_ssh_keys(sandbox.id, opts) 453 + end 454 + 455 + @doc """ 456 + Stores an SSH key pair for the sandbox. The private key is encrypted 457 + client-side before transmission. 458 + 459 + ## Example 460 + 461 + sandbox |> Sandbox.set_ssh_keys(private_pem, public_key) 462 + """ 463 + @spec set_ssh_keys(t() | {:ok, t()}, String.t(), String.t(), keyword()) :: 464 + {:ok, map()} | {:error, term()} 465 + def set_ssh_keys(sandbox_or_result, private_key, public_key, opts \\ []) 466 + 467 + def set_ssh_keys({:ok, %__MODULE__{} = sandbox}, private_key, public_key, opts), 468 + do: set_ssh_keys(sandbox, private_key, public_key, opts) 469 + 470 + def set_ssh_keys(%__MODULE__{} = sandbox, private_key, public_key, opts) do 471 + API.put_ssh_keys(sandbox.id, private_key, public_key, opts) 472 + end 473 + 474 + # --------------------------------------------------------------------------- 475 + # Tailscale 476 + # --------------------------------------------------------------------------- 477 + 478 + @doc """ 479 + Fetches the Tailscale auth key for the sandbox. 480 + 481 + ## Example 482 + 483 + {:ok, ts} = sandbox |> Sandbox.get_tailscale_auth_key() 484 + """ 485 + @spec get_tailscale_auth_key(t() | {:ok, t()}, keyword()) :: 486 + {:ok, TailscaleAuthKey.t()} | {:error, term()} 487 + def get_tailscale_auth_key(sandbox_or_result, opts \\ []) 488 + 489 + def get_tailscale_auth_key({:ok, %__MODULE__{} = sandbox}, opts), 490 + do: get_tailscale_auth_key(sandbox, opts) 491 + 492 + def get_tailscale_auth_key(%__MODULE__{} = sandbox, opts) do 493 + API.get_tailscale_auth_key(sandbox.id, opts) 494 + end 495 + 496 + @doc """ 497 + Stores a Tailscale auth key for the sandbox. The key is encrypted 498 + client-side before transmission and must start with `"tskey-auth-"`. 499 + 500 + ## Example 501 + 502 + sandbox |> Sandbox.set_tailscale_auth_key("tskey-auth-xxxx") 503 + """ 504 + @spec set_tailscale_auth_key(t() | {:ok, t()}, String.t(), keyword()) :: 505 + {:ok, map()} | {:error, term()} 506 + def set_tailscale_auth_key(sandbox_or_result, auth_key, opts \\ []) 507 + 508 + def set_tailscale_auth_key({:ok, %__MODULE__{} = sandbox}, auth_key, opts), 509 + do: set_tailscale_auth_key(sandbox, auth_key, opts) 510 + 511 + def set_tailscale_auth_key(%__MODULE__{} = sandbox, auth_key, opts) do 512 + API.put_tailscale_auth_key(sandbox.id, auth_key, opts) 370 513 end 371 514 372 515 # ---------------------------------------------------------------------------
+65
lib/sandbox/types.ex
··· 73 73 } 74 74 end 75 75 end 76 + 77 + defmodule Secret do 78 + @moduledoc "Represents a secret stored in a sandbox." 79 + 80 + @type t :: %__MODULE__{ 81 + id: String.t(), 82 + name: String.t(), 83 + created_at: String.t() | nil 84 + } 85 + 86 + defstruct [:id, :name, :created_at] 87 + 88 + @doc "Build a Secret from the raw API map." 89 + def from_map(map) when is_map(map) do 90 + %__MODULE__{ 91 + id: map["id"], 92 + name: map["name"], 93 + created_at: map["createdAt"] 94 + } 95 + end 96 + end 97 + 98 + defmodule SshKey do 99 + @moduledoc "Represents an SSH key pair associated with a sandbox." 100 + 101 + @type t :: %__MODULE__{ 102 + id: String.t(), 103 + private_key: String.t() | nil, 104 + public_key: String.t() | nil, 105 + created_at: String.t() | nil 106 + } 107 + 108 + defstruct [:id, :private_key, :public_key, :created_at] 109 + 110 + @doc "Build an SshKey from the raw API map." 111 + def from_map(map) when is_map(map) do 112 + %__MODULE__{ 113 + id: map["id"], 114 + private_key: map["privateKey"], 115 + public_key: map["publicKey"], 116 + created_at: map["createdAt"] 117 + } 118 + end 119 + end 120 + 121 + defmodule TailscaleAuthKey do 122 + @moduledoc "Represents a Tailscale auth key associated with a sandbox." 123 + 124 + @type t :: %__MODULE__{ 125 + id: String.t(), 126 + auth_key: String.t() | nil, 127 + created_at: String.t() | nil 128 + } 129 + 130 + defstruct [:id, :auth_key, :created_at] 131 + 132 + @doc "Build a TailscaleAuthKey from the raw API map." 133 + def from_map(map) when is_map(map) do 134 + %__MODULE__{ 135 + id: map["id"], 136 + auth_key: map["authKey"], 137 + created_at: map["createdAt"] 138 + } 139 + end 140 + end 76 141 end
+1
mix.exs
··· 26 26 [ 27 27 {:req, "~> 0.5"}, 28 28 {:jason, "~> 1.4"}, 29 + {:kcl, "~> 0.1"}, 29 30 {:ex_doc, "~> 0.31", only: :dev, runtime: false} 30 31 ] 31 32 end
+8
mix.lock
··· 1 1 %{ 2 + "chacha20": {:hex, :chacha20, "0.3.6", "6cef6d6cea44351c6009576ed4a78791e3df818ce511e92aa6be8ca058516edf", [:mix], [], "hexpm", "40bc6b1f4816661c07a3244d46d74640f108f69eb61f96d2dd22dcba0e7fca38"}, 3 + "curve25519": {:hex, :curve25519, "0.1.4", "819382affdc8d6c87567cec071fedb91d94ba81581f51413809e8cb945527988", [:mix], [], "hexpm", "3460590592da61d5d0c309e2ec469290963129bfb6ee6e5f692ae8e0334161b3"}, 2 4 "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 5 + "ed25519": {:hex, :ed25519, "0.2.5", "13577a6ff5035468ffb63ac1b0a249020d78b1d771a6ec221d476a87144acc4d", [:mix], [], "hexpm", "87233bfc85d0be366eddf870b6c021396fa34bdc48472aa8582b7333d1459147"}, 6 + "enacl": {:hex, :enacl, "1.2.1", "7776480b9b3d42a51d66dbbcbf17fa3d79285b3d2adcb4d5b5bd0b70f0ef1949", [:rebar3], [], "hexpm", "67bbbeddd2564dc899a3dcbc3765cd6ad71629134f1e500a50ec071f0f75e552"}, 7 + "equivalex": {:hex, :equivalex, "0.1.4", "3c6e00c47c6cbac63872e69eb2204ba71e0818afd9d0875a97389f7ff24f5ec8", [:mix], [], "hexpm", "e63af7625d18d1be6cb88aaeef5046be6c0b3d7aa8e735a51203ed17076be8ba"}, 3 8 "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, 4 9 "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, 5 10 "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 6 11 "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 + "kcl": {:hex, :kcl, "0.6.6", "156de704ac2e118d9617a56a4703831cad57a1f70cbf3ecbab4cd2dd2f395037", [:mix], [{:curve25519, "~> 0.1", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 0.2", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 0.4", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 0.3", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "4470b99394dc32ea015e586e66a031e4df1ab8ad8329b49b8ca79c41711c3c16"}, 7 13 "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 8 14 "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 9 15 "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, ··· 12 18 "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 13 19 "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 20 "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 21 + "poly1305": {:hex, :poly1305, "0.4.5", "239d19597af369e2b570aed7f2a05e1a999a0a372774b28bcf387f47bd7958d4", [:mix], [{:chacha20, "~> 0.3", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 0.1", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "2a24b02a57d56c2b459f1d6265391843a6f3591137db7400d32b7ea26b9e3ef1"}, 15 22 "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"}, 23 + "salsa20": {:hex, :salsa20, "0.3.4", "d2ca110888879eccef9d69241bbf9a3260cdfeda3f71236cbc30b03dfe5433b9", [:mix], [], "hexpm", "b6bd54042e4fc419d9b7956d2d1c0a730dc3c549d847842c0ac3553be8ebedf0"}, 16 24 "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, 17 25 }