diff --git a/www/SITE.md b/www/SITE.md index cc7f1312..acae8611 100644 --- a/www/SITE.md +++ b/www/SITE.md @@ -136,6 +136,7 @@ maturity. ## Primary nav The logo is the only Home control. Book stays at `/book` and in the footer. +Security lives at `/security` and in the footer About group. Catalog, footer, and nav destinations are real HTML files at those paths. | Item | Path | Notes | @@ -145,6 +146,7 @@ Catalog, footer, and nav destinations are real HTML files at those paths. | Echo 2026 | `/e26` | Edition + Spec TOC + suite | | Try | `/try` | In-browser check + playground run (wasm host) | | **Install** (CTA) | `/install` | Solid button; get `xo` | +| Security | `/security` | Vulnerability mailbox + `SECURITY.md` | ## Docs left rail @@ -162,13 +164,22 @@ The home page is a language-docs front door. Copy and links live in 3. First-class links: Documents (`/docs`), Packages (`/docs/std`), Spec (`/e26`) 4. Footer -Each of those links, plus Install, First program, Book, Try, Privacy, and -Terms, is a real page (`path/index.html`) with that page title and body. A -destination without a document is removed from the catalog or footer. +Each of those links, plus Install, First program, Book, Try, Privacy, Terms, +and Security, is a real page (`path/index.html`) with that page title and +body. A destination without a document is removed from the catalog or footer. Homepage trust stays factual. Rust, LLVM, the public edition, and the machine-checked suite are implementation facts. +## Security + +`/security` is the public reporting page. It points to +`security@modoterra.xyz` and the repository `SECURITY.md`. The footer About +group links there. Public mail uses `@modoterra.xyz` only. Discord stays +omitted from the footer until there is a real invite. + +Copy and URLs live in `src/docs/site.ts` (`securityContact`, `footerLinkGroups`). + ## Documents hub `/docs` is a short catalog. Groups: Start (install, first program, project), @@ -191,11 +202,11 @@ Cross-links: Reference ↔ Spec ↔ suite pages keep the triangle explicit. ## Static pages `npm run build` writes `index.html` for every content route: the homepage, -`/install`, `/try`, `/privacy`, `/terms`, and each page in `docsPages` -(Documents, Packages, Spec, Book, First program, and the rest of the -Reference / std / suite pages). Each file keeps the SPA shell and a noscript -body from the same modules the React app renders. Unknown paths still use the -`404.html` bounce. +`/install`, `/try`, `/privacy`, `/terms`, `/security`, and each page in +`docsPages` (Documents, Packages, Spec, Book, First program, and the rest of +the Reference / std / suite pages). Each file keeps the SPA shell and a +noscript body from the same modules the React app renders. Unknown paths +still use the `404.html` bounce. Wasm bindings stay in `www/public/echo-wasm/`. @@ -211,11 +222,11 @@ Compile and native run stay on `xo` (LLVM). Learn, Community, and About. Copy lives in `src/docs/site.ts` (`footerLinkGroups`). -| Group | Links | -| --------- | --------------------------------------------------------------------------- | -| Learn | Install, Try Echo, First program, Documents, Book, Echo 2026 | -| Community | GitHub. Omit Discord until a public invite URL exists. | -| About | Modoterra (`https://modoterra.xyz`), Privacy (`/privacy`), Terms (`/terms`) | +| Group | Links | +| --------- | --------------------------------------------------------------------------------------------------- | +| Learn | Install, Try Echo, First program, Documents, Book, Echo 2026 | +| Community | GitHub. Omit Discord until a public invite URL exists. | +| About | Modoterra (`https://modoterra.xyz`), Privacy (`/privacy`), Terms (`/terms`), Security (`/security`) | Public project mail on the site is `@modoterra.xyz` only (`hello@`, `security@`, `oss@`). Do not publish `@modoterra.com`. @@ -226,10 +237,10 @@ They are not a consumer-app policy. ## Discovery `/sitemap.xml` lists the public catalog on `https://xo.run`: home, Install, -Try, catalog and footer routes, and every shipped docs, Book, Echo 2026, and -std page. `/robots.txt` allows crawlers and points at that sitemap. Privacy -and Terms are listed only when those pages exist. Do not list the GitHub -Pages host. +Try, Security, catalog and footer routes, and every shipped docs, Book, +Echo 2026, and std page. `/robots.txt` allows crawlers and points at that +sitemap. Privacy and Terms are listed only when those pages exist. Do not +list the GitHub Pages host. ## Out of scope (later) diff --git a/www/scripts/verify-docs-pages.mjs b/www/scripts/verify-docs-pages.mjs index 2d07395d..a28e5a1a 100644 --- a/www/scripts/verify-docs-pages.mjs +++ b/www/scripts/verify-docs-pages.mjs @@ -5,6 +5,7 @@ * - footer About links Privacy and Terms; Discord stays hidden * - legal pages use @modoterra.xyz mail only * - Documents hub catalog groups + * - security mailbox, SECURITY.md, and footer /security pointer * - every language-feature catalog entry is a real page with a summary * and at least one Echo code block * - catalog, footer, and nav destinations have static HTML snapshots @@ -45,6 +46,7 @@ try { publicChromePaths, publicMailAddresses, renderStaticHomeAndHub, + securityContact, termsPage, } = site; const { docsPageByPath } = content; @@ -95,6 +97,9 @@ try { if (footerByLabel.get("Terms") !== "/terms") { fail(`footer Terms should be /terms, got ${footerByLabel.get("Terms")}`); } + if (footerByLabel.get("Security") !== securityContact.path) { + fail(`footer Security should be ${securityContact.path}, got ${footerByLabel.get("Security")}`); + } if (footerByLabel.get("Modoterra") !== "https://modoterra.xyz") { fail("footer About must keep the Modoterra company link"); } @@ -135,6 +140,25 @@ try { } } + if (securityContact.email !== "security@modoterra.xyz") { + fail(`security mailbox must be security@modoterra.xyz, got ${securityContact.email}`); + } + if (/@modoterra\.com\b/.test(securityContact.email)) { + fail("public mail must use @modoterra.xyz, never @modoterra.com"); + } + if (securityContact.mailto !== `mailto:${securityContact.email}`) { + fail("securityContact.mailto must match the public mailbox"); + } + if (securityContact.path !== "/security") { + fail("securityContact.path must be /security"); + } + if (!securityContact.policyUrl.includes("SECURITY.md")) { + fail("securityContact.policyUrl must point at SECURITY.md"); + } + if (!securityContact.policyUrl.includes("github.com/modoterra/echo")) { + fail("securityContact.policyUrl must point at this repository"); + } + if (!primaryNavItemIsActive("/docs", "/docs/leaders")) { fail("Documents nav should be active on /docs/leaders"); } @@ -282,6 +306,9 @@ try { "/e26", "/docs/leaders", "/install", + securityContact.email, + securityContact.mailto, + "SECURITY.md", ]) { if (!snapshot.includes(needle)) { fail(`renderStaticHomeAndHub missing ${needle}`); @@ -294,6 +321,7 @@ try { ["/docs/std", "Standard library"], ["/e26", "Echo 2026"], ["/install", "Install Echo"], + ["/security", "Security"], ["/docs/first-program", "First program"], ["/book", "Introduction"], ["/privacy", "Privacy"], diff --git a/www/scripts/verify-prose.mjs b/www/scripts/verify-prose.mjs index 72569ad8..2e57acf9 100644 --- a/www/scripts/verify-prose.mjs +++ b/www/scripts/verify-prose.mjs @@ -210,6 +210,7 @@ try { "src/app.tsx", "src/install.tsx", "src/legal.tsx", + "src/security.tsx", "src/router.tsx", "src/docs/site.ts", "src/docs/install-content.ts", diff --git a/www/src/docs/site.ts b/www/src/docs/site.ts index 27a8c01a..23e86419 100644 --- a/www/src/docs/site.ts +++ b/www/src/docs/site.ts @@ -51,6 +51,22 @@ export const primaryNav: SiteNavItem[] = [ export const installCta: SiteNavItem = { label: "Install", to: "/install" }; +/** + * Public vulnerability reporting. Mailbox is @modoterra.xyz only. + * Discord stays omitted from the footer until there is a public invite. + */ +export const securityContact = { + email: "security@modoterra.xyz", + mailto: "mailto:security@modoterra.xyz", + path: "/security", + policyUrl: "https://github.com/modoterra/echo/blob/main/SECURITY.md", +} as const; + +export const securityPage = { + title: "Security", + lead: "Report vulnerabilities by email. Do not open a public GitHub issue for a security report.", +}; + export type FooterLink = { label: string; href: string; @@ -96,6 +112,7 @@ export const footerLinkGroups: FooterLinkGroup[] = [ }, { label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }, + { label: "Security", href: securityContact.path }, ], }, ]; @@ -512,6 +529,11 @@ export function renderStaticHomeAndHub(): string { `
${escapeHtml(homePage.sample)}
`, ``, catalog, + ``, ``, ].join(""); } diff --git a/www/src/docs/static-html.ts b/www/src/docs/static-html.ts index 5c5664ec..a9db4657 100644 --- a/www/src/docs/static-html.ts +++ b/www/src/docs/static-html.ts @@ -9,6 +9,8 @@ import { homePage, legalPages, renderStaticHomeAndHub, + securityContact, + securityPage, tryPage, type LegalPageContent, } from "./site"; @@ -148,6 +150,17 @@ export function renderStaticTry(): string { ].join(""); } +export function renderStaticSecurity(): string { + return [ + `
`, + `

