# `Ankusa.SourceStore.Persistent`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/source_store/persistent.ex#L1)

Writable source store: seed sources from config plus API-managed, tenant-scoped
sources persisted to disk.

Seeds (`sources:` in the store opts, the same shape `Ankusa.SourceStore.Static`
takes) are resolved at boot and are read-only: they never appear in
`list_tenant/2` and cannot be written through `put/5`. Sources created or
updated through the admin API are kept in ETS and written to this node's
`Ankusa.Store` (one key per source), so they survive a restart. They are this
node's alone: a fleet that manages sources through the API on more than one
node shares them through `Ankusa.SourceStore.Redis` (`ankusa_redis`) instead.

## Reads and writes

Reads (`fetch/2`, `get/3`, `list_tenant/2`) go straight to a `:protected` ETS
table, so they never serialize behind the GenServer. Writes go through the
GenServer, which is the table's owner and the only process that may modify it,
and persist the one changed key, synced, before replying.

## The decoder

The store does not know how to turn a JSON spec into a `Ankusa.Source` — that
is the operator's schema (`AnkusaServer.Config.source_from_map!/2` in the
server). It takes a `decoder:` function of `(source_id, spec_map -> keyword)`
and calls it on every write, and on every entry loaded from disk. A decoder
that raises fails the write with `{:error, :invalid, Exception.message(e)}`,
and a persisted entry that no longer decodes is skipped with a warning rather
than taken down the boot.

## Secrets at rest

With `secrets.kek` (or `secrets.kek_provider`) configured, the secret-bearing
fields of a spec (`Ankusa.Admin.Redact.reduce_hidden/3`: verify secrets, sink
credentials, header values) are encrypted before the row is written, so
neither the node's store files nor a backup of them hold a secret in the
clear (`Ankusa.Crypto.Envelope`). Rows are decrypted as they are loaded; ETS
and the admin API's merge-the-stored-secret update see plaintext, as before.
A row this node cannot decrypt (no KEK, a key it was not given, a damaged
value) **refuses the boot**: skipping it would `404` every hook for that
source. `rewrap/1` (and `mix ankusa.secrets.rewrap` for a stopped node)
moves every row to the current key, and encrypts rows written before a KEK
existed.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `rewrap`

```elixir
@spec rewrap(atom()) ::
  {:ok, %{rewrapped: non_neg_integer(), current: non_neg_integer()}}
  | {:error, term()}
```

Rewrite every stored row under the current KEK: rows still in plaintext, and
rows encrypted under a `secrets.kek_previous` key. Returns how many rows were
rewritten and how many were already current.

# `rewrap_rows`

```elixir
@spec rewrap_rows(atom(), Ankusa.Crypto.Envelope.keyring(), keyword()) ::
  {:ok, %{rewrapped: non_neg_integer(), current: non_neg_integer()}}
  | {:error, term()}
```

`rewrap/1` on a store nobody has open as a source store (the
`mix ankusa.secrets.rewrap` task, a test): `Ankusa.Store` must be running for
`instance`. `dry_run: true` counts and writes nothing.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

---

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