Packages
crosswake_threadline
0.1.0
Threadline audit and correlation observer for the Crosswake route-policy system.
Current section
Files
Jump to
Current section
Files
lib/crosswake/audit/ledger.ex
defmodule Crosswake.Audit.Ledger do
@moduledoc """
Defines the core Audit Ledger contract struct and HMAC anonymization helper.
"""
@type t :: %__MODULE__{
thread_id: String.t() | nil,
correlation_id: String.t() | nil,
route_id: String.t() | nil,
actor_ref: String.t() | nil,
actor_kind: String.t() | nil,
event_class: String.t() | nil,
event_type: String.t() | nil,
outcome: String.t() | nil,
provenance: String.t() | nil,
occurred_at: DateTime.t() | nil,
recorded_at: DateTime.t() | nil,
idempotency_key: String.t() | nil,
metadata: map() | nil,
row_hash: String.t() | nil,
prev_hash: String.t() | nil
}
defstruct [
:thread_id,
:correlation_id,
:route_id,
:actor_ref,
:actor_kind,
:event_class,
:event_type,
:outcome,
:provenance,
:occurred_at,
:recorded_at,
:idempotency_key,
:metadata,
:row_hash,
:prev_hash
]
@doc """
Computes an HMAC-SHA256 of the given `id` using a secret.
If `opts[:secret]` is provided, it is used as the secret.
Otherwise, it falls back to `Application.get_env(:crosswake, :audit_hmac_secret)`.
Raises `ArgumentError` if no secret is available.
Returns the HMAC as a lowercase base16 string.
"""
@spec actor_ref(any(), keyword()) :: String.t()
def actor_ref(id, opts \\ []) do
secret = opts[:secret] || Application.get_env(:crosswake, :audit_hmac_secret)
unless secret do
raise ArgumentError,
"Missing HMAC secret for Crosswake.Audit.Ledger.actor_ref/2. " <>
"Provide it via the :secret option (actor_ref(id, secret: \"...\")) " <>
"or set Application.put_env(:crosswake, :audit_hmac_secret, \"...\") in your config."
end
:crypto.mac(:hmac, :sha256, secret, to_string(id))
|> Base.encode16(case: :lower)
end
end