SendOff — a file lands in a real directory in the recipient's own tree
Implements docs/sendoff.md's v2: a folder convention, one endpoint and a push. No staging area, no transfer table, no ack, nothing to expire. Every account already has a home, a file browser over it and a Files app on the phone, so a delivered file is an ordinary file from the moment it arrives. `Documents/SendOff/<machine>/` for something you sent yourself, and `Documents/SendOff/<sender>/` for another account's files. The home is the one that account actually browses — the owner's real login home, a member's managed home — because the two are different directories and writing to the wrong one makes a delivery invisible rather than wrong. CONSENT IS THE RECIPIENT'S, held as a row per pair. Not a role grant: a grant answers "may this role send files", and the person who should decide whether somebody may write into your home is you. Absence denies, and a sender who was not allowed does not see the recipient in their picker at all — the endpoint answers 404 for "no such account" and for "did not allow you" alike, so it cannot be used to enumerate accounts. The owner bypasses this as a SENDER only; receiving still needs their say-so. The sender's folder name comes from the token and never from the payload, so it cannot lie about who put a file there. Names are sanitised to one path segment and filenames to their basename — verified: `../../../../etc` becomes a literal folder, and a filename of `../../../../tmp/pwned.txt` writes `pwned.txt` inside the target. Collisions suffix ` (1)`; nothing is ever overwritten, because the sender cannot see the directory and would have no way to know. The push carries the folder name and a file count, and the catalogue renders "3 file(s) from andre" on the device. That relaxes "content does not travel" by a folder name and an integer — the owner's call, on the grounds that the file never leaves the tailnet and a notification nobody can act on is not worth sending. A filename would be content; whose folder it is, is addressing. `kind: 'app'` rather than `confined` even though it writes into a home: the path is one fixed shape chosen by the server from the caller's token, so there is nothing to traverse and nothing to read back. Requiring a Linux user would mean an account without one could not be sent a file, which is most of the point. Backend and verified end to end. The web send form is next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,22 @@ const CORE_ENTRIES: Record<string, NotificationEntry> = {
|
||||
route: 'officer://jobs/{id}',
|
||||
thread: 'officer/core:job',
|
||||
},
|
||||
/*
|
||||
SendOff. `id` is the FOLDER — a username, or one of your own machine names — and `n` is how many
|
||||
files landed, so the device renders "3 file(s) from andre" and the route opens that directory.
|
||||
|
||||
A folder name and a count travel, and nothing else: no filename, no path, no size. That is a
|
||||
deliberate relaxation of "content does not travel" rather than an oversight — the owner's call, on
|
||||
the grounds that the file itself never leaves the tailnet, and a notification nobody can act on is
|
||||
not worth sending. A filename would be content; whose folder it is, is addressing.
|
||||
*/
|
||||
'officer/core:sendoff.landed': {
|
||||
title: 'SendOff',
|
||||
body: '{n} file(s) from {id}',
|
||||
target: 'mobile',
|
||||
route: 'officer://files?path=Documents/SendOff/{id}',
|
||||
thread: 'sendoff:{id}',
|
||||
},
|
||||
'officer/core:job-failed': {
|
||||
title: 'Job failed',
|
||||
target: 'mobile',
|
||||
|
||||
Reference in New Issue
Block a user