did-hosting-daemon 0.10.1

DID Hosting Daemon — Unified binary embedding server, witness, watcher, and control plane
# Affinidi DID Hosting Daemon

The DID Hosting Daemon is a unified binary that embeds all four
services — [server](../did-hosting-server/),
[witness](../webvh-witness/), [watcher](../webvh-watcher/), and
[control plane](../did-hosting-control/) — behind a single TCP listener.
This is the easiest way to get started with a complete DID Hosting
deployment.

Each service is mounted at a path prefix:

| Service | Path prefix | Description |
| ------- | ----------- | ----------- |
| Server  | `/`         | Public DID hosting and resolution |
| Witness | `/witness`  | Witness proof signing |
| Watcher | `/watcher`  | Read-only DID mirror |
| Control | `/`         | DID management as Trust Tasks (TSP, DIDComm, `POST /api/trust-tasks`), management UI |

> **IMPORTANT:**
> did-hosting-service crates are provided "as is" without any
> warranties or guarantees, and by using this framework, users
> agree to assume all risks associated with its deployment and
> use including implementing security, and privacy measures in
> their applications. Affinidi assumes no liability for any
> issues arising from the use or modification of the project.

## Getting Started

### Prerequisites

- Rust 1.94.0+ (2024 Edition)
- Node.js 20+ (only if building with the management UI)

### 1. Build

```bash
# Without UI
cargo build --locked -p did-hosting-daemon --release

# With embedded management UI
cd did-hosting-ui && npm install && npm run build:web && cd ..
cargo build --locked -p did-hosting-daemon --release --features ui
```

The binary is produced at `target/release/did-hosting-daemon`.

### 2. Configure

The fastest way to produce a working `config.toml` is to run
`did-hosting-daemon setup`, which prompts for the values below and emits a
file you can hand-edit later. A worked example follows for reference:

```toml
server_did = "did:webvh:example.com"
mediator_did = "did:webvh:mediator.example.com"
public_url = "https://example.com"

# Identity mode. Default: "vta". Set to "self-managed" to skip the
# VTA round-trip and have the daemon self-host its own did:webvh
# identifier. See docs/self-managed-mode-spec.md for the trade-offs.
[identity]
mode = "vta"

[server]
host = "0.0.0.0"
port = 8534

[log]
level = "info"

[auth]
access_token_expiry = 900
refresh_token_expiry = 86400

# Secret backend. The daemon's default features include `keyring`; other
# backends require recompiling with `--features aws-secrets|gcp-secrets|
# azure-secrets|vault-secrets|k8s-secrets` and setting the matching
# `[secrets]` fields (the configured backend is selected by priority at
# runtime). HashiCorp Vault authenticates via the pod's Kubernetes
# ServiceAccount by default.
[secrets]
keyring_service = "did-hosting-daemon"
# vault_addr = "https://vault.example.com:8200"   # --features vault-secrets
# vault_secret_path = "webvh/daemon-secrets"
# vault_k8s_role = "did-hosting-daemon"
# k8s_secret_name = "did-hosting-daemon-secrets"  # --features k8s-secrets

# Main store (server, watcher, control share this)
[store]
data_dir = "data/daemon/store"

# Witness store (separate to avoid keyspace collisions)
[witness_store]
data_dir = "data/daemon/witness"

# Which services to enable (all default to true except watcher)
[enable]
server = true
witness = true
watcher = false
control = true
```

For self-managed mode (no VTA), set `[identity] mode = "self-managed"`
and omit the `[vta]` section entirely. The daemon will mint its own
Ed25519 + X25519 keys at first run, host its own `did:webvh`
identifier on `public_url`, and seed an empty ACL — admin enrolment is
done through `did-hosting-daemon invite --did <DID> --role admin` and the
passkey enrolment flow. See [docs/self-managed-mode-spec.md](../docs/self-managed-mode-spec.md)
for the full walkthrough.

### Non-interactive setup (CI / scripted)

`did-hosting-daemon setup --from <recipe.toml>` drives the wizard with no
TTY. Recipe is declarative TOML; no secrets inside. Example:

```bash
# VTA-managed (after `--setup-key-out` enrolment elsewhere):
did-hosting-daemon setup --from examples/did-hosting-daemon-build.toml \
                   --setup-key-file setup.key

