Profpatsch/users/Profpatsch/observations-inbox

observations-inbox(1)

observations-inbox - accept audio replies to observations episodes

observations-inbox [-addr host:port] [-db path] [-base-url url] -passphrase-file path [-mail-file path] [-gemini-key-file path]

observations-inbox is the endpoint behind the drop zone on the observations(7) website. A listener drops a recording, it is stored in one SQLite file, and a notification carrying an unguessable link arrives by mail.

There is no account, no listing route, and no way to get from one submission to another: the mail is the only record that a submission exists. For why the format has an inbox at all, and what was given up by adding one, see observations(7).

-addr host:port

Address to listen on. Default 127.0.0.1:8778. Intended to sit behind a reverse proxy; see DEPLOYMENT.

-db path

The SQLite database. Default /var/lib/observations/inbox.db. Created, with its parent directory, if missing.

-base-url url

Public base URL, used to build the links in notifications. Default https://observations.profpatsch.de.

-passphrase-file path

File holding the submission passphrase. Required: without it anyone who finds the endpoint could submit.

Matching is deliberately generous, because the passphrase is heard in an episode and typed in from memory. Case and every kind of whitespace are ignored, including none at all, so 'OpenSesame' and 'open sesame' are the same phrase; punctuation and accents are dropped; and a small number of typos is tolerated, scaled to length - none below six characters, one up to twelve, two beyond that, with a transposition of adjacent letters counting as one.

The phrase is held in memory rather than as a hash, since an edit distance cannot be computed against a digest. This gives up nothing: it is spoken aloud in every episode, so it was never secret. See CAVEATS.

-mail-file path

File holding the whole mail configuration: server, account, password and
recipient.
When empty, submissions are still accepted and stored, and their links are
written to the log instead.

One
*key = value*
per line;
'`#`'
comments and blank lines are ignored.
Recognised keys are
*host*,
*port*
(default 465, implicit TLS),
*user*,
*pass*,
*from*
(defaults to
*user*)
and
*to*.
An unknown key is an error rather than a warning, so a typo cannot silently
disable notifications.

The server and the account live in this file rather than in flags on purpose.
Anything in the command line is visible in the process table to every user on
the machine, and when the unit is generated by Nix it is also readable in the
store.
The password is the sensitive part, but the account it belongs to is worth no
more publicity than it needs.

-gemini-key-file path

File holding a Gemini API key.
When empty, transcription is unavailable and submissions are simply stored.
A key is only ever used for a submission whose sender ticked the consent box.

POST /submit

Accept one submission.
Multipart, with a
`file`
part and the fields
`passphrase`,
`consent`
and
`note`.
Answers JSON:
`{"ok":true}`
or
`{"ok":false,"error":"<?>"}`,
the latter written to be shown to whoever sent it.

GET /submit

Report whether the inbox is open, as
`{"open":true,"maxBytes":5242880}`.
The drop zone asks this on load so that a full inbox is announced before
someone records a reply rather than after.
It deliberately reveals nothing about how much is stored or how many
submissions exist.

GET /inbox/{token}

The review page for one submission: player, metadata, transcript, and a delete
button.

GET /inbox/{token}/audio

The recording.
Supports range requests, which is what lets the player seek.

POST /inbox/{token}/delete

Delete the recording and free its space.

An unknown token, a malformed one and one whose submission was deleted all answer 404 identically. Distinguishing them would confirm that a token was once valid.

file size

5 MB, about five minutes from a phone.
Enforced with a
`MaxBytesReader`,
so a request that lies about its length cannot make the server allocate more.

total

50 MB.
When reached, submissions are refused with 503 until something is deleted, and
one mail is sent to say so.

rate

Twelve submissions per address per hour, counted only for attempts that got as
far as being valid - junk is refused on its own merits and does not use up
anyone's budget.

Deliberately loose: it is a brake on a script, not a quota on a person.
The passphrase and the 50 MB ceiling are the real limits, and a rate low
enough to catch an ordinary retake produces a flat refusal that is
indistinguishable from the site being broken.

kind

Audio only, decided by inspecting the leading bytes.
The filename is a claim by whoever named the file and is never what decides.

/var/lib/observations/inbox.db

Every submission, audio included.
Nothing is written to the filesystem under a name derived from a submission,
so the database is the whole of the inbox: one file to copy, inspect or
delete.

/var/lib/secrets/observations-inbox/passphrase

/var/lib/secrets/observations-inbox/mail

/var/lib/secrets/observations-inbox/gemini-key

Secrets, passed to the service as systemd credentials rather than read
directly, so the unit's own user never has filesystem access to them.

observations-inbox expects a reverse proxy in front of it. Two pieces of that configuration are load-bearing rather than cosmetic:

The NixOS module in users/Profpatsch/website/module.nix does both.

A mail configuration file:

host = smtp.example.org port = 465 user = someone@example.org pass = the-password to = someone@example.org

Run against a scratch database, without mail or transcription:

printf 'some spoken phrase' > /tmp/pass observations-inbox -db ./tmp/inbox.db -passphrase-file /tmp/pass
-base-url http://localhost:8778

Look at what is waiting, without the web interface:

sqlite3 /var/lib/observations/inbox.db
"SELECT id, datetime(received_at,'unixepoch'), byte_size, token FROM submission WHERE deleted_at IS NULL"

observations(1), observations(7)

Profpatsch

Submissions are unreviewed audio from strangers. Nothing has looked at them before you do.

The database is not backed up by anything, and it lives on a filesystem with little headroom that the rest of the machine also depends on. That is what the 50 MB ceiling is for; it is not a storage system, and a submission worth keeping should be moved out of it.

A capability URL is a credential, and it cannot be revoked: anyone who obtains the link can listen until the recording is deleted. Deleting is therefore the only way to withdraw access, and it is also the only way to free space.

Losing the notification mail loses the link, and with it any way to reach that submission through the web interface. This is the intended trade for having no listing, but it does mean the mail is load-bearing. The database can still be queried directly, as above.

The passphrase is a filter against scanners, not a secret. Every listener has it, it travels in a form post, it is held in memory by the running service, and it is only as private as the least discreet person who heard the episode. Changing it costs one file and a restart, which is the intended way to deal with that.