${escapeHtml(securityPage.title)}

`, + `

${escapeHtml(securityPage.lead)}

`, + `

Email ${escapeHtml(securityContact.email)} with a description of the issue and its impact, steps to reproduce or a proof of concept if available, and affected versions, commits, or platforms if known.

`, + `

The repository policy is SECURITY.md.

`, + `
`, + ].join(""); +} + function documentTitle(pageTitle: string): string { return `${pageTitle} · Echo`; } @@ -172,6 +185,12 @@ export function staticPages(): StaticPage[] { description: tryPage.lead, body: renderStaticTry(), }, + { + path: securityContact.path, + title: documentTitle(securityPage.title), + description: securityPage.lead, + body: renderStaticSecurity(), + }, ]; for (const page of legalPages) { diff --git a/www/src/router.tsx b/www/src/router.tsx index b830273a..87b47524 100644 --- a/www/src/router.tsx +++ b/www/src/router.tsx @@ -26,6 +26,7 @@ import { EchoCode } from "./components/echo-code"; import { Logo } from "./components/logo"; import { InstallPage } from "./install"; import { LegalPage } from "./legal"; +import { SecurityPage } from "./security"; import { TryPage } from "./try"; import { docsPageByPath, @@ -342,8 +343,8 @@ function SiteFooter() { {link.label} @@ -951,6 +952,12 @@ const installRoute = createRoute({ component: InstallPage, }); +const securityRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/security", + component: SecurityPage, +}); + const tryRoute = createRoute({ getParentRoute: () => rootRoute, path: "/try", @@ -1047,6 +1054,7 @@ const e26Children = [ const routeTree = rootRoute.addChildren([ indexRoute, installRoute, + securityRoute, tryRoute, privacyRoute, termsRoute, diff --git a/www/src/security.tsx b/www/src/security.tsx new file mode 100644 index 00000000..2532bf7c --- /dev/null +++ b/www/src/security.tsx @@ -0,0 +1,75 @@ +import { securityContact } from "./docs/site"; + +/** + * Public vulnerability reporting. Same mailbox and policy file as SECURITY.md. + */ +export function SecurityPage() { + return ( +
+
+

+ Security +

+

+ Report vulnerabilities by email. Do not open a public GitHub issue for a security report. +

+ +
+

+ Mailbox +

+

+ Email{" "} + + {securityContact.email} + {" "} + with a description of the issue and its impact, steps to reproduce or a proof of concept + if available, and affected versions, commits, or platforms if known. +

+
+ +
+

+ Policy +

+

+ The repository policy is{" "} + + SECURITY.md + + . We acknowledge receipt when we can and work with you on coordinated disclosure. Give a + reasonable window to investigate and ship a fix before public discussion. +

+
+ +
+

+ Other bugs +

+

+ Non-security bugs belong on{" "} + + GitHub issues + + . +

+
+
+
+ ); +} + +export default SecurityPage;