# `Ankusa.Admin.Redact`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/admin/redact.ex#L1)

Turn a `%Ankusa.Config{}` into a JSON-encodable map safe to hand to anyone
who can reach the admin API.

Adapter options are an open vocabulary (every adapter, and every third-party
adapter, names its own credentials), so redaction is an allowlist, not a
blocklist. There are two modes.

**Open** covers `%Ankusa.Config{}`'s own fields and a source entry's own
fields (`tenant`, `name`, `source_id`, `ingest_path`, `on_verify_failure`,
`trust_url_tenant`):
none of them holds a secret, so values are converted as they are. Maps and
keyword lists become string-keyed maps, atoms become strings (module atoms
via `inspect/1`), numbers, booleans, `nil` and binaries stay, functions
become `"#Function"`, and any other term goes through `inspect/1`. A value
shaped like a `{module, opts}` pair (an atom head, an Erlang module included,
with list or map opts), or a list of them, is never converted as it is: its
opts switch to closed mode.

**Closed** covers everything inside a `{module, opts}` pair (sinks,
verifiers, source store, blob store, codec, retry policy, route resolver,
routes store, lifecycle sinks) and a source's stored JSON spec. For each
key/value, the first matching rule wins:

  1. under `headers`, names stay and values become `"[REDACTED]"`;
  2. a `{module, opts}` pair (or a list of them) becomes
     `%{"module" => "Ankusa.Sink.Log", "opts" => %{}}`, its opts closed;
  3. a `{module, fun, args}` callback becomes `"Module.fun/arity"`, never its
     args (a static GCS token is a bare arg);
  4. numbers, booleans, `nil` and atoms stay; functions become `"#Function"`;
  5. structs, maps and non-empty keyword lists become closed maps;
  6. a list made only of such maps, keyword lists and pairs is converted
     element by element;
  7. under `url`, `endpoint` or `resource` a binary is shown with the
     password of `user:pass@` userinfo hidden, a lone `token@` userinfo
     hidden, and every query value hidden (names stay);
  8. a value under a key on the shown-key allowlist (ids, names, regions,
     topics and other non-secret identifiers) is shown;
  9. anything else, strings and charlists included, is `"[REDACTED]"`.

A new adapter option is therefore hidden until its key is added to
`@shown_keys`. Path and host of a URL are shown, so a capability URL whose
secret sits in the path is not protected.

# `config`

```elixir
@spec config(Ankusa.Config.t()) :: map()
```

A redacted, JSON-encodable view of the whole config.

# `map_hidden`

```elixir
@spec map_hidden(map(), (String.t() -&gt; String.t())) :: map()
```

`reduce_hidden/3` without an accumulator: replace every hidden string by `fun.(string)`.

# `reduce_hidden`

```elixir
@spec reduce_hidden(map(), acc, (String.t(), acc -&gt; {String.t(), acc})) ::
  {map(), acc}
when acc: term()
```

Walk a stored source spec (string keys, plain JSON values) and hand every
string the closed-mode rules would hide to `fun`, threading an accumulator.
Everything else comes back as it was.

This is the inverse of the allowlist above, and it is what decides which
fields of a spec are encrypted at rest (`Ankusa.Crypto.Envelope.seal/2`):
header values, every string under a key that is not on the shown-key
allowlist (`secret` included, in a list too), and a `url`/`endpoint`/
`resource` that carries credentials. A new adapter option is hidden, and so
encrypted, until its key is added to `@shown_keys`.

# `source_entry`

```elixir
@spec source_entry(Ankusa.SourceStore.stored()) :: map()
```

A redacted, JSON-encodable view of one tenant-scoped source, built from the
`Ankusa.SourceStore.stored()` map: the shape the admin API's source routes
return and the `data` of a source lifecycle event.

The stored spec is plain JSON (string keys, plus strings, numbers, maps, and
lists), so its `verify` and `sinks` take the closed-mode rules of the module
doc.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
