contact messages: easypow proof-of-work for sends, pending-upload cleanup
New dependency: easypow (PBKDF2-SHA512 challenges, bundled JS solver). Sends are PoW-gated: registration/resync and every accepted message come with a fresh single-use challenge (~5 s solve), held in per-socket RAM state only — nothing in the database. A message burns one solution; a missing, stale or spent solution is dropped and answered with a fresh 'required' challenge. The bundled browser solver is served at /_contact/easypow.js straight from the installed package, so client and validator versions always match. Uploads are deliberately not PoW-gated (the transfer rate is the real cost). Instead, uploads are tracked as pending per session and a quota caps pending files at 10 — answered 429, not 403, which could trip auth flows. A sent message strikes the files it references off the pending set; when the session's last socket closes, the remaining pending files are deleted from the store.
This commit is contained in:
1 parent
6b34719397
commit
ec35bc71e8
4 files changed
+114
-24
No files matched your search
+35
-16
@@ -49,12 +49,28 @@ no extra connection). Frames **to** the server:
|
||||
`pagerite.js` re-sends this on every socket (re)connect, which is the
|
||||
resync mechanism (the socket idle-closes after 5 min and reconnects on
|
||||
activity).
|
||||
- `{token, msg, fr}` — append a message; `msg` is the Markdown-subset
|
||||
source, `fr` the page path it was sent from (stored for context).
|
||||
- `{token, msg, pow, fr}` — append a message; `msg` is the Markdown-subset
|
||||
source, `pow` the solved proof-of-work (below), `fr` the page path it
|
||||
was sent from (stored for context).
|
||||
|
||||
Frames **from** the server: `{type: "thread", id, created, unread,
|
||||
messages: [{id, t, admin, html, page}]}` — a full thread snapshot
|
||||
(replace/upsert by `id`), or `{type: "contact-error", detail}`.
|
||||
(replace/upsert by `id`); `{type: "pow", challenge, required}` — a fresh
|
||||
single-use PoW challenge (`required` means the last message was rejected
|
||||
for a missing/stale solution and should be re-sent once solved); or
|
||||
`{type: "contact-error", detail}`.
|
||||
|
||||
### Proof of work (anti-spam)
|
||||
|
||||
Sends are gated by easypow (PBKDF2 challenges, ~5 s solving cost). The
|
||||
server hands a fresh single-use challenge with every registration/resync
|
||||
and after every accepted message; the panel solves it in the background
|
||||
with the bundled solver module (served from the installed package at
|
||||
`/_contact/easypow.js`, so client and validator versions always match),
|
||||
typically finishing while the user is still writing. Each message burns
|
||||
one solution — a send with a missing, stale or already-spent solution is
|
||||
dropped and answered with a fresh `required` challenge, which the client
|
||||
solves and retries once. Challenges live in per-socket RAM state only.
|
||||
|
||||
## Admin side
|
||||
|
||||
@@ -110,11 +126,16 @@ filename needed — the URL's last segment names the download) and
|
||||
rendered or sniffed into something executable in the site's origin;
|
||||
subresource fetches (`<img>`, `<video>`, favicons) ignore the
|
||||
disposition, so inline media is unaffected. The chip anchors
|
||||
additionally carry a `download` attribute. Gates today: a
|
||||
session-shaped `x-contact-token` header and a 20 MB size cap. Admin
|
||||
uploads from the same composer go to the admin-gated `/_api/files`
|
||||
instead. Files uploaded but never referenced by a sent message
|
||||
currently linger (no orphan GC).
|
||||
additionally carry a `download` attribute. Uploads are deliberately
|
||||
**not** PoW-gated (the transfer rate is the real cost there); the gates
|
||||
are a session-shaped `x-contact-token` header, a 20 MB size cap, and a
|
||||
pending-upload quota — 10 un-sent files per session, answered 429 (not
|
||||
403, which could trip auth flows). Every upload stays *pending* per
|
||||
session: a sent message strikes the files it references off the pending
|
||||
set, and when the session's last socket closes the remaining pending
|
||||
files are deleted from the store. Admin uploads from the same composer
|
||||
go to the admin-gated `/_api/files` instead. Files left behind by a
|
||||
server restart (the pending set is RAM-only) still linger — no GC.
|
||||
|
||||
## Storage
|
||||
|
||||
@@ -129,11 +150,9 @@ transactions (`contact` / `contact:reply` / `contact:read`).
|
||||
|
||||
## Limits and deliberate gaps (MVP)
|
||||
|
||||
- Message source ≤ 8000 chars; uploads ≤ 20 MB; payloads cap at 100
|
||||
threads × last 200 messages.
|
||||
- **No proof-of-work yet**: the send and upload paths are the two seams
|
||||
where the PoW stamp plugs in (challenge on token registration, verify on
|
||||
`msg`/upload; one stamp may cover several messages).
|
||||
- No thread moderation (delete/block), no orphan-upload GC, panel strings
|
||||
are English-only, and the admin inbox lists threads by token-hash prefix
|
||||
rather than enriched visitor identity.
|
||||
- Message source ≤ 8000 chars; uploads ≤ 20 MB and ≤ 10 pending per
|
||||
session; payloads cap at 100 threads × last 200 messages.
|
||||
- No thread moderation (delete/block), no orphan-upload GC for files
|
||||
orphaned by a server restart, panel strings are English-only, and the
|
||||
admin inbox lists threads by token-hash prefix rather than enriched
|
||||
visitor identity.
|
||||
Reference in new issue
Block a user