cowl

Types

Selects which characters to expose in a Peek strategy.

pub type PeekMode {
  First(n: Int)
  Last(n: Int)
  Both(n: Int, m: Int)
}

Constructors

  • First(n: Int)

    First n characters, then the filler.

  • Last(n: Int)

    Filler, then the last n characters.

  • Both(n: Int, m: Int)

    First n characters, filler, then the last m.

An opaque wrapper that keeps a value secret.

The value lives inside a closure, so io.debug, echo, and string.inspect never reveal it.

An optional default_masker controls what mask returns when no explicit Strategy is given. Constructors like token set a smart default; secret and labeled leave it unset, falling back to "***".

pub opaque type Secret(a)

How to render a string secret as text.

pub type Strategy {
  Stars
  Fixed(text: String)
  Label
  Peek(mode: PeekMode, filler: String)
  Custom(f: fn(String) -> String)
}

Constructors

  • Stars

    Always "***".

  • Fixed(text: String)

    A fixed replacement string; the secret value is ignored.

  • Label

    The label in brackets, or "[secret]" if no label is set.

  • Peek(mode: PeekMode, filler: String)

    Show a small window of the secret with filler around it.

  • Custom(f: fn(String) -> String)

    Apply an arbitrary function to the raw value.

    ⚠️ The function receives the raw secret value. Never pass a logging function directly — it will expose the secret. Use a pure transform only.

Values

pub fn and_then(
  secret: Secret(a),
  f: fn(a) -> Secret(b),
) -> Secret(b)

Chain a transformation that itself returns a Secret, avoiding Secret(Secret(b)).

The outer secret’s label is preserved. The inner secret’s default masker is carried forward (since it already knows how to display b). The inner secret’s label is discarded.

cowl.secret("hunter2")
|> cowl.and_then(fn(pw) { hash_password(pw) |> cowl.secret })
pub fn equal(a: Secret(a), b: Secret(a)) -> Bool

Compare two secrets by value — labels and maskers are ignored.

pub fn field(secret: Secret(a)) -> #(String, String)

Return #(label, "***") for structured logging. Falls back to "secret" if unlabeled.

pub fn field_with(
  secret: Secret(String),
  strategy: Strategy,
) -> #(String, String)

Like field, with an explicit masking strategy. Requires Secret(String).

pub fn from_option(
  opt: option.Option(a),
) -> option.Option(Secret(a))

Wrap the Some value of an Option as a secret, returning None unchanged.

pub fn from_result(res: Result(a, e)) -> Result(Secret(a), e)

Wrap the Ok value of a Result as a secret, passing errors through.

pub fn get_label(secret: Secret(a)) -> option.Option(String)

Return the label, if any.

pub fn labeled(value: a, label: String) -> Secret(a)

Wrap value as a secret with a label.

pub fn labeled_from_option(
  opt: option.Option(a),
  label: String,
) -> option.Option(Secret(a))

Like from_option, but also attaches a label.

pub fn labeled_from_result(
  res: Result(a, e),
  label: String,
) -> Result(Secret(a), e)

Like from_result, but also attaches a label.

pub fn map(secret: Secret(a), f: fn(a) -> b) -> Secret(b)

Transform the wrapped value while keeping it secret.

The label is preserved. The default masker is not transferred because the masker type fn(a) -> String cannot apply to the new type b. Attach a new masker via new or token if needed.

pub fn map_label(
  secret: Secret(a),
  f: fn(String) -> String,
) -> Secret(a)

Transform only the label, leaving the value and masker untouched.

pub fn mask(secret: Secret(a)) -> String

Render the secret as a safe string using its default masker.

  • If a default_masker was set (e.g. via token or new), it is called.
  • Otherwise returns "***".

Use mask_with to apply an explicit Strategy at the call site.

pub fn mask_with(
  secret: Secret(String),
  strategy: Strategy,
) -> String

Render a string secret using the given strategy, ignoring any default masker.

pub fn new(value: a, masker: fn(a) -> String) -> Secret(a)

Wrap value with an explicit masker function used by mask.

The masker receives the raw value and must return a safe string representation. Never use a logging function as the masker.

pub fn remove_label(secret: Secret(a)) -> Secret(a)

Remove the label from a secret. The value and masker are untouched.

pub fn secret(value: a) -> Secret(a)

Wrap value as a secret with no label and no custom masker. mask falls back to "***".

pub fn string(value: String) -> Secret(String)

Wrap a String value as a secret. Equivalent to secret but constrains the type to String at the call site.

pub fn tap_masked(
  secret: Secret(a),
  f: fn(String) -> b,
) -> Secret(a)

Run f on the masked representation for its side effects; return the original secret unchanged. Safe to use with logging functions.

api_key
|> cowl.tap_masked(fn(masked) { logger.info("key in use: " <> masked) })
|> make_request
pub fn to_string(secret: Secret(a)) -> String

A safe debug string — always "Secret(***)" or "Secret(<masked>)". Never reveals the actual value.

pub fn token(value: String) -> Secret(String)

Wrap an API token with a smart partial-reveal masker.

mask will show the first 4 and last 4 characters with "..." in between — enough to identify a token without exposing it.

cowl.token("sk-abc123xyz789") |> cowl.mask
// "sk-a...y789"  (if len > 8)
pub fn with_label(secret: Secret(a), label: String) -> Secret(a)

Set or replace the label on a secret. The value and masker are untouched.

pub fn with_secret(secret: Secret(a), f: fn(a) -> b) -> b

Pass the raw value to f without letting it escape the return type.

This is the preferred way to consume a secret. The raw value is confined to the callback scope and cannot propagate further through your code.

cowl.with_secret(api_key, fn(raw) { send_request(raw) })
Search Document