2026-07-09 17:58:17 +02:00
|
|
|
# wisp
|
|
|
|
|
|
|
|
|
|
Short-lived, end-to-end encrypted file sharing. Send a file to someone who
|
|
|
|
|
isn't online right now — they get a link, they get the file once, and then
|
|
|
|
|
it's gone. The server never has the ability to read what it's holding.
|
|
|
|
|
|
|
|
|
|
## Why
|
|
|
|
|
|
|
|
|
|
`flit` already does encrypted file transfer between devices, but it's
|
|
|
|
|
synchronous — both sides have to be online at the same time for the WebRTC
|
|
|
|
|
handshake. That's fine for "send this to my other laptop right now," and
|
|
|
|
|
wrong for "here's a file for you, grab it whenever."
|
|
|
|
|
|
|
|
|
|
wisp is the asynchronous counterpart: upload once, the recipient retrieves
|
|
|
|
|
later, on their own schedule. That async gap is the one thing flit's
|
|
|
|
|
architecture (zero server storage, direct P2P) structurally can't do — it
|
|
|
|
|
requires the server to actually hold ciphertext for a while, which is new
|
|
|
|
|
territory for this project family, not a variant of something already built.
|
|
|
|
|
|
|
|
|
|
## What it isn't
|
|
|
|
|
|
|
|
|
|
- **Not zipline.** Zipline is persistent link/screenshot hosting — you keep
|
|
|
|
|
the URL, the file stays. wisp is the opposite: single retrieval, then the
|
|
|
|
|
file is unrecoverable, by design.
|
|
|
|
|
- **Not flit.** Flit needs both peers online simultaneously and never stores
|
|
|
|
|
anything server-side. wisp is deliberately async and requires temporary
|
|
|
|
|
server-side ciphertext storage.
|
|
|
|
|
|
|
|
|
|
## Core model
|
|
|
|
|
|
|
|
|
|
1. Sender encrypts the file client-side (symmetric, libsodium secretbox —
|
|
|
|
|
same primitive flit's browser client already uses).
|
|
|
|
|
2. Ciphertext uploads to the server. The encryption key never leaves the
|
|
|
|
|
sender's browser except inside the URL fragment (`#key=...`), which
|
|
|
|
|
browsers never transmit in HTTP requests. The server only ever sees
|
|
|
|
|
opaque bytes.
|
|
|
|
|
3. Sender shares the link through any channel they like.
|
|
|
|
|
4. Recipient opens the link, downloads the ciphertext, decrypts client-side.
|
|
|
|
|
5. On successful decrypt, the client sends a short confirmation to the
|
|
|
|
|
server. Only then does the server delete the blob.
|
|
|
|
|
6. A TTL (default: a few days) is the backstop for links nobody ever opens,
|
|
|
|
|
so nothing sits on disk forever.
|
|
|
|
|
|
|
|
|
|
Full design, including why "delete on first byte served" is the wrong
|
|
|
|
|
default, is in [IMPLEMENTATION.md](./IMPLEMENTATION.md).
|
|
|
|
|
|
2026-07-12 19:29:46 +02:00
|
|
|
## Upload access
|
|
|
|
|
|
|
|
|
|
Uploading is gated by `UPLOAD_PASSWORD` (required — the server refuses to
|
|
|
|
|
start without it), shared by everyone who's allowed to upload.
|
|
|
|
|
|
|
|
|
|
For granting access to one person for one reason without handing them the
|
|
|
|
|
master password, admins can mint **scoped upload invites**: a link
|
|
|
|
|
(`?invite=<token>`) that skips the password screen, valid for a limited
|
|
|
|
|
number of uses and a limited time, independently revocable. Invite
|
|
|
|
|
management lives at `/admin`, gated by a separate `ADMIN_PASSWORD` — unset,
|
|
|
|
|
the admin surface is disabled entirely (503s) and everything else works as
|
|
|
|
|
usual.
|
|
|
|
|
|
2026-07-09 17:58:17 +02:00
|
|
|
## Status
|
|
|
|
|
|
2026-07-12 19:29:46 +02:00
|
|
|
Implemented: upload/download, encryption, TTL cleanup, password gate, and
|
|
|
|
|
scoped upload invites. See [IMPLEMENTATION.md](./IMPLEMENTATION.md) for the
|
|
|
|
|
deletion-race design and [PROPOSAL-invite-links.md](./PROPOSAL-invite-links.md)
|
|
|
|
|
for the invite feature's design rationale.
|