feat(legal): serve Terms of Service and Privacy Policy pages

Add bilingual (zh/en) /terms and /privacy pages served by the Worker for
configuring the Discord application's Terms of Service and Privacy Policy
URLs. Contact line is configurable via optional LEGAL_CONTACT env var.
This commit is contained in:
RhenCloud 2026-08-02 07:12:54 +08:00
parent fcf3413634
commit 225d5b015e
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
3 changed files with 250 additions and 0 deletions

247
src/legal-routes.ts Normal file
View file

@ -0,0 +1,247 @@
import { Hono } from "hono";
import type { Env } from "./types";
type Lang = "zh" | "en";
const CONTACT_FALLBACK = "the repository maintainer (open an issue on the project repository)";
const CONTACT_FALLBACK_ZH = "项目维护者(在项目仓库提交 issue";
function pickLang(raw: string | undefined): Lang {
return raw === "en" ? "en" : "zh";
}
function layout(opts: {
lang: Lang;
active: "terms" | "privacy";
title: string;
updated: string;
body: string;
}): string {
const { lang, active, title, updated, body } = opts;
const altLang: Lang = lang === "zh" ? "en" : "zh";
const t = (zh: string, en: string): string => (lang === "zh" ? zh : en);
const q = (p: string): string => `${p}?lang=${lang}`;
const langLabel = t("English", "中文");
return `<!doctype html>
<html lang="${lang === "zh" ? "zh-CN" : "en"}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="index,follow">
<title>${title} · WebHooker</title>
<style>
:root{--bg:#f6f7f9;--surface:#fff;--border:#e5e7eb;--text:#1f2328;--muted:#57606a;--accent:#4f46e5}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:'Plus Jakarta Sans',system-ui,-apple-system,'Segoe UI',sans-serif;line-height:1.7;-webkit-font-smoothing:antialiased}
.wrap{max-width:760px;margin:0 auto;padding:48px 20px 80px}
header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px;flex-wrap:wrap}
.brand{font-weight:800;font-size:16px;letter-spacing:-.01em;text-decoration:none;color:var(--text)}
.brand span{color:var(--accent)}
.tabs{display:flex;gap:8px}
.tab{font-size:13px;text-decoration:none;color:var(--muted);padding:6px 14px;border-radius:999px;border:1px solid transparent}
.tab.active{color:var(--accent);background:#eef2ff;border-color:#e0e7ff}
.tab:hover{color:var(--text)}
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:36px 40px;box-shadow:0 1px 3px rgba(0,0,0,.05)}
h1{font-size:26px;margin:0 0 6px;letter-spacing:-.02em}
.updated{color:var(--muted);font-size:13px;margin:0 0 24px}
h2{font-size:17px;margin:28px 0 8px}
p,li{color:#30363d;font-size:15px}
a{color:var(--accent)}
ul{padding-left:20px}
code{background:#f0f1f3;border:1px solid var(--border);border-radius:6px;padding:1px 6px;font-family:'JetBrains Mono',ui-monospace,monospace;font-size:13px}
footer{margin-top:24px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.langlink{font-size:13px;text-decoration:none;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:6px 14px;background:var(--surface)}
.langlink:hover{color:var(--text)}
.muted{color:var(--muted);font-size:13px}
</style>
</head>
<body>
<div class="wrap">
<header>
<a class="brand" href="${q("/terms")}">Web<span>Hooker</span></a>
<nav class="tabs">
<a class="tab ${active === "terms" ? "active" : ""}" href="${q("/terms")}">${t("服务条款", "Terms")}</a>
<a class="tab ${active === "privacy" ? "active" : ""}" href="${q("/privacy")}">${t("隐私政策", "Privacy")}</a>
</nav>
</header>
<article class="card">
<h1>${title}</h1>
<p class="updated">${t("最后更新", "Last updated")}: ${updated}</p>
${body}
</article>
<footer>
<span class="muted">WebHooker · GitHub Discord</span>
<a class="langlink" href="${q(active === "terms" ? "/terms" : "/privacy").replace(`lang=${lang}`, `lang=${altLang}`)}">${langLabel}</a>
</footer>
</div>
</body>
</html>`;
}
function termsBody(lang: Lang, contact: string): string {
if (lang === "en") {
return `
<p>These Terms of Service ("Terms") govern your use of the WebHooker Discord application and bot ("the Service"), which forwards GitHub webhook events to Discord channels and lets you comment on GitHub from Discord using your own linked GitHub account.</p>
<h2>1. Acceptance</h2>
<p>By adding the bot to a Discord server, using its slash commands, or linking your GitHub account, you agree to these Terms. If you do not agree, do not use the Service.</p>
<h2>2. The Service</h2>
<ul>
<li>Delivers GitHub repository notifications to configured Discord channels.</li>
<li>Provides <code>/gh</code> commands to link a GitHub account and to create, edit, or delete GitHub issue/PR comments as that linked account.</li>
<li>All GitHub write actions are performed with your own OAuth authorization and are subject to your GitHub permissions and to GitHub's Terms of Service.</li>
</ul>
<h2>3. Acceptable Use</h2>
<ul>
<li>Do not use the Service to post spam, harassment, or unlawful content.</li>
<li>Do not attempt to disrupt, overload, reverse-engineer, or gain unauthorized access to the Service.</li>
<li>You are responsible for all activity performed through your linked account.</li>
</ul>
<h2>4. Account Linking &amp; Revocation</h2>
<p>Linking is optional and initiated by you via <code>/gh login</code>. You may unlink at any time with <code>/gh logout</code>, or revoke access from your GitHub account settings under authorized OAuth applications.</p>
<h2>5. Availability &amp; Warranty</h2>
<p>The Service is provided "as is" and "as available", without warranties of any kind. It may be modified, suspended, or discontinued at any time without notice.</p>
<h2>6. Limitation of Liability</h2>
<p>To the maximum extent permitted by law, the operator shall not be liable for any indirect, incidental, or consequential damages arising from your use of the Service.</p>
<h2>7. Changes</h2>
<p>These Terms may be updated. Continued use after changes constitutes acceptance of the revised Terms.</p>
<h2>8. Contact</h2>
<p>Questions about these Terms can be directed to ${contact}.</p>`;
}
return `
<p>"条款" WebHooker Discord "本服务"使 GitHub webhook Discord Discord GitHub GitHub </p>
<h2>1. </h2>
<p> Discord 使 GitHub 使</p>
<h2>2. </h2>
<ul>
<li> GitHub Discord </li>
<li> <code>/gh</code> GitHub GitHub issue/PR </li>
<li> GitHub 使 OAuth GitHub GitHub </li>
</ul>
<h2>3. 使</h2>
<ul>
<li></li>
<li>访</li>
<li></li>
</ul>
<h2>4. </h2>
<p> <code>/gh login</code> 使 <code>/gh logout</code> GitHub OAuth </p>
<h2>5. </h2>
<p>"现状""现有可用"</p>
<h2>6. </h2>
<p>使</p>
<h2>7. </h2>
<p>使</p>
<h2>8. </h2>
<p>${contact}</p>`;
}
function privacyBody(lang: Lang, contact: string): string {
if (lang === "en") {
return `
<p>This Privacy Policy explains what data the WebHooker Discord application ("the Service") processes, why, and how it is stored.</p>
<h2>1. Data We Process</h2>
<ul>
<li><b>Discord identifiers</b>: your Discord user ID, only when you run <code>/gh login</code>, to link you to a GitHub account.</li>
<li><b>GitHub OAuth tokens</b>: an access token (and refresh token, if provided) issued when you authorize the app, used to act on GitHub on your behalf.</li>
<li><b>GitHub account info</b>: your GitHub user ID and login name, returned during authorization.</li>
<li><b>Webhook payloads</b>: GitHub event data is received and forwarded to Discord. It is processed in transit and is not stored beyond short-lived delivery de-duplication keys.</li>
<li><b>Operational logs</b>: minimal send/delivery records kept temporarily for troubleshooting.</li>
</ul>
<h2>2. How Data Is Stored</h2>
<ul>
<li>Data is stored in Cloudflare Workers KV within the operator's Cloudflare account.</li>
<li>Delivery de-duplication keys expire automatically (about 5 minutes).</li>
<li>Send logs expire automatically (about 1 hour).</li>
<li>OAuth tokens persist until you unlink or the token expires/is revoked.</li>
</ul>
<h2>3. How Data Is Used</h2>
<p>Solely to operate the Service: forwarding notifications and performing GitHub actions (comment create/edit/delete) that you explicitly request. We do not sell your data or use it for advertising.</p>
<h2>4. Sharing</h2>
<p>Data is shared only with the platforms required to deliver the Service GitHub and Discord through their official APIs, and with Cloudflare as the hosting/storage provider. No other third-party sharing occurs.</p>
<h2>5. Your Choices</h2>
<ul>
<li>Run <code>/gh logout</code> to delete the Discord-to-GitHub link.</li>
<li>Revoke the OAuth authorization from your GitHub settings (Applications Authorized OAuth Apps) to invalidate stored tokens.</li>
<li>Remove the bot from a server to stop notification delivery.</li>
</ul>
<h2>6. Data Retention</h2>
<p>We retain data only as long as needed for the Service. Transient data expires automatically as described above; account links and tokens are removed when you unlink or revoke access.</p>
<h2>7. Children</h2>
<p>The Service is not directed to individuals under the age required by Discord's Terms of Service, and we do not knowingly collect their data.</p>
<h2>8. Changes</h2>
<p>This policy may be updated. Material changes will be reflected by the "Last updated" date above.</p>
<h2>9. Contact</h2>
<p>For privacy questions or data-removal requests, contact ${contact}.</p>`;
}
return `
<p> WebHooker Discord "本服务"</p>
<h2>1. </h2>
<ul>
<li><b>Discord </b> <code>/gh login</code> Discord ID GitHub </li>
<li><b>GitHub OAuth </b>访 GitHub </li>
<li><b>GitHub </b> GitHub ID </li>
<li><b>Webhook </b> GitHub Discord</li>
<li><b></b>便/</li>
</ul>
<h2>2. </h2>
<ul>
<li> Cloudflare Cloudflare Workers KV </li>
<li> 5 </li>
<li> 1 </li>
<li>OAuth /</li>
</ul>
<h2>3. 使</h2>
<p> GitHub //广</p>
<h2>4. </h2>
<p> API GitHub Discord/ Cloudflare</p>
<h2>5. </h2>
<ul>
<li> <code>/gh logout</code> Discord GitHub </li>
<li> GitHub Applications Authorized OAuth Apps OAuth 使</li>
<li></li>
</ul>
<h2>6. </h2>
<p></p>
<h2>7. </h2>
<p> Discord </p>
<h2>8. </h2>
<p>"最后更新"</p>
<h2>9. </h2>
<p>${contact}</p>`;
}
const UPDATED = "2026-08-01";
export function createLegalRoutes(): Hono<{ Bindings: Env }> {
const app = new Hono<{ Bindings: Env }>();
app.get("/terms", (c) => {
const lang = pickLang(c.req.query("lang"));
const contact = c.env.LEGAL_CONTACT ?? (lang === "zh" ? CONTACT_FALLBACK_ZH : CONTACT_FALLBACK);
return c.html(
layout({
lang,
active: "terms",
title: lang === "zh" ? "服务条款" : "Terms of Service",
updated: UPDATED,
body: termsBody(lang, contact),
}),
);
});
app.get("/privacy", (c) => {
const lang = pickLang(c.req.query("lang"));
const contact = c.env.LEGAL_CONTACT ?? (lang === "zh" ? CONTACT_FALLBACK_ZH : CONTACT_FALLBACK);
return c.html(
layout({
lang,
active: "privacy",
title: lang === "zh" ? "隐私政策" : "Privacy Policy",
updated: UPDATED,
body: privacyBody(lang, contact),
}),
);
});
return app;
}

View file

@ -5,6 +5,7 @@ import { dispatchEvent } from "./discord";
import { createOAuthRoutes } from "./oauth-routes";
import { createActionRoutes } from "./action-routes";
import { createAdminRoutes } from "./admin-routes";
import { createLegalRoutes } from "./legal-routes";
import { log } from "./log";
const MAX_BODY_SIZE = 1024 * 1024;
@ -16,6 +17,7 @@ export function createServer(): Hono<{ Bindings: Env }> {
app.route("/auth", createOAuthRoutes());
app.route("/", createActionRoutes());
app.route("/", createLegalRoutes());
app.route("/admin", createAdminRoutes());
app.post("/webhook", async (c) => {

View file

@ -9,6 +9,7 @@ export interface Env {
BASE_URL?: string;
ADMIN_USER_IDS?: string;
DISCORD_GATEWAY_ENABLED?: string;
LEGAL_CONTACT?: string;
ASSETS?: Fetcher;
KV: KVNamespace;
DISCORD_GATEWAY: DurableObjectNamespace;