fix(app): treat a black instance theme colour as unset (#3502)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-15 23:04:40 +02:00
1 parent 41da6b5985
commit b9ea837c75
5 files changed
+34 -20

No files matched your search

@@ -57,6 +57,7 @@ import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {resolveSmtpTlsMode, type SmtpTlsMode, smtpTlsModeFromSecure} from '@fluxer/constants/src/SmtpConstants';
import {ConflictError} from '@fluxer/errors/src/domains/core/ConflictError';
import {ServiceUnavailableError} from '@fluxer/errors/src/domains/core/ServiceUnavailableError';
import {normalizeThemeColor} from '@fluxer/instance_bootstrap/src/ThemeColor';
import {
InstanceConfigResponse,
InstanceConfigUpdateRequest,
@@ -414,14 +415,6 @@ function normalizeOptionalPublicString(value: string | null | undefined, fallbac
return value === undefined ? fallback : normalizeOptionalString(value);
}
function normalizeThemeColor(value: unknown): string | null {
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(normalizeOptionalString(value) ?? '');
if (match === null) return null;
const digits = match[1].toLowerCase();
const color = `#${digits.length === 3 ? digits.replace(/./g, '$&$&') : digits}`;
return color === '#000000' ? null : color;
}
function getDefaultPremiumProductName(): string {
return Config.instance.selfHosted ? 'Premium' : 'Plutonium';
}
@@ -270,6 +270,21 @@ describe('runtimeSnapshotFromDiscovery', () => {
).toThrow(/appPublic\.branding\.icon_url/u);
});
test('treats a black or malformed theme colour as unset and normalises the rest', () => {
const snapshot = runtimeSnapshotFromDiscovery(parsedDocument('t.test'));
const themeColorOf = (theme_color: unknown) =>
requireRuntimeConfigSnapshot({
...snapshot,
appPublic: {...snapshot.appPublic, branding: {...snapshot.appPublic.branding, theme_color}},
}).appPublic.branding.theme_color;
for (const unset of ['#000000', '#000', '000000', '', 'rgb(1, 2, 3)', 'rebeccapurple', 0, null]) {
expect(themeColorOf(unset)).toBeNull();
}
expect(themeColorOf('#FF0000')).toBe('#ff0000');
expect(themeColorOf(' f00 ')).toBe('#ff0000');
expect(themeColorOf('#000001')).toBe('#000001');
});
test('carries the advertised domain migration', () => {
const domainMigration = {
enabled: true,
@@ -14,6 +14,7 @@ import {
} from '@fluxer/instance_bootstrap/src/Discovery';
import {InstanceEndpointKind, normalizeInstanceEndpoint} from '@fluxer/instance_bootstrap/src/EndpointNormalization';
import {CanonicalNetworkProtocol} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
import {normalizeThemeColor} from '@fluxer/instance_bootstrap/src/ThemeColor';
import {
AGE_POLICY_ACTIONS,
type InstanceAgePolicy,
@@ -413,7 +414,7 @@ function readAppPublic(value: unknown): InstanceAppPublic {
logo_url: readNullableAbsoluteHttpUrl(branding, 'logo_url', 'appPublic.branding.logo_url'),
wordmark_url: readNullableAbsoluteHttpUrl(branding, 'wordmark_url', 'appPublic.branding.wordmark_url'),
favicon_url: readNullableAbsoluteHttpUrl(branding, 'favicon_url', 'appPublic.branding.favicon_url'),
theme_color: readNullableNonEmptyString(branding, 'theme_color', 'appPublic.branding.theme_color'),
theme_color: normalizeThemeColor(branding.theme_color),
status_page_url: readOptionalText(branding, 'status_page_url'),
status_page_incident_history_url: readOptionalText(branding, 'status_page_incident_history_url'),
premium_product_name: readOptionalText(branding, 'premium_product_name') ?? DEFAULT_PREMIUM_PRODUCT_NAME,
@@ -10,19 +10,11 @@ export const INSTANCE_BRAND_VARIABLES = [
type InstanceBrandVariable = (typeof INSTANCE_BRAND_VARIABLES)[number];
const HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/;
function parseHexColor(value: string): [number, number, number] | null {
const trimmed = value.trim();
if (!HEX_COLOR_PATTERN.test(trimmed)) return null;
const digits =
trimmed.length === 4
? trimmed
.slice(1)
.split('')
.map((digit) => digit + digit)
.join('')
: trimmed.slice(1);
if (!HEX_COLOR_PATTERN.test(value)) return null;
const digits = value.slice(1);
return [
Number.parseInt(digits.slice(0, 2), 16),
Number.parseInt(digits.slice(2, 4), 16),
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const HEX_THEME_COLOR_PATTERN = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
const UNSET_THEME_COLOR = '#000000';
export function normalizeThemeColor(value: unknown): string | null {
if (typeof value !== 'string') return null;
const match = HEX_THEME_COLOR_PATTERN.exec(value.trim());
if (match === null) return null;
const digits = match[1].toLowerCase();
const color = `#${digits.length === 3 ? digits.replace(/./g, '$&$&') : digits}`;
return color === UNSET_THEME_COLOR ? null : color;
}