Previously the burn was triggered client-side with a 1.5 s delay,
leaving a window where opening the paste in two tabs (or two quick
requests) would both successfully serve the content before either
burn request reached the server.
Fix: on GET /<paste_id> for a burn paste, the server now does an
atomic conditional UPDATE:
UPDATE pastes SET burn_claimed=1, views=views+1
WHERE id=? AND burn_claimed=0
SQLite serialises writes, so only one concurrent request can ever
see rowcount > 0. The first request claims the slot and gets the
page; every subsequent request immediately gets 410 Gone — the
encrypted blob is never served to them.
The client-side /api/burn call still fires after successful
decryption to physically delete the row, keeping the data lifetime
as short as possible.
When a paste is created with burn_after_read enabled, redirect to the
paste view was consuming the burn before the recipient could open it.
Fix: after creating a burn paste, the creator never navigates to the
view page. Instead, paste_create.js swaps the editor for a centered
'Burn link ready' share panel showing the full URL (with key fragment)
pre-selected in a monospace input and a Copy button. The editor and nav
controls are hidden. A 'Create another' link returns to a fresh editor.
The first person to actually open the URL is always the intended
recipient, who triggers the burn as designed.
- Add 🔥 Burn checkbox on the create page (next to Discuss)
- New DB columns: burn_after_read (INTEGER) and burn_token (TEXT) with
automatic migration for existing databases
- POST /api/burn/<paste_id> endpoint: validates a one-time burn_token
(constant-time comparison), deletes paste + comments atomically
- burn_token embedded as a JSON island in view.html so paste_view.js
can read it without Jinja in external scripts
- After successful decryption, scheduleBurn() fires 1.5 s later so
the user has time to read/copy before the paste is destroyed
- showBurnBanner() inserts a warm amber warning strip (slides in with
animation) informing the viewer the paste will self-destruct
- Burn is intentionally NOT persisted to localStorage — it's a
deliberate per-paste choice, not a sticky preference
- Server responds 200 even if paste is already gone (idempotent)
- config.json now in .gitignore — it contained admin password hash and
secret_key placeholder which should never be in version control
- Add config.example.json as a safe, secrets-free template for new deployments
- Add nginx.conf.example with proper reverse-proxy guards:
- Static files served by Nginx directly (not Flask)
- Explicit deny on .py, .json, .db, .log, .env and other sensitive extensions
- Block dot-files and __pycache__ paths
- No directory listing (autoindex off)
- HTTPS enforcement with HSTS