From 51f5fa3e4621b74b84dc4ba35b7b852ba4cb3dc4 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Wed, 4 Nov 2026 00:00:07 +0100 Subject: [PATCH] fix(themes): enforce strict semver versions Require SemVer 2.0.0 when saving, validating, or publishing a theme, and validate again at the workshop upload boundary. This rejects partial versions, v-prefixed versions, leading-zero components, and arbitrary strings while preserving valid prerelease and build metadata. --- src/lib/communityThemes.ts | 10 +++++++++- .../components/theme/ThemeEditorWindow.svelte | 17 +++++++++++++---- src/lib/themeV2.ts | 10 ++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/lib/communityThemes.ts b/src/lib/communityThemes.ts index 74c4aae..2169c9f 100644 --- a/src/lib/communityThemes.ts +++ b/src/lib/communityThemes.ts @@ -1,4 +1,5 @@ import { config } from './config'; +import { normalizeThemeVersion } from './themeV2'; import { reloadThemes, themesDirectory, @@ -727,6 +728,13 @@ export async function publishThemeToWorkshop(options: { throw new Error('You must be logged in to EZPPFarm to publish themes.'); } + const version = normalizeThemeVersion(options.version ?? '1.0.0'); + if (!version) { + throw new Error( + 'Theme version must use semantic versioning, for example 1.2.3 or 1.2.3-beta.1.' + ); + } + onProgress('Packaging theme archive...', 5); const baseThemesDir = await themesDirectory(); @@ -771,7 +779,7 @@ export async function publishThemeToWorkshop(options: { themeId: options.workshopId || options.themeId, name: options.name, description: options.description || '', - version: options.version || '1.0.0', + version, apiVersion: options.apiVersion || '2.0.0', totalSize, totalChunks, diff --git a/src/lib/components/theme/ThemeEditorWindow.svelte b/src/lib/components/theme/ThemeEditorWindow.svelte index d19ce0e..482d123 100644 --- a/src/lib/components/theme/ThemeEditorWindow.svelte +++ b/src/lib/components/theme/ThemeEditorWindow.svelte @@ -75,6 +75,7 @@ normalizeChrome, normalizeThemeTags, normalizeThemeV2, + normalizeThemeVersion, themeAssetName, themeHasLaunchButton, type ThemeV2Asset, @@ -2923,6 +2924,12 @@ notifyError('Add an author name so people know who made the theme.'); return false; } + const version = normalizeThemeVersion(definition.version); + if (!version) { + notifyError('The version must use semantic versioning, for example 1.2.3 or 1.2.3-beta.1.'); + return false; + } + if (definition.version !== version) definition.version = version; const folderName = savedFolder ?? toSafeName(definition.name); if (!folderName.replace(/_/g, '')) { notifyError('The theme name needs at least one letter or number.'); @@ -3019,8 +3026,10 @@ if (!definition.name.trim()) error('The theme needs a name.'); if (!definition.author.trim()) error('The theme needs an author.'); - if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(definition.version.trim())) { - warning('The version should use semantic versioning, for example 1.0.0.'); + if (!normalizeThemeVersion(definition.version)) { + error( + 'The version must use semantic versioning, for example 1.0.0 or 1.0.0-beta.1, with no "v" prefix.' + ); } if (!themeHasLaunchButton(definition)) { error( @@ -3346,7 +3355,7 @@ async function checkPublishVersion(): Promise< { ok: true; version: string } | { ok: false; error: string } > { - const proposed = semver.valid(definition.version.trim()); + const proposed = normalizeThemeVersion(definition.version); if (!proposed) { return { ok: false, @@ -3369,7 +3378,7 @@ }; } - const validRemote = semver.valid(remoteVersion); + const validRemote = normalizeThemeVersion(remoteVersion); if (!validRemote) { return { ok: false, diff --git a/src/lib/themeV2.ts b/src/lib/themeV2.ts index 497764b..f54712e 100644 --- a/src/lib/themeV2.ts +++ b/src/lib/themeV2.ts @@ -748,6 +748,16 @@ export const normalizeThemeTags = (raw: unknown): string[] => { return tags; }; +const THEME_VERSION_PATTERN = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/; + +/** Accept only strict SemVer 2.0.0, preserving prerelease and build metadata. */ +export const normalizeThemeVersion = (raw: unknown): string | undefined => { + if (typeof raw !== 'string') return undefined; + const version = raw.trim(); + return version.length > 0 && THEME_VERSION_PATTERN.test(version) ? version : undefined; +}; + const WORKSHOP_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/; /** Keep a server-assigned workshop ID while preserving legacy slug IDs. */