Skip to content

Release v1.2.0 beta.1 - #25

Merged
Amateur-God merged 9 commits into
mainfrom
release-v1.2.0-beta.1
Aug 12, 2026
Merged

Amateur-God merged 9 commits into
mainfrom
release-v1.2.0-beta.1

Conversation

@Amateur-God

@Amateur-God Amateur-God commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

should fix the following issues

#3 #8 #9 #23

Summary

First beta of v1.2.0, covering the client UI, favourite sync, and the sync server.

Favourites now merge instead of replace. Logging back in used to overwrite anything
saved while offline. Sync merges the server copy with local faves and uploads the
local-only ones on Electron, web, and Android. Deletions are tracked server-side for 180
days so an offline device can't resurrect a fave another device removed, with the newer
timestamp winning so a deliberate re-fave still beats an older deletion. Faving also
favourites on the source site when credentials are configured (Danbooru, Moebooru, e621),
and cards show how many StreamBooru users faved a post. "Save" is now "Fave" everywhere,
since it never wrote files to disk.

Client. Tag autocomplete, a collapsible lightbox tag panel and card tag chips,
per-site enable/disable, a settings panel (theme, grid density, card layout, autoplay,
safe-mode blur, filename template), bulk-download progress with cancel, toasts in place of
every blocking alert(), empty and skeleton states, and touch gestures plus Android
back-button handling. Popular-feed re-sorts reuse cards through keyed reconciliation, and
the scroll handler and natural-aspect grid no longer thrash layout.

Server. Refuses to boot on a weak JWT_SECRET or ENC_SECRET, pins JWT verification
to HS256, verifies Postgres TLS by default, and drops password-via-query-string on login.
Browsers connect to the event stream with a 60-second stream-only ticket rather than
putting the 90-day account token in a URL. Adds a rate-limited public
/api/favourites/counts, persists each site's enabled flag, and bulk upsert writes in one
round trip instead of one statement per fave.

Security. The Electron window blocks in-app navigation to remote origins, Discord
OAuth callbacks require a client-generated nonce on both the desktop loopback listener and
the Android deep link, the image proxy refuses loopback/private/link-local targets, and
downloads reject parent-directory path segments. On Linux, Chromium's caches move out of
~/.config/streambooru into ~/.cache.

Branding. StreamBooru gets its own icon and a cyan/magenta accent pair, replacing the
stock Capacitor placeholder and the desktop build's missing icon.

server/webapp was a hand-maintained copy of renderer that had drifted, so the hosted
web app shipped without fixes the desktop build already had; it's generated now and CI
fails if the two diverge.

Deployment note (breaking)

Two environment changes are required before this deploys cleanly:

  • ENC_SECRET must be set to a real value (16+ chars, not the .env.example placeholder)
    or the server will refuse to start. ALLOW_INSECURE_ENC_SECRET=1 is the local-dev escape
    hatch. Do not change an existing value — stored site credentials become unreadable.
  • JWT_SECRET is held to the same standard. Changing it invalidates every issued token and
    logs all users out.
  • PGSSL_REJECT_UNAUTHORIZED now defaults to true. Set it to false only if your
    managed Postgres presents a self-signed cert.

Migrations 0006–0008 apply on startup.

Related issues

Fixes #3
Fixes #8
Fixes #9
Fixes #23

Checklist

  • I have read CONTRIBUTING.md
  • Every commit is signed off (git commit -s) for DCO
  • CI passes locally (or I explain why not applicable)
  • Documentation updated if user-facing behavior changed
  • Tests added or updated where applicable

Refuse to boot on a weak or placeholder JWT_SECRET unless
ALLOW_INSECURE_JWT_SECRET=1 is set for local development, and pin token
verification to HS256. Postgres TLS now verifies the server certificate
by default, with PGSSL_REJECT_UNAUTHORIZED as the opt-out for
self-signed managed databases. The local login route no longer accepts
a password via query string, and DB error logs no longer include query
parameters.

Add a rate-limited public /api/favourites/counts endpoint, backed by a
new index on favorites(key), so cards can show how many StreamBooru
users faved a post, and persist each site's enabled flag through
/api/sites. The British and US spellings of the favourites routes now
share a single handler, and bulk upsert writes every row in one round
trip instead of a statement per fave.

