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

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), where poolSize is 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 via entropyApproximate and entropyNote, 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.

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-test on 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.yml is a manual-only template. If you want a push-to-deploy hook, set a DEPLOY_URL repository variable (the Dokploy deploy webhook URL) and switch the pipeline trigger back to event: push on trunk.

License

MIT for the code. The bundled word lists are CC BY 3.0, © Electronic Frontier Foundation.

S
Description
No description provided
Readme
107 KiB
Languages
JavaScript 84.1%
CSS 7.5%
HTML 7.3%
Shell 0.6%
Dockerfile 0.5%