rechrome 1.28.1 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/serve.ts CHANGED
@@ -264,6 +264,88 @@ export function shouldExitOrphanedServe(opts: {
264
264
  return opts.idleTimeoutMs > 0 && opts.orphaned && opts.idleMs >= opts.idleTimeoutMs;
265
265
  }
266
266
 
267
+ export const LANDING_HEADERS = {
268
+ "Content-Type": "text/html; charset=utf-8",
269
+ "Cache-Control": "no-store",
270
+ "Referrer-Policy": "no-referrer",
271
+ "X-Content-Type-Options": "nosniff",
272
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
273
+ };
274
+
275
+ /**
276
+ * The page a shared connection URL shows in a browser: install-and-connect commands to copy,
277
+ * one per shell. It is static; the script reads the full URL (with its #key) from the address
278
+ * bar, shows it with the key masked, and copies it whole.
279
+ */
280
+ export function landingPage(): string {
281
+ return `<!doctype html>
282
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
283
+ <title>Connect to a shared Chrome · rechrome</title>
284
+ <style>
285
+ :root { --bg:#fff; --fg:#1d1d1f; --muted:#6e6e73; --card:#f5f5f7; --line:#d2d2d7; --accent:#0a66c2; --warn:#b25000; }
286
+ @media (prefers-color-scheme: dark) { :root { --bg:#161617; --fg:#f5f5f7; --muted:#a1a1a6; --card:#232325; --line:#3a3a3c; --accent:#4c9bff; --warn:#ffb86b; } }
287
+ * { box-sizing: border-box; }
288
+ body { margin:0; background:var(--bg); color:var(--fg); font:16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
289
+ main { max-width: 760px; margin: 0 auto; padding: 40px 16px 56px; }
290
+ h1 { font-size: 1.6rem; margin: 0 0 8px; }
291
+ p { margin: 0 0 16px; color: var(--muted); }
292
+ .row { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0 0 12px; }
293
+ .label { font-size: .8rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 6px; }
294
+ .cmd { display: flex; gap: 10px; align-items: flex-start; }
295
+ code { flex: 1; font: 13px/1.5 ui-monospace, "SF Mono", Menlo, Consolas, monospace; word-break: break-all; white-space: pre-wrap; }
296
+ button { flex: none; font: inherit; font-size: .9rem; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); cursor: pointer; }
297
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
298
+ .warn { color: var(--warn); font-weight: 600; }
299
+ .small { font-size: .85rem; }
300
+ a { color: var(--accent); }
301
+ </style></head>
302
+ <body><main>
303
+ <h1>Connect to a shared Chrome</h1>
304
+ <p>Someone shared a Chrome profile with you through <a href="https://github.com/snomiao/rechrome">rechrome</a>.
305
+ On your computer, inside the project folder that should use it, run:</p>
306
+ <p id="nokey" class="warn" hidden>This link is missing its key (the part after #key=). Ask for the full link from <code>rech share</code>.</p>
307
+ <div id="cmds"></div>
308
+ <p class="small">Needs <a href="https://bun.sh">Bun</a>. Then try <code>rechrome open https://example.com</code>.
309
+ This link contains a secret key: anyone with it can use this browser profile, so share it privately.</p>
310
+ </main>
311
+ <script>
312
+ (() => {
313
+ const url = location.href;
314
+ if (!/[#&?]key=/.test(location.hash)) document.getElementById("nokey").hidden = false;
315
+ const posix = s => "'" + s.replace(/'/g, "'\\\\''") + "'";
316
+ const pwsh = s => '"' + s.replace(/[\`"$]/g, c => "\`" + c) + '"';
317
+ const cmd = s => '"' + s.replace(/"/g, "%22") + '"';
318
+ const shells = [
319
+ ["macOS / Linux", "bun i -g rechrome && rechrome connect " + posix(url)],
320
+ ["Windows PowerShell", "bun i -g rechrome; rechrome connect " + pwsh(url)],
321
+ ["Windows cmd", "bun i -g rechrome && rechrome connect " + cmd(url)],
322
+ ];
323
+ const mask = s => s.replace(/(key=)[^&"'\`]+/, "$1…");
324
+ const root = document.getElementById("cmds");
325
+ for (const [label, text] of shells) {
326
+ const row = document.createElement("div"); row.className = "row";
327
+ const name = document.createElement("div"); name.className = "label"; name.textContent = label;
328
+ const line = document.createElement("div"); line.className = "cmd";
329
+ const code = document.createElement("code"); code.textContent = mask(text);
330
+ const button = document.createElement("button"); button.type = "button"; button.textContent = "Copy";
331
+ button.setAttribute("aria-label", "Copy the " + label + " command");
332
+ button.addEventListener("click", async () => {
333
+ try { await navigator.clipboard.writeText(text); }
334
+ catch {
335
+ // Plain-HTTP pages (e.g. a LAN address) have no clipboard API: copy via a hidden textarea.
336
+ const area = document.createElement("textarea"); area.value = text; document.body.append(area);
337
+ area.select(); document.execCommand("copy"); area.remove();
338
+ }
339
+ button.textContent = "Copied"; setTimeout(() => { button.textContent = "Copy"; }, 1500);
340
+ });
341
+ line.append(code, button); row.append(name, line); root.append(row);
342
+ }
343
+ })();
344
+ </script>
345
+ </body></html>
346
+ `;
347
+ }
348
+
267
349
  export async function serve() {
268
350
  // The daemon owns logs/ and output/, so it migrates them before writing anything.
269
351
  const migrated = migrateLegacyDataDir();
@@ -381,6 +463,10 @@ export async function serve() {
381
463
  consecutiveTimeouts, degraded: degraded || !healthy,
382
464
  });
383
465
  }
466
+ // A browser opening a shared URL gets copyable connect instructions. The key stays in the
467
+ // #fragment, which browsers never send, so the page is static and the key never reaches here.
468
+ if (reqUrl.pathname === "/" && req.method === "GET" && (req.headers.get("accept") ?? "").includes("text/html"))
469
+ return new Response(landingPage(), { headers: LANDING_HEADERS });
384
470
  if (reqUrl.pathname !== "/run") return new Response("rech server\n");
385
471
  const denied = authCheck(req, key);
386
472
  if (denied) return denied;