Public Access
Records the decisions the milestone plan already made, with their costs stated rather than only their benefits: - 0001 e2ee-trust-model: key hierarchy, the AAD-to-row binding that stops the server moving ciphertext between rows, and the four-layer public-key trust story. States plainly that revocation is not retroactive, that Connect cannot be a security boundary, and that the IdP becomes a key-distribution trust root. - 0002 minimal-apis: feature modules with explicit registration; capability negotiation instead of Asp.Versioning, since client and server upgrade independently when self-hosted. - 0003 sync-protocol: single write path, revision cursors, and the bigserial pre-commit sequence gap that silently corrupts sync — plus the per-vault advisory lock that fixes it and the test that must prove it. - 0004 relay-authorization: relay forwards bytes rather than terminating SSH, so zero-knowledge survives; server-resolved target IPs in the ticket to defeat DNS rebinding; why host addresses must be plaintext when relay is enabled. - 0005 no-application-layer: why the usual Application/mediator layer earns nothing here, with the trigger that would make us revisit it. - 0006 observability-stack: OTel plus built-in ILogger; liveness excludes dependencies so a database blip cannot restart the container and kill live SSH sessions. Also adds a README covering layout, build, enforced conventions and milestones.
51 lines
2.7 KiB
Markdown
51 lines
2.7 KiB
Markdown
# ADR 0002 — Minimal APIs with explicitly registered feature modules
|
|
|
|
- Status: accepted
|
|
- Date: 2026-07-28
|
|
|
|
## Context
|
|
|
|
The API is roughly 60 endpoints: identity and enrollment, a public-key directory, teams,
|
|
vaults and grants, sync, read-only item queries, relay, audit and admin. It is consumed by
|
|
one first-party desktop client.
|
|
|
|
## Decision
|
|
|
|
Minimal APIs, grouped into feature modules under `Features/`, registered **explicitly** from
|
|
`Setup/EndpointRegistration.cs`. Typed results (`Results<Ok<T>, ForbidHttpResult,
|
|
ProblemHttpResult>`) throughout, one endpoint per file.
|
|
|
|
Version with a hard-coded `/api/v1` prefix and **no `Asp.Versioning` package**. Instead ship
|
|
capability negotiation:
|
|
|
|
- `GET /api/v1/meta` — server version, sync protocol version, crypto spec version, feature
|
|
flags, `minClientVersion`, and the push caps.
|
|
- `GET /.well-known/dodossh-configuration` — OIDC authority, client id, scopes, relay URL.
|
|
|
|
## Consequences
|
|
|
|
- Per-endpoint filters and metadata compose cleanly, and typed results give an accurate
|
|
OpenAPI document without attribute noise.
|
|
- Explicit registration over reflection scanning: predictable startup cost, trimming-friendly,
|
|
and every route is greppable. The cost is one line per module, which is worth paying.
|
|
- Minimal APIs' real weakness is that a cross-cutting concern is easy to forget. Two
|
|
mitigations, both required: group-level `RequireAuthorization`, and an **endpoint-inventory
|
|
test** that enumerates `EndpointDataSource` and fails the build if any endpoint is absent
|
|
from an explicit authorization expectations table. A new endpoint therefore cannot be added
|
|
without making an authorization decision.
|
|
- Capability negotiation over version routing is the right trade for a self-hosted product,
|
|
where client and server upgrade independently and skew is normal rather than exceptional.
|
|
`.well-known` also *is* the onboarding story: the user types one server URL and the client
|
|
discovers the rest. `Asp.Versioning` gets added when a v2 actually exists, not before.
|
|
- The generated OpenAPI document is for third parties and a future CLI. It is emitted at build
|
|
time to `artifacts/openapi/v1.json`, committed and CI-diffed. **The client's real contract is
|
|
the `DodoSSH.Contracts` assembly**, guarded by PublicApiAnalyzers so an accidental DTO change
|
|
is a build error rather than a runtime deserialisation failure on a user's laptop.
|
|
|
|
### Rejected
|
|
|
|
- **MVC controllers.** Would work, but bring filter/model-binding machinery this API does not
|
|
need and blur the one-endpoint-one-file structure that keeps the authz surface reviewable.
|
|
- **Reflection-based endpoint discovery.** Convenient until a route silently disappears
|
|
because an assembly was not loaded, or trimming removes it.
|