ConduitMcp.Client.Secret (ConduitMCP v0.11.0)

Copy Markdown View Source

An opaque wrapper for values that must never be printed or encoded by accident: bearer tokens, refresh tokens, MRTR requestState, legacy session ids, and environment values.

iex> secret = ConduitMcp.Client.Secret.new("s3cr3t")
iex> inspect(secret)
"#Secret<redacted>"
iex> ConduitMcp.Client.Secret.expose(secret)
"s3cr3t"

What it guards against

  • inspect/2 (and so IO.inspect, ExUnit diffs, Logger messages built with inspect, Elixir crash-report translation of exceptions and arguments) prints #Secret<redacted>.
  • String interpolation and to_string/1 raise Protocol.UndefinedError: there is deliberately no String.Chars implementation, and the error message itself inspects the value, so it stays redacted.
  • Logger metadata is dropped by Logger.Formatter, which renders only metadata values that implement String.Chars.
  • JSON.encode!/1 raises ArgumentError without the value. Raising rather than writing a placeholder such as "[redacted]" is deliberate: encoding a secret is always a programming error (wire code calls expose/1 exactly where the value belongs), a placeholder would send a bogus credential or requestState to the server and fail later and far away, and the raise happens while the iodata is being built, so no byte reaches the wire. The exception message names the module, never the value, so a crash report for the raise does not leak either.

What it does not guard against

Raw Erlang term printing (:io_lib.format("~p", ...), :sys.get_state/1, :erlang.term_to_binary/1) sees the struct's fields. Every client process that holds a Secret in its state therefore implements format_status/1 to redact it.

Summary

Functions

Returns the wrapped value. Call it only where the value is put on the wire or handed to code that needs the plain value.

Wraps value. Wrapping an existing Secret returns it unchanged.

Whether term is a Secret.

Types

t()

@opaque t()

Functions

expose(secret)

@spec expose(t()) :: term()

Returns the wrapped value. Call it only where the value is put on the wire or handed to code that needs the plain value.

new(secret)

@spec new(term()) :: t()

Wraps value. Wrapping an existing Secret returns it unchanged.

secret?(arg1)

@spec secret?(term()) :: boolean()

Whether term is a Secret.