Public Access
Merge main into the phone connections branch
Main had already taken this branch's first two commits, so what merged is the Connections work against three things that landed beside it. Four of the six conflicts were prose about arrangements both sides changed; two were real. **The phone hub gained a Teams row while this branch was moving the keychain onto it.** Both are additions to `IsMoreSurface` and both belong: teams because the desktop reaches them from its rail and the phone through the hub, the keychain because a bottom bar is for the places a session moves between. The membership test, the back gesture's first case and the hub's own arithmetic all take the union. The distinction is now written down rather than implied — teams is the design's count plus one, and the keychain is the only rearrangement of it: the bar lost a slot to gain that row. **`ConnectAndAnnounceAsync` was the real one.** Main gave it `RememberTypedPasswordAsync`, which binds the password that just worked to the host it worked on; this branch had replaced the `HostRowViewModel` that method needs with a four-field `ConnectionTarget`. Keeping both meant deciding what a manual connection does with a password that succeeded, and the answer was already written on the screen it is typed into: nothing. There is no item to bind a credential to and none to bind it on, and that path saves nothing by design. So `ConnectionTarget` carries the row again — as a nullable, in place of the host id it had, with `HostId` derived from it. Two things read it and both are things that can only be done to a keychain item rather than to an address: naming the log entry, and keeping the password. Null is not missing data there; it is the whole of what makes the manual path different, and having one field rather than two keeps "was this a keychain host" a question with one answer. The desktop's rail lost SFTP and S3 to the tab strip on main, so the README's "a rail with nine slots has room" was true when it was written this afternoon and is not now. It says the room rather than the number. Phase 11's four new device checks and main's Phase 12 on teams were the same conflict twice — two appends to the end of one file — and both are kept. Verified after resolving: the solution builds, the Android head builds clean, and 837 tests pass across the seven client suites, including main's own additions (233 shell, 79 layout, 240 domain, 118 sync, 54 session, 74 terminal, 39 storage).
This commit is contained in:
+77
-14
@@ -29,6 +29,13 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> over a view model that already existed, plus preferences. `ShellScreen` gained `More` and `Buckets`;
|
||||
> SFTP and S3 are one screen over one `TransfersViewModel`, differing only in which picker they offer.
|
||||
>
|
||||
> **A sixth is behind MORE that v2 never drew: TEAMS.** It is the reverse case — a shipped screen the
|
||||
> design had no slot for — and it is on the phone for a reason the design could not have anticipated,
|
||||
> because invitations did not exist when it was drawn. An invitation is claimed by *signing in*, and the
|
||||
> person being invited is at least as likely to be holding a phone as sitting at a desktop; a team the
|
||||
> server has just put somebody in, visible only on a head they may not have installed, is a membership
|
||||
> they cannot see. It runs over the same view model the desktop screen drives, like the other four.
|
||||
>
|
||||
> | v2 element | What ships instead |
|
||||
> | --- | --- |
|
||||
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
||||
@@ -77,7 +84,38 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> | **Split ⌘D** | Still omitted — the renderer stacks panes and shows one; tiling needs a pane geometry it has not got. |
|
||||
> | macOS traffic lights, and `⌘K` | The window's own minimise/maximise/close, and `CTRL K`. Development is Windows-first and the chrome is `BorderOnly` for a documented reason. |
|
||||
> | No status bar | Kept, and cut down to the one thing the titlebar does not now carry: `Vault.Status`, which is the only channel this application has for saying a save failed or a merge picked a winner. The design is a mock-up of a working afternoon and has nowhere to put a sentence like that. |
|
||||
> | The sidebar's five destinations, and a **Team vault** card at its foot | Nine destinations, because Pins, Teams, Import and Preferences are built screens and dropping their entry would strand them. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. |
|
||||
> | The sidebar's five destinations, and a **Team vault** card at its foot | Seven destinations, because Pins, Teams and Preferences are built screens and dropping their entry would strand them — and two fewer than v2 shipped with, because SFTP and S3 became tabs; see v3 below. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. |
|
||||
>
|
||||
> ## The desktop's v3
|
||||
>
|
||||
> A third pass, and the smallest of the three: it moves furniture rather than adding screens. Nothing in it
|
||||
> needed a layer below `client-app`, which is why it has no table of its own — there was nothing to omit.
|
||||
>
|
||||
> **The tab strip became the window's, not the terminal's.** Three fixed tabs sit at its head — Vaults,
|
||||
> SFTP, S3 — and terminal tabs follow them. SFTP and S3 left the nav rail to get there, which is the one
|
||||
> semantic change: they are the two destinations you *stay in* while something runs, and a rail entry is
|
||||
> for somewhere you go and come back from. The rail is drawn under Vaults alone, so SFTP, S3 and a terminal
|
||||
> each get the full window width instead of `826`. See `MainWindowViewModel.IsVaultsTab` for why the tab is
|
||||
> a page test rather than a fourth `ShellSurface`.
|
||||
>
|
||||
> **The hosts screen became a grid of cards** — groups above, hosts below — and the 268-pixel host sidebar
|
||||
> went with it. That column was choosing among forty machines *and* editing one of them at two-thirds
|
||||
> width; the grid took the first job at full width and a 304-pixel right-hand drawer took the second. The
|
||||
> drawer collapses when nothing is selected, which is most of the time. Pressing a group card narrows the
|
||||
> grid to that group; `SHOW ALL` is the way back.
|
||||
>
|
||||
> **The type scale went up a point and the text ramp went white.** `#E3E7F4` was a blue-tinted white on
|
||||
> blue-black surfaces, which costs contrast twice — once for being darker than white and once for sharing
|
||||
> a hue with what it is drawn on. Every step of the ramp moved with the top, so the intervals the design
|
||||
> chose are kept. Both are in the shared palette, so the phone has them too.
|
||||
>
|
||||
> | v3 element | What ships instead |
|
||||
> | --- | --- |
|
||||
> | The hosts toolbar's view-mode switch (grid / list / table) | One view. A switch between three layouts where only one is built is two disabled buttons. |
|
||||
> | The hosts toolbar's tag filter, calendar and share control | Omitted. There are no saved filters, nothing in a vault carries a date, and sharing outside a team vault does not exist. Tags are real and are searched by being read off the cards. |
|
||||
> | **Serial** beside **Terminal** in the toolbar | Omitted. Every session here is an SSH channel; a serial transport is not a button, it is a second session kind. |
|
||||
> | The strip's tabs inside the titlebar row, with a hamburger | A row of its own under the titlebar. The titlebar already carries the drag region, the search box, the sync light and three window controls, and the strip scrolls — putting both on one 44-pixel row would make the thing that scrolls fight the thing that drags. |
|
||||
> | A count on the S3 tab | Omitted. The rail entry it replaces carried one; a number on two of five tabs reads as a fact about those two rather than as the tab's own state, and a terminal tab has nothing to count. The count is on the S3 screen. |
|
||||
|
||||
Most of it landed. This file is the rest: every element of that design with nothing behind it, which
|
||||
project each piece would have to land in, and **what the shipped interface does instead**. That last
|
||||
@@ -109,6 +147,13 @@ unused tables bought. See [Teams](#teams). What has *not* changed is the split u
|
||||
decides what it will serve, and only a client can decide who can read it — so "shared with" is two facts on
|
||||
this screen, not one.
|
||||
|
||||
One table did have to be added, and what it shows is the limit of that reservation. An invitation names an
|
||||
**address**, and `team_membership.user_id` is not nullable and carries a foreign key, so somebody who has
|
||||
never signed in has nothing for that row to point at — which is why `MembershipStatus.Invited` has been
|
||||
reserved since the first migration and is still never written. `team_invitation` is its own table for that
|
||||
one reason. The schema was right about the shape of a team and had said nothing about the shape of joining
|
||||
one.
|
||||
|
||||
**The client has no preferences store.** It writes exactly two files — `cache.db` and `device.key` — and the
|
||||
cache has six tables, none of them settings. Nothing on the design's TERMINAL preferences panel can be
|
||||
saved, and there is no frame on the terminal data plane that would carry a change to the renderer anyway.
|
||||
@@ -228,7 +273,7 @@ caption buttons and window title drawn on top of the application's own — two s
|
||||
| Pane header `aes256-gcm` | client-ssh | **The closest miss on this list.** `SshNetConnection` holds the `SshClient`, so `ConnectionInfo.CurrentServerEncryption` is right there — it just is not on `ISshConnection` or surfaced by `TerminalWorkspace`. | Omitted; the tab strip shows the account and endpoint actually dialled. |
|
||||
| Pane header showing the running command and `following` | client-ssh | The host moves opaque bytes and never parses terminal output. Would need shell integration (OSC 133) on the remote. | Omitted. |
|
||||
| A `local · zsh` tab | client-ssh | Every session here is an SSH channel. Needs ConPTY and a second session kind. | Omitted. |
|
||||
| Tab strip `+` button | ui | Not missing so much as redundant: the real operation is *select a host, press Connect*, which the sidebar already is. | Omitted. Connect opens a tab; Ctrl+K opens one by name. |
|
||||
| Tab strip `+` button | ui | Not missing so much as redundant: the real operation is *select a host, press Connect*, which the hosts grid already is. | **Shipped**, as the palette rather than a menu: it opens what Ctrl+K opens, so the strip and the shortcut are one way of doing one thing. A `MenuFlyout` offering "SSH" and "local shell" is the nicer answer and is not verifiably safe above the terminal's native child window — and there is no local shell to offer. |
|
||||
| Terminal font size (`--termfs`, 11–16px) | client-storage | See preferences. | Fixed at the renderer's 13px. |
|
||||
|
||||
---
|
||||
@@ -298,9 +343,11 @@ and both editors. What follows is what the design drew around them.
|
||||
|
||||
## Teams
|
||||
|
||||
**Built in M3.** The screen ships: a team list, a members table, the team's vaults, and the two buttons the
|
||||
whole design was really about — add a member, and share a vault key. What follows is what it still does not
|
||||
do, and one thing this document got wrong before it was built.
|
||||
**Built in M3.** The screen ships: a team list, a members table with a real last-active column, the
|
||||
invitations standing against addresses that have no account here yet, the team's vaults, and the two buttons
|
||||
the whole design was really about — add a member, and share a vault key. A team can also be renamed, handed
|
||||
to another member, and archived, the last only while it owns no vaults. What follows is what it still does
|
||||
not do, and one thing this document got wrong before it was built.
|
||||
|
||||
**The correction.** The rows below used to describe a screen with nothing behind it, on the grounds that
|
||||
`VaultAccessService.ResolveAsync` denied every vault that was not the caller's own. That is now the one
|
||||
@@ -309,6 +356,16 @@ place that changed, exactly as its remark predicted, and no migration was needed
|
||||
What the row did not anticipate is that the interesting half is not the endpoints at all. It is that
|
||||
**membership and readability are different things**, and the screen is arranged around saying so.
|
||||
|
||||
**There are two ways into a team and they are not interchangeable.** Adding a member takes a *user id* the
|
||||
caller has already got from the directory, so that account must have signed in here at least once — and the
|
||||
ordering is deliberate rather than incidental, because whoever adds a member is usually about to wrap a
|
||||
vault key to the public key that lookup returned. Inviting takes an *address*, grants nothing readable, and
|
||||
cannot be a step towards sharing: there is no account, so there is no key to wrap to. Inviting an address
|
||||
that already belongs to a member of this team is refused and says so — that is a fact about a team the
|
||||
caller can already see. Inviting one that merely has an account somewhere on this deployment is **not**
|
||||
refused, because answering that would turn the endpoint into an oracle for which addresses have accounts,
|
||||
answerable by anybody willing to create a team first. It simply gets claimed sooner.
|
||||
|
||||
| Design element | Layer | What ships |
|
||||
| --- | --- | --- |
|
||||
| The team itself | server | `POST/GET /api/v1/teams`, plus members, roles and team vaults. Ids are client-chosen, so a create whose response was lost is safe to repeat. |
|
||||
@@ -316,17 +373,20 @@ What the row did not anticipate is that the interesting half is not the endpoint
|
||||
| Roles | contracts + server | `TeamMemberRole` on the wire, numerically pinned to `DodoSSH.Domain.TeamRole` by a test. Viewer reads, Member writes, Admin and Owner also share and administer. |
|
||||
| Members table | server | `TeamMemberSummary`, and a directory that resolves an exact email to a public key. |
|
||||
| Sharing an item | client | `VaultSession.ShareVaultAsync`: verify the recipient's key against the key log, wrap, sign, record. The server stores the wrap and the signature and can check neither. |
|
||||
| Pending invites, and withdrawing one | server | A `team_invitation` row per (team, address), listed beside the members it is about and withdrawable until it is taken up. It becomes a membership when an account with that address signs in — **and only if the access token asserts `email_verified`**, because membership is authorisation and an invitation anybody could take by naming somebody else's address is a way in. Fourteen days, because an address that is reassigned would otherwise carry a standing offer to whoever holds the job next. |
|
||||
| Ownership transfer | server | `POST /api/v1/teams/{id}/owner`, owner only. One transaction: the named member becomes owner and the outgoing owner becomes an admin. Not two role changes — ownership is sole, so promoting first leaves the team owned twice and demoting first leaves it owned by nobody. The outgoing owner is demoted rather than removed, because removing them would revoke their vault key grants and flag every team vault for rekey, which is a far larger act than the one being asked for. |
|
||||
| `LAST ACTIVE` | server | Real, and coarse on purpose. `UserAccount.LastSeenAtUtc` is now refreshed on ordinary authenticated requests, at most once per account per hour: writing it per request would put an UPDATE on the hot path of every authenticated call and start losing races on `user_account`'s own concurrency token. So the column answers "this week or not", which is the granularity the question is actually asked at, and is shown coarsely rather than to the minute. |
|
||||
| Renaming and archiving a team | server | `PUT` and `DELETE /api/v1/teams/{id}`. The slug is deliberately not renameable: it is unique only among *live* teams, so a rename could take a slug an archived team still holds and strand it. Archiving soft-deletes the team, every membership and every pending invitation in one transaction — and is refused outright while the team owns any vault. |
|
||||
|
||||
| Design element | Layer | What it would take | What ships instead |
|
||||
| --- | --- | --- | --- |
|
||||
| `CONNECT-ONLY` role | — | Nothing that would be true. Connect is a user-interface hint, not a boundary: SSH terminates on the client, so a session needs the credential's plaintext on that machine. See ADR 0001. | Four roles, all of which are enforceable. `Connect` rides along with `Read` and is documented as a hint. |
|
||||
| `2FA ENFORCED` and the per-member 2FA column | server | No two-factor concept exists anywhere — the only hit in the whole worktree is an aside in `docs/crypto.md`. | Omitted. The member column carries what *is* known and matters: whether they have published a key a vault can be wrapped to. |
|
||||
| `LAST ACTIVE` | server | `UserAccount.LastSeenAtUtc` is written at just-in-time provisioning and at enrollment and never on an ordinary authenticated request, so the column cannot answer "last active". | Omitted. |
|
||||
| `2FA ENFORCED` and the per-member 2FA column | server | No two-factor concept exists anywhere — the only hit in the whole worktree is an aside in `docs/crypto.md`. | Omitted. The member columns carry what *is* known and matters: whether they have published a key a vault can be wrapped to, and when they were last here. |
|
||||
| Avatars | server | No picture is stored anywhere. | Omitted; the row shows a name and an address. |
|
||||
| Pending invites, resend, revoke | server | An invitation entity, a token with a lifetime, and an outbound mail path. `MembershipStatus.Invited` remains unwritten. | Adding a member resolves an address the caller types against the directory, so the account has to have signed in here once. The screen says that when the lookup finds nothing. |
|
||||
| The invitation mail, and **resend** | server | An outbound mail path: an SMTP configuration, a template, a bounce story and a deliverability problem, none of which this server has. | **Nothing is sent, and the interface says so.** An invitation is a standing instruction rather than a message — the next account to sign in with that address joins the team — so there is no token, no link, and nothing to resend. Telling somebody to sign in is done over a channel this server does not carry. A link nobody can deliver would be worse than no link. |
|
||||
| Archiving a team that owns vaults | — | Nothing that would be safe. A team vault resolves through membership, so archiving would take those vaults away from everybody holding a key, silently, including the caller — and nothing in this product deletes a vault, so there is no sequence of calls that turns the refusal into a success. | Refused, with `team-not-empty` and a count of the vaults in the way. A stated limit rather than a coming feature, for the reason the SFTP layer refuses a recursive delete: a refusal is visible and a quiet removal is not. |
|
||||
| `SSO · OIDC · okta.dodotech.dev` | server | Per-team SSO. Authentication is one global JWT scheme bound to one authority. | Omitted. |
|
||||
| A rekey after a membership change | client | Re-wrapping every item's data key under a fresh vault key, which only a client holding the current one can do. M5. | The vault is flagged `RekeyRequired` and the row says a rotation is owed. |
|
||||
| Ownership transfer | server | A confirmation flow and a rule for what happens to the outgoing owner. | The owner cannot be removed or demoted, with its own problem code rather than a bare 400. |
|
||||
|
||||
> **The trap this document warned about is still a trap.** `GET /api/v1/meta` advertises
|
||||
> `features: ["teams"]` *unconditionally* (`MetaEndpoints.cs`). It was meaningless when nothing implemented
|
||||
@@ -355,7 +415,7 @@ lists the rest as absent rather than omitting it silently.
|
||||
| `GENERAL` section | ui | There is no general setting to put in it. The theme is fixed by decision, and window size is not persisted. |
|
||||
| `KEYS & AGENT` section | client-ssh | **There is no agent, at all** — no own agent, no forwarding, no Pageant or OpenSSH-agent interop. |
|
||||
| `SYNC & VAULT` section | client-domain | Nothing here is adjustable. The auto-sync interval is a `private static readonly` with a remark arguing for its value. |
|
||||
| `SECURITY & SSO` section | server | The SSO half needs team endpoints; there is no policy for the screen to show. |
|
||||
| `SECURITY & SSO` section | server | Per-team SSO, which is a refusal rather than a pending endpoint: authentication is one global JWT scheme bound to one authority, so there is no per-team policy for this screen to show. |
|
||||
| `SHORTCUTS` section | ui | There is no keybinding infrastructure and no rebinding surface. |
|
||||
| Auto-lock after idle | client-session | An activity source, a decision about what counts as idle, and — the hard part — a policy for a shell mid-job. `LockAsync`'s own remark already argues that an unattended timeout which killed a running job would be worse than the exposure it removed. |
|
||||
| Require biometric to sign | crypto | Three separate falsehoods in one row. There is no signing service — the private key is decrypted and handed to SSH.NET whole, so there is no per-signature moment to interrupt; there are no connect-only keys; and per-use consent would need the key to live in the TPM, which is a different key hierarchy from the one in `docs/crypto.md`. |
|
||||
@@ -365,10 +425,13 @@ lists the rest as absent rather than omitting it silently.
|
||||
|
||||
## Two things the import changed on purpose
|
||||
|
||||
**Hosts left the vault column.** They have their own sidebar beside the terminal, and the vault screen
|
||||
holds keys, passwords and pinned host keys. This follows the design, and it is also the better split: the
|
||||
host list is what you look at while you work, and the rest is what you go and manage. `VaultSection` lost
|
||||
its `Hosts` member and gained `All`.
|
||||
**Hosts left the vault column.** They have their own screen and the vault screen holds keys, passwords and
|
||||
pinned host keys. This follows the design, and it is also the better split: the host list is what you look
|
||||
at while you work, and the rest is what you go and manage. `VaultSection` lost its `Hosts` member and
|
||||
gained `All`.
|
||||
|
||||
That screen was a 268-pixel sidebar beside the terminal when this paragraph was written, and v3 made it a
|
||||
grid of cards with a drawer — see above. The split it describes did not change; only which half is wide.
|
||||
|
||||
**Tabs moved to the shell, not the vault.** Locking disposes the vault and deliberately leaves shells
|
||||
running, so a tab list rebuilt per unlock would lose track of sessions that are still connected — the very
|
||||
|
||||
Reference in New Issue
Block a user