Signed-off-by: Stephen Howe <[email protected]>
Logging back in no longer overwrites favourites saved while offline:
sync merges the server copy with local faves and uploads the local-only
ones on Electron, web, and Android, and live removals from other
devices still apply through targeted stream events. Faving a post also
favourites it on the source site when credentials are configured
(Danbooru, Moebooru, e621), and cards show how many StreamBooru users
faved each post. Save is now Fave everywhere, since it never wrote
files to disk.

Add tag autocomplete, a collapsible lightbox tag panel and card tag
chips, per-site enable/disable with a source-chip row, a settings panel
(theme, grid density, card layout, autoplay, safe-mode blur, filename
template), bulk-download progress with cancel, toast notifications in
place of every blocking alert dialog, empty and skeleton states, and
touch gestures plus Android back-button handling in the lightbox.
Popular-feed re-sorts reuse cards through keyed reconciliation, folder
and save dialogs are async, and the scroll handler and natural-aspect
grid no longer thrash layout.

Security: the Electron window blocks in-app navigation to remote
origins, Discord OAuth callbacks require a client-generated nonce on
both the desktop loopback listener and the Android deep link, the image
proxy refuses loopback, private, and link-local targets, and downloads
reject parent-directory path segments. On Linux, Chromium's caches move
out of ~/.config/streambooru into ~/.cache.

Fixes the desktop Gelbooru XML fallback, Unlink Discord, the Settings
version string, lightbox keyboard zoom, lightbox navigation across
Popular-feed re-sorts, user_favorited normalization, the unreachable
name-format popover, Derpibooru's missing auth fields, and the mobile
menu opening Account twice.

Signed-off-by: Stephen Howe <[email protected]>
server/webapp was a hand-maintained copy of renderer that had drifted,
so the hosted web app shipped without fixes the desktop build already
had. scripts/build-webapp.mjs mirrors renderer into it and prunes
removed files, npm run webapp:check fails CI when the two diverge, and
the Nixpacks deploy runs the same script instead of an inline copy.

Update dependencies to current releases (Electron 43.4, Capacitor CLI
8.5, Express 4.22, pg 8.23, jsonwebtoken 9.0.3, dotenv 16.6) and pin
uuid and body-parser through overrides; npm audit and bun audit are
both clean.

