Files
pastilhasandClaude Fable 5 8b4a5b3b0b docs: SendOff — files land in the user's tree, a push says where
Spec from the mobile side, v2. One endpoint (POST /api/sendoff/send — the one
sanctioned door into another user's filesystem, sender identity from the token),
a push category whose route deep-links the Files apps, and a minimal devices
registry for the send-to pickers. The mobile half is already shipped against
this and degrades silently until it exists.

Lives here rather than in COMMS because it is a spec to implement, not a
message: the repo is where it can be corrected as the implementation teaches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 02:02:38 +01:00

94 lines
4.0 KiB
Markdown

# SendOff — files land in the user's own tree, and a push says where
**From:** mobile · **Date:** 2026-08-23 · **v2, replacing this morning's spec entirely.** The v1
design (staging area, transfer/target/file tables, ack lifecycle, TTL cleanup) is withdrawn — Andre's
observation kills it: every user already has a filesystem on the server, a file browser on the web,
and a Files app on the phone. SendOff is not a new storage system; it is a folder convention, one
endpoint, and a push.
## The convention
```
~/Downloads/sendoff/from-<username>/ ← a file another USER sent you
~/Downloads/sendoff/<machine>/ ← a file you sent yourself, aimed at a named device
```
That is the whole data model. Nothing to ack, nothing to expire, nothing to clean up — the files are
ordinary files in the recipient's tree, visible in every file UI that already exists, managed like
anything else they own.
## The endpoint
```
POST /api/sendoff/send
multipart: any number of `file` parts + a `meta` part:
{ "toUser": "<username>" } — cross-user: writes into THEIR tree, from-<sender>/
{ "machine": "<device name>" } — self: writes into own tree, <machine>/
→ { path: "Downloads/sendoff/…", files: [{ name, size }] }
```
Server-side because of the cross-user case: `/api/file-browser/upload` correctly writes only the
caller's own tree, and that must not change. This endpoint is the one sanctioned door into another
user's filesystem, and it writes ONLY under `Downloads/sendoff/from-<sender>/` — the sender's
identity comes from their token, never from the payload, so the folder name cannot lie about who
sent it. Filenames sanitised, collisions suffixed `(1)` — never overwritten; a second send of the
same name is a second file.
Whether user A may send to user B at all is a platform policy decision (everyone on the server? a
grant?) — flagging it rather than deciding it here.
## The push
On write, push to the recipient's devices through the existing pipeline:
```
{ "officer": { "c": "officer/core:sendoff.landed", "path": "Downloads/sendoff/from-andre" } }
alert: "photo.jpg from andre" / "3 files from omega"
```
The push catalogue's route for the category:
```
officer://files?path={path}
```
Mobile opens routes from the catalogue verbatim (`openNotificationRoute`), and Officer now parses
`officer://files?path=…` into the Files app at that folder. The web can deep-link its file browser
the same way.
For a `machine`-targeted send, push only to that machine's device if it can be identified; to all
the user's devices otherwise — the path in the folder name already says who it was for.
## Devices
The `machine` name needs a picker on the sending side, so a minimal registry survives from v1:
```
devices: id (client uuid), user_id, name, platform, last_seen_at
POST /api/devices/register { id, name, platform } (upsert; called on every app start)
GET /api/devices → { devices } (the "send to my…" picker)
PATCH /api/devices/:id { name } · DELETE /api/devices/:id
```
No link to transfers — nothing references a device row except the picker and, best-effort, push
targeting.
## Web UI
One send form: files + target (one of my machines / another user), POST, done. Received files need
no UI at all — they are in the file browser already.
## What mobile already does (shipped against this)
- Registers the device on start (silent failure until the endpoint exists).
- Parses `officer://files?path=…` and opens the Files app at that path — this is the piece that
makes the push tap land somewhere.
- SendOff screen: the device's name (the folder senders will aim at), and an explanation of where
arrivals land. Sending FROM the phone comes later, likely through the existing share sheet.
## Out of scope
Receiving in the browser (already exists — it is the file browser); cross-user permission model
(platform decision); phone-side arbitrary-file sending (needs a document picker; the share sheet is
the natural door and is a later step).