web: state the no-server/in-browser privacy clearly + self-host fonts

- Make the privacy promise true: bundle the fonts (Poppins/Inter/JetBrains Mono
  latin subsets + the text-security mask font) under helwasm/assets/fonts/ and
  drop the Google Fonts + jsDelivr links. The page now makes zero external
  requests once loaded — it talks to no third party.
- Say it plainly throughout the UI: a "Private by design" callout in the About
  panel, the meta description, the quick-gen and console hints, and the footer —
  nothing you type leaves the browser, no server, no account, no tracking.
This commit is contained in:
Oleksandr Kozachuk
2026-06-08 20:42:33 +02:00
parent 3031105000
commit ba884037b0
10 changed files with 35 additions and 18 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18 -12
View File
@@ -5,17 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0a2540" /> <meta name="theme-color" content="#0a2540" />
<title>LesS/KEY — password generator</title> <title>LesS/KEY — password generator</title>
<meta name="description" content="A password manager that stores nothing and never talks to a server. Passwords are generated entirely in your browser with WebAssembly; your master password and your data never leave your device." />
<link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon-16x16.png" />
<link rel="icon" href="./assets/favicon.ico" sizes="any" /> <link rel="icon" href="./assets/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" sizes="180x180" href="./assets/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="./assets/apple-touch-icon.png" />
<link rel="manifest" href="./assets/site.webmanifest" /> <link rel="manifest" href="./assets/site.webmanifest" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <!-- Fonts are self-hosted in style.css (./assets/fonts/) — no external requests. -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" href="./style.css" />
</head> </head>
<body> <body>
@@ -49,6 +45,13 @@
master always produce the same password — so there is nothing secret to leak, master always produce the same password — so there is nothing secret to leak,
sync, or back up, and you only ever remember one phrase. sync, or back up, and you only ever remember one phrase.
</p> </p>
<p class="about-privacy">
<strong>Private by design.</strong> Everything happens on your device. Your master
password and the passwords it generates are never sent anywhere, never stored on a
server, and never logged — there is no account, no sync, and no tracking. The app is a
single page plus a WebAssembly module, with its fonts bundled in; once it has loaded,
it makes no network requests at all.
</p>
<div class="about-grid"> <div class="about-grid">
<div> <div>
<h4>Quick password</h4> <h4>Quick password</h4>
@@ -62,8 +65,9 @@
<h4>Console</h4> <h4>Console</h4>
<p> <p>
The full command line — type <code>help</code> for every command. Your list of The full command line — type <code>help</code> for every command. Your list of
names lives only in this browser (localStorage); use <strong>Export</strong> / names lives only in this browser (localStorage), never on a server; use
<strong>Import</strong> to move it (e.g. to and from a Notion page). <strong>Export</strong> / <strong>Import</strong> to move it yourself (e.g. to and
from a Notion page).
</p> </p>
</div> </div>
<div> <div>
@@ -77,7 +81,7 @@
</div> </div>
<p class="about-note"> <p class="about-note">
Runs the real hel engine compiled to WebAssembly — the exact same generator as the Runs the real hel engine compiled to WebAssembly — the exact same generator as the
command-line tool. Nothing is ever sent to a server. command-line tool. No server, no account, no tracking — nothing to leak.
</p> </p>
</div> </div>
</details> </details>
@@ -88,8 +92,9 @@
<h2>Quick password</h2> <h2>Quick password</h2>
<p class="hint"> <p class="hint">
Deterministic — the same name + master always regenerate the same Deterministic — the same name + master always regenerate the same
password. The result updates live as you type. Nothing is stored unless password. The result updates live as you type, entirely in your browser —
you press <strong>Store</strong>. your master and the generated password never leave this device. Nothing is
stored unless you press <strong>Store</strong>.
</p> </p>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
@@ -132,6 +137,7 @@
<footer class="foot wrap"> <footer class="foot wrap">
Runs the real hel core compiled to WebAssembly — same generator as the CLI. Runs the real hel core compiled to WebAssembly — same generator as the CLI.
Everything runs in your browser; nothing you type is ever sent to a server.
</footer> </footer>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
@@ -470,7 +476,7 @@
bg.addEventListener("click", (e) => e.target === bg && bg.classList.remove("show")) bg.addEventListener("click", (e) => e.target === bg && bg.classList.remove("show"))
); );
appendLine('<span class="muted"># LesS/KEY ready — runs the real hel core via WASM. Try: help</span>'); appendLine('<span class="muted"># LesS/KEY ready — runs entirely in your browser (WASM); nothing is sent to a server. Try: help</span>');
} }
boot(); boot();
</script> </script>
+17 -6
View File
@@ -20,14 +20,18 @@
--radius: 16px; --radius: 16px;
} }
/* Self-hosted fonts — bundled with the app so it makes NO external requests and
talks to no third party (no Google Fonts, no CDN). Latin subset only. */
@font-face { font-family:"Poppins"; font-style:normal; font-weight:600; font-display:swap; src:url("./assets/fonts/Poppins-600.woff2") format("woff2"); }
@font-face { font-family:"Poppins"; font-style:normal; font-weight:700; font-display:swap; src:url("./assets/fonts/Poppins-700.woff2") format("woff2"); }
@font-face { font-family:"Inter"; font-style:normal; font-weight:400; font-display:swap; src:url("./assets/fonts/Inter-400.woff2") format("woff2"); }
@font-face { font-family:"Inter"; font-style:normal; font-weight:500; font-display:swap; src:url("./assets/fonts/Inter-500.woff2") format("woff2"); }
@font-face { font-family:"Inter"; font-style:normal; font-weight:600; font-display:swap; src:url("./assets/fonts/Inter-600.woff2") format("woff2"); }
@font-face { font-family:"JetBrains Mono"; font-style:normal; font-weight:400; font-display:swap; src:url("./assets/fonts/JetBrainsMono-400.woff2") format("woff2"); }
@font-face { font-family:"JetBrains Mono"; font-style:normal; font-weight:500; font-display:swap; src:url("./assets/fonts/JetBrainsMono-500.woff2") format("woff2"); }
/* Masked-secret font: dots glyphs so the real chars stay selectable/copyable. /* Masked-secret font: dots glyphs so the real chars stay selectable/copyable.
WebKit/Blink use -webkit-text-security; Firefox falls back to this font. */ WebKit/Blink use -webkit-text-security; Firefox falls back to this font. */
@font-face { @font-face { font-family:"text-security-disc"; font-display:swap; src:url("./assets/fonts/text-security-disc.woff2") format("woff2"); }
font-family: "text-security-disc";
src: url("https://cdn.jsdelivr.net/npm/text-security/dist/text-security-disc.woff2") format("woff2"),
url("https://cdn.jsdelivr.net/npm/text-security/dist/text-security-disc.woff") format("woff");
font-display: swap;
}
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
html { height: 100%; } html { height: 100%; }
@@ -115,6 +119,13 @@ main { padding: clamp(20px, 3vw, 34px) 0 56px; display: grid; gap: clamp(16px, 2
.about-grid h4 { font-family: var(--disp); font-weight: 600; font-size: 14px; color: var(--ink); margin-bottom: 5px; } .about-grid h4 { font-family: var(--disp); font-weight: 600; font-size: 14px; color: var(--ink); margin-bottom: 5px; }
.about-grid p { font-size: 13.5px; } .about-grid p { font-size: 13.5px; }
.about-note { font-family: var(--mono); font-size: 12px; margin-top: 10px; } .about-note { font-family: var(--mono); font-size: 12px; margin-top: 10px; }
/* Privacy callout — make the "no server" promise visually prominent. */
.about-privacy {
margin: 6px 0 2px; padding: 13px 16px; border-radius: 12px;
border: 1px solid var(--line); border-left: 3px solid var(--ocean);
background: rgba(27, 65, 97, 0.045); color: var(--ink); font-size: 14.5px; max-width: 76ch;
}
.about-privacy strong { color: var(--ocean); }
@media (max-width: 680px) { .about-grid { grid-template-columns: 1fr; } } @media (max-width: 680px) { .about-grid { grid-template-columns: 1fr; } }
/* Cards */ /* Cards */