Files
reze 1965c6efaa reze: Fix unicode splitting, strict booleans, symbol validation and entropy
Six fixes, each with a test:

1. Character pools are built as arrays of code points, so astral characters
   (emoji) are never split into lone surrogates. Entropy counts code points.
2. Boolean options are parsed strictly: true/false, 1/0, "true"/"false",
   "1"/"0", "yes"/"no", "on"/"off" (case-insensitive). Anything else
   is a 400 naming the option instead of a silent truthy fallback.
3. customSymbols is validated: at most 64 code points, duplicates dropped,
   control characters and whitespace rejected with a 400.
4. Random mode entropy with requireEach now counts only the passwords that
   contain a character of every enabled class (inclusion-exclusion). The
   value is an upper bound and is labelled as such in the API
   (entropyApproximate/entropyNote) and in the UI.
5. docker-compose.yml uses expose instead of ports, so it no longer
   collides with Dokploy's own port 3000.
6. SIGTERM/SIGINT shut the server down gracefully: close the listener, let
   in-flight requests finish, force-exit after 5s. Measured at ~60ms.

The server now exports createServer() so the API can be tested over HTTP
without binding a fixed port.
2026-10-21 09:26:32 +00:00

243 lines
4.9 KiB
CSS

:root {
--bg: #fafafa;
--fg: #1a1a1a;
--muted: #666;
--line: #ddd;
--code-bg: #f0f0f0;
--link: #0b5cad;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #111;
--fg: #e6e6e6;
--muted: #999;
--line: #333;
--code-bg: #1c1c1c;
--link: #c4a7ff;
}
}
:root[data-theme="dark"] {
--bg: #111;
--fg: #e6e6e6;
--muted: #999;
--line: #333;
--code-bg: #1c1c1c;
--link: #c4a7ff;
color-scheme: dark;
}
:root[data-theme="light"] { color-scheme: light; }
[hidden] { display: none !important; }
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
main {
max-width: 42rem;
margin: 0 auto;
padding: 3rem 1.25rem;
}
h1 { font-size: 1.75rem; margin: 0 0 2rem; }
h2 {
font-size: .8rem;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--muted);
margin: 2.5rem 0 .75rem;
}
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
.topbar { display: flex; justify-content: flex-end; margin: -1.5rem 0 1.5rem; }
.theme-toggle {
padding: .25rem .7rem;
background: transparent;
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
font-family: inherit;
font-size: .85rem;
line-height: 1.4;
cursor: pointer;
}
.theme-toggle:hover { border-color: var(--muted); }
.theme-toggle:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
dl {
display: grid;
grid-template-columns: max-content 1fr;
gap: .5rem 1.5rem;
margin: 0;
}
dt { color: var(--muted); }
dd { margin: 0; overflow-wrap: anywhere; }
.key { margin: 0 0 1.25rem; }
.key p { margin: 0 0 .35rem; color: var(--muted); font-size: .9rem; }
pre {
margin: 0;
padding: .75rem 1rem;
background: var(--code-bg);
border: 1px solid var(--line);
border-radius: 6px;
overflow-x: auto;
font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
main.error h2 { margin-top: 0; }
main.error h1 { margin-bottom: .75rem; }
main.error p { color: var(--muted); margin: 0 0 2rem; }
.lead { margin: -1.5rem 0 0; color: var(--muted); }
.hint { margin: -.25rem 0 1.25rem; color: var(--muted); font-size: .85rem; }
.radios { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; }
.radio,
.check {
display: flex;
align-items: center;
gap: .5rem;
font-size: .95rem;
}
.radio input,
.check input {
width: 1rem;
height: 1rem;
margin: 0;
accent-color: var(--link);
}
.checks { display: grid; gap: .4rem; margin-bottom: 1rem; }
.field { margin-bottom: 1rem; }
.field > label {
display: block;
margin-bottom: .3rem;
color: var(--muted);
font-size: .9rem;
}
input[type="text"],
input[type="number"],
select {
width: 100%;
max-width: 24rem;
padding: .4rem .55rem;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
font: inherit;
font-size: .95rem;
}
select { cursor: pointer; }
input[type="range"] {
width: 100%;
max-width: 24rem;
accent-color: var(--link);
}
.range-row { display: flex; align-items: center; gap: .75rem; }
.range-row output {
min-width: 2.5rem;
color: var(--muted);
font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
input:focus-visible,
select:focus-visible,
button:focus-visible {
outline: 2px solid var(--link);
outline-offset: 2px;
}
.button {
padding: .45rem .9rem;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
font-family: inherit;
font-size: .9rem;
line-height: 1.4;
cursor: pointer;
}
.button:hover { border-color: var(--muted); }
.button.primary {
background: var(--link);
color: #fff;
border-color: var(--link);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .button.primary { color: #111; }
}
:root[data-theme="dark"] .button.primary { color: #111; }
.actions { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1.25rem 0; }
.error-line {
margin: 0 0 1.25rem;
padding-left: .6rem;
border-left: 2px solid var(--link);
color: var(--fg);
font-size: .9rem;
}
.pw { margin-bottom: 1.5rem; }
.pw:last-child { margin-bottom: 0; }
.pw-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .5rem .9rem;
margin-top: .5rem;
color: var(--muted);
font-size: .85rem;
}
.pw-entropy { font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.strength { display: inline-flex; gap: 3px; }
.strength i {
display: inline-block;
width: 14px;
height: 6px;
border-radius: 3px;
background: var(--line);
}
.strength i.on { background: var(--link); }
.pw-strength-label { text-transform: capitalize; }
@media (max-width: 30rem) {
dl { grid-template-columns: 1fr; gap: 0; }
dd { margin-bottom: .75rem; }
input[type="text"],
input[type="number"],
select,
input[type="range"] { max-width: 100%; }
.actions .button { flex: 1 1 auto; }
}