Commit graph

3 commits

Author SHA1 Message Date
ScreenTinker 9823aaf595 White-label: stop naming the upstream product to a reseller's customers (#292)
A partner reselling this platform reported that white-labelling changed the sidebar
title and the browser tab, and nothing else. Three fixes, in the order they matter to
them.

THE APK FILENAME, which they called the highest priority and which is a commercial
leak rather than a cosmetic one: every download landed on their customer's disk as
"ScreenTinker.apk", naming the upstream product — and where to buy it directly — to
the people they were selling to. /download/apk now resolves branding by DOMAIN, since
that route is unauthenticated and has no workspace to read, which is also exactly how
a reseller deploys: their own hostname, their own brand.

The name is sanitised through a whitelist, in lib/brand-filename.js so it can be
tested. That is security code, not cosmetics: brand_name is arbitrary operator text
landing in a Content-Disposition header, where a quote ends the filename parameter
early and a CR/LF ends the header line entirely. The tests are mostly hostile input.

ADMIN-CREATED USERS ARE VERIFIED. POST /api/admin/users left email_verified at the
schema default of 0, so every admin-provisioned user met a "Please confirm your email
address" banner they could not dismiss — and on an instance with no SMTP, could never
clear. Operators were fixing it by editing the database by hand. An address typed in
by an administrator is as verified as this system can make it. Note the test fixture
had drifted from the real schema and lacked the column entirely; adding it there is
what let the fix be tested at all.

THE HARDCODED STRINGS. Nine user-facing strings named the product — setup steps, the
empty-dashboard hint, onboarding, sign-in errors. They are translated strings, so the
substitution belongs in the translation layer: they now say {brandName}, and i18n.js
fills it in inside format(), so every t() call gets it without threading a variable
through several hundred call sites. Read at CALL time, not captured, so a workspace
switch shows the new brand rather than the one cached at module load. 43 strings across
7 locales; the default is the product's own name, so an un-branded install is unchanged.

Deliberately NOT changed, because substituting a brand there would be wrong rather than
incomplete:
  - the White Label brand_name input's placeholder, which shows the default when empty;
  - the install-statistics explanation, which describes what the upstream project can
    and cannot see, and is not about the reseller's brand;
  - the widget security warning, which describes the privileges of this software; that
    is copy worth changing deliberately rather than by regex.

Full suite 1779 pass / 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
2026-08-18 22:55:56 -05:00
ScreenTinker 4d81bb112f fix(branding): inject instance branding into the app shell, no default flash (#76)
A never-visited org had no cached white-label, so brand-prime fell through to the
ScreenTinker default baked into the static index.html and flashed it before
branding.js fetched the org brand. Now the /app route injects the resolved
instance / custom-domain branding into the shell as a <meta name="ssr-brand">
(CSP blocks inline <script>, so a meta carries it), and brand-prime applies that
as the fallback when the per-workspace brand is not cached yet - so the page
paints the configured brand on first load instead of ScreenTinker.

- server.js: /app resolves branding (publicBranding strips internal columns) and
  injects the HTML-escaped JSON as a meta tag; falls back to plain sendFile on
  any error so branding can never break the app shell.
- brand-prime.js: read meta[name=ssr-brand] when there is no rd_branding_<ws>.

Verified: the meta carries the resolved brand (default ScreenTinker and a
platform-default white-label), internal columns do not leak, 66 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:30:23 -05:00
ScreenTinker 2de99a12e9 fix(branding): no ScreenTinker default flash on load/switch (#38)
The logo/title/theme/favicon are static 'ScreenTinker' in index.html, and
applyBranding() only overrode them AFTER an async /api/white-label fetch - that
network delay was the flash, on every load and on switch (which reloads).

Now applyBranding caches the resolved white-label per workspace (keyed by the
JWT's current_workspace_id), and a tiny same-origin brand-prime.js loads
render-blocking right after the logo - so it applies the cached colors/name/
title/favicon/custom-css BEFORE first paint. CSP-safe (external 'self' script,
not inline). applyBranding still runs to refresh + re-cache. First-ever visit to
an uncached branded workspace still shows the default once; every load after is
flash-free.
2026-06-09 11:43:42 -05:00