Allow beta-typed Modrinth builds for EasyAuth (easyauth:beta)

EasyAuth publishes its 26.x builds as beta on Modrinth, and the itzg
resolver only accepts release-typed versions by default, so the mc
container failed to start. Pin easyauth:beta in the default mod list and
teach the panel's mod manager to compare entries by base slug so version
qualifiers don't break protected/duplicate checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Fable 5 committed 2026-07-10 12:26:33 -05:00
1 parent 279d137ce5
commit 795c2af50b
6 files changed
+32 -11

No files matched your search

+3 -1
View File
@@ -24,7 +24,9 @@ MC_SEED=
# Server-side mods (Modrinth project slugs, comma-separated). # Server-side mods (Modrinth project slugs, comma-separated).
# Auto-resolved to the newest compatible release on every container start. # Auto-resolved to the newest compatible release on every container start.
MODRINTH_PROJECTS=fabric-api,easyauth,spark,lithium,ferrite-core # Append ":beta" to allow beta-typed builds for a project (EasyAuth ships
# its 26.x builds as beta on Modrinth).
MODRINTH_PROJECTS=fabric-api,easyauth:beta,spark,lithium,ferrite-core
# To run a full modpack instead, set its Modrinth slug (e.g. adrenaline) # To run a full modpack instead, set its Modrinth slug (e.g. adrenaline)
# and see docs/mods-and-modpacks.md before enabling. # and see docs/mods-and-modpacks.md before enabling.
MODRINTH_MODPACK= MODRINTH_MODPACK=
+8 -4
View File
@@ -31,6 +31,10 @@ type SearchHit = {
const PROTECTED = new Set(["fabric-api", "easyauth"]); const PROTECTED = new Set(["fabric-api", "easyauth"]);
// Managed entries may carry a version qualifier ("easyauth:beta") — compare
// by the base slug.
const baseSlug = (entry: string) => entry.split(":")[0].toLowerCase();
export default function ModsPage() { export default function ModsPage() {
const { data, refresh } = useApi<ModsData>("/api/mods", 0); const { data, refresh } = useApi<ModsData>("/api/mods", 0);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@@ -82,7 +86,7 @@ export default function ModsPage() {
} }
}; };
const managedSet = new Set((data?.managed ?? []).map((s) => s.toLowerCase())); const managedSet = new Set((data?.managed ?? []).map(baseSlug));
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -121,7 +125,7 @@ export default function ModsPage() {
)} )}
{hits?.length === 0 && <p className="py-6 text-center text-xs text-muted">No compatible mods found.</p>} {hits?.length === 0 && <p className="py-6 text-center text-xs text-muted">No compatible mods found.</p>}
{hits?.map((h) => { {hits?.map((h) => {
const already = managedSet.has(h.slug.toLowerCase()); const already = managedSet.has(baseSlug(h.slug));
return ( return (
<div key={h.slug} className="flex items-center gap-3 rounded-lg border border-border bg-surface2 p-2.5"> <div key={h.slug} className="flex items-center gap-3 rounded-lg border border-border bg-surface2 p-2.5">
{h.icon_url ? ( {h.icon_url ? (
@@ -166,13 +170,13 @@ export default function ModsPage() {
{(data?.managed ?? []).map((slug) => ( {(data?.managed ?? []).map((slug) => (
<li key={slug} className="flex items-center gap-2 rounded-lg border border-border bg-surface2 px-3 py-2"> <li key={slug} className="flex items-center gap-2 rounded-lg border border-border bg-surface2 px-3 py-2">
<span className="font-mono text-sm">{slug}</span> <span className="font-mono text-sm">{slug}</span>
{PROTECTED.has(slug.toLowerCase()) && ( {PROTECTED.has(baseSlug(slug)) && (
<Badge color="info"> <Badge color="info">
<Lock size={9} /> required <Lock size={9} /> required
</Badge> </Badge>
)} )}
<span className="ml-auto"> <span className="ml-auto">
{!PROTECTED.has(slug.toLowerCase()) && ( {!PROTECTED.has(baseSlug(slug)) && (
<Button <Button
size="sm" size="sm"
variant="danger" variant="danger"
+11 -3
View File
@@ -40,6 +40,10 @@ export async function searchMods(query: string): Promise<ModSearchResult[]> {
return data.hits; return data.hits;
} }
/**
* Entries may carry a version qualifier after a colon (e.g. "easyauth:beta"
* allows beta-typed builds); comparisons always use the base slug.
*/
export function managedSlugs(): string[] { export function managedSlugs(): string[] {
return (readEnvFile()["MODRINTH_PROJECTS"] ?? "") return (readEnvFile()["MODRINTH_PROJECTS"] ?? "")
.split(",") .split(",")
@@ -47,22 +51,26 @@ export function managedSlugs(): string[] {
.filter(Boolean); .filter(Boolean);
} }
export function baseSlug(entry: string): string {
return entry.split(":")[0].toLowerCase();
}
const PROTECTED_SLUGS = new Set(["fabric-api", "easyauth"]); const PROTECTED_SLUGS = new Set(["fabric-api", "easyauth"]);
export function addManagedMod(slug: string): string[] { export function addManagedMod(slug: string): string[] {
if (!/^[a-z0-9][a-z0-9\-_]{0,63}$/i.test(slug)) throw new Error("Invalid Modrinth slug"); if (!/^[a-z0-9][a-z0-9\-_]{0,63}$/i.test(slug)) throw new Error("Invalid Modrinth slug");
const slugs = managedSlugs(); const slugs = managedSlugs();
if (slugs.some((s) => s.toLowerCase() === slug.toLowerCase())) return slugs; if (slugs.some((s) => baseSlug(s) === slug.toLowerCase())) return slugs;
const next = [...slugs, slug.toLowerCase()]; const next = [...slugs, slug.toLowerCase()];
writeEnvFile({ MODRINTH_PROJECTS: next.join(",") }); writeEnvFile({ MODRINTH_PROJECTS: next.join(",") });
return next; return next;
} }
export function removeManagedMod(slug: string): string[] { export function removeManagedMod(slug: string): string[] {
if (PROTECTED_SLUGS.has(slug.toLowerCase())) { if (PROTECTED_SLUGS.has(baseSlug(slug))) {
throw new Error(`${slug} is required by the panel (auth/loader) and can't be removed here`); throw new Error(`${slug} is required by the panel (auth/loader) and can't be removed here`);
} }
const next = managedSlugs().filter((s) => s.toLowerCase() !== slug.toLowerCase()); const next = managedSlugs().filter((s) => baseSlug(s) !== baseSlug(slug));
writeEnvFile({ MODRINTH_PROJECTS: next.join(",") }); writeEnvFile({ MODRINTH_PROJECTS: next.join(",") });
return next; return next;
} }
+4 -2
View File
@@ -33,8 +33,10 @@ services:
ENABLE_RCON: "true" ENABLE_RCON: "true"
RCON_PASSWORD: ${RCON_PASSWORD:?generate with scripts/setup.sh} RCON_PASSWORD: ${RCON_PASSWORD:?generate with scripts/setup.sh}
# Server-side mods, auto-resolved for the current MC version on every start # Server-side mods, auto-resolved for the current MC version on every start.
MODRINTH_PROJECTS: ${MODRINTH_PROJECTS:-fabric-api,easyauth,spark,lithium,ferrite-core} # ":beta" allows beta-typed builds for that project (EasyAuth publishes
# its 26.x builds as beta).
MODRINTH_PROJECTS: ${MODRINTH_PROJECTS:-fabric-api,easyauth:beta,spark,lithium,ferrite-core}
MODRINTH_DOWNLOAD_DEPENDENCIES: required MODRINTH_DOWNLOAD_DEPENDENCIES: required
# Modpack support (leave empty for lean Fabric; see docs/mods-and-modpacks.md) # Modpack support (leave empty for lean Fabric; see docs/mods-and-modpacks.md)
+5 -1
View File
@@ -36,9 +36,13 @@ restart the server.
Managed mods are pinned by *project slug*, not version, in `.env`: Managed mods are pinned by *project slug*, not version, in `.env`:
``` ```
MODRINTH_PROJECTS=fabric-api,easyauth,spark,lithium,ferrite-core MODRINTH_PROJECTS=fabric-api,easyauth:beta,spark,lithium,ferrite-core
``` ```
A `:beta` (or `:alpha`) suffix widens which release types are accepted for that
project — EasyAuth publishes its 26.x builds as beta on Modrinth, so it needs the
suffix. Without one, only release-typed builds are considered.
Every time the server container (re)starts, the itzg image resolves each slug to the Every time the server container (re)starts, the itzg image resolves each slug to the
**newest release compatible with your Minecraft version** and downloads it. So: **newest release compatible with your Minecraft version** and downloads it. So:
+1
View File
@@ -8,6 +8,7 @@
|---|---| |---|---|
| `EULA` complaints | `EULA=TRUE` is set by compose — make sure you didn't override it | | `EULA` complaints | `EULA=TRUE` is set by compose — make sure you didn't override it |
| A mod failed to resolve for your version | Remove that slug on the Mods page, Apply. (New MC version + mod hasn't updated yet.) | | A mod failed to resolve for your version | Remove that slug on the Mods page, Apply. (New MC version + mod hasn't updated yet.) |
| `No candidate versions of '<mod>' … matched versionType=release` | The mod only ships beta builds for this MC version — append `:beta` to its entry in `MODRINTH_PROJECTS` (e.g. `easyauth:beta`) and `docker compose up -d mc` |
| `OutOfMemoryError` | Lower `MC_VIEW_DISTANCE`, or raise `MC_MEMORY`/`MC_CONTAINER_MEM_LIMIT` if the host has headroom | | `OutOfMemoryError` | Lower `MC_VIEW_DISTANCE`, or raise `MC_MEMORY`/`MC_CONTAINER_MEM_LIMIT` if the host has headroom |
| Port already in use | Something else on 25565 — change `MC_PORT` in `.env` | | Port already in use | Something else on 25565 — change `MC_PORT` in `.env` |
| Crash right after "Done" | A mod conflict — disable recently added jars (Mods page → power icon) | | Crash right after "Done" | A mod conflict — disable recently added jars (Mods page → power icon) |