redacted-error 0.2.0

Stable public error messages with debug-only diagnostic detail
Documentation
# redacted-error

Stable public error messages with debug-only diagnostic detail.

`redacted-error` is a small facade for Rust errors that cross crate, process,
API, or protocol boundaries. It lets applications expose stable public error
codes and messages while preserving rich diagnostics in debug builds.

## Features

- `message!` for displayable static public-facing messages.
- `message_string!` for owned public-facing messages.
- `detail!`, `detail`, and `display` for debug-only runtime diagnostics.
- `ErrorCode` and `PublicError` traits for stable public API responses.
- `impl_redacted_debug!` to keep release `Debug` from leaking enum fields.
- Optional static string obfuscation behind the default `obfuscate` feature.

The public API does not expose the obfuscation implementation. Today the
default backend is `obfstr`; later releases can replace that backend without
requiring callers to rename macros.

`message!` returns a `Message` wrapper, not a borrowed `&'static str`. That is
intentional: it keeps backend-specific lifetime behavior out of caller code.
Use `message_string!` when a trait or response type needs an owned `String`.

## Usage

```toml
[dependencies]
redacted-error = "0.2"
```

Disable static string obfuscation:

```toml
[dependencies]
redacted-error = { version = "0.2", default-features = false }
```

## Example

```rust
use redacted_error::message as m;
use thiserror::Error;

#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(Error)]
pub enum TransportError {
    #[cfg_attr(debug_assertions, error("{prefix} {0}", prefix = m!("listener bind failed:")))]
    #[cfg_attr(not(debug_assertions), error("{}", m!("listener bind failed")))]
    ListenerBindFailed(String),
}

redacted_error::impl_redacted_debug!(TransportError);

impl redacted_error::ErrorCode for TransportError {
    fn code(&self) -> redacted_error::Message {
        match self {
            Self::ListenerBindFailed(_) => redacted_error::message!("transport.listener_bind_failed"),
        }
    }
}

impl redacted_error::PublicError for TransportError {
    fn public_message(&self) -> redacted_error::Message {
        match self {
            Self::ListenerBindFailed(_) => redacted_error::message!("listener bind failed"),
        }
    }
}

fn bind_failed(addr: &str, err: impl std::fmt::Display) -> TransportError {
    TransportError::ListenerBindFailed(redacted_error::detail!(
        "{addr}: {err}"
    ))
}
```

In debug builds, `Display` can include diagnostic detail:

```text
listener bind failed: 127.0.0.1:8080: address already in use
```

In release builds, `Display` and `Debug` stay public-safe:

```text
listener bind failed
```

For API responses, use stable structured fields rather than parsing `Display`:

```json
{
  "code": "transport.listener_bind_failed",
  "message": "listener bind failed"
}
```

## Security

This crate is a leakage-reduction tool, not a confidentiality boundary.

- The `obfuscate` feature only raises the bar against trivial `strings`-style
  inspection of compiled binaries. It is **not** a defense against a debugger,
  dynamic instrumentation, symbol tables, or any motivated reverse engineer.
  Do not treat obfuscated literals as secret.
- Diagnostic-detail stripping is gated on `cfg(debug_assertions)`. Cargo's
  standard `release` profile turns this off, but `[profile.release]
  debug-assertions = true` re-enables it — and with it, every `detail!` /
  `display` / `detail` call leaks runtime detail in release builds. Avoid
  that combination if redaction matters.
- The `detail!` macro skips evaluating its format arguments in release. The
  `detail` and `display` free functions still evaluate (and drop) their
  argument; prefer the macro when the argument has nontrivial cost or side
  effects.

## Migration Notes

For existing code using `obfstr` in error messages:

```rust
use obfstr::obfstr as s;
```

Prefer:

```rust
use redacted_error::message as s;
```

Then replace dynamic error details with `redacted_error::detail!` or
`redacted_error::display`.

For code currently using the local `error-detail` crate, the intended mapping
is:

| Local API | Public crate API |
| --- | --- |
| `error_detail::obfstr!("...")` | `redacted_error::message!("...")` |
| `error_detail::obfstring!("...")` | `redacted_error::message_string!("...")` |
| `error_detail::detail!("...")` | `redacted_error::detail!("...")` |
| `error_detail::display(err)` | `redacted_error::display(err)` |
| `error_detail::ErrorCode` | `redacted_error::ErrorCode` |
| `error_detail::impl_redacted_debug!` | `redacted_error::impl_redacted_debug!` |

## License

Licensed under either of Apache License, Version 2.0 or MIT license at your
option.