Commit Graph

14 Commits

Author SHA1 Message Date
bflatastic 501e4a6737 M9: online files: the app remembers the files it put online
A file put on a server without the server's key stays its owner's after its
tab closes or the app quits. The app records it with the preferences (server,
id, name, owner token, the owner's links, what others join with; at most 200;
validated when read back) and never shows the token: not in command results,
ui.inspect, logs or debug output.

- Home > Online lists your online files: open one as its owner (a new tab, or
  its tab comes to the front), start and stop sharing, copy the code or link,
  rename on the server, remove from the server (confirmed), forget here. One
  request per server when the section opens; offline and gone states.
- collab.files, collab.openFile, collab.forgetFile; collab.manageFile takes
  its server and token from the record and gains share / unshare.
- Server: Admin owned (a compatible addition within protocol version 2) says
  what became of the files a list of owner tokens owns. Older servers leave
  the list as recorded.
- Tabs mark files in a live session (shared, or open only to you); toasts
  about a file that isn't in front name it; after a crash a recorded file
  reopens from its server as its owner.
- Draft catalogs (de, fr, ja, ko, pt-BR, zh-Hans) translate the 71 strings
  and 5 command labels they lacked and the new ones; es and es-419 follow.
- Docs: collab-protocol, server, mcp, control-protocol, parity pages, gaps,
  ROADMAP.
2026-10-13 01:41:14 +07:00
bflatastic 8f6a123f29 M9: share with a join code: stop sharing keeps the file
Online, the file is its owner's: others get links and codes of their own, and
Stop sharing (collab.stop) revokes those on the server. Whoever joined with
them is disconnected at once and told sharing was stopped (the session's end,
with their copy); the owner stays in the file, which stays on the server, and
Start sharing gives the same file new links and codes. Removing the file is a
separate, confirmed step under Advanced.

