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.
7.8 KiB
reze-test
A small, dependency-free password generator. It has two modes:
- Memorable — diceware-style passphrases shaped like
[num][word]-[num][word]-…, e.g.42abacus-91abdomen. - Random — fully random characters from a configurable character set.
Everything is generated server-side with Node's CSPRNG (crypto.randomInt). There is no
account, no login, no database, and nothing you generate is stored or logged.
Features
- Two randomization levels: memorable passphrases and fully random passwords.
- EFF word lists (7776-word and 1296-word) selectable per request.
- Configurable digits per word, digit separator, group separator and capitalization.
- Configurable length, character classes, custom symbol set, ambiguous-character exclusion and "at least one of each selected set".
- Generate up to 10 passwords at once, each with an entropy estimate and a strength bar.
- One-click copy with a clipboard fallback.
- Light/dark theme with a system-theme fallback, persisted in
localStorage. - Options persisted in
localStorage; generated passwords are never persisted. - Unicode-safe: character pools are code points, so emoji are never split.
- Strict option validation — bad booleans and symbol sets are rejected, not guessed.
- No npm dependencies at all — Node.js built-ins only.
Requirements
- Node.js 22 or newer (ESM,
"type": "module").
Run locally
node server.js
The server listens on 0.0.0.0:3000 by default. Override the port with PORT:
PORT=8080 node server.js
Open http://localhost:3000.
Or via the POSIX sh helper, which defaults PORT to 3000:
sh scripts/run.sh
PORT=8080 sh scripts/run.sh
Tests
node --test
API
All API responses are JSON. Every response carries the security headers
X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer,
X-Frame-Options: DENY and a strict Content-Security-Policy.
GET /healthz
Liveness probe used by the Docker HEALTHCHECK.
curl -s localhost:3000/healthz
{"status":"ok"}
GET /api/meta
Describes the available word lists.
curl -s localhost:3000/api/meta
{"lists":[{"id":"eff-large","name":"EFF Large Word List","size":7776},{"id":"eff-short","name":"EFF Short Word List (2.0)","size":1296}]}
POST /api/generate
Body is a JSON object. mode selects the algorithm; count (1–10, default 1) is the
number of passwords to return. Unknown modes, malformed JSON and out-of-range options
are rejected with 400 and a JSON {"error": "…"} body. Request bodies are limited to
64 KB.
Memorable options: words (2–10), digits (0–4), digitSeparator ('' or '-'),
separator ('-', '_', '.', ' ' or ''), capitalization
('none', 'title', 'upper'), list ('eff-large', 'eff-short').
curl -s -X POST localhost:3000/api/generate \
-H 'Content-Type: application/json' \
-d '{"mode":"memorable","words":2,"digits":2,"separator":"-","digitSeparator":"","capitalization":"title","list":"eff-large","count":2}'
{"mode":"memorable","count":2,"passwords":[{"value":"07sample-29breach","mode":"memorable","entropy":39.1,"entropyApproximate":false},{"value":"69reassign-85probation","mode":"memorable","entropy":39.1,"entropyApproximate":false}],"entropy":39.1,"entropyApproximate":false,"entropyNote":"exact: every character is drawn uniformly from the enabled sets"}
Random options: length (8–64, in code points), useLower, useUpper, useDigits,
useSymbols, customSymbols (replaces the default symbol set when non-empty),
excludeAmbiguous (removes 0 O 1 l I |) and requireEach.
The boolean options are parsed strictly: true/false, 1/0, "true"/"false",
"1"/"0", "yes"/"no" and "on"/"off" (case-insensitive) are accepted, anything
else is rejected with a 400 that names the option. "no" really means off, it is not
treated as a truthy string.
customSymbols is validated: at most 64 code points, duplicates are dropped, and
control characters or whitespace are rejected with a 400. Characters are handled as
code points, so emoji and other astral characters are never split in half.
curl -s -X POST localhost:3000/api/generate \
-H 'Content-Type: application/json' \
-d '{"mode":"random","length":24,"useLower":true,"useUpper":true,"useDigits":true,"useSymbols":true,"requireEach":true,"count":1}'
{"mode":"random","count":1,"passwords":[{"value":"3V#P@bQ2C4c3;=igw}E(EpnG","mode":"random","entropy":154.6,"entropyApproximate":true}],"entropy":154.6,"entropyApproximate":true,"entropyNote":"upper bound: the generator samples the valid passwords non-uniformly"}
Entropy
- Random mode without
requireEach:length * log2(poolSize), wherepoolSizeis the number of code points in the union of all enabled character classes. - Random mode with
requireEach: the number of passwords that contain at least one character of every enabled class (inclusion-exclusion over the classes), converted to bits. Because the generator samples those passwords non-uniformly, the value is an upper bound; the response says so viaentropyApproximateandentropyNote, and the UI marks it with a~and "approx.". - Memorable mode:
words * log2(listSize) + words * digits * log2(10). Separators and the capitalization scheme add no entropy. This value is exact.
Every response carries entropy, entropyApproximate and entropyNote, and each
password carries its own entropy and entropyApproximate.
Other paths
Unknown /api/* paths return a JSON 404. Unknown page paths return
public/404.html with HTTP 404. Only GET and HEAD are accepted for static files.
Word lists
| id | list | size |
|---|---|---|
eff-large |
EFF Large Word List | 7776 |
eff-short |
EFF Short Word List (2.0) | 1296 |
Both files live in public/words/ and are loaded once at startup.
- Source: Electronic Frontier Foundation — the EFF Diceware wordlists.
- Licence: Creative Commons Attribution 3.0 Unported (CC BY 3.0), https://www.eff.org/copyright.
The lists contain a few hyphenated entries (drop-down, felt-tip, t-shirt, yo-yo),
which are used verbatim.
Environment variables
| variable | default | description |
|---|---|---|
PORT |
3000 |
TCP port the HTTP server binds to on 0.0.0.0. |
NODE_ENV |
— | Set to production in the image. |
Docker
docker build -t reze-test .
docker run --rm -p 3000:3000 reze-test
Or with compose:
docker compose up --build -d
The image is a single stage based on node:22-alpine, runs as the non-root node user
and ships a HEALTHCHECK that polls http://127.0.0.1:3000/healthz with wget.
docker-compose.yml uses expose instead of ports, so the service is only reachable
on the container network — publishing it on the host would collide with Dokploy's own
port 3000.
SIGTERM and SIGINT shut the server down gracefully: it stops accepting connections,
lets the in-flight requests finish and exits, with a 5 second forced-exit fallback. That
keeps docker stop fast instead of waiting for the full 10 second timeout.
Deployment (Dokploy)
- Repository:
druid520/reze-teston Gitea (git.kg.boson.party). - Branch:
trunk. - Build type: Dockerfile (or "Nixpacks" — the app needs nothing but Node).
- Environment variable:
PORT=3000. - Exposed port:
3000. - Deploys are triggered from the Dokploy UI (or a Gitea webhook configured there).
CI does not deploy automatically:
.woodpecker.ymlis a manual-only template. If you want a push-to-deploy hook, set aDEPLOY_URLrepository variable (the Dokploy deploy webhook URL) and switch the pipeline trigger back toevent: pushontrunk.
License
MIT for the code. The bundled word lists are CC BY 3.0, © Electronic Frontier Foundation.