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 <[email protected]>
This commit is contained in:
Ludovic MarieandClaude Opus 5.5 committed 2026-10-28 00:50:51 +01:00
1 parent 588dd6e2b3
commit 56ef27ebbd
4 files changed
+49 -4

No files matched your search

+19 -2
View File
@@ -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]) => ` <button type="button" class="button sec
<p class="meta release-count" data-mod-count>${modProjects.length} of ${modProjects.length}</p>
<div class="mod-list" id="mod-list" aria-live="polite">
${listHtml(sorted(modProjects, "downloads"))}
</div>`;
</div>
<p class="meta mod-owner-note">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. <a href="/mods/policy">Mod policy</a></p>`;
const policyGithub = "https://github.com/horizon-rust/horizon-mods/blob/main/MOD-POLICY.md";
const modsPolicy = ` <p class="kicker"><span>Mods</span>Policy</p>
<div class="prose">${modPolicy ? markdown(modPolicy.replace(/^# .*\n/, "# Mod policy\n")) : `<h1>Mod policy</h1><p>Read it in the registry: <a href="${policyGithub}">MOD-POLICY.md</a>.</p>`}</div>
<p class="meta">The source of this page: <a href="${policyGithub}">MOD-POLICY.md</a>. To ask for a takedown: <a href="https://github.com/horizon-rust/horizon-mods/issues/new?template=takedown.yml">the takedown form</a>, or privately through <a href="https://lu4ovic.com/#contact">lu4ovic.com</a>.</p>`;
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(/</g, "\\u003c");
+12 -1
View File
@@ -169,6 +169,7 @@ export function projectHtml(p, rating) {
<p class="mod-badges">${trust(p)} <span class="badge">${escape(p.license)}</span> <span class="badge">${escape(v.version)}</span>${v.side ? ` <span class="badge">${escape(v.side)}</span>` : ""}</p>
<p class="mod-stats mod-page-stats"><span data-mod-rating>${rating === undefined ? "" : `${starsHtml(rating)} · `}</span>${downloads(p)}</p>
<div class="mod-rate" id="mod-rate" aria-live="polite"></div></div></header>
${ownerNote(p)}
${p.description ? `<div class="prose">${markdown(p.description)}</div>` : ""}
${p.kind === "mod" ? `<h2 class="heading">What it may do</h2>${capsHtml}<p class="meta">Mods run in a sandbox with no file, network or clock access. The game refuses a mod whose code uses more than this list.</p>` : ""}
${requires}
@@ -184,6 +185,16 @@ export function projectHtml(p, rating) {
</article>`;
}
// 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)
? `<p class="meta mod-owner-note">Made by Horizon. <a href="${POLICY}">Mod policy</a></p>`
: `<p class="meta mod-owner-note">Made by ${escape((p.authors || []).join(", ") || "its authors")}. Its authors own it and answer for it. Not made or endorsed by the Horizon project. <a href="${POLICY}">Mod policy</a></p>`;
}
// /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", `<input id="f-signature" name="signature" type="file" accept=".minisig">`, "The file's .minisig.")}
</fieldset>
<label class="mod-check"><input type="checkbox" id="f-mine" required> I made it, or I may publish it under this licence, and its source is at the address above.</label>
<label class="mod-check"><input type="checkbox" id="f-mine" required> I made it, or I may publish it under this licence, and its source is at the address above. I accept the <a href="/mods/policy">mod policy</a> and I am responsible for this project.</label>
<p class="mod-actions"><button class="button primary" type="submit" id="f-send">Submit for review</button></p>
<div class="mod-result" id="submit-result" role="status" aria-live="polite"></div>
</form>`;
+13
View File
@@ -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: ["<b>x</b>"], source: "" }), /&lt;b&gt;x&lt;\/b&gt;/);
});
+5 -1
View File
@@ -59,9 +59,13 @@
<h2>Deleting your data</h2>
<p>Settings → Data collection → Delete my data, or <code>horizon-launcher data delete</code>. 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 <code>telemetry/</code> folder.</p>
<h2>The mod registry</h2>
<p>To submit a project you sign in with GitHub. The submission service at playhorizon.dev/api/mods keeps a signed session cookie (<code>hm_session</code>, 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 <a href="https://github.com/horizon-rust/horizon-mods">horizon-rust/horizon-mods</a>, under your GitHub name, and stays in its history.</p>
<h2>Discord and Kira</h2>
<p>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 <code>/link-github</code>, 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 <a href="https://discord.com/privacy">privacy policy</a> covers everything else on Discord.</p>
<h2>Your rights and contact</h2>
<p>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 <a href="https://lu4ovic.com/#contact">lu4ovic.com</a> (give your install ID, shown in Settings → Data collection). You can also complain to the data protection authority where you live.</p>
<p>This website itself sets no cookies and has no analytics; its footer says what it loads from GitHub.</p>
<p>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.</p>
</div>
<div class="hero-actions"><a class="button secondary" href="/">Back to Horizon</a></div>
<p class="disclaimer">Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft.</p>