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 {
`
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.
+ 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.
+