FIGCRAFT_DEFAULT_SERVER, set when compiling, names another server or, set to nothing, ships a
build with no default: online sharing then stays off until the user enters a server in
Preferences. Normal builds and the release workflow are unchanged.
The recovery copy of one of your online files is no longer discarded when
the file reopens from its server. It is kept until the outcome is known:
- the server has everything in it: the copy goes;
- the copy has changes that never reached the server and nothing else
changed there: they are applied on top as one edit and sent to everyone;
- anything else (others changed the file, the server forgot the client):
the copy opens beside the file as "<name> (recovered copy)", with a toast;
- the server can't be reached: the copy opens as an unsaved document.
The note beside a copy is written after it and records who the app was in
the file and the server's last change it had. The app joins again as that
client (the first hello resumes; a new sync client numbers its batches
after the ones the server processed), so the layers it made are its own to
send. The old copy is removed only once this run has copied what it became.
collab.recoverFile; a tooltip on the tab mark; docs.
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.
The README gains a Privacy and online sharing section (what Share > Online uploads and where, how long it stays, encrypted in transit but not end-to-end, codes and links are the access, local network sharing is direct and unencrypted, no guarantees), its 'Offline and private' blurb no longer says 'no cloud', and docs/server.md's Security section says the same for operators.
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.
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.
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
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
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)
packaging/server/install.sh sets up Debian/Ubuntu behind Caddy: binary from
its release archive, the source tree it sits in, the latest release or a
clone (temporary swap under 2 GB of memory), user, dirs and config only when
missing, systemd unit, Caddy site, nightly backup timer, ufw for 22/80/443;
safe to re-run for upgrades. docs/server.md gains Running a public server
(limits, expiry, moderation, operator responsibility, no end-to-end
encryption, threat model) and an install.sh walk-through; protocol, MCP,
CLI and parity docs updated.
- **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).
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.
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.
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).
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.