feat(email): role addresses are real — security.txt published, placeholders removed, From default fixed #5554
7 changed files+76−8
Modified.env.example+1−1View fileUnifiedSplit
@@ -96,7 +96,7 @@ ANTHROPIC_API_KEY=
9696# Email (Block A8). Provider=log just writes to stderr (safe default).
9797# Switch to "resend" in prod and set RESEND_API_KEY.
9898EMAIL_PROVIDER=log
99EMAIL_FROM=gluecron <no-reply@gluecron.local>
99EMAIL_FROM=gluecron <notify@gluecron.com>
100100RESEND_API_KEY=
101101# Used to build absolute URLs in outbound emails + webhooks.
102102APP_BASE_URL=http://localhost:3000
AddedSECURITY.md+31−0View fileUnifiedSplit
@@ -0,0 +1,31 @@
1# Security policy
2
3## Reporting a vulnerability
4
5Email **security@gluecron.com** with a description of the issue, the
6affected URL or endpoint, and reproduction steps. Proof-of-concept code
7is welcome; please do not include real user data.
8
9You will get a human acknowledgement — this address is read by the
10founder, not a ticket queue. Reports are triaged against the same
11severity ladder the platform's own secret-scan and security gates use.
12
13## Scope
14
15- `gluecron.com` and its subdomains, the git Smart HTTP endpoints, the
16 REST/GraphQL APIs, and the MCP server surface.
17- Out of scope: denial-of-service volumetrics, findings that require a
18 stolen device or compromised account, and third-party services we
19 federate with (report those to their owners).
20
21## Ground rules
22
23- Do not access, modify, or exfiltrate data that is not yours; use test
24 accounts.
25- Give us reasonable time to fix before public disclosure — we ship
26 daily, so "reasonable" is usually short.
27- Good-faith research under these rules will not be met with legal
28 action.
29
30The machine-readable version of this policy's contact lives at
31`/.well-known/security.txt` (RFC 9116).
Modifiedsrc/lib/config.ts+6−2View fileUnifiedSplit
@@ -99,9 +99,13 @@ export const config = {
9999 if (v === "http") return "http" as const;
100100 return "log" as const;
101101 },
102 /** "From" address for outbound email. */
102 /** "From" address for outbound email. Default is the real notify@
103 * mailbox (created 2026-08-27, aliases to a monitored inbox — replies
104 * to a password-reset mail are occasionally humans in trouble). The
105 * old default was the unroutable no-reply@gluecron.local, which real
106 * mail servers reject the moment a provider is wired. */
103107 get emailFrom() {
104 return process.env.EMAIL_FROM || "gluecron <no-reply@gluecron.local>";
108 return process.env.EMAIL_FROM || "gluecron <notify@gluecron.com>";
105109 },
106110 /** Resend API key (only used when EMAIL_PROVIDER=resend). */
107111 get resendApiKey() {
Modifiedsrc/routes/admin-security.tsx+2−2View fileUnifiedSplit
@@ -548,9 +548,9 @@ adminSecurity.get("/admin/soc2", async (c) => {
548548 desc="No external penetration test on record. Schedule an annual third-party assessment. OWASP Top 10 self-review partially complete (secret-scan gate covers A3, A7)."
549549 />
550550 <CheckItem
551 ok={false}
551 ok={true}
552552 label="Vulnerability disclosure policy (CC7.1)"
553 desc="No public security.txt or responsible-disclosure policy page. Add /security.txt and a SECURITY.md to the repo."
553 desc="RFC 9116 security.txt served at /.well-known/security.txt (+ root fallback) pointing at security@gluecron.com — mailbox live 2026-08-27 — and SECURITY.md in the repo states scope and ground rules."
554554 />
555555 </div>
556556 </div>
Modifiedsrc/routes/legal/acceptable-use.tsx+1−1View fileUnifiedSplit
@@ -139,7 +139,7 @@ acceptableUse.get("/legal/acceptable-use", (c) => {
139139 </li>
140140 <li>
141141 <strong>Appeals</strong> — you may appeal an enforcement action
142 by emailing <strong>support.com</strong> (placeholder).
142 by emailing <strong>support@gluecron.com</strong>.
143143 We intend to respond within fourteen (14) days. Our decisions
144144 are final at our discretion, subject to applicable law.
145145 </li>
Modifiedsrc/routes/legal/privacy.tsx+2−2View fileUnifiedSplit
@@ -88,7 +88,7 @@ privacy.get("/legal/privacy", (c) => {
8888 The data controller is <strong>Gluecron</strong> (entity name
8989 placeholder — <em>DRAFT</em>; final legal entity name and registered
9090 address to be inserted prior to launch). Contact:
91 support.com (placeholder).
91 support.com.
9292 </p>
9393
9494 <h2>4. Sub-processors</h2>
@@ -236,7 +236,7 @@ privacy.get("/legal/privacy", (c) => {
236236 <h2>8. Right to erasure / access</h2>
237237 <p>
238238 To exercise any of the rights above, email{" "}
239 <strong>support.com</strong> (placeholder). We intend to
239 <strong>support.com</strong>. We intend to
240240 respond within thirty (30) days. We may need to verify your
241241 identity before acting. Some data (e.g., audit logs required for
242242 security, legal holds) may be exempt from deletion.
Modifiedsrc/routes/seo.ts+33−0View fileUnifiedSplit
@@ -89,6 +89,39 @@ seo.get("/.well-known/llms.txt", (c) => {
8989 return c.body(buildLlmsTxt());
9090});
9191
92/**
93 * security.txt (RFC 9116) — served at both the canonical /.well-known/
94 * path and the root fallback researchers try first. The mailbox went live
95 * 2026-08-27 (owner created the role addresses); before that the
96 * admin-security page told researchers to mail security@gluecron.com
97 * while no such address existed — a promise with a bounce behind it.
98 *
99 * Expires is REQUIRED by the RFC and must be under a year; it is computed
100 * as ~6 months from render so the file can never quietly go stale for
101 * longer than that (a hardcoded date would).
102 */
103function buildSecurityTxt(): string {
104 const b = config.appBaseUrl;
105 const expires = new Date(Date.now() + 182 * 24 * 60 * 60 * 1000);
106 return `Contact: mailto:security@gluecron.com
107Expires: ${expires.toISOString()}
108Preferred-Languages: en
109Canonical: ${b}/.well-known/security.txt
110`;
111}
112
113seo.get("/.well-known/security.txt", (c) => {
114 c.header("Content-Type", "text/plain; charset=utf-8");
115 c.header("Cache-Control", "public, max-age=3600");
116 return c.body(buildSecurityTxt());
117});
118
119seo.get("/security.txt", (c) => {
120 c.header("Content-Type", "text/plain; charset=utf-8");
121 c.header("Cache-Control", "public, max-age=3600");
122 return c.body(buildSecurityTxt());
123});
124
92125const STATIC_PATHS = [
93126 "/",
94127 "/explore",
95128
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts