Repository navigation
docs(account): signing in to link devices, and what the account service keeps - #2867
Conversation
A phone with only the PC's public key (a link without a code) pairs by a commit-then-reveal comparison bound to the Noise handshake: both screens show the same six digits, and the pairing finishes only when the phone confirms and the owner approves on the PC. Feature-negotiated with pair-sas; QR pairing is unchanged.
A PC endpoint could admit a comparing phone right after its reveal, so the phone saved that endpoint without its person confirming the digits. Admission now needs PAIR_CONFIRM sent first. The PC also checks the relay pairing pause again before showing a prompt.
… a comparison A phone with a saved key that scans a new QR code is given a new device id, which the phone refused since the codeless change. An approved comparison also no longer cancels a pairing code still on screen.
…ess link on plain http Connect a phone, where the link is copied, covered the approval prompt. The prompt now sits in its own layer above every dialog as a live region, the link falls back to copyText and stays on screen when the clipboard is refused, and a retried attempt starts without the last one's comparison.
An ungated LAN window is never told of a phone waiting for approval, so it may neither make a codeless link nor answer one (403 owner_only). The codeless link the clipboard refuses is now selected once it is drawn.
A phone that still held a key the PC had revoked signed in with it from a codeless link, was refused, and showed the same failure every time. The shell now drops that key once and compares digits instead.
# Conflicts: # src/components/RelayPanel.css # src/components/RelayPanel.tsx # src/components/RelayShell.css # src/lib/i18n.ja.ts # src/lib/i18n.ko.ts # src/lib/i18n.zh.ts # src/lib/i18n/devices.ja.ts # src/lib/i18n/devices.ko.ts # src/lib/i18n/devices.zh.ts # src/lib/i18n/relay.ja.ts # src/lib/i18n/relay.ko.ts # src/lib/i18n/relay.zh.ts # src/lib/relay/shell.tsx # src/lib/relay/shellState.test.ts
Revoke all left an approved codeless comparison waiting, so the phone's PAIR_CONFIRM afterwards still made a drive device. Revoking every device now ends every comparison under way and withdraws its prompt.
Signed add/remove/link records (ovis-account/1, length-prefixed fields, ECDSA P-256 via WebCrypto), the PC and account service APIs in docs/account.md, an in-memory fake of the service for tests, the server proof's account role, and the account/pair-intent feature names.
…by link, refuse lone surrogates Review fixes: another account that lists a PC could fill its mailbox and stall removals; ack now covers every processed record. A self-signed link over the row's own session needs no prior member. Labels with lone surrogates are refused (they would share a signature).
…fresh, Apple identity tokens
The contract and the in-memory service gain sign-in as the lead set it:
GET /v1/signin/{github|apple} with an S256 code_challenge and an exact
redirect allow-list (app.ovis.sh, ovis://, loopback on any port), and
POST /v1/token for authorization_code, refresh_token (rotating; reusing
one ends its family) and apple_id_token, and POST /v1/signout. Tokens
reach the directory and the doorbell only; they admit no device.
…e account's PCs
As agreed with the client lane: a device an add record puts on a PC gets
id = hex(SHA-256('ovis-account-device/1' || server_id || key))[0:16], so
the client can sign in without asking (accountDeviceId); and the
approving member can introduce the account's PCs it verified to a newly
added device through the PC that admitted it (POST /api/account/introduce,
GET /api/account/introduction, in memory for 10 minutes).
…hell reach the account service As agreed with the client lane: the web redirect is exactly https://app.ovis.sh/ and always gets code and state in the fragment, so the code never reaches that host's logs; ovis:// may ask for the fragment; the loopback redirect keeps the query. The hosted shell's connect-src gains https://account.ovis.sh.
…ember A pairing code, a comparison of digits and an account join are admitted by the same people: this PC itself (its windows, or `pair --approve` in its terminal), or a drive account member through the relay, who must pick the new device's digits out of three. Every admission is announced with who approved it. Comparisons that ask to join the account (pair-intent-account, negotiated in WELCOME) count their failures apart from codes, so they cannot pause QR pairing.
…s from watch devices Review fixes: an approval now ends for good when its approver is revoked or given another role before the phone confirms, even if the role comes back; a watch device on the PC's own address can no longer read the digits of a waiting comparison. The docs say an account admission makes a plain row the new device then links, and what the two failure counts give a relay in the middle.
…count The omo review found that no relay device could start a pairing code until devices can become account members. The account-only rule now applies once the PC belongs to an account; a PC in no account behaves as before.
…t the mailbox
The PC side of linking devices through an account (docs/account.md):
- records: a drive member's add inserts a member row (added_by), its
remove writes a tombstone and revokes the key; no account record clears
a tombstone, a pairing on this PC does. A member removed signs nothing.
- /api/account/{status,link,records,enroll}: drive only, through the
relay too; link proves the row's key over its own session and gives
the PC its first member; enroll only from a member, adds only.
- mailbox: pulls this PC's records with a server proof for the account
role (the relay never accepts it), acks every processed record.
- the account feature in WELCOME; reset relay identity leaves the account.
…e mailbox Review fixes (omo and Codex on #2843): - a revoke on the PC leaves a tombstone, so a replayed add cannot undo it; - a code started through the relay, or a remote approval, never clears or bypasses a tombstone, and an unreadable tombstone file fails closed; - a stop between two writes is finished on start, told apart by time; until then a stale row neither signs in, signs nor approves; - a remove signed before the PC paired the key again leaves it be; - a link must start from the key its session signed in with, and never takes a key another row holds; - the mailbox collects nothing while removals could not be applied, sends a last ack at its page limit, and a bad service address is its error; - tombstones merge across sibling state dirs; Portal and open LAN windows are refused on /api/account/*.
The omo re-review reproduced a PC that was off taking a member's add and remove together and keeping the device: the rule that a newer local pairing wins also spared rows the add itself had just made. Only rows without added_by (paired on this PC) are spared now.
…'s addition The omo review reproduced a row paired with a code an account member started through the relay outlasting an older remove, as if it had been paired on this PC. Such a row now records the member as added_by, so a remove takes it; only pairings made on this PC are spared.
…ough a broken file or a merge Final review fixes on #2843: - an add whose key is not a P-256 point is refused: stored, it left a devices.json no store could read, locking out every device; - a revoke while account-tombstones.json cannot be read writes its tombstone to a pending file, folded back in once the file is fixed; - merging two state dirs keeps the account key of a row linked in either; - a member's relay code lets nobody in once the member is revoked.
… no account The omo review reproduced a regression from the previous round: a code a relay device started on a PC in no account recorded its starter and then failed the member check. A code now records whether it was started through the relay (never clearing or bypassing a tombstone) apart from the member that started it, if any. A member's code is checked on the cookie path too, and the sibling-dir merge reads pending tombstones.
…e account's PCs
An add record's device gets id hex(SHA-256('ovis-account-device/1' ||
server_id || key))[0:16] (accountDeviceId), so the client signs in with
it without asking. A drive member may introduce the account's PCs it
verified to a device a member added (POST /api/account/introduce); the
device reads them over its own session (GET /api/account/introduction).
Kept in memory for 10 minutes; each PC still admits by its own list.
The hosted shell's front page can sign in to the account service (GitHub or Sign in with Apple, OAuth code + PKCE, the code returned in the fragment) and list the account's PCs merged with the PCs this device is paired with. - src/lib/account/store.ts: one non-extractable P-256 account key per client in its own IndexedDB database, ovis-account; ovis-relay and the service worker are untouched. - src/lib/account/service.ts: directory calls with refresh and sign-out; a page offers sign-in only when it names an account service in <meta name="ovis-account-service">, so no build shows dead buttons. - src/lib/account/myPcs.ts: directory rows are never trusted for keys; only a paired PC opens. - lastSeen moves to src/lib/lastSeen.ts, shared by Devices and My PCs.
… verifier Codex review of the sign-in contract: the service accepts an Apple identity token only when Apple's signature, issuer, audience, expiry and hashed nonce hold, once each; clients make state and code_verifier fresh per sign-in and drop a callback with another state; a device already on a PC keeps its id; a watch device may read its own introduction. The fake refuses short nonces and spends a code at exactly 60 seconds.
… members' accounts to drivers Codex review of #2843: a device admitted with a code or by comparing digits never signed with its key, so its session may not link, introduce or read an introduction; a session from a row's older key acts as no member; a code started through the relay always records its starter as added_by, so an older remove still takes the device. As agreed with the client lane, GET /api/devices adds account, account_key and added_by to members' rows for callers with drive access only.
As agreed with the client lane: PUT /v1/pcs/{server_id} takes label,
public_key (X25519, 64 hex) and relay (a wss origin), and GET /v1/pcs
returns them. The service refuses a key that does not give the
server_id, so the stored key cannot be forged, and clients derive the
server_id again before use; the relay is only a route.
# Conflicts: # src/lib/account/account.test.ts
Also takes the PC's answer to HELLO after a provisional WELCOME (same nonce), as a PC that selects the account feature sends one.
# Conflicts: # app/electron/src/main.ts
# Conflicts: # src/App.tsx # src/lib/useMachineRoster.ts
# Conflicts: # server/relay/session.ts # shared/relay-channel.ts # shared/relay-mux.ts # src/App.tsx # src/lib/i18n/devices.ja.ts # src/lib/i18n/devices.ko.ts # src/lib/i18n/devices.zh.ts # src/lib/i18n/settings.ja.ts # src/lib/i18n/settings.ko.ts # src/lib/i18n/settings.zh.ts
Local merge gate: FAIL
The Linux suites of the required checks (Fast checks, Integration and browser), run on main 1dd4f3f with this PR's head ac86567 merged in (local merge 84017b2e4fd8, not pushed), on
Reruns:
Total 1836s. |
|
Merge note: account sign-in and device linking (whole stack: #2834 contract, #2840 admission rule, #2843 PC side, #2846 sign-in, #2851 join/remote approval, #2856 device management, #2858 native sign-in, #2867 docs). Owner decided it ships in 0.5.0. omo CLEAR at ac86567 (incl. the relay/session conflict resolution), Codex Terra xhigh across all parts with reproduced HIGHs fixed (approver cross-checks admissions with the service). Local merge gate on 84017b2e: check fast + integration pass; the two browser failures (terminal-ctrl-enter, terminal-arrows-clicks) are main-inherited from #2798 and reproduce on 1dd4f3f; a fix is in progress. Account UI stays hidden unless an account service is configured. |
Stacked on #2858 (sign-in from the desktop and phone apps).
What
account.ovis.shstores (a random account id, the provider and a hash of its id for you, token hashes, the PC list with each PC's name, relay id, public key and relay, signed add/remove records for at most 30 days, join requests with the device's name and public key for 10 minutes) and what it never receives (what you type, what PCs show, files, the digits you compare, private keys); and the account service in the list of what the app connects to.Validation
bun run check fastat b30f255: ok (typecheck, build, unit tests);scripts/ovis-site.test.ts,scripts/ovis-site-platform.test.tspass.