Introduce a pluggable metadata persistence layer: a MetaStore of typed
repositories (AccountRepo, ProjectRepo, OrgRepo, ShareRepo, DeviceRepo).
The five registries keep their in-memory maps + logic and now persist each
change as one record through a repo, instead of rewriting a whole JSON file
inline. The `file` backend (db_file.go) reproduces the exact on-disk JSON
formats, so a running hub loads unchanged. Open*(path) constructors stay as
thin wrappers over the file backend.
Zero behavior change: full webapp suite green; the example hub boots and
existing auth.json/orgs.json/projects.json load and sign-in works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
A hub on a public URL could be signed up to with any fake email. Make the
safe posture the default and turn the dangerous ones into startup errors.
Three supported postures:
- invite-only (NEW default): allow_signup defaults false. A valid org
invite link bootstraps an account even when self-signup is closed — the
only way in. pageLogin/pageSignup detect a /join/<token> target via
BuiltinAuth.InviteValid (wired to OrgDB.ValidInvite) and offer account
creation; signupInvited skips the domain/approval/verification gates and
activates immediately (the owner's invite is the vetting).
- approval-gated: allow_signup + require_approval (no SMTP needed).
- domain-restricted + verified: allow_signup + allowed_domains +
require_verification (needs smtp).
Hardening:
- BuiltinAuth.ValidateSignupPolicy (run at hub startup in web.go) refuses to
boot an open hub with no gate, or require_verification without a mailer
(the link would otherwise only reach the server log).
- handleAdminPolicy rejects enabling verification without SMTP; the UI
toggle is disabled with clearer copy in that case.
Tests: TestValidateSignupPolicy, TestInviteBootstrapsAccountWhenSignupClosed,
TestPolicyVerificationNeedsMailer. Verified end to end in the running hub
(outside-domain invitee onboards via a link; direct signup stays closed).
Docs updated (README, CLAUDE.md, SKILL.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Rework the bdrive web UI away from the violet Obsidian-ish theme to a
near-black, honey-accented, hairline-crafted look in the spirit of Linear:
- Replace the mixed emoji glyphs (chevrons, clocks, palette icons, admin
badge, history markers, share dialog) with one coherent SVG line-icon
sprite + a svgIcon() helper; colored letter-marks for projects.
- Repoint the design tokens and rewrite style.css: near-black grounds,
7% hairline borders, tightened type, honey spent only on active state,
the ⌘K selection, and brand marks.
- Restyle the server-rendered auth pages and the public /s/ share shell
to the same tokens so sign-in and the app read as one product.
File-tree behavior:
- Folders start collapsed; a lone root folder opens on load.
- Opening a file (search, wikilink, deep link) unfolds the path to it and
scrolls the row into view. State now tracks open dirs (not collapsed),
so it survives the periodic tree refresh.
- Plain-text/JSONL views wrap long unbreakable lines instead of
overflowing the reading column.
Verified in the running example hub across desktop + mobile; go build,
go vet, go test ./... all green; zero horizontal page overflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Flips the final objective gate (contrast_aa) true; design-only.
- Palette match-highlight on a SELECTED row: recolor .plabel b to
--accent-bright (#c9b3ff) → 6.19:1 on the tinted row (was accent 3.98:1).
- Auth-page secondary text .alt #8a8a8a→#969696 → 5.12:1 on the card
(was 4.38:1).
- Polish: active-project marker uses --accent-bright to match its label;
Upload/Download join Share/History as uniform ghost buttons so no
secondary action shouts louder than the rest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Addresses the baseline design review's failing objective gates and layout
defects; no backend or flow changes.
Accessibility (was the ceiling — 3 of 4 objective gates failed):
- Contrast to AA: lifted --text-faint (#6e6e6e→#8a8a8a) and --text-dim;
darkened --accent-dim (#7c5cd6→#6a48e0) so white button labels reach
4.5:1; new --accent-bright (#c9b3ff) for accent text on the tinted
active background (tree/project active rows, admin badge, invite/ghost
buttons all lifted off sub-AA pairings). Same fix applied to the
server-rendered auth pages, which now share the app's token values.
- Keyboard + focus: file tree, project, and org-name rows are now
focusable (tabindex/role + Enter/Space) with a global :focus-visible
ring; restored input focus rings on app and auth pages.
- Touch targets: every header + sidebar control is a 44x44 hit area on
mobile; secondary file actions (History/Upload/Download) collapse under
a "⋯ More" menu so the row still fits with zero horizontal overflow.
Layout defects:
- #meta no longer wraps to 5 lines / shoves the action buttons — single
truncating line; #crumb truncates too.
- Long tree filenames ellipsize (label span flex:1 min-width:0) instead
of hard-clipping.
Consistency:
- Fixed the .markdown specificity leak: admin/history/onboarding views no
longer inherit markdown type rules (content class toggles per view), so
the admin type scale renders as declared.
- Recolored the off-system gold Admin badge to the accent family; added a
3-step radius token scale; swapped the mojibake-ish ▣/⛛ markers.
- Added a global [hidden] guard so explicit display rules can't override
the hidden attribute.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Fixes the two round-2 majors.
User: on mobile the per-file actions (Share/History/Upload/Download) are
now icon buttons in the header — reachable again (round 2 wrongly hid
them). Search tolerates simple English plurals (ideas→idea), the
no-matches state states what search covers, the sidebar shows the hub
brand instead of the raw device name, and history-row downloads carry a
download attribute. Logged-out loads redirect to sign-in from /api/config
instead of firing 401-ing API calls.
Admin: a hub-admin "Signup & access" settings screen (⚙ Admin in the
sidebar) toggles email verification and admin approval live — persisted to
auth.json and surviving restart — while the domain allowlist and admin
list are shown read-only (deliberately server-config-owned so a browser
session can't widen access). Pending approvals live on the same screen.
Config toggles are now *bool so an explicit config value pins the setting
each boot, else the UI-saved policy stands. Invite revoke confirms; role
change re-renders the panel.
Tests: policy persistence + reload, policy API admin-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Admin: top-level ⚙ settings entry (owners/admins), signup form states the
domain restriction up front, invite list shows creator + join count,
org-wide share audit shows creator/date and confirms before revoke,
self-row role/remove controls disabled to avoid footguns.
User: mobile header no longer overflows (per-file actions move to the ⌘K
palette on narrow viewports; tables/pre scroll in their own container),
empty-state copy works on mobile, share confirmation is now an explicit
"anyone with this link can view" dialog with copy/open/revoke, invite
links carry a "you've been invited" banner through login, joining opens
the joined project, brand shown as the title, logout labeled, palette
placeholder clarified to "file names".
Tests: invite use-counter + creator in the owner list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Makes a self-hosted hub safe to expose on a public URL and operable
without hand-editing JSON — addressing the blocker/major findings from
the persona usability evaluations.
Signup gating (config auth block, all optional):
- allowed_domains: signup email must match (e.g. only @runbear.io)
- require_verification: email-link activation before sign-in (reuses mailer)
- require_approval: hub admins approve new accounts (admins list)
- brand shown on the sign-in page; allow_signup:false already hid Sign up
Accounts carry a Status (active/unverified/pending); non-active accounts
cannot authenticate.
Admin lifecycle (endpoints + web UI):
- org: rename, member role change, member remove (last-owner guarded),
invite list + revoke
- project: create (web), rename, delete (from the org panel)
- hub admins: approve/deny pending signups (sidebar bell + panel)
- org-wide public-share audit with revoke
UX: onboarding empty-state (explains invites, paste-invite + create-project)
instead of a blank sidebar; visible "Search ⌘K" button; toasts replace
blocking alert(); responsive layout with an off-canvas sidebar; joining
via #join now survives a logged-out click (token carried through login).
Web uploads are attributed to the signed-in account, not the server.
Login/signup are rate-limited per IP.
Tests: domain/verification/approval gates, auth rate limit, org+project
lifecycle, owner-only guards, invite→join→role→remove over HTTP.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Every hub project now belongs to exactly one org (file-backed orgs.json,
same load-at-open + atomic-rewrite discipline as the other registries).
Membership (email -> owner|member) gates every per-project route: the
viewer APIs, uploads, history, blobs, shares management, and the device
sync store proxy; /api/projects lists only your orgs' projects, and
project names are now scoped per org. Public share links (/s/) stay
public by design.
Design choices, per the simplest-consistent rule:
- Migration: a pre-org hub sweeps all org-less projects into a "default"
org at startup; ALL existing accounts join it (they could all see every
project before, so anything narrower would lock someone out), oldest
account as owner. Zero manual steps.
- An account in no org that creates a project gets a fresh org named
after itself, so nobody is ever blocked from starting to sync.
- Invites are expiring multi-use links (default 7 days): an owner mints
/#join/<token>, any signed-in account that opens it joins as member.
The web UI shows the org in a sidebar footer (members on click,
Invite button for owners).
bdrive init needed no changes: its connect-existing flow lists projects
through the now-filtered API.
Tests: OrgDB + migration units; a 403/access matrix over every
per-project route; invite mint/redeem flow; and a multi-device syncer
test proving a device holding an org-B token can neither pull org A's
files nor push into its store (sync degrades to Offline, never partial).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Authentication (previous phase, now landed together with its follow-ups):
- Email+password+name accounts behind an AuthProvider interface; the OSS
server ships BuiltinAuth only (file-backed auth.json: bcrypt password
hashes + SHA-256 token digests, plaintext never stored; server-owned
/auth/* pages; managed deployments can swap in another provider).
- bdrive login: loopback-callback browser flow (sign-up on the page, the
terminal finishes itself) with a device-code fallback for headless
machines; long-lived revocable device tokens in settings.json.
- Password reset via plain SMTP (stdlib) with a log-link fallback when no
SMTP is configured.
Move-proof projects:
- .bdrive is now a directory; config.json carries a stable mount id.
The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed
by that id — never the folder path — so renames/moves are free.
- The daemon re-reads the project config each tick and exits cleanly
(propagating nothing) when its folder vanishes; the registry self-heals
and the next bdrive command at the new location resumes with zero
spurious changes.
bdrive init is the front door (mnt/umnt removed; bdrive stop pauses):
- Interactive on a TTY (create new / connect existing project from the
server's list; whole folder / shared subfolder via the include list),
full flag bypass (--name/--project/--shared/--yes), never prompts
without a TTY. Runs the login flow first when there is no session.
Default server: beardrive.ai (config.DefaultServer).
Web history (revert-ready):
- Hubs now always require auth; journal ops carry the signed-in account
(user/user_name) alongside the git/OS fallback author.
- File-backed device registry: per-device name, OS, account, and the
public IP the server observed, joined into history at read time.
- GET /api/p/<id>/history?path=|prefix= (newest first) and
GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained
forever, so the next phase's revert is re-putting an old blob.
- UI: History button (file versions or project feed), per-folder history
shortcut, view/download of any past version.
Tests: auth flows (callback, device-code, reset single-use, persistence,
gating), history API + device registry, folder-move survival, registry
self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs