@@ -989,6 +989,20 @@ interface RendererCrashScreenContext {
989989}
990990
991991const CRASH_REPORT_ISSUES_URL = "https://github.com/nexu-io/open-design/issues/new" ;
992+ const SUPPORT_EMAIL = "support@open-design.ai" ;
993+
994+ // Narrow allowlist for the crash screen's "Email us" action: only a mailto
995+ // addressed to our own support address opens, so widening `shell:open-external`
996+ // past http can't be abused into arbitrary-scheme launches. The ?subject/&body
997+ // query is ignored for the check (URL parses the address into `pathname`).
998+ export function isSupportMailtoUrl ( url : string ) : boolean {
999+ try {
1000+ const parsed = new URL ( url ) ;
1001+ return parsed . protocol === "mailto:" && parsed . pathname . toLowerCase ( ) === SUPPORT_EMAIL ;
1002+ } catch {
1003+ return false ;
1004+ }
1005+ }
9921006
9931007function osLabelForReport ( platform : NodeJS . Platform ) : string {
9941008 if ( platform === "darwin" ) return "macOS" ;
@@ -1030,8 +1044,25 @@ function buildCrashReportUrl(ctx: RendererCrashScreenContext): string {
10301044 return `${ CRASH_REPORT_ISSUES_URL } ?${ new URLSearchParams ( { title, body } ) . toString ( ) } ` ;
10311045}
10321046
1047+ // Prefilled mailto for the "Email us" action — same auto-filled diagnostics as
1048+ // the issue, for users who'd rather email than open a GitHub account.
1049+ function buildCrashMailtoUrl ( ctx : RendererCrashScreenContext ) : string {
1050+ const subject = `Open Design keeps crashing (renderer ${ ctx . reason } )` ;
1051+ const body = [
1052+ "The Open Design desktop app crashed several times in a row on my device." ,
1053+ "" ,
1054+ "(If possible, attach the diagnostics file you saved with the “Save logs…” button.)" ,
1055+ "" ,
1056+ `App version: ${ ctx . appVersion } ` ,
1057+ `OS: ${ osLabelForReport ( ctx . platform ) } ${ ctx . osVersion } ` ,
1058+ `Renderer exit: ${ ctx . reason } , code ${ formatRendererExitCode ( ctx . exitCode ) } ` ,
1059+ ] . join ( "\n" ) ;
1060+ return `mailto:${ SUPPORT_EMAIL } ?${ new URLSearchParams ( { subject, body } ) . toString ( ) } ` ;
1061+ }
1062+
10331063function createRendererCrashHtml ( ctx : RendererCrashScreenContext ) : string {
10341064 const issueUrl = buildCrashReportUrl ( ctx ) ;
1065+ const mailtoUrl = buildCrashMailtoUrl ( ctx ) ;
10351066 return `data:text/html;charset=utf-8,${ encodeURIComponent ( `<!doctype html>
10361067<html>
10371068 <head>
@@ -1099,6 +1130,14 @@ function createRendererCrashHtml(ctx: RendererCrashScreenContext): string {
10991130 margin: 12px 0 0;
11001131 min-height: 16px;
11011132 }
1133+ .email {
1134+ color: #8a929a;
1135+ font-size: 13px;
1136+ line-height: 1.5;
1137+ margin: 14px 0 0;
1138+ }
1139+ .email a { color: #2b6cb0; text-decoration: none; }
1140+ .email a:hover { text-decoration: underline; }
11021141 .hint {
11031142 color: #8a929a;
11041143 font-size: 13px;
@@ -1108,7 +1147,8 @@ function createRendererCrashHtml(ctx: RendererCrashScreenContext): string {
11081147 @media (prefers-color-scheme: dark) {
11091148 html, body { background: #1c2024; color: #e6e9ec; }
11101149 .body { color: #aeb6bd; }
1111- .hint, .status { color: #7c848b; }
1150+ .hint, .status, .email { color: #7c848b; }
1151+ .email a { color: #6ea8e0; }
11121152 .secondary { color: #e6e9ec; border-color: rgba(174, 182, 189, 0.35); }
11131153 .secondary:hover { border-color: rgba(174, 182, 189, 0.6); }
11141154 }
@@ -1124,24 +1164,35 @@ function createRendererCrashHtml(ctx: RendererCrashScreenContext): string {
11241164 <button id="logs" class="secondary">Save logs…</button>
11251165 </div>
11261166 <p class="status" id="status" aria-live="polite"></p>
1167+ <p class="email" id="email-line">Prefer email? <a href="#" id="email">Contact ${ SUPPORT_EMAIL } </a></p>
11271168 <p class="hint">If this keeps happening, quitting and reinstalling Open Design usually resolves it.</p>
11281169 </div>
11291170 <script>
11301171 (function () {
11311172 var issueUrl = ${ JSON . stringify ( issueUrl ) } ;
1173+ var mailtoUrl = ${ JSON . stringify ( mailtoUrl ) } ;
11321174 var host = window.__od__;
11331175 var diag = window.openDesignDesktop;
11341176 var report = document.getElementById("report");
11351177 var logs = document.getElementById("logs");
1178+ var emailLine = document.getElementById("email-line");
1179+ var email = document.getElementById("email");
11361180 var status = document.getElementById("status");
11371181 function say(t) { if (status) status.textContent = t; }
1138- // Buttons reuse IPC the preload already exposes; if a bridge is missing
1139- // (preload failed to load) hide the dead control instead of a no-op.
1182+ var canOpen = host && typeof host.openExternal === "function";
1183+ // Actions reuse IPC the preload already exposes; if the bridge is
1184+ // missing (preload failed to load) hide the dead control instead of a
1185+ // no-op.
11401186 if (report) {
1141- if (host && typeof host.openExternal === "function" ) {
1187+ if (canOpen ) {
11421188 report.addEventListener("click", function () { host.openExternal(issueUrl); });
11431189 } else { report.style.display = "none"; }
11441190 }
1191+ if (email) {
1192+ if (canOpen) {
1193+ email.addEventListener("click", function (e) { e.preventDefault(); host.openExternal(mailtoUrl); });
1194+ } else if (emailLine) { emailLine.style.display = "none"; }
1195+ }
11451196 if (logs) {
11461197 if (diag && typeof diag.exportDiagnostics === "function") {
11471198 logs.addEventListener("click", function () {
@@ -1806,7 +1857,9 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
18061857 ipcMain . removeHandler ( channel ) ;
18071858 }
18081859 ipcMain . handle ( "shell:open-external" , async ( _event , url : string ) => {
1809- if ( ! isHttpUrl ( url ) ) return false ;
1860+ // http(s) as before, plus a mailto strictly to our support address (the
1861+ // crash screen's "Email us"); no other scheme opens.
1862+ if ( ! isHttpUrl ( url ) && ! isSupportMailtoUrl ( url ) ) return false ;
18101863 try {
18111864 await shell . openExternal ( url ) ;
18121865 return true ;
0 commit comments