The join field is the shared text input: as tall as the Join button beside it.
2026-10-12 19:42:28 +07:00
bflatastic 6720ea4a21 M9: share with a join code: online through the built-in server or on the local network
The Share popover is one flow: Online (the file is published to the server in
Preferences and the server names its links with short codes) or Local network
(the app hosts; the code carries its address and the session's secret), one
Start sharing button, the code large with Copy code and Copy link, who is here,
can edit / can view, and Stop sharing. Join with a code is one field in the
popover, on the home screen and in the Join dialog, for a code of either kind
or an invite link.

Protocol (within version 2, new variants last): Resolve / Resolved and Admin
code. The server keeps a stretched hash and the wrapped token, never the code,
and limits wrong codes per address (with a lockout) and for everyone. Older
servers leave the links in place of a code; older apps join by link.

collab.share, collab.join {code}, code and share in collab.peers; the server
preference defaults to the built-in server and can be reset or turned off.
2026-10-12 19:42:28 +07:00
bflatastic 94349d86b0 M9: live sessions per document
Live sessions are per document instead of one for the whole app.

- **Engine** (`crates/engine/src/live.rs`): `Session.live` is now `Lives`: the app-wide parts (the
  installed link, the invite-link registration) plus one `Live` per session. One place resolves
  "the session of this document": `Session::live()` (the active document's; status off when it
  has none), `live_mut()`, `live_of(uid)`. Sync, undo, read-only checks, follow, cursor chat,
  spotlight and presence all go through the session of the document they are about, so edits and
  presence never cross. Sessions behind the active tab keep syncing; your presence there is empty.
- **Link** (`LiveLink`): every call takes the `SessionId` the engine picked and `poll` returns
  `(SessionId, Event)`, so one link carries several sessions. The desktop link and the browser
  link keep them in a shared table with one mailbox per session
  (`crates/collab-net/src/sessions.rs`); each hosted document listens on its own port.
- **Commands**: `collab.*` act on the active document's session; no parameters changed.
  `collab.peers` keeps its shape and gains `session` (its id, null when the document isn't in
  one) and `sessions` (every session: `{session, document, status, reason, role, fileName, link,
  peers}`). `collab.join` returns the new session and opens its file in a tab of its own without
  touching other sessions; until then (or if it fails) it shows on the tab it was started from.
  Starting a session for a document replaces only that document's own session. Closing a document
  ends its session only.
- **UI** (`crates/ui-egui/src/live.rs`): the Share popover, avatars, cursors, chat, spotlight
  banner, follow frame and read-only reason read the session of the tab in front, per window. What
  belongs to one session in the window (cursors shown, your chat box, a report being written) is
  dropped when another tab comes to the front. `ui.inspect`'s `live` gains `session` and
  `sessions`. No new strings or command ids.
- **Docs**: `docs/collab-protocol.md`, `docs/mcp.md`, `docs/control-protocol.md`,
  `docs/collaboration-parity.md`, `docs/gaps.md` (gap 2), `ROADMAP.md`.
2026-10-12 17:54:18 +07:00
bflatastic 9fde7fed3a M9: oversized-message test sends only the frame header (no broken pipe on Linux) 2026-10-12 11:40:03 +07:00
bflatastic 98c4753118 M9: open hosting: fair limits for shared addresses
Third review:
- a broadcast counts once per recipient block, welcomes don't count
  against block caps, and presence, cursor chat and pings have their own
  per-address budget, so an office behind one address works together
- when everyone's reading budget runs out, the biggest readers are cut
  off until it fits (their bytes freed at once), no cliff
- block counters are forgotten once only the table holds them
- install.sh: root's downloads, clone, swap file and built binary in a
  root-only directory; the build user builds its own copy; the binary is
  copied out with the build user's rights
- recommended open-mode settings in install.sh's config and the docs
2026-10-12 11:40:03 +07:00
bflatastic e0435d4eb4 M9: close the remaining open-hosting DoS paths
Second security review:
- a broadcast counts once against the queued-bytes budget; each block of
  addresses gets at most an eighth; when the budget runs out the biggest
  consumer is shed, not the receiver
- forced compactions at most once a minute per file
- plugin downloads check the per-address share before the global budget
  and before reading
- install.sh never replaces a Caddyfile with other sites (one import line,
  once; install-test.sh in packaging lint); the built binary runs only as
  the server's account
- bytes being read count against per-block and global budgets, the
  biggest readers dropped first
- block multipliers x2 (/56) and x4 (/48), connection caps at most a
  sixteenth/eighth of all; global publish and download budgets eight times
  a /48's share
- problems capped at 200; writes refused below max(1 GiB, two files) free;
  the private-server file cap documented
2026-10-12 11:40:03 +07:00
bflatastic 7edc61b8d2 M9: harden open hosting against resource exhaustion
Fixes from the security review of the public server:
- team libraries only in key-published files on open servers; unpacking
  bounded by the file cap (zip bombs refused)
- byte-bounded send queues per connection and in all, a bound on bytes
  waiting for the hub, per-address and global download budgets
- batches checked against the file, per-address and total caps as files
  grow (refused with a Reject the app rolls back); the core's recent-batch
  memory bounded in bytes; nothing confirmed when a log can't be written
- usage reports kept in memory; per-kind message caps checked while read
- layered IPv6 limits (/64, /56, /48), a global publish rate, connections
  reserved for sessions, short idle timeout for one-shot requests
- revoked links and removed files cut library connections
- data directory 0700, server key 0600 from creation
- install.sh: SSH ports from sshd, ufw left off when other services
  listen, unprivileged builds of the latest tag, settings checked before
  use, own Caddy snippet; systemd memory and network caps; backups check
  free space and say they hold the key
- linger reads raw bytes after an oversized frame (tungstenite asserted)
2026-10-12 11:40:03 +07:00
bflatastic b7af7c7ef7 M9: open hosting mode for public figcraft-server
Config-gated (open_publish): anyone publishes without the server key and
gets an owner token (128-bit, shown once, stored hashed) that manages only
that file. Per-address limits (X-Forwarded-For only from trusted_proxy,
bounded tables), global caps, idle expiry, reports, persisted blocks,
files list --sort, check, GET /healthz on the same listener, startup
warnings. The app asks the server first, publishes without a key when it
is open, shows the owner token once, the host/terms line, the expiry date
and a report button; collab.serverInfo, collab.manageFile, collab.report
and pref.publishServer for MCP/CLI. Protocol additions stay within
version 2, gated like signals.
2026-10-12 11:40:03 +07:00
bflatastic 01bc5a9dac M11: plugin installs saved at once; directory and library requests off the UI thread
- **Installed plugins survive a crash.** Importing, installing, updating or removing a plugin, and a
  plugin changing its settings, now sets `Runtime::unsaved`; at the end of each frame the app hands
  the preferences to a new `Services::save_prefs`, and the desktop app writes `state.json` at once
  (temporary file + rename, the same atomic write the quit path uses, now shared). Nothing waits for
  quit any more.
- **The UI never waits on the network.** New `Session::start_background(id, params)`: the command
  runs as usual until it makes a server request; the request goes out on the link's own thread
  (`LiveLink::background`, implemented by the desktop `NetLink` with one short-lived thread per
  request) and the command stops; when the answer arrives (`Event::Net`, taken by `live_poll`, which
  the network's waker already triggers) the command runs again from the start on the file it
  started on, answered from the answers so far, until it needs no new request. All server requests
  go through one funnel (`Session::net`), so `plugin.directory.*`, `library.publishToServer`,
  `library.add` (links), `library.usage` and library fetches work this way without being split.
  A re-run of `library.publishToServer` caches the library it actually sent (a fresh copy differs
  in its time and, on a first publish, its id). One job per command; `cancel_background` drops a
  late answer.
- **UI:** `FigApp::run_background(ctx, id, params, done)` with a boxed completion. Manage plugins >
  Browse shows "Fetching from the server..." with Cancel instead of Browse; the install confirmation
  shows "Installing..." with Cancel and stays open until the download is in (an error leaves it open
  to retry); row buttons wait during an install; closing the window cancels. Assets > Libraries:
  Publish to server shows "Publishing..." + Cancel, Show usage and pasting a team library link show
  "Fetching from the server..." + Cancel. No polling repaints: the window draws again when the
  answer's waker fires; pending state is a scan of a short Vec, no per-frame allocation.
- **Programmatic callers are unchanged:** MCP, CLI, control channel and scripts still run the same
  commands synchronously (and the browser link, which has no background requests, too).
- New string "Installing..." (en/es); docs: plugins.md (immediate saves, Browse/Install never wait),
  server.md (team library requests never wait).
2026-10-12 06:15:00 +07:00
bflatastic def6f84b22 M11: plugin directory on figcraft-server
A self-hosted plugin directory, with no third-party service: `figcraft-server` keeps plugin bundles,
the app browses, installs and updates from it, and `figcraft-cli` publishes to it.

- **Bundles** (`figcraft_plugin::bundle`): a plugin's manifest, module and up to 32 plain-named
  assets in one uncompressed, length-prefixed binary form (nothing can expand beyond the bytes
  sent). Its SHA-256 names it. `Bundle::decode` never allocates beyond its input and refuses
  trailing bytes; `Bundle::check` parses the manifest (refusing a plugin API this build doesn't
  run), requires a version of 1-4 numbers, compiles the module without running it (only the
  sandbox's imports allowed) and checks asset names (no paths, no dot files, no Windows device
  names, unique ignoring case). Manifests gain an optional `author`.
- **Protocol**: a new connection kind next to managing and team-library connections:
  `OpenPlugins {key}` then `Plugins {list | fetch | publish | unpublish}`. Browsing needs no
  credential; publishing and unpublishing need the server key (a wrong key is refused). Older
  servers refuse the connection and the app says the server has no plugin directory. The protocol
  version is unchanged; old clients are unaffected.
- **Server** (`apps/figcraft-server/src/directory.rs`): `<data>/plugins/index.json` plus
  `<data>/plugins/<sha256>.bin`, so no publisher-chosen name reaches the file system. Every publish
  is checked in full and must be a newer version; caps `max_plugins` (versions, 0 turns it off),
  `max_plugin_mb` (<= 32), `max_plugins_mb`. Fetches re-hash the file before sending (a damaged
  bundle is never sent) and share a download budget (~32 MiB/s) so a flood is refused, not queued.
  Damaged index entries are skipped and reported, never deleted; an unreadable index lists nothing
  and refuses writes until the operator fixes it. Operator moderation:
  `figcraft-server plugins [list]` and `plugins remove ID [--version V]` (offline or through the
  running server). Logs name ids and versions, never keys.
- **App**: Plugins > Manage plugins gets Installed / Browse tabs. Browse lists a server's plugins
  (version, author as typed, size, access) with a search; nothing is fetched until Browse is
  pressed. Install / Update open a confirmation listing the permissions asked for, the source and
  the SHA-256; only its button downloads. The download must match the listed SHA-256 (and the one
  shown), then is checked like an imported plugin before anything is written. Installed plugins
  live per user under `<settings>/FigCraft/Plugins/<hash of id>/` and run in the same sandbox;
  Remove deletes the folder.
- **Commands**: `plugin.directory.list {server?, query?}`, `plugin.directory.install {id, version?,
  confirm, server?, sha256?}` (refused without `confirm: true`; never opens a dialog),
  `plugin.directory.publish {path | plugin, server}`, `plugin.directory.unpublish {id, version?,
  server}`. `ui.set {pluginsTab, pluginConfirm}` on the control channel.
- **CLI**: `figcraft-cli plugin publish FOLDER --server ws://host:port/` with the key from
  `FIGCRAFT_SERVER_KEY` (or `?key=`), never printed.
- Docs: plugins.md (directories, publishing, commands, manifest), server.md (operating the
  directory: access, checks, caps, moderation, backups, security), collab-protocol.md, mcp.md,
  cli.md, control-protocol.md, target-app-parity area 18 and Ecosystem, ROADMAP, roadmap.md M11,
  gaps.md gap 6. Plugins & widgets 55%/32% -> 60%/35%, 10-30 h -> 8-26 h.
2026-10-12 05:28:53 +07:00
BFlat 76df2d4136
M9: hosting figcraft-server: wss in the desktop app, server releases, deployment guide (#125)
The desktop app joins wss:// sessions (rustls with webpki roots, certificates always checked, clear errors; FIGCRAFT_CA_FILE for private authorities), with one TLS session shared by the reader and writer threads. figcraft-server stops gracefully on SIGTERM/SIGINT/SIGHUP/Ctrl+C, ships in Linux x86_64/aarch64 release archives, and docs/server.md gains a Deploy on a Linux server guide with a hardened systemd unit, Caddyfile, nginx config and Dockerfile in packaging/server.
2026-10-11 15:52:27 +07:00
BFlat 09600d6da2
M9: team libraries on the server; swap library (#117)
A file's library can be published to figcraft-server and enabled in other files by a share link (view access is enough); placing, styles, variables and library updates with overrides kept work as for local libraries, with a background check while the Assets panel is open. library.swap re-points instances, styles and variables to another library by name (one undo step, unmatched items listed). The server keeps per-file usage reports (library.usage).
2026-10-11 13:05:46 +07:00
BFlat 07fa4d92ed
M9: standalone collaboration server with share links and permissions (#112)
figcraft-server hosts many files over the same hub the app uses for sessions, persists each as a checksummed snapshot plus an append-only log (synced before replies, compacted, damaged files skipped and reported) so restarts lose nothing confirmed, and gives per-file share links with edit or view access (hashed 128-bit tokens, revocable). The app can publish a file to a server and join server-hosted files; view-only tabs refuse edits. Protocol version 2.
2026-10-11 10:19:03 +07:00