Files
reze-test/README.md
T
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

214 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```sh
node server.js
```
The server listens on `0.0.0.0:3000` by default. Override the port with `PORT`:
```sh
PORT=8080 node server.js
```
Open <http://localhost:3000>.
Or via the POSIX sh helper, which defaults `PORT` to 3000:
```sh
sh scripts/run.sh
PORT=8080 sh scripts/run.sh
```
## Tests
```sh
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`.
```sh
curl -s localhost:3000/healthz
```
```json
{"status":"ok"}
```
### `GET /api/meta`
Describes the available word lists.
```sh
curl -s localhost:3000/api/meta
```
```json
{"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'`).
```sh
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}'
```
```json
{"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.
```sh
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}'
```
```json
{"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.
- Source: [Electronic Frontier Foundation](https://www.eff.org/dice) — the
[EFF Diceware wordlists](https://www.eff.org/dice-roll-the-dice).
- 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
```sh
docker build -t reze-test .
docker run --rm -p 3000:3000 reze-test
```
Or with compose:
```sh
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.