# Self-managed (no VTA, no phase-1 needed):
did-hosting-daemon setup --from examples/did-hosting-daemon-build.toml
# → recipe has [deployment].vta_mode = "self-managed"

# Air-gapped (no VTA network access from CI):
did-hosting-daemon setup --from recipe.toml   # phase 1: writes request file
# (operator ferries to VTA admin, gets sealed bundle back)
did-hosting-daemon setup --from recipe.toml   # phase 2: opens sealed bundle
```

Other flags: `--force-reprovision` rotates an existing install (backs
up `config.toml` first), `did-hosting-daemon uninstall` tears down (clears
managed secrets + removes config). See
[docs/bootstrap_startup.md](../docs/bootstrap_startup.md#non-interactive-setup-recipe-driven)
for the recipe schema and exit codes.

### 3. Start the daemon

```bash
did-hosting-daemon --config config.toml
```

The daemon starts all enabled services on a single port and logs which
services are active. The control plane and `did-hosting-server` (public DID
resolver) both merge at root; `webvh-witness` and `webvh-watcher` nest
under their own prefixes:

```
--- daemon services ---
  server (/)
  witness (/witness)
  control (/)
daemon listening on 0.0.0.0:8534
```

## Configuration

The daemon is configured via a TOML file. By default it looks
for `config.toml` in the current directory. You can specify a
different path with the `--config` flag or the
`DAEMON_CONFIG_PATH` environment variable.

### Shared vs Per-Service Settings

Settings like `server_did`, `mediator_did`, `public_url`,
`[auth]`, `[secrets]`, and `[log]` are shared across all
services. Each service gets its own store directory to avoid
keyspace name collisions (both server and witness use `sessions`
and `acl` keyspaces).

### Service-Specific Settings

| Section          | Service | Description |
| ---------------- | ------- | ----------- |
| `[limits]`       | Server  | Upload body limit, per-account quotas |
| `[vta]`          | Witness | VTA remote key management |
| `[watcher_sync]` | Watcher | Push tokens and source servers |
| `[registry]`     | Control | Service instance registry |

> A default daemon has an empty registry and does not use the reverse proxy, so
> no extra configuration is needed. **If** you populate `[[registry.instances]]`
> (or accept `MSG_SERVER_REGISTER`) and proxy to remote instances, you must set
> `registry.url_allowlist` — the proxy is fail-closed without it. See
> "Reverse proxy & the registry allowlist" in the
> [control plane README]../did-hosting-control/README.md.

### Enable/Disable Services

The `[enable]` section controls which services start:

```toml
[enable]
server = true    # DID hosting (default: true)
witness = true   # Witness proofs (default: true)
watcher = false  # DID mirror (default: false)
control = true   # Management UI (default: true)
```

### Environment Variable Overrides

Environment variables use the `DAEMON_` prefix:

| Variable              | Description             |
| --------------------- | ----------------------- |
| `DAEMON_CONFIG_PATH`  | Path to config file     |
| `DAEMON_SERVER_DID`   | Server DID identifier   |
| `DAEMON_MEDIATOR_DID` | Mediator DID identifier |
| `DAEMON_PUBLIC_URL`   | Public URL              |
| `DAEMON_SERVER_HOST`  | Bind host               |
| `DAEMON_SERVER_PORT`  | Bind port               |
| `DAEMON_LOG_LEVEL`    | Log level               |
| `DAEMON_SERVER_RESOLVE_CACHE_MAX_AGE_SECS` | DID-resolution `max-age` (0 = `no-cache`; default 300) |
| `DAEMON_SERVER_RESOLVE_CACHE_SHARED_MAX_AGE_SECS` | DID-resolution `s-maxage` (empty clears) |
| `DAEMON_SERVER_RESOLVE_CACHE_STALE_WHILE_REVALIDATE_SECS` | DID-resolution `stale-while-revalidate` (empty clears) |

The same settings live in `[server.resolve_cache]` (`max_age_secs`,
`shared_max_age_secs`, `stale_while_revalidate_secs`).

## CLI Commands

```
did-hosting-daemon                      # Run daemon (default)
did-hosting-daemon setup                # Interactive config wizard
did-hosting-daemon health               # Run health check diagnostics
did-hosting-daemon add-acl              # Add ACL entry
did-hosting-daemon list-acl             # List ACL entries
did-hosting-daemon remove-acl           # Remove ACL entry
did-hosting-daemon invite               # Create passkey enrollment invite
did-hosting-daemon list-dids            # List all DIDs in the store
did-hosting-daemon remove-did           # Remove a DID from the store
did-hosting-daemon load-did             # Load a DID from existing files
did-hosting-daemon bootstrap-did        # Bootstrap a DID (defaults to .well-known)
did-hosting-daemon recreate-did         # Recreate a DID at a given path
did-hosting-daemon recover-did          # Recover a soft-deleted DID
did-hosting-daemon import-secrets       # Import secrets from VTA bundle or keys
did-hosting-daemon backup               # Export data to backup file
did-hosting-daemon restore              # Restore data from backup file
```

## API Path Mapping

When all services are enabled, the daemon exposes endpoints
at the following paths. `did-hosting-server` (the public-DID-resolver edge
node) and `did-hosting-control` (the Trust Task listener + UI) both merge at
root — the daemon is a unified front door rather than a multiplexer.
Witness and watcher are nested under their own prefixes because their
APIs are operator-facing and benefit from a clean URL boundary.

### Server + Control plane (root, merged)

| Path | Description | Source |
| ---- | ----------- | ------ |
| `/api/health` | Combined health endpoint | server / control |
| `/api/trust-tasks` | Every management operation, as a signed Trust Task (the HTTPS binding) | control plane |
| `/api/sign-in/readiness` | Whether the service DID lists the services wallet sign-in needs (read by the login page) | control plane |
| `/api/auth/challenge`, `/api/auth/`, `/api/auth/refresh` | Browser SIOPv2 sign-in and console session refresh | control plane |
| `/{mnemonic}/did.jsonl` | Public DID resolution | server |
| `/.well-known/did.jsonl` | Service-DID resolution | server |
| `/`, `/assets/*` | Management UI (when `ui` feature enabled) | control plane |

### Witness (nested at `/witness`)

| Path | Description |
| ---- | ----------- |
| `/witness/api/health` | Witness health |
| `/witness/api/trust-tasks` | The witness's Trust Tasks (`webvh/witness/*`, `acl/*`), HTTPS binding |

### Watcher (nested at `/watcher`)

| Path | Description |
| ---- | ----------- |
| `/watcher/api/health` | Watcher health |
| `/watcher/{mnemonic}/did.jsonl` | Mirrored public DID resolution |

### Daemon

| Path | Description |
| ---- | ----------- |
| `/health` | Daemon-level health |

## Graceful Shutdown

The daemon handles `SIGINT` (Ctrl+C) and `SIGTERM` for graceful
shutdown. On shutdown, all service stores are persisted before
the process exits.

## Feature Flags

Features propagate to the underlying service crates:

| Feature | Description |
| ------- | ----------- |
| `ui` | Embed the management UI in did-hosting-control |
| `keyring` | OS keyring secrets backend |
| `store-fjall` | Fjall embedded storage backend |
| `did-methods` | Enable did:webvh + did:web resolution (route + dispatcher) |

Default features: `keyring`, `store-fjall`, `ui`, `did-methods`.

Passkey/WebAuthn auth is always compiled in (via the embedded control
plane) and is not a build feature. It activates at runtime when
`public_url` is set; otherwise login uses DID challenge-response.

## Support & feedback

If you face any issues or have suggestions, please don't
hesitate to contact us using
[this link](https://share.hsforms.com/1i-4HKZRXSsmENzXtPdIG4g8oa2v).

### Reporting technical issues

If you have a technical issue with the Affinidi DID Hosting Service
codebase, you can also create an issue directly in GitHub.

1. Ensure the bug was not already reported by searching on
   GitHub under
   [Issues]https://github.com/affinidi/did-hosting-service/issues.

2. If you're unable to find an open issue addressing the
   problem,
   [open a new one]https://github.com/affinidi/did-hosting-service/issues/new.
   Be sure to include a **title and clear description**, as
   much relevant information as possible, and a **code sample**
   or an **executable test case** demonstrating the expected
   behaviour that is not occurring.

## Contributing

Want to contribute? Head over to our
[CONTRIBUTING](https://github.com/affinidi/did-hosting-service/blob/main/CONTRIBUTING.md)
guidelines.