From 56ef27ebbdf162b0ee2f0c94bc988f72c0dcd98d Mon Sep 17 00:00:00 2001
From: Ludovic Marie <136255825+ludovic111@users.noreply.github.com>
Date: Wed, 28 Oct 2026 00:50:51 +0100
Subject: [PATCH] Mod policy page, who made each project, and the registry and
Kira in the privacy page
- /mods/policy shows the registry's MOD-POLICY.md, with the takedown form and the private contact;
- every project page says who made it: Horizon for official projects (author Horizon, source in horizon-rust),
otherwise its authors, who own it and answer for it, not made or endorsed by the Horizon project;
- the list page says a listing is not an endorsement; the submit form's required box accepts the policy;
- privacy: the registry's sign-in cookie, and what Kira keeps on Discord (and that questions to her go to the
Claude API).
Co-Authored-By: Claude Opus 5.5
---
build.mjs | 21 +++++++++++++++++++--
src/mods.js | 13 ++++++++++++-
src/mods.test.mjs | 13 +++++++++++++
src/privacy.html | 6 +++++-
4 files changed, 49 insertions(+), 4 deletions(-)
diff --git a/build.mjs b/build.mjs
index 170f54e..543c32b 100644
--- a/build.mjs
+++ b/build.mjs
@@ -78,6 +78,17 @@ try {
console.warn(`mod index: ${e.message}; the pages will load it themselves`);
}
+// The mod policy (who owns and answers for a listed project, takedowns): the registry's MOD-POLICY.md, shown at
+// /mods/policy. Without it the page links to the file on GitHub.
+let modPolicy = null;
+try {
+ const res = await fetch("https://raw.githubusercontent.com/horizon-rust/horizon-mods/main/MOD-POLICY.md", { signal: AbortSignal.timeout(15000) });
+ if (!res.ok) throw new Error(`${res.status}`);
+ modPolicy = await res.text();
+} catch (e) {
+ console.warn(`MOD-POLICY.md: ${e.message}; /mods/policy links to it on GitHub`);
+}
+
// An optional token (a Railway variable, or `gh auth token` locally) lifts the rate limit.
const githubHeaders = { Accept: "application/vnd.github+json", ...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}) };
// Every release (the Releases page lists them all), a page of 100 at a time.
@@ -518,11 +529,17 @@ ${KINDS.map(([k, label]) => `
${listHtml(sorted(modProjects, "downloads"))}
-
`;
+
+
Each project belongs to its authors, who answer for it. Only the ones marked as made by Horizon are the Horizon project's; a listing is not an endorsement. Mod policy
`;
const modPages = [
{ file: "mods.html", path: "/mods", title: "Mods", description: "Mods, modpacks and resource packs made for Horizon, reviewed, each with what it may do and how to install it.", main: modsList },
{ file: "mods/submit.html", path: "/mods/submit", title: "Submit to the registry", description: "Submit a mod, a modpack or a resource pack made for Horizon: it is checked, reviewed, then listed here and in the launcher.", main: submitHtml(), js: "mods-submit.js" },
- ...modProjects.filter((p) => p.id !== "submit").map((p) => ({ file: `mods/${p.id}.html`, path: `/mods/${p.id}`, title: p.name, description: p.summary || p.name, main: projectHtml(p) })),
+ { file: "mods/policy.html", path: "/mods/policy", title: "Mod policy", description: "Who owns and answers for the projects in Horizon's registry, what authors promise, and how to ask for a takedown.", main: modsPolicy },
+ ...modProjects.filter((p) => p.id !== "submit" && p.id !== "policy").map((p) => ({ file: `mods/${p.id}.html`, path: `/mods/${p.id}`, title: p.name, description: p.summary || p.name, main: projectHtml(p) })),
];
await mkdir(join(dist, "mods"), { recursive: true });
const modsData = JSON.stringify(modIndex && modProjects.length ? { schema: modIndex.schema, projects: modProjects } : null).replace(/${trust(p)} ${escape(p.license)}${escape(v.version)}${v.side ? ` ${escape(v.side)}` : ""}
Mods run in a sandbox with no file, network or clock access. The game refuses a mod whose code uses more than this list.
` : ""}
${requires}
@@ -184,6 +185,16 @@ export function projectHtml(p, rating) {
`;
}
+// Official = made by Horizon: the author is "Horizon" and the source is in the horizon-rust organisation
+// (MOD-POLICY.md). Every other project belongs to its authors, and its page says so.
+export const isOfficial = (p) => (p.authors || []).length === 1 && p.authors[0] === "Horizon" && /^https:\/\/github\.com\/horizon-rust\//.test(p.source || "");
+export const POLICY = "/mods/policy";
+export function ownerNote(p) {
+ return isOfficial(p)
+ ? `
Made by ${escape((p.authors || []).join(", ") || "its authors")}. Its authors own it and answer for it. Not made or endorsed by the Horizon project. Mod policy
`;
+}
+
// /mods/submit: the form. Its script (mods-submit.js) signs in, then sends the page and the file to the service, which
// checks the file and opens the pull request the reviewers read.
const field = (name, label, input, hint = "") =>
@@ -221,7 +232,7 @@ export function submitHtml() {
${field("key", "Public key", text("key", `placeholder="RW…" autocomplete="off"`), "A new project's. Changing a published project's key is a pull request by hand.")}
${field("signature", "Signature", ``, "The file's .minisig.")}
-
+
`;
diff --git a/src/mods.test.mjs b/src/mods.test.mjs
index c546ce3..e1f4f2a 100644
--- a/src/mods.test.mjs
+++ b/src/mods.test.mjs
@@ -108,3 +108,16 @@ test("rating: a sign-in link signed out, stars signed in, none for the project's
assert.ok(mine.includes('data-stars="3" aria-pressed="true"'));
assert.ok(rateHtml(p, { login: "bob" }, null).includes("Rate it"));
});
+
+test("a project's page says who made it: Horizon, or its authors with the policy's disclaimer", async () => {
+ const { ownerNote, isOfficial } = await import("./mods.js");
+ const official = { authors: ["Horizon"], source: "https://github.com/horizon-rust/horizon-mod-zoom" };
+ const theirs = { authors: ["levzzz5154"], source: "https://github.com/levzzz5154/horizon-veinminer" };
+ assert.equal(isOfficial(official), true);
+ assert.equal(isOfficial(theirs), false);
+ assert.equal(isOfficial({ authors: ["Horizon"], source: "https://github.com/someone/horizon-thing" }), false, "the name alone is not enough");
+ assert.match(ownerNote(official), /Made by Horizon\./);
+ assert.match(ownerNote(theirs), /Made by levzzz5154\. Its authors own it and answer for it\. Not made or endorsed by the Horizon project\./);
+ assert.match(ownerNote(theirs), /href="\/mods\/policy"/);
+ assert.match(ownerNote({ authors: ["x"], source: "" }), /<b>x<\/b>/);
+});
diff --git a/src/privacy.html b/src/privacy.html
index 57feee7..8ec1258 100644
--- a/src/privacy.html
+++ b/src/privacy.html
@@ -59,9 +59,13 @@
Deleting your data
Settings → Data collection → Delete my data, or horizon-launcher data delete. The server deletes everything sent under your install ID, your share of the totals is recomputed, anything waiting on your computer is removed, and you get a new install ID. You can also turn everything off, or delete the telemetry/ folder.
+
The mod registry
+
To submit a project you sign in with GitHub. The submission service at playhorizon.dev/api/mods keeps a signed session cookie (hm_session, only for that address, 12 hours or until you sign out) and keeps no other data. What you submit becomes a public pull request on horizon-rust/horizon-mods, under your GitHub name, and stays in its history.
+
Discord and Kira
+
On Horizon's Discord server, the Kira bot keeps what it needs to run the server, linked to your Discord ID: your XP and level, warnings and timeouts, the GitHub name you link with /link-github, your bug reports, claims and agent tokens. For moderation, the mods' private log keeps joins (with the invite used), leaves, and the text of deleted and edited messages; a DM to Kira is shown to the mods as a thread. A bug report you post becomes a public GitHub issue on Horizon's repository. When you ask Kira a question, the question and up to four messages it replies to are sent to Anthropic's Claude API to write the answer. Discord's own privacy policy covers everything else on Discord.
Your rights and contact
Horizon is made by Ludovic Marie, who is responsible for this data. To ask for a copy of what was sent under your install ID, have it corrected or deleted, or ask anything about it, write through lu4ovic.com (give your install ID, shown in Settings → Data collection). You can also complain to the data protection authority where you live.
-
This website itself sets no cookies and has no analytics; its footer says what it loads from GitHub.
+
Apart from the registry's sign-in cookie, this website sets no cookies and has no analytics; its footer says what it loads from GitHub.