- history/mine table column 'Language' -> 'Type' (data-sort key 'type')
- /api/public and /api/mine rows gain a 'type' field: file extension for
attachment pastes, stored language otherwise (default 'text')
- paste page stats label 'Language' -> 'Type'; summary line uses the same
label; get-paste JSON gains 'type'
- store list queries LEFT JOIN attachments to expose the filename
- table.js sorting accepts the 'type' key
The merged #224 fix covered the details grid but left the summary
pill using humanSize(len(row.Content)), which reads 0 for attachment
pastes (text content is empty). Use the attachment size there too.
- image pastes render the image scaled to fit the viewer box (aspect
ratio preserved, max-height 70vh), no text/code box below it
- link pill moved under the image as a small inline chip
- /raw serves image attachment bytes as an image instead of empty text
- view details size reports the actual attachment file size
Missing/expired/burned paste IDs and unknown routes now render the main
UI (topbar + centered result card) with a 'Paste ID not found' message,
returning HTTP 404 status for correctness.
- Move all inline <script> blocks (layout head/theme, topbar dark toggle,
foot, paste, new, history, mine, settings, admin, unlock) to external
files under internal/web/static/. Page data reaches scripts via data-*
attributes (data-paste-id, data-default-dark) instead of template vars.
- Replace inline onclick handlers (copy, delete, stats toggle) with
addEventListener wiring.
- Convert inline style="" attributes to CSS utility classes; swatch
colors are now set via CSSOM/DOM APIs instead of innerHTML strings.
- script-src/style-src are now plain 'self'; img-src data: stays for the
SVG data-URI backgrounds. Verified with headless chromium: zero CSP
violations on all pages in dark and light presets, theme swatches,
admin lock, tables and paste view render correctly.
The deletion token was accepted via the ?token= query parameter on both
DELETE /api/pastes/{id} and /redeem, and round-tripped through the paste
URL after creation. URL-carried bearer secrets leak into reverse-proxy
access logs and browser history.
- API: deletion tokens are now accepted only via the Authorization header
(Bearer/Token/bare); query params are ignored on both endpoints
- Web create flow: token moves to the browser via a short-lived tok_<id>
HttpOnly cookie instead of the redirect URL; the paste view reads it
from the cookie, never from ?token=
- Web view: the delete button calls redeem() which takes the token from
sessionStorage and sends it as an Authorization header
- Tests: correct token in query must be rejected (403/400); header path
still deletes/redeems; extraction unit cases updated
Fixes#143
Two compounding causes:
- CSP img-src fell back to default-src (self), blocking the data: URI
chevron image on all served pages, so no select ever painted it.
- .editor-head select used the background shorthand, wiping the
background-image set by the later select rule.
Chevron stroke is now a per-preset --chevron token (light stroke for
dark presets, dark for light presets) instead of hardcoded #888.
CSP gains img-src 'self' data: so the data-URI icons can load.
- renderPaste now looks up the paste's attachment and passes it as
'Attachment' so the paste.html attachment chip/image preview renders
- remove duplicated .iconbtn.dark-toggle blocks (3x from stacked
branches #127/#128) and duplicate mobile-media variants
Verified: go vet + go test pass; live render on :18098 shows
attachment-chip + img src=/f/id/name for a png upload (served 200
image/png) and chip-with-filename, no preview, for an html upload.
- Collapse the 10 theme cards into 5 pairs: light swatches top row, dark bottom row
- Dark mode toggle in settings and topbar (sun/moon inline SVG)
- Toggling dark mode switches to the other variant of the selected pair
- Persist pair id in localStorage palette-theme, dark flag in palette-dark
- Head script resolves base+dark to variant; URL ?theme= accepts both ids and wins
- Settings gear highlights like nav tabs on /settings
- PALETTE_DEFAULT_DARK env var sets server default dark state (default on)
- Server-side resolution helpers + tests
- Collapse the 10 theme cards into 5 pairs: light swatches top row, dark bottom row
- Dark mode toggle in settings and topbar (sun/moon inline SVG)
- Toggling dark mode switches to the other variant of the selected pair
- Persist pair id in localStorage palette-theme, dark flag in palette-dark
- Head script resolves base+dark to variant; URL ?theme= accepts both ids and wins
- Settings gear highlights like nav tabs on /settings
- PALETTE_DEFAULT_DARK env var sets server default dark state (default on)
- Server-side resolution helpers + tests
SoftDelete now reports whether it performed the delete (conditional
UPDATE ... WHERE deleted_at IS NULL checked via RowsAffected).
RegisterRead returns an admitted flag: legacy burn pastes admit exactly
one reader (the atomic soft-delete winner), and burn-after-N pastes
increment reads_used via a conditional UPDATE guarded on
reads_used < reads_limit, so concurrent readers cannot both consume the
final read. API, HTML, and raw read paths return 404 when the reader
loses the burn claim; content is never served twice.
OpenStore pins the SQLite pool to one connection: concurrent writes on
separate pooled connections surfaced SQLITE_BUSY as spurious 500s
instead of serializing.
Adds concurrency regression tests: 24 parallel readers of a burn paste
(exactly one receives content, none of the others leak it) and 30
parallel readers vs a 3-read budget (exactly 3 admitted, then 404).
- web.SecurityHeaders middleware wired into the chi router
- Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' (page scripts are inline); frame-ancestors 'none'
- Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff
- Applied only to text/html responses; JSON API and /raw pass through unchanged
- Regression test internal/web/securityheaders_test.go