Packages

Run WebAssembly from Elixir. Load WASM modules in Rust, Go, C — call them like native functions.

Current section

Files

Jump to
firebird lib firebird phoenix session.ex
Raw

lib/firebird/phoenix/session.ex

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