Skip to content

docs(account): signing in to link devices, and what the account service keeps - #2867

Merged
devswha merged 113 commits into
mainfrom
feat/account-docs
Nov 5, 2026
Merged

devswha merged 113 commits into
mainfrom
feat/account-docs

Conversation

@devswha

@devswha devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Stacked on #2858 (sign-in from the desktop and phone apps).

What

  • docs/guide.md, under On your phone: Through an account. Signing in is optional; QR pairings move into the account by themselves the next time each PC opens; a PC marked Needs approval is joined by comparing digits; a device already in the account approves a new one by picking its number out of three, and one approval reaches every PC. Who gets into a PC is still decided by that PC alone, where Revoke always works.
  • Privacy policy (ovis.sh/privacy, English and Korean): Your account, saying what the account service at account.ovis.sh stores (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 fast at b30f255: ok (typecheck, build, unit tests); scripts/ovis-site.test.ts, scripts/ovis-site-platform.test.ts pass.
  • Docs only: no behaviour change, so no new tests.

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.
@devswha
devswha changed the base branch from feat/account-native to main November 5, 2026 06:40
# 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
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Local merge gate: FAIL

Local merge gate: FAIL #2867 merge=84017b2e4fd8 head=ac865671ad77 main=1dd4f3fed4ac on herdr-ci-runner:local on devswha-ThinkStation-P620 CPUs 96-127 in 1836s, reruns=2

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 herdr-ci-runner:local on devswha-ThinkStation-P620 CPUs 96-127 (a reserved slice, 33% busy before the start, host load 147.15; shards=4 browser_main_workers=2 browser_other_workers=6). Native Windows install and the macOS checks are not part of this gate.

Step Command Result Seconds Counts
release-guards bun scripts/release-guards.ts --base 1dd4f3fed4ac pass 3 -
install bun install --frozen-lockfile pass 0 -
fast bun run check fast pass 297 tests 23486 pass / 0 fail; check steps 7 ok / 0 failed
integration HERDR_TEST_SHARDS=<shards> bun run check integration pass 212 tests 1153 pass / 0 fail; check steps 3 ok / 0 failed
browser-build bun run build && bun run build:shell pass 8 -
browser-main BROWSER_SCRIPTS=ui-regression,ui-contexts-regression bun run check run -- bun scripts/ci-browser-parallel.ts fail (exit 1) 370 check steps 0 ok / 1 failed; browser scripts 1/2 passed
browser-other BROWSER_EXCLUDE=<those two> bun run check run -- bun scripts/ci-browser-parallel.ts fail (exit 1) 936 check steps 0 ok / 1 failed; browser scripts 146/147 passed

Reruns:

  • browser scripts/workspace-touch-reorder-demo-regression.ts failed, then passed on its automatic second run
  • browser scripts/health-flap-demo-regression.ts failed, then passed on its automatic second run

Total 1836s.

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

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.

@devswha
devswha merged commit 5c56f80 into main Nov 5, 2026
25 of 29 checks passed
@devswha
devswha deleted the feat/account-docs branch November 5, 2026 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant