Skip to content

hub-client: connection-gated auth + local-first default, with offline editing of cached hub projects#397

Draft
shikokuchuo wants to merge 15 commits into
mainfrom
feature/connection-gated-local-first
Draft

hub-client: connection-gated auth + local-first default, with offline editing of cached hub projects#397
shikokuchuo wants to merge 15 commits into
mainfrom
feature/connection-gated-local-first

Conversation

@shikokuchuo

Copy link
Copy Markdown
Member

Makes the hub client local-first, with authentication gated on connecting to a hub rather than on entry.

Local-first default

  • The LoginScreen entry gate is gone; first run auto-creates a local project set.
  • Projects without a sync server are created and edited entirely in-browser against a storage-only Repo, authored under a persisted per-browser local actor.

Connection-gated auth

  • Sign-in is now an opt-in "Connect to a hub" flow.
  • A hub project that needs a session we don't have prompts for sign-in instead of silently no-oping.

Offline editing of cached hub projects

  • A cached hub project opens and is editable offline/logged-off under the local actor.
  • On reconnect, the sync client re-authors open handles to the server-trusted HMAC actor and bridges identities so the offline+online history reads as one author.

Also included

  • Restores the editable Sync Server URL field in Create/Import, with follow-up fixes for the local-creation default and a flash-and-bounce teardown regression.
  • Local-first E2E spec and LOCAL-FIRST.md user docs.
  • local-prod static port configurable via PORT.

Plans: claude-notes/plans/2026-07-06-hub-client-connection-gated-local-first.md and 2026-07-15-hub-client-offline-cached-hub-projects.md (epic bd-xxjy9yfp), under the auth-reshape umbrella.

Add buildRepo() that omits the WebSocket adapter when no syncServer is
given; connect/createNewProject skip the peer wait in that mode.
CreateProjectOptions.syncServer is now optional. Add SyncClient.flush()
and flush-on-create so a local project survives an immediate reload.
createLocalProjectSet()/connectLocal() build a storage-only Repo (no WS
adapter, no waitForPeer) via a shared buildRepo() helper; flush() and
flush-on-disconnect keep local edits durable. ProjectSetEntry.syncServer
is now optional (absent = local).
getOrCreateLocalActor() mints a stable 32-hex Automerge actor once per
browser (UserSettings.localActorId) so local edits attribute to one
coherent author across reloads instead of a fresh random per-handle
stub. App wiring lands in A4.
…hdc)

Remove the LoginScreen entry gate; sign-in is now an opt-in overlay via a
header 'Connect to a hub' control. useProjectSet auto-creates a local
project set on first run; create/open branch local vs hub by syncServer
(local authored under the per-browser local actor). useAuthProbe and the
auth-loss teardown are scoped to hub projects so a local project is never
torn down.
Add e2e/local-first.spec.ts (real-browser: no login gate; create local
project offline → navigate into editor → reload persists). Add
LOCAL-FIRST.md user docs, cross-linked from README. The OIDC sign-in leg
is not automated (no test IdP); manual path documented.
…u4p8xhdc)

Logged-off selection of a hub project (with a syncServer) resolved a null
actor (401) and the open path returned silently. Extract resolveActorForOpen:
local projects always open; a hub project needing a session we don't have now
prompts sign-in instead of a silent no-op.
Scopes full offline editing of cached hub projects as the adoption
forward-switch+display-bridge run recurrently on each reconnect (local
actor offline, HMAC on reconnect). Coordinated with D1 durability
(bd-10bdjmjb); supersedes the interim prompt-sign-in for cached projects.
Reusable timeline fixtures for the offline-cached-hub epic (bd-xxjy9yfp):
test-hub gains setHolding/dropConnections (stable-URL online→offline→online),
cached-hub.ts composes them with fake-indexeddb, and a green baseline test
exercises the full timeline. The baseline passing answers B4's open question:
an offline edit to an existing cached doc syncs up on reconnect via normal
automerge sync, no D1 fix needed. B1-B3 behavioral specs are staged as it.todo.
…-qklxdkwh)

A logged-off/offline open of a *cached* hub project now falls back to the
persisted local actor and opens from cache instead of prompting sign-in;
an uncached project prompts sign-in when online or reports offline-unopenable.
The openActor seam derives online-vs-offline from whether resolveHubActor
resolves null (logged off) or throws a TypeError (offline). Cache membership
is a new stateless isDocCached() probe in quarto-sync-client.
…b projects (B2-B5, bd-xxjy9yfp)

Offline edits to a cached hub project author under the local actor with an
identities row (B2, existing connect path). On reconnect, SyncClient.switchActor
re-authors open handles to the server-trusted HMAC actor and bridges the
identities so the offline+online timeline reads as one human; the App wires this
via reconcileHubActorOnReconnect on onConnectionChange (B3). Offline edits to N
existing cached docs all sync on reconnect via normal automerge sync, no D1 fix
(B4). Docs + honest E2E status in LOCAL-FIRST.md and the plan (B5).
STATIC_PORT (and the baked-in VITE_DEFAULT_SYNC_SERVER for build:local-prod)
now honor a PORT override, defaulting to 8080, so local-prod can run on a
port other than the default (e.g. PORT=5173).
…bd-ivkf752c)

A4 (bd-u4p8xhdc) removed this field, defaulting silently to the active
project set's server. Restored per user request: the field is back in both
forms, defaulting to DEFAULT_SYNC_SERVER and editable; clearing it still
creates a local-only project.
…on (bd-lkjdu6fn)

The field restored in bd-ivkf752c defaulted unconditionally to
DEFAULT_SYNC_SERVER, which silently turned local creation into a hub-creation
attempt with no session: the project got created and wired to a real WS
adapter, then immediately torn down by the auth-loss-teardown effect — a
flash of the editor before bouncing back to the selector, on create and on
every reopen of that project.

Now defaults to projectSetSyncServer ?? '' (empty/local when disconnected,
the connected hub when connected), via a new newProjectSyncServer state reset
on each Create/Import open. The Connect form's own field is unaffected.
…(bd-lficgfwi)

Two halves of the same regression against B1 (bd-qklxdkwh): App's auth-loss
teardown was state-based (!auth && hub project → disconnect), unmounting every
logged-off cached-hub open on the next render; and createNewProject swallowed
resolveActorId's null, silently creating a hub-wired project with no session
for the teardown to kill. Teardown is now transition-based
(shouldTeardownOnAuthChange, ref-tracked hadAuth); the sync client aborts
creation with a typed ActorAuthRequiredError which App turns into a sign-in
prompt; a consumed showLogin is reset on successful sign-in.
@posit-snyk-bot

posit-snyk-bot commented Jul 17, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

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.

2 participants