Browser profiles and sync¶
The browser client's Preferences → Profiles and sync section mirrors the
Flutter implementation on top of the shared
shared/profile-core contract: explicit Google provider consent, a durable
per-identity catalog, reviewed publication and
enrollment, and first-setup onboarding. The beta
identity gate still only identifies the user; Drive access is a separate,
reviewed grant. Ongoing reconciliation, the remaining portable categories,
credential protection and live Google access remain open in the
sync handover.
Google consent through the beta server¶
backend/src/profiles.rs runs a second OAuth consent bound to the signed-in
beta session. POST /api/profiles/connect records the next-sign-in choices
(Drive app data, Calendar off/read/edit) and returns the Google URL with
PKCE/state/nonce and the exact scope list from desktop consent.rs;
GET /auth/google/callback exchanges the code through the ProfileProvider
trait, requires the consenting subject to equal the session identity, verifies
the Drive principal (about.user.permissionId) and interprets the returned
scopes intersected with the request. Tokens live only in the in-memory session
store, are refreshed server-side before Drive calls, disappear with logout,
session expiry or a replacing login, and are never logged or sent to the
browser. Denied, failed or mismatching consent redirects to
/app/#profiles=denied|failed|mismatch with the previous grant and choices
untouched; a changed choice changes saved access only after a successful
sign-in. POST /api/profiles/disconnect removes this session's grant only,
never the project-wide grant of other devices.
POST /api/profiles/drive proxies a fixed, validated set of app-data
operations (about, list, start_page_token, changes, metadata,
media, generate_ids, create) with bounded page tokens, file identities,
1 MiB media and 256 KiB JSON. SHEP_PROFILE_NAMESPACE configures the shared
application namespace; without it the connection endpoint reports profile sync
as unavailable. The production provider reports live: true; the test fixture
reports live: false, and the Preferences card says when live provider access
is not connected.
Browser storage and history¶
web/src/profile_store.ts opens shep.profiles.v1.<identity> per beta
identity, separate from the mail cache: journal records, staged edits, the
discovery catalog and scan state, profile summaries, account mappings,
publication/enrollment reviews and preference receipts. Every write is one
strict transaction; disconnect marks cleanup durably before clearing, and an
interrupted clear is offered again as Retry local cleanup.
web/src/profile_history_worker.ts owns WASM ProfileHistory journals, the
new browser entry of shared/profile-core over history::memory, which
runs the same command contract as the native SQLite journal (imports, edits,
conflicts, tombstones, the initialization barrier, uploads) and is parity
tested against it in shared/profile-core/tests/memory.rs. The worker holds
no storage: the page persists each accepted record before treating the write
as acknowledged and restores journals from those records in sequence order. A
local journal gets a fresh device UUID kept in the store; observation journals
are never copied into local ones, and another device's local records are
refused on restore.
Discovery, publication and enrollment¶
web/src/profile_discovery.ts follows durable discovery:
capture the change token, list pages of at most 50, stage each page, verify
metadata, exact size, SHA-256 and decoded identity (web/src/profile_drive.ts)
before importing the original into the profile's observation journal, then
replay changes. Incomplete listings, repeated tokens, duplicate files, changed
identities, altered media and missing known files are saved failures; retry
continues from the saved step, pause finishes the accepted step, and an
incomplete or failed scan never presents an empty Google account.
web/src/profile_publication.ts freezes a review of the browser's account
definitions (shared UUID mappings, legacy IDs mapped durably) and the four
portable browser preferences by fingerprint, then stages the preparing root,
content operations and completion marker as exact durable edits, and uploads
one owned file per step: generate an ID, reserve it, create only after a
confirmed 404, verify the read-back metadata and exact media, record the
own-upload receipt in the catalog, then confirm the queue. Lost replies retry
the same operation and reserved ID without duplicating files.
web/src/profile_enrollment.ts copies originals one record at a time from
the observation journal into an independently owned local journal, builds
paged rows (account definitions with connection details, names, settings;
conflicts and unsupported fields unavailable), and applies one account per
step. Imported accounts are saved without passwords and show Reconnect
required until a reviewed reconnect verifies a password pair; mapped
accounts keep their identity, cached mail, drafts and Sent preferences; a
remotely changed connection is offered as a separate, unselected account.
Preferences apply through web/src/profile_settings.ts: per-field revisions
advance on every explicit save (including change-and-revert), newer local
edits are kept, and the receipt freezes the original revisions so a retry
returns the same proof.
Onboarding runs on the Mail tab after completed discovery on a browser without accounts: no profiles offers Sync accounts and settings with a durable Not now; one profile offers automatic enrollment; several show a picker and never merge. Passwords, mail, drafts, tokens and device identity are never part of any profile record.
Verification¶
Run the commands in client testing. Rust tests cover
exchange, scope binding, denial, mismatch, refresh, cleanup and proxy
validation; web/src/profile_flows.test.ts covers the store, history port,
discovery, publication, enrollment and preference device with fake IndexedDB
and the real WASM; web/e2e/profiles.spec.ts drives the actual controls in
Chromium with light/dark captures and axe checks; the ignored
real_browser_beta_gate exchanges consent and publishes through the real Rust
router. These use isolated fixtures and do not establish live Google, same
project cross-client visibility or continuous sync.