Signed-off-by: Stephen Howe <[email protected]>
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@Amateur-God Amateur-God linked an issue Aug 12, 2026 that may be closed by this pull request
The periwinkle blue accent is replaced by cyan (#22e3ff) for interactive
state and magenta (#ff2fb0) for favourites and emphasis, with a gradient
between them for the wordmark, the active tab, Download All, and the
download progress bar.

Most of the old accent colours were hardcoded literals scattered through
the stylesheet rather than tokens, so they now go through --accent,
--accent-2, --accent-grad, --ring and friends, which is what makes the
light theme possible: bright cyan is unreadable on white, so that theme
resolves the same tokens to a deeper teal and magenta. Modal surfaces
lose their navy tint so they read as neutral next to the new accents,
and mobile.css drops its duplicate --acc token.

Signed-off-by: Stephen Howe <[email protected]>
The Android launcher icon and splash screen were still the stock
Capacitor placeholder, and the desktop build had no icon at all because
electron-builder's buildResources directory did not exist.

The mark is four gallery tiles arranged around a play cut-out: the grid
says booru, the play says stream. Vector masters live in branding/ and
everything else is rendered from them, so the set stays consistent and
can be regenerated at any size. That covers build/icon.png for the
desktop packages, all five Android densities for the legacy, round, and
adaptive-foreground variants, the eleven splash sizes, and the web
favicon and apple-touch-icon.

The adaptive foreground is scaled to 46% of its canvas so the tile
corners survive a launcher masking it to a circle, and the adaptive
background colour changes from white to the plate colour.

Signed-off-by: Stephen Howe <[email protected]>
The local server tests need the sync server's dependencies, which no CI
job installed, and the DCO check walked every commit in the PR range
including the merge commit the forge generates itself, which no
contributor can sign off.

Signed-off-by: Stephen Howe <[email protected]>
@atlas-commons-bot

atlas-commons-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

@Amateur-God

Atlas Commons PR review

Requirement Status
DCO sign-off pass
Required checks pass
Summary filled pass
Checklist pass
Linked issue pass

Overall: pass

  • All commits include DCO sign-off.
  • Required checks passed.
  • Summary provided.
  • All checklist items ticked or none present.
  • Issue link present.

View PR #25 · Atlas-Commons/StreamBooru

@atlas-commons-bot

atlas-commons-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

@Amateur-God

Atlas Commons code review (LLM)

Summary

Large, well-scoped v1.2.0 beta covering favourite merge sync (with server-side deletion tombstones), meaningful server hardening (secret guards, HS256 pinning, stream-only SSE tickets, Postgres TLS verification), client UX (settings, toasts, tag autocomplete, bulk-download cancel), and operational fixes (generated server/webapp, CI drift check). The tombstone + SQL-enforced bulk_upsert design is sound, and the follow-up commit addressing merge resurrection and clampPost field loss shows responsive iteration. A few correctness gaps remain around favourite key normalization across platforms and some untested sync paths.

Potential issues

  1. Web/Android sync does not re-key remote favourites. Electron's pullFavoritesMerge() in electron/main.js normalizes legacy server keys via favKey(), but syncReplaceFavorites() in renderer/js/platform.js uses the raw it.key from the server. Users on web/Android who had entries stored under pre-normalization keys (trailing slash, casing) can end up with duplicate local entries or tombstone lookups that miss.
// electron/main.js — re-keys on pull
const key = it.post?.site?.baseUrl ? favKey(it.post) : String(it.key);

// renderer/js/platform.js — uses server key as-is
const k = String(it.key || '');
  1. Tombstone keys are not normalized during merge. Both clients build deletedAt from server deletions[].key verbatim. If the server tombstone used a legacy key format but local storage uses normalizeBaseUrl-based keys, (deletedAt.get(k) ?? -1) < addedAt can evaluate incorrectly and a lagging device may re-upload a favourite another device removed.

  2. PUT /api/favourites/:key bypasses tombstone timestamp logic. bulk_upsert enforces "newer timestamp wins" in SQL, but the single-item PUT handler unconditionally deletes the tombstone before upserting. Any caller sending PUT with a stale added_at (not just interactive re-faves with Date.now()) can resurrect a deletion that bulk sync would block. Low risk today because sync push uses bulk, but it is an API consistency hole in server/src/index.js.

  3. 180-day tombstone expiry is a documented but real data edge case. Tombstones are trimmed on GET /api/favourites when older than 180 days. A device offline longer than that can push stale local-only faves back and the server has no record of the prior deletion. Acceptable tradeoff if documented for users; worth calling out in release notes (partially done in code comments, less so for end users).

  4. Server media proxy lacks IP-level SSRF hardening. Electron's isBlockedProxyTarget() in electron/main.js rejects loopback/private ranges; the server's isProxyAllowed() in src/shared/refererFor.js only checks hostname against a booru allowlist. A hostname on the allowlist resolving to a private address (DNS rebinding) is not blocked server-side. Redirect validation in server/src/proxyFetch.js helps, but the initial fetch target is not IP-checked.

  5. SSE credential fallback still exposes long-lived tokens in URLs. renderer/js/platform.js falls back to access_token= when /api/stream/ticket is unavailable (pre-1.2 servers). That puts a 90-day JWT in proxy/CDN logs — the exact problem the ticket endpoint solves. Fine for backward compatibility, but clients talking to 1.2+ servers should treat fallback as exceptional.

  6. In-memory rate limits don't scale horizontally. server/src/requestGuards.js stores counters in a process-local Map. Multiple server instances each maintain separate limits; a client can multiply effective quota. Fine for single-node deploys, problematic if the sync server is ever load-balanced.

  7. No automated tests for tombstone merge sync. scripts/test-server-local.js covers stream tickets and clampPost, but the core favourite merge/deletion/tombstone SQL paths in server/src/index.js have no integration tests. Regression risk is high given the subtle timestamp semantics.

Suggestions

  • Extract a shared favKey(post) / normalizeFavKey(key, post?) helper used by Electron, platform.js, SSE removal handlers, and optionally server-side sanitization; add a one-time migration or lazy re-key on read for existing favorites and favorite_deletions rows.
  • Align PUT /api/favourites/:key with bulk semantics: only clear a tombstone when added_at > deleted_at, unless the request is explicitly marked as a user-initiated re-fave.
  • Add IP-range checks to server-side proxy validation (mirror Electron's blocklist after DNS resolution, or resolve and check before fetch).
  • Add integration tests: delete on device A → bulk push from device B with older added_at is rejected; newer re-fave succeeds; stream ticket cannot call authenticated endpoints; legacy key normalization does not duplicate.
  • Consider logging (without credential values) when SSE falls back to access_token so operators can detect mixed-version deployments.
  • For /api/favourites/counts, document that aggregate counts are public-by-design; optional hardening would require auth or tighter per-IP budgets if scraping becomes a concern.

Overall assessment

This is a strong release candidate: the favourite merge redesign, security tightening, and webapp generation fix address real production pain, and the PR description's deployment notes for ENC_SECRET, JWT_SECRET, and PGSSL_REJECT_UNAUTHORIZED are clear and necessary. Before calling the sync story fully done, I'd want favourite key normalization unified across Electron and web/Android and at least one integration test exercising the tombstone path — those are the highest-impact remaining correctness gaps. The security improvements are directionally excellent; server-side proxy IP validation and horizontal rate-limiting are the main follow-ups if the sync server sees broader deployment.


Advisory review only — not a merge approval.

View PR #25 · Atlas-Commons/StreamBooru

The unpacked build only gets a root-owned setuid chrome-sandbox from the
.deb/rpm installers, so Chromium falls back to user namespaces. The GitHub
runner image restricts those, so it tried the misconfigured SUID helper and
aborted with SIGTRAP. The previous guard only dropped the sandbox when running
as root, which covered the Forgejo container but never the non-root runner.
Check the helper itself instead.

Signed-off-by: Stephen Howe <[email protected]>
@Amateur-God Amateur-God removed a link to an issue Aug 12, 2026
Merge sync treated every local-only favourite as something to re-upload, so a
device that was offline when another one unfaved a post pushed it straight back.
Removals are recorded in favorite_deletions for 180 days and the newer timestamp
wins, so a deliberate re-fave still beats an older deletion. Bulk upload enforces
this in SQL rather than trusting the client to have checked, and the favourites
list hands clients the tombstones so they can drop what they still hold.

clampPost's allowlist predated the artist, copyright and character fields and
the video fields, so any favourite that made a server round trip came back
without its grouped tags and without grid autoplay. Electron also keyed
favourites off the raw site URL while the renderer normalized it, which split
one post across two keys whenever an older config had a trailing slash or
different host casing; keys now go through normalizeBaseUrl and existing
favourites are re-keyed and de-duplicated on load.

ENC_SECRET is held to the same standard as JWT_SECRET and checked at startup
rather than on the first credential write — crypto.getKey() already refused
anything under 16 characters, but a long placeholder is just as public.
Browsers now trade the account token for a 60-second stream-only ticket instead
of putting a 90-day credential in a URL that proxy logs keep; tickets are
rejected everywhere else, and the stream endpoint pins HS256 like the rest of
the API.

Raised in review on #25.

Signed-off-by: Stephen Howe <[email protected]>
…w palette

Manage Sites rendered API keys and Moebooru password hashes as plain text
inputs, so they sat on screen in full view of anyone glancing over, screen
sharing, or screenshotting the window. Secret fields are masked with a per-field
Show/Hide toggle; login, user ID, and the Derpibooru filter ID are not secrets
and are left alone.

The landing, health, and OAuth callback pages still wore the old periwinkle blue
on a navy background, which no longer matched anything. They pick up the app's
tokens, the brand gradient on the wordmark and primary button, and the icon and
favicon.

Signed-off-by: Stephen Howe <[email protected]>
@Amateur-God
Amateur-God merged commit 3ae2319 into main Aug 12, 2026
4 checks passed
@Amateur-God
Amateur-God deleted the release-v1.2.0-beta.1 branch August 12, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Can't play Videos login with discord issue text box issue android Caching issues

1 participant