From 4137fca0efcced46f1d6038dd5d6050b14421dd5 Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Wed, 8 Jul 2026 17:10:12 -0700 Subject: [PATCH] docs: organizations, quota seam, share footer/rate limit in README, SKILL, CLAUDE.md Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs --- CLAUDE.md | 4 ++-- README.md | 33 ++++++++++++++++++++++++-------- plugin/skills/beardrive/SKILL.md | 4 +++- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f299ca8..6de48fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,11 +39,11 @@ Package roles (`internal/`): - **`syncer`** — the heart: `Session.Cycle()` runs one pass: scan → commit local ops → pull peer journals → preserve conflict copies → materialize merged state → push blobs + own journal. Read the package doc comment in `syncer.go` first. `ignore.go` holds the path filter (`.bdriveignore` rules + the `.bdrive` include list), applied symmetrically in scan and materialize; a newly filtered path is dropped from the cache *without* a delete op so opting out locally never deletes remotely. - **`daemon`** — per-mount background loop (detached process, `daemon.pid`/`daemon.log` in the mount's volume dir). Scans every `--scan-interval` (3s), talks to the remote every `--remote-interval` (10s) or immediately after local edits. Re-reads `.bdrive/config.json` each tick; if it vanishes (folder moved/renamed/deleted) the daemon **exits cleanly without propagating deletes** — the next bdrive command at the new location resumes it (self-heal on next touch). - **`config`** — global state under `$BDRIVE_HOME` (default `~/.bdrive`): device identity (`device.json`), settings (`settings.json`: default server + device token + signed-in account), and the mount registry (`mounts.json`, keyed by **stable mount id**, holding only each mount's last-known path). The per-folder `.bdrive/` directory (`project.go`) holds `config.json` with the mount id + volume/remote/include; **nothing is keyed by the folder path**, so renames/moves are free — `ResolveMount` self-heals the registry path, and the volume store lives at `~/.bdrive/volumes//`. `.bdrive/` is never synced and holds no credentials. -- **`webapp`** — the `bdrive web` server, in two modes. Single-volume: `Source` is a `DirSource` (plain folder from disk) or `RemoteSource` (folds journals into a file tree with per-file provenance). Hub: `Root` + `Projects` host many projects on one storage root, each under `//` via `remote.Prefixed`; `ProjectDB` (`projects.go`) is a file-backed registry (JSON, loaded at open, rewritten atomically per change) with create-or-join-by-name semantics. Renders markdown (goldmark + Obsidian `[[wikilinks]]`). With `--upload` it accepts writes: browser uploads (`upload.go` — direct-to-storage via presigned URLs when the backend implements `remote.PutSigner`, relayed otherwise; ops journaled under the server's own device) and the per-project `/api/p//store/*` proxy (`store.go`) that whole devices sync through — the `https://` remote backend (`remote/http.go`) is its client; journals are never presigned, only immutable blobs. Frontend is dependency-free vanilla JS embedded via `go:embed static`; it learns everything from `/api/config` (+ `/api/projects` in hub mode) and never sees storage info or credentials. +- **`webapp`** — the `bdrive web` server, in two modes. Single-volume: `Source` is a `DirSource` (plain folder from disk) or `RemoteSource` (folds journals into a file tree with per-file provenance). Hub: `Root` + `Projects` host many projects on one storage root, each under `//` via `remote.Prefixed`; `ProjectDB` (`projects.go`) is a file-backed registry (JSON, loaded at open, rewritten atomically per change) with create-or-join-by-name semantics, name-scoped per organization. Orgs (`orgs.go`, file-backed `orgs.json`) wall projects by membership (email → owner|member): every per-project route — viewer APIs, uploads, history, shares management, the `/store/*` sync proxy — 403s for non-members, `/api/projects` lists only your orgs' projects, owners mint expiring multi-use invite links (`/#join/`), and a pre-org hub migrates all projects into a "default" org (all existing accounts join, oldest owns) at startup. `QuotaProvider` (`quota.go`) is the plan-enforcement seam mirroring `AuthProvider` — CheckWrite/RecordUsage on every write path, CheckSeat on invite redemption; OSS ships only `UnlimitedQuota`, managed deployments swap the provider. Renders markdown (goldmark + Obsidian `[[wikilinks]]`). With `--upload` it accepts writes: browser uploads (`upload.go` — direct-to-storage via presigned URLs when the backend implements `remote.PutSigner`, relayed otherwise; ops journaled under the server's own device) and the per-project `/api/p//store/*` proxy (`store.go`) that whole devices sync through — the `https://` remote backend (`remote/http.go`) is its client; journals are never presigned, only immutable blobs. Frontend is dependency-free vanilla JS embedded via `go:embed static`; it learns everything from `/api/config` (+ `/api/projects` in hub mode) and never sees storage info or credentials. `cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `init`, `stop`, `sync`, `status`, `log`, `remote`, `web`, `whoami`, `daemon`, `version` — `mnt`/`umnt` are gone; `init` is the front door and `stop` pauses). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs `--shared `, which becomes the include list) with full flag bypass (`--name/--project/--shared/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, and starts sync via `startSync`; re-running it resumes — including after a folder move. `bdrive web -c config.json` configures the server from a file, explicit flags winning. -Authentication (`webapp/auth.go`, `authlocal.go`, `mail.go`) is **mandatory in hub mode** — the config's `auth` block only tunes `users_db`/`allow_signup`/`smtp`; the plain-folder viewer stays auth-free — and sits behind the `AuthProvider` interface — the OSS server ships only `BuiltinAuth` (email+password accounts and device tokens in a file-backed `auth.json`; bcrypt for passwords, SHA-256 digests for tokens, plaintext never stored; server-owned `/auth/*` pages; one-time codes for the CLI callback and device flows; SMTP reset mail with a log-link fallback). A managed deployment can swap in a different provider (e.g. PropelAuth) without touching the CLI or API — keep provider-specific code out of this repo. The sync client picks up its token from `BDRIVE_TOKEN` or `settings.json` and sends `X-Bdrive-Device{,-Name,-Os}` headers (`remote/http.go`); the hub's file-backed device registry (`webapp/devices.go`) records per-device name/OS/account/server-observed IP. Journal ops carry the signed-in account (`Op.User`/`UserName` from `Session.Account`; `Author` remains the git/OS fallback). History (`webapp/history.go`): `GET /api/p//history?path=|prefix=` (newest first, device-registry join) and `GET /api/p//blob?sha=` stream any exact version — blobs are retained forever, so the future revert phase is just re-putting an old blob as a new op. Share links (`webapp/shares.go`, file-backed `shares.json`): any signed-in member mints `/s/` public URLs (`bdrive share`, or the UI's Share button) serving the file's LATEST content until revoked (optional expiry); `/s/*` responses are sandboxed (CSP `sandbox allow-scripts`, no auth cookies) so shared HTML can't attack hub sessions — keep that header on any change. +Authentication (`webapp/auth.go`, `authlocal.go`, `mail.go`) is **mandatory in hub mode** — the config's `auth` block only tunes `users_db`/`allow_signup`/`smtp`; the plain-folder viewer stays auth-free — and sits behind the `AuthProvider` interface — the OSS server ships only `BuiltinAuth` (email+password accounts and device tokens in a file-backed `auth.json`; bcrypt for passwords, SHA-256 digests for tokens, plaintext never stored; server-owned `/auth/*` pages; one-time codes for the CLI callback and device flows; SMTP reset mail with a log-link fallback). A managed deployment can swap in a different provider (e.g. PropelAuth) without touching the CLI or API — keep provider-specific code out of this repo. The sync client picks up its token from `BDRIVE_TOKEN` or `settings.json` and sends `X-Bdrive-Device{,-Name,-Os}` headers (`remote/http.go`); the hub's file-backed device registry (`webapp/devices.go`) records per-device name/OS/account/server-observed IP. Journal ops carry the signed-in account (`Op.User`/`UserName` from `Session.Account`; `Author` remains the git/OS fallback). History (`webapp/history.go`): `GET /api/p//history?path=|prefix=` (newest first, device-registry join) and `GET /api/p//blob?sha=` stream any exact version — blobs are retained forever, so the future revert phase is just re-putting an old blob as a new op. Share links (`webapp/shares.go`, file-backed `shares.json`): any signed-in member mints `/s/` public URLs (`bdrive share`, or the UI's Share button) serving the file's LATEST content until revoked (optional expiry); `/s/*` responses are sandboxed (CSP `sandbox allow-scripts`, no auth cookies) so shared HTML can't attack hub sessions — keep that header on any change; `/s/*` also sits behind a per-IP token bucket (`ratelimit.go`, `share_rpm` config), and markdown share pages get a "Shared with BearDrive" footer (raw HTML is never injected into). ## Invariants — do not break these diff --git a/README.md b/README.md index acbf75f..423c01d 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ the positional argument), `--upload` (allow client writes, off by default), "upload_ttl": "15m", "refresh": "10s", "projects_db": "/var/lib/bdrive/projects.json", + "share_rpm": 120, // per-IP rate limit on public /s/* links "auth": { // optional knobs; hub auth is always on "allow_signup": true, "users_db": "/var/lib/bdrive/auth.json", @@ -202,6 +203,17 @@ whole folders through the hub without ever knowing where the storage is or holding any cloud credentials; the server device is the only one configured with the bucket. +Projects are walled by **organization**: every project belongs to one org +(file-backed `orgs.json`), and only that org's members — accounts with the +`owner` or `member` role — can see, browse, or sync it. Your first +`bdrive init` creates an org for you automatically; an owner invites +teammates from the web UI (the org name in the sidebar footer — Invite +mints an expiring join link, `/#join/`, that any signed-in account +can open to become a member). A hub upgraded from an earlier version +sweeps its existing projects into a `default` org that all existing +accounts join, so nothing breaks. Public share links stay outside the +wall on purpose. + ```sh # On the server device (knows the storage) bdrive web -c config.json @@ -248,12 +260,15 @@ pages and living reports), and live until revoked — `bdrive share --list` and `--revoke ` manage them, `--expires 24h` makes one self-destruct. The web UI has a Share button on every file. -Shared HTML renders as a real page, markdown renders like the viewer, PDFs -open inline. Rendering is sandboxed: `/s/*` responses carry a strict CSP -and never see auth cookies, so a malicious shared file's scripts can't -touch hub sessions. Anyone signed in can mint links, and a link is public -to whoever has the URL — don't share folders that hold secrets, and note a -LAN-bound hub means LAN-only links. +Shared HTML renders as a real page, markdown renders like the viewer +(with a small "Shared with BearDrive" footer; raw HTML is served +byte-for-byte), PDFs open inline. Rendering is sandboxed: `/s/*` responses +carry a strict CSP, never see auth cookies, and sit behind a generous +per-IP rate limit (`share_rpm`), so a malicious shared file's scripts +can't touch hub sessions and a scraper can't turn the hub into a CDN. +Any org member can mint links, and a link is public to whoever has the +URL — don't share folders that hold secrets, and note a LAN-bound hub +means LAN-only links. ### Claude Code integration @@ -270,8 +285,10 @@ whether or not they installed the plugin. The payoff: "write a report and share it" becomes Claude generating `wiki/report.html` and replying with a public URL. -The web UI lists the hub's projects in the sidebar; selecting one browses -that project's files, and the **History** view shows every change — which +The web UI lists your orgs' projects in the sidebar (⌘K opens a command +palette: fuzzy file search, project switching, share/history/upload +actions); selecting one browses that project's files, and the **History** +view shows every change — which account made it, when, from which device (name, OS, and the IP the server observed), with view/download of any past version (content is content-addressed and retained forever; reverting to a version is the next diff --git a/plugin/skills/beardrive/SKILL.md b/plugin/skills/beardrive/SKILL.md index b8d996c..e4565f8 100644 --- a/plugin/skills/beardrive/SKILL.md +++ b/plugin/skills/beardrive/SKILL.md @@ -25,7 +25,7 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing | Share a synced file publicly by URL | `bdrive share ` — prints a link anyone can open (HTML renders as a page, markdown rendered, PDFs inline; sandboxed; always the latest content; no account needed). `--expires 24h` for self-destructing links; `--list` / `--revoke ` to manage. Put generated reports in the shared folder, sync, then share. | | Set up a project for a Claude Code team | `/beardrive:install` — installs the CLI, signs in, runs init (whole/shared folder), offers a CLAUDE.md section about the shared folder, and registers project-level hooks (blocking pull at prompt-submit, async push after Write/Edit) in `.claude/settings.json` | | Per-file / folder change history in the web UI | History button (file versions or project feed) and per-folder ⌚ — each entry: account, time, device (name/OS/IP), view/download of that exact version. API: `GET /api/p//history?path=\|prefix=`, `GET /api/p//blob?sha=` | -| Web server: viewer + multi-project sync hub (read-only unless `--upload`) | `bdrive web [ \| ]` (serves cwd by default, `--addr :4173`; `-c config.json` reads remote/addr/upload/projects_db settings from a file, explicit flags win; a storage root URL makes it a hub hosting many projects at `//`, registry in `--projects-db` file, default `$BDRIVE_HOME/projects.json`; `--upload` lets browsers add files, client devices push, and projects be created — direct to storage via expiring presigned URLs on S3/GCS, relayed through the server for `file://`; `--upload-ttl 15m`; clients never see the remote URL or credentials) | +| Web server: viewer + multi-project sync hub (read-only unless `--upload`) | `bdrive web [ \| ]` (serves cwd by default, `--addr :4173`; `-c config.json` reads remote/addr/upload/projects_db settings from a file, explicit flags win; a storage root URL makes it a hub hosting many projects at `//`, registry in `--projects-db` file, default `$BDRIVE_HOME/projects.json`; `--upload` lets browsers add files, client devices push, and projects be created — direct to storage via expiring presigned URLs on S3/GCS, relayed through the server for `file://`; `--upload-ttl 15m`; clients never see the remote URL or credentials; hub projects are walled by org membership — invite teammates from the web UI; the viewer has a ⌘K palette for fuzzy file search, project switching, and quick actions) | `` is created if missing. Omitting it on `sync`/`status`/`log` defaults to the current working directory. @@ -93,6 +93,8 @@ cd ~/agent-workspace && bdrive init --name agent-workspace Devices connecting the same project (by name or id) converge through the hub. Direct-to-bucket setups (no hub) remain possible via `bdrive remote set s3://…` after an offline init. +Hub projects belong to an **organization**: only members of the project's org can see or sync it (project names are scoped per org too). Your first `bdrive init` creates your org automatically. To give a teammate access, an org **owner** opens the web UI and clicks **Invite** in the sidebar footer — it mints an expiring join link (`…/#join/`); the teammate opens it, signs in (or up), and is in. If a teammate's `bdrive init --project ` gets 403/404 or the project list looks empty, the missing invite is the reason. Public share links (`bdrive share`) intentionally bypass the org wall. + ### Renames and moves Renaming/moving a project folder is safe: the daemon notices its folder vanished and exits **without propagating any deletes**; run `bdrive init` (or any bdrive command) at the new location and it resumes with zero spurious changes. `bdrive status` shows stale paths as "folder missing".