Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

49 lines · 2.2 KBRaw History
1import { Hono } from "hono";
2import type { AppVars } from "../auth/principal.js";
3import type { Env } from "../env.js";
4
5/**
6 * /.well-known/security.txt (RFC 9116): where to report a vulnerability in this instance. The
7 * first contact is always the instance's own private report form, /security/report
8 * (web/security-reports.tsx), so the file is served without an email address; any entries of
9 * SECURITY_CONTACT follow it. Expires is a year from the request, so the file never goes stale.
10 */
11
12/** How far ahead Expires is (RFC 9116 recommends less than a year). */
13export const EXPIRES_DAYS = 360;
14
15/** One Contact URI per entry of SECURITY_CONTACT: email addresses become mailto: URIs; https URLs stay as they are. */
16export function contactUris(v: string | undefined): string[] {
17 const out: string[] = [];
18 for (const raw of (v ?? "").split(",")) {
19 const s = raw.trim();
20 if (!s) continue;
21 if (/^mailto:[^\s@]+@[^\s@]+$/i.test(s) || /^https:\/\/\S+$/i.test(s) || /^tel:\+?[\d-]+$/i.test(s)) out.push(s);
22 else if (/^[^\s@:/]+@[^\s@/]+\.[^\s@/]+$/.test(s)) out.push(`mailto:${s}`);
23 }
24 return out;
25}
26
27/** The private report form's URL: the first Contact. */
28export const reportUrl = (publicUrl: string): string => `${new URL(publicUrl).origin}/security/report`;
29
30/** The file's text. */
31export function securityTxt(env: Pick<Env, "PUBLIC_URL" | "SECURITY_CONTACT">, nowMs = Date.now()): string {
32 const base = new URL(env.PUBLIC_URL).origin;
33 const form = reportUrl(env.PUBLIC_URL);
34 const contacts = [form, ...contactUris(env.SECURITY_CONTACT).filter((c) => c !== form)];
35 const expires = new Date(nowMs + EXPIRES_DAYS * 86400_000);
36 expires.setUTCHours(0, 0, 0, 0);
37 return [
38 ...contacts.map((c) => `Contact: ${c}`),
39 `Expires: ${expires.toISOString().replace(".000Z", "Z")}`,
40 `Policy: ${base}/docs/security/disclosure`,
41 "Preferred-Languages: en",
42 `Canonical: ${base}/.well-known/security.txt`,
43 "",
44 ].join("\n");
45}
46
47export const securityTxtRoutes = new Hono<{ Bindings: Env; Variables: AppVars }>();
48
49securityTxtRoutes.get("/.well-known/security.txt", (c) => c.body(securityTxt(c.env), 200, { "content-type": "text/plain; charset=utf-8", "cache-control": "public, max-age=86400" }));