defmodule Firebird.Phoenix.Session do @moduledoc """ WASM-accelerated Phoenix-style session and cookie handling. Provides cookie parsing, cookie building, session encoding/decoding, flash message management, and secure cookie signing compiled to WebAssembly. ## Features - Cookie header parsing - Set-Cookie header building with options (path, domain, max-age, etc.) - Session data encoding/decoding (hex encoding) - Flash message management (put/get/clear) - Cookie signing and verification (HMAC-like with constant-time comparison) ## Examples # Parse cookies from header {:ok, cookies} = Session.parse_cookies(instance, "session=abc123; theme=dark") # => {:ok, %{"session" => "abc123", "theme" => "dark"}} # Sign and verify cookies {:ok, signed} = Session.sign_cookie(instance, "secret_key", "user_data") {:ok, "user_data"} = Session.verify_cookie(instance, "secret_key", signed) """ alias Firebird.Phoenix.FastHelper @doc """ Parse a Cookie header into a key-value map. ## Examples {:ok, cookies} = Session.parse_cookies(instance, "session_id=abc123; theme=dark; lang=en") # => {:ok, %{"session_id" => "abc123", "theme" => "dark", "lang" => "en"}} """ @spec parse_cookies(pid() | reference(), String.t()) :: {:ok, map()} | {:error, term()} def parse_cookies(instance, cookie_header) when is_binary(cookie_header) do case FastHelper.call_string(instance, "parse_cookies", cookie_header) do {:ok, ""} -> {:ok, %{}} {:ok, result} -> cookies = result |> String.split("\n") |> Enum.filter(&(&1 != "")) |> Enum.into(%{}, fn line -> case String.split(line, "=", parts: 2) do [k, v] -> {k, v} [k] -> {k, ""} end end) {:ok, cookies} {:error, reason} -> {:error, reason} end end @doc """ Build a Set-Cookie header string. ## Options - `:path` - Cookie path (default: none) - `:domain` - Cookie domain - `:max_age` - Max age in seconds - `:secure` - Secure flag (boolean) - `:http_only` - HttpOnly flag (boolean) - `:same_site` - SameSite attribute ("Strict", "Lax", "None") ## Examples {:ok, cookie} = Session.build_cookie(instance, "session", "abc123", path: "/", http_only: true, secure: true, same_site: "Strict", max_age: "3600" ) """ @spec build_cookie(pid() | reference(), String.t(), String.t(), keyword()) :: {:ok, String.t()} | {:error, term()} def build_cookie(instance, name, value, opts \\ []) when is_binary(name) and is_binary(value) do option_lines = opts |> Enum.map(fn {:path, v} -> "path=#{v}" {:domain, v} -> "domain=#{v}" {:max_age, v} -> "max_age=#{v}" {:expires, v} -> "expires=#{v}" {:secure, true} -> "secure=true" {:http_only, true} -> "http_only=true" {:same_site, v} -> "same_site=#{v}" _ -> nil end) |> Enum.filter(& &1) |> Enum.join("\n") input = "#{name}\n#{value}\n---OPTIONS---\n#{option_lines}" case FastHelper.call_string(instance, "build_cookie", input) do {:ok, "error|" <> reason} -> {:error, reason} {:ok, cookie} -> {:ok, cookie} {:error, reason} -> {:error, reason} end end @doc """ Encode session data as a cookie-safe string. ## Examples {:ok, encoded} = Session.encode_session(instance, %{ "user_id" => "42", "role" => "admin" }) """ @spec encode_session(pid() | reference(), map()) :: {:ok, String.t()} | {:error, term()} def encode_session(instance, data) when is_map(data) do input = data |> Enum.map(fn {k, v} -> "#{k}=#{v}" end) |> Enum.join("\n") FastHelper.call_string(instance, "encode_session", input) end @doc """ Decode session data from a cookie-safe string. ## Examples {:ok, data} = Session.decode_session(instance, encoded) # => {:ok, %{"user_id" => "42", "role" => "admin"}} """ @spec decode_session(pid() | reference(), String.t()) :: {:ok, map()} | {:error, term()} def decode_session(instance, encoded) when is_binary(encoded) do case FastHelper.call_string(instance, "decode_session", encoded) do {:ok, "error|" <> reason} -> {:error, reason} {:ok, result} -> data = result |> String.split("\n") |> Enum.filter(&(&1 != "")) |> Enum.into(%{}, fn line -> case String.split(line, "=", parts: 2) do [k, v] -> {k, v} [k] -> {k, ""} end end) {:ok, data} {:error, reason} -> {:error, reason} end end @doc """ Manage flash messages. ## Actions - `:put` - Add/update flash messages - `:get` - Get a specific flash message by key - `:clear` - Clear all flash messages ## Examples # Put flash messages {:ok, state} = Session.flash(instance, :put, %{}, %{"info" => "User created"}) # Get a flash message {:ok, msg} = Session.flash(instance, :get, state, "info") # Clear flash {:ok, %{}} = Session.flash(instance, :clear, state) """ @spec flash(pid() | reference(), :put | :get | :clear, map(), map() | String.t()) :: {:ok, map() | String.t()} | {:error, term()} def flash(instance, action, current_state \\ %{}, data \\ %{}) def flash(instance, :put, current_state, data) when is_map(data) do flash_lines = Enum.map(current_state, fn {k, v} -> "#{k}=#{v}" end) |> Enum.join("\n") data_lines = Enum.map(data, fn {k, v} -> "#{k}=#{v}" end) |> Enum.join("\n") input = "put\n---FLASH---\n#{flash_lines}\n---DATA---\n#{data_lines}" case FastHelper.call_string(instance, "manage_flash", input) do {:ok, "error|" <> reason} -> {:error, reason} {:ok, result} -> {:ok, parse_flash_state(result)} {:error, reason} -> {:error, reason} end end def flash(instance, :get, current_state, key) when is_binary(key) do flash_lines = Enum.map(current_state, fn {k, v} -> "#{k}=#{v}" end) |> Enum.join("\n") input = "get\n---FLASH---\n#{flash_lines}\n---DATA---\n#{key}" case FastHelper.call_string(instance, "manage_flash", input) do {:ok, ""} -> {:ok, nil} {:ok, value} -> {:ok, value} {:error, reason} -> {:error, reason} end end def flash(instance, :clear, current_state, _data) do flash_lines = Enum.map(current_state, fn {k, v} -> "#{k}=#{v}" end) |> Enum.join("\n") input = "clear\n---FLASH---\n#{flash_lines}\n---DATA---\n" case FastHelper.call_string(instance, "manage_flash", input) do {:ok, _} -> {:ok, %{}} {:error, reason} -> {:error, reason} end end @doc """ Sign a cookie value for tamper protection. ## Examples {:ok, signed} = Session.sign_cookie(instance, "my_secret_key", "user_id=42") # => {:ok, "user_id=42.a1b2c3d4e5f6a7b8"} """ @spec sign_cookie(pid() | reference(), String.t(), String.t()) :: {:ok, String.t()} | {:error, term()} def sign_cookie(instance, secret, value) when is_binary(secret) and is_binary(value) do input = "#{secret}\n#{value}" case FastHelper.call_string(instance, "sign_cookie", input) do {:ok, "error|" <> reason} -> {:error, reason} {:ok, signed} -> {:ok, signed} {:error, reason} -> {:error, reason} end end @doc """ Verify a signed cookie value. ## Returns - `{:ok, original_value}` - Valid signature - `{:error, :invalid}` - Invalid signature - `{:error, reason}` - Other error ## Examples {:ok, signed} = Session.sign_cookie(instance, "secret", "data") {:ok, "data"} = Session.verify_cookie(instance, "secret", signed) {:error, :invalid} = Session.verify_cookie(instance, "wrong_secret", signed) """ @spec verify_cookie(pid() | reference(), String.t(), String.t()) :: {:ok, String.t()} | {:error, :invalid | term()} def verify_cookie(instance, secret, signed_value) when is_binary(secret) and is_binary(signed_value) do input = "#{secret}\n#{signed_value}" case FastHelper.call_string(instance, "verify_cookie", input) do {:ok, "valid|" <> value} -> {:ok, value} {:ok, "invalid"} -> {:error, :invalid} {:ok, "error|" <> reason} -> {:error, reason} {:error, reason} -> {:error, reason} end end defp parse_flash_state(""), do: %{} defp parse_flash_state(result) do result |> String.split("\n") |> Enum.filter(&(&1 != "")) |> Enum.into(%{}, fn line -> case String.split(line, "=", parts: 2) do [k, v] -> {k, v} [k] -> {k, ""} end end) end end