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 soIO.inspect, ExUnit diffs,Loggermessages built withinspect, Elixir crash-report translation of exceptions and arguments) prints#Secret<redacted>.- String interpolation and
to_string/1raiseProtocol.UndefinedError: there is deliberately noString.Charsimplementation, and the error message itself inspects the value, so it stays redacted. Loggermetadata is dropped byLogger.Formatter, which renders only metadata values that implementString.Chars.JSON.encode!/1raisesArgumentErrorwithout the value. Raising rather than writing a placeholder such as"[redacted]"is deliberate: encoding a secret is always a programming error (wire code callsexpose/1exactly where the value belongs), a placeholder would send a bogus credential orrequestStateto 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
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.