dove-core 0.1.1

The shared library behind dove — client-side-encrypted, expiring file sharing from a cloud you own.
Documentation
# dove-core — design

**Status:** approved design, ready for an implementation plan.

## Goal

`dove-core` is the shared library behind [dove](https://dove.sh). It is extracted
from the `dove` CLI so that more than one front end — the CLI and the Dove desktop
app — drive the exact same logic, and so that additional storage **backends** can
be added without touching either front end.

The library holds all the logic — client-side encryption, the S3 + access-gate
transfer, provisioning, and the backend registry — and does **no terminal I/O of
its own**.

## Crate layout

Two crates in two repos, so each consumer depends on `dove-core` independently:

```
dove-core/     (this repo)      lib  — all logic, backend-agnostic
dove/          (the CLI repo)   bin  — `dove-cli`, the `dove` binary (clap + ui + prompts)
```

- The CLI repo's current `src/` is split (see the module map): pure logic moves
  into `dove-core`; the CLI-only pieces (clap, `ui.rs`, prompts) stay in `dove`.
  `dove-cli` depends on `dove-core`.
- `[[bin]] name = "dove"` stays on `dove-cli`, so the binary and every install
  channel (brew/scoop/curl/crates.io) remain `dove` / `cargo install dove-cli`.
- Dependency wiring: `dove-cli``dove-core` via a path dep while co-developing,
  a crates.io version once `dove-core` is published. `cargo publish` publishes
  `dove-core` first, then `dove-cli`.
- Other front ends (the desktop app) depend on `dove-core` the same way, so the
  container bytes and links they produce are identical to the CLI's.

## The dividing line

**`dove-core` never does terminal I/O and never prompts.** No `println`, no stdin,
no `ui.rs`. It takes fully-resolved inputs and reports progress through a callback.
Everything interactive — profile selection, `y/N` confirmation, progress bars,
colored output — lives in `dove-cli`.

**Keys and secrets never leave Rust as raw values.** Operations that produce a
shareable artifact return the *finished* thing: `share` returns the complete link
with the key already in the `#fragment`. The front end prints or hands off the
link; it never holds a raw key. A GUI front end can therefore keep secret-bearing
data out of its UI layer entirely.

### Module map (from the CLI's current `src/`)

| Today | Goes to | Notes |
|---|---|---|
| `crypto.rs` | dove-core | container format + PBKDF2 + HMAC ids + meta crypto; shared by every backend |
| `s3.rs` | dove-core | inside the `SelfHosted` backend |
| `config.rs` | dove-core | becomes the **backend registry** (below) |
| `secrets.rs` | dove-core | keyed store; never returned raw |
| `duration.rs` | dove-core | pure |
| `ledger.rs` | dove-core | local id→filename map for `list` |
| `provision.rs` | **split** | provisioning logic → dove-core; `choose_profile`/`confirm` → dove-cli |
| `apigw/cloudfront/breaker/domain/gate.rs` | dove-core | provisioning + gate mechanics |
| `share.rs`, `get.rs`, `gatectl.rs` | **split** | core operation → dove-core; printing/prompts → dove-cli |
| `main.rs`, `ui.rs` | dove-cli | clap dispatch + terminal rendering |

## dove-core API

### The transfer seam

```rust
pub trait Transfer {
    fn share(&self, req: ShareRequest, progress: &dyn Progress) -> Result<Share, Error>;
    fn get(&self, req: GetRequest, progress: &dyn Progress) -> Result<Fetched, Error>;
    fn list(&self) -> Result<Vec<ShareInfo>, Error>;
    fn revoke(&self, id: &ShareId) -> Result<(), Error>;
    fn status(&self) -> Result<BackendStatus, Error>;
}
```

- `dove-core` ships **one** implementation, `SelfHosted` (S3 + gate + presign).
- `ShareRequest`/`GetRequest` carry resolved inputs (file path, expiry, downloads,
  pin, from/message) — no interactivity.
- `Share` is the finished artifact: the link (key already embedded), the id, the
  size, expiry — nothing a GUI can't safely display.
- `BackendStatus` is the legible "where does this run" summary the front ends show
  (for `SelfHosted`: account / bucket / region).

**Provisioning and domain are self-hosted-only and are not part of `Transfer`** —
they live in dove-core as functions on the self-hosted backend, also
progress-reporting and non-interactive.

### Backend registry (the new config)

Config stops being "the one setup" and becomes a registry of named backends with a
pointer to the active one:

```
backends:
  - name: default          # migrated from today's single config.toml
    kind: self-hosted
    <existing fields: bucket, region, profile, table, gate_url, ...>
active: default
```

- A **backend** = `name + kind + kind-specific config`. `kind` is `self-hosted`
  today; the registry admits other kinds (below).
- **Setup mints and activates.** `provision` writes a self-hosted backend and
  points `active` at it. You are using what you just set up.
- **`dove use <name>`** repoints `active`. **`dove status`** leads with the active
  backend and where its credentials and storage live.
- **Migration:** on first run an existing `config.toml` is read into a backend
  named `default` with `active: default`, so nothing breaks and `status`/`use`
  work from day one.

### Pluggable backends

The backend factory resolves the active backend by `kind`:

```rust
pub fn resolve(active: &Backend) -> Result<Box<dyn Transfer>, Error>;
```

- `kind: self-hosted` → the built-in `SelfHosted`.
- Any other `kind` → look for a signed helper binary `dove-<kind>` in the plugins
  dir (`~/.config/dove/plugins/`) and dispatch the operation to it as a subprocess.
- No helper found → a **legible** error naming the plugin to install, never a
  stack trace.

A helper is a separate binary that **links `dove-core` itself**, so it reuses the
identical crypto and container format — parity is the same code, not a test to
maintain. This is a clean extension point: a hosted service, a Cloudflare R2 or
MinIO backend, or anything else can ship as an out-of-tree `dove-<kind>` helper
without a change to `dove-core` or `dove-cli`. (The CLI↔helper wire protocol is
specified when the first external backend is built; this design fixes only
discovery and dispatch-by-kind.)

### Errors

`dove-core` returns a typed `Error` enum (thiserror) with categories a front end
can act on (`NotFound`, `Expired`, `Exhausted`, `PinRequired`, `Locked`, `Network`,
`Aws`, `Config`, `Integrity`, …). The CLI renders them as today's messages; a GUI
maps them to states. Errors never carry secrets or signed URLs.

### Progress

```rust
pub trait Progress {
    fn step(&self, label: &str);       // "creating bucket"
    fn done(&self, label: &str);
    fn field(&self, key: &str, value: &str);
    fn bytes(&self, uploaded: u64, total: u64);
}
```

Core operations take `&dyn Progress`. `dove-cli` implements it over `ui.rs`;
a GUI implements it as UI events. `s3.rs`'s existing upload callback folds into
`Progress::bytes`.

## dove-cli (the shim)

- clap definitions, `ui.rs`, and the interactive resolvers (`choose_profile`,
  `confirm`) — the only place stdin/stdout is touched.
- Each command resolves inputs (prompting if needed), constructs the request,
  calls dove-core, and renders the result/progress. `provision`/`domain` call the
  self-hosted functions directly; `share`/`get`/`list`/`revoke`/`status` go through
  the resolved `Box<dyn Transfer>`.
- New surface: `dove use <name>`; `dove status` gains the active-backend header.

## Data flow — `dove share f --encrypt` (self-hosted active)

1. dove-cli parses args, loads the registry, resolves `active``Box<dyn Transfer>`
   via `dove_core::resolve` (built-in `SelfHosted`).
2. dove-cli builds a `ShareRequest`, calls `transfer.share(req, &cli_progress)`.
3. `SelfHosted::share` encrypts (crypto), uploads (s3, reporting bytes), writes the
   gate policy, records the ledger entry, and returns a `Share` with the finished
   link.
4. dove-cli prints the link. No key ever left dove-core.

The same call from another front end differs only in the `Progress` impl and how
the returned link is displayed — container bytes and link are identical.

## Testing

- Unit tests move with their logic into dove-core (crypto round-trips + attack
  rejection, duration parsing, id/mac, policy JSON, …).
- **Golden parity:** fixtures asserting the container bytes/framing and the link
  format are stable, so every front end and backend produces byte-identical
  artifacts.
- The no-I/O boundary is itself testable: dove-core has no `stdin`/`print` usage
  (enforceable with a grep test), and operations are driven with a test `Progress`.

## Out of scope (separate work)

- Building specific external backends and their setup commands.
- The CLI↔plugin wire protocol.
- Multipart / resumable large-file uploads.

## Decisions of record

- Crate names: `dove-core` (lib), `dove-cli` (bin, ships `dove`). `dove` is taken
  on crates.io.
- Backend naming: first is `default`; `dove use <name>` swaps; setup mints and
  activates.
- Backends are pluggable: `self-hosted` built in, others as external signed
  `dove-<kind>` helper binaries that link `dove-core`.