knowless 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,75 @@ Versioning is [SemVer](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ - **Turnkey Docker image** (`knowless/knowless-server:0.2.x`)
11
+ bundling Postfix + null-route + the binary so a self-hoster
12
+ runs `docker compose up` and has a working auth gateway in
13
+ one step. Material UX win for the PRD §4.2 self-hoster
14
+ audience. Targeted for v0.2.0.
10
15
  - Caddy forward-auth Docker integration test (TASKS.md 6.8).
16
+ - `knowless-server --check-null-route`: CLI probe that submits a
17
+ test message to `shamRecipient` and confirms the local MTA
18
+ discarded it. Targeted for v0.2.0.
19
+
20
+ ## [0.1.10] — 2026-04-28
21
+
22
+ addypin manual smoke continued. Two DX docs improvements; no code
23
+ changes.
24
+
25
+ ### Documentation
26
+
27
+ - **GUIDE: "Local development setup" section (AF-16).** Covers the
28
+ five flags that turn knowless from "production-tuned, defensive"
29
+ to "developer-friendly, get-out-of-my-way" — `cookieSecure: false`,
30
+ `devLogMagicLinks: true`, `maxLoginRequestsPerIpPerHour: 0`,
31
+ `maxNewHandlesPerIpPerHour: 0`, `openRegistration: true`. Each
32
+ flag explained with what it solves and a sharp warning about
33
+ shipping it. Considered auto-disabling rate limits whenever
34
+ `devLogMagicLinks: true` to save typing, but rejected the
35
+ coupling — operators turning on `devLogMagicLinks` briefly to
36
+ debug a single email in prod should NOT have rate limits silently
37
+ dropped at the same time.
38
+ - **GUIDE: silent-miss debug line is now promoted as a feature
39
+ (AF-17).** The `[knowless dev:<from>] silent-miss: handle for
40
+ "X" does not exist (openRegistration=false)` stderr hint
41
+ introduced in AF-7.2 was buried in the CHANGELOG; it now leads
42
+ the dev-setup section. First-time closed-reg friction was costing
43
+ every adopter the same ~30 min; the hint cuts that to seconds
44
+ but only if you know it exists.
45
+
46
+ ## [0.1.9] — 2026-04-28
47
+
48
+ addypin manual smoke turned up one real bug, one defaults footgun,
49
+ and one DX gap.
50
+
51
+ ### Fixed
52
+
53
+ - **`auth.deriveHandle(email)` now normalizes the email before
54
+ HMAC (AF-13).** Prior versions skipped `normalize()` while
55
+ `auth.startLogin` and `POST /login` ran it — adopters using
56
+ `deriveHandle` to precompute owner-keyed lookups got silent
57
+ handle mismatches whenever email casing varied between
58
+ create-time and click-time. Symptom was "user's records
59
+ disappear after login," which is awful to debug. The bare
60
+ `deriveHandle(emailNormalized, secret)` re-export still
61
+ expects pre-normalized input — that contract is unchanged.
62
+
63
+ ### Documentation
64
+
65
+ - **GUIDE flags the `failureRedirect` Mode-A footgun (AF-14).**
66
+ Adopters running programmatic-only (`startLogin` without
67
+ mounting `loginForm`) hit a default `failureRedirect = /login`
68
+ pointing at a route they don't serve — expired/replayed
69
+ magic-link clicks 302 to a 404. The GUIDE now leads with this
70
+ in the Mode-A walkthrough and adds a callout in the config
71
+ table. Default unchanged to avoid breaking Mode-B users with
72
+ custom paths.
73
+ - **OPS.md §11b — MailHog dev workflow (AF-15).** `docker run
74
+ mailhog/mailhog`, point knowless at port 1025, inspect every
75
+ outgoing mail (including sham submissions) in a UI at port
76
+ 8025. Verifies `bodyFooter`, `subjectOverride`, and the
77
+ URL-line-isn't-QP-soft-broken invariant without spinning up
78
+ real Postfix.
11
79
 
12
80
  ## [0.1.8] — 2026-04-28
13
81
 
package/GUIDE.md CHANGED
@@ -62,8 +62,8 @@ built-in auth is either missing or weak. The existing alternatives
62
62
  (Authelia, Authentik, Keycloak, oauth2-proxy) are heavyweight for
63
63
  the job: "redirect to login if no cookie, otherwise let through."
64
64
 
65
- knowless's standalone server (v0.2.0, in development) sits behind
66
- Caddy / nginx / Traefik via forward-auth. One auth subdomain, one
65
+ knowless's standalone server (`bin/knowless-server`, shipped in
66
+ v0.1.3) sits behind Caddy / nginx / Traefik via forward-auth. One auth subdomain, one
67
67
  session cookie scoped to the parent eTLD+1, SSO across all your
68
68
  services for free.
69
69
 
@@ -106,7 +106,7 @@ nothing else*.
106
106
  - **Walks away at v1.0.0.** Maintenance mode (security patches +
107
107
  bug fixes) after that, by design.
108
108
 
109
- ## Walkthrough: library mode (v0.1.0)
109
+ ## Walkthrough: library mode
110
110
 
111
111
  The shape: import `knowless`, configure it, mount the handlers on
112
112
  your HTTP framework.
@@ -175,7 +175,9 @@ sending domain):
175
175
  Without all three, Gmail / Outlook will silently drop your auth
176
176
  mail. This is the operator commitment knowless asks of you.
177
177
 
178
- > Full Postfix walkthrough lives in `OPS.md` (shipping with v0.2.0).
178
+ > Full Postfix walkthrough lives in [`OPS.md`](OPS.md) Postfix
179
+ > install, null-route, SPF/DKIM/PTR, systemd, reverse-proxy
180
+ > forward-auth examples, multi-process deployments.
179
181
 
180
182
  ### Step 4: Mount the handlers
181
183
 
@@ -295,7 +297,19 @@ callers. See SPEC §7.3a for the full contract.
295
297
 
296
298
  `auth.deriveHandle(email)` returns the same opaque HMAC handle
297
299
  that the form path uses, without you having to import the helper
298
- or pass the secret around.
300
+ or pass the secret around. The instance method **normalizes the
301
+ email** (lowercase, trim) before HMAC (AF-13), so `Alice@X.com`
302
+ and `alice@x.com` produce the same handle — match what the form
303
+ and `startLogin` would compute. The bare `deriveHandle` re-export
304
+ takes pre-normalized input; use the instance method unless you
305
+ have a specific reason to call the lower-level primitive.
306
+
307
+ > **Mode-A heads-up: set `failureRedirect`.** If you only mount
308
+ > `auth.callback` (not `auth.loginForm`), the default
309
+ > `failureRedirect` cascade points at `/login` — a route you
310
+ > don't serve. An expired or replayed magic-link click will 302
311
+ > to a 404. Set `failureRedirect: '/'` (or any route you do
312
+ > serve) when wiring Mode A.
299
313
 
300
314
  ### Step 5: Pre-seed users (closed-registration mode, default)
301
315
 
@@ -384,6 +398,59 @@ reverse proxy gates upstreams via `/verify` returning 200/401 +
384
398
  `handleFromRequest` — same answer, no sub-request round-trip, no
385
399
  header parsing.
386
400
 
401
+ ### Local development setup
402
+
403
+ Production defaults are tuned to bite bots, not to be friendly to a
404
+ developer hammering the same address from `127.0.0.1` for the
405
+ hundredth time. Use a dedicated dev config:
406
+
407
+ ```js
408
+ const auth = knowless({
409
+ // ...required fields
410
+ cookieSecure: false, // localhost-only HTTP origins (AF-4.4)
411
+ devLogMagicLinks: true, // print magic links to stderr when SMTP fails (AF-6.2)
412
+ maxLoginRequestsPerIpPerHour: 0, // disable per-IP login cap
413
+ maxNewHandlesPerIpPerHour: 0, // disable per-IP create cap
414
+ openRegistration: true, // skip the pre-seeding step in dev
415
+ });
416
+ ```
417
+
418
+ Why each flag matters in dev:
419
+
420
+ - **`cookieSecure: false`** — without it, `http://localhost` browsers
421
+ reject the session cookie silently. The library logs a stderr
422
+ warning at startup so you can't accidentally ship this to prod.
423
+ - **`devLogMagicLinks: true`** — when SMTP is unreachable (no local
424
+ Postfix yet), magic-link URLs print to stderr tagged
425
+ `[knowless dev:<from>] magic link: ...`. Click straight from the
426
+ terminal. **Bonus diagnostic** (AF-7.2): on a sham/silent-miss
427
+ path, you get `[knowless dev:<from>] silent-miss: handle for
428
+ "X" does not exist (openRegistration=false)` instead — surfaces
429
+ the closed-reg gotcha that costs everyone the same 30 minutes
430
+ the first time.
431
+ - **`maxLoginRequestsPerIpPerHour: 0` and `maxNewHandlesPerIpPerHour:
432
+ 0`** — disable per-IP rate caps. The defaults (30 / 3 per hour)
433
+ are sane for prod but shoot you in the foot during repeated test
434
+ runs. The counters **persist in the SQLite file** across process
435
+ restarts, so even rebooting the dev server doesn't clear them —
436
+ you'd have to delete the DB or wait an hour. Setting both to 0
437
+ in dev avoids the surprise.
438
+ - **`openRegistration: true`** — saves you from manually pre-seeding
439
+ every test email via `auth.deriveHandle` + your own store insert.
440
+
441
+ > **Don't ship this config.** Each of these flags weakens a specific
442
+ > defense. They are coupled to your environment, not to each other —
443
+ > intentionally. (We considered auto-disabling rate limits whenever
444
+ > `devLogMagicLinks` is true, but rejected: an operator turning on
445
+ > `devLogMagicLinks` to debug a single email in production should
446
+ > NOT have rate limits silently dropped at the same time.)
447
+
448
+ For end-to-end mail rendering checks (verify the `bodyFooter`,
449
+ inspect the magic-link line for QP soft-breaks, confirm the
450
+ right `subjectOverride` shipped), point dev knowless at MailHog
451
+ on `localhost:1025`. Setup walkthrough lives in
452
+ [`OPS.md` §11b](OPS.md).
453
+
387
454
  ### Step 7: GDPR right-to-erasure
388
455
 
389
456
  The store interface exposes `deleteHandle(handle)` — atomic delete
@@ -402,11 +469,10 @@ Library doesn't ship a built-in HTTP endpoint for this — operator
402
469
  chooses the UX (admin CLI, in-app self-service, ticket-driven
403
470
  support).
404
471
 
405
- ## Walkthrough: standalone server mode (v0.2.0, coming)
472
+ ## Walkthrough: standalone server mode
406
473
 
407
- The shape: run `npx knowless-server`, point Caddy / nginx /
408
- Traefik at it for forward-auth, protect any HTTP service behind
409
- magic-link login.
474
+ Run `npx knowless-server`, point Caddy / nginx / Traefik at it for
475
+ forward-auth, protect any HTTP service behind magic-link login.
410
476
 
411
477
  The deployment-shape pattern:
412
478
  ```
@@ -418,7 +484,9 @@ The deployment-shape pattern:
418
484
  [Caddy redirects to auth.example.com/login?next=...]
419
485
  ```
420
486
 
421
- Sample Caddyfile (forthcoming OPS.md will have the full setup):
487
+ Sample Caddyfile (full setup including TLS/ACME + multiple gated
488
+ services lives in [`OPS.md`](OPS.md) §7):
489
+
422
490
  ```caddy
423
491
  auth.example.com {
424
492
  reverse_proxy localhost:8080
@@ -427,7 +495,7 @@ auth.example.com {
427
495
  kuma.example.com {
428
496
  forward_auth localhost:8080 {
429
497
  uri /verify
430
- copy_headers X-User-Handle
498
+ copy_headers X-Knowless-Handle
431
499
  }
432
500
  reverse_proxy localhost:3001 # Uptime Kuma
433
501
  }
@@ -435,7 +503,7 @@ kuma.example.com {
435
503
  adguard.example.com {
436
504
  forward_auth localhost:8080 {
437
505
  uri /verify
438
- copy_headers X-User-Handle
506
+ copy_headers X-Knowless-Handle
439
507
  }
440
508
  reverse_proxy localhost:3000 # AdGuard Home
441
509
  }
@@ -444,9 +512,11 @@ adguard.example.com {
444
512
  One auth subdomain, one cookie, SSO across all gated services
445
513
  because the cookie is scoped to the parent eTLD+1.
446
514
 
447
- Until v0.2.0, you can replicate this yourself with ~30 lines of
448
- `node:http` wrapping the library-mode handlers — see
449
- `knowless.context.md` for the pattern.
515
+ Configuration is via `KNOWLESS_*` env vars see
516
+ [`config.example.env`](config.example.env) and run
517
+ `knowless-server --help` for the full list. `knowless-server
518
+ --config-check` validates your env, SMTP reachability, and DB
519
+ write access; suitable for systemd `ExecStartPre`.
450
520
 
451
521
  ## Configuration reference
452
522
 
@@ -479,7 +549,7 @@ Full options table:
479
549
  | `trustedProxies` | no | `['127.0.0.1', '::1']` | IPs allowed to set `X-Forwarded-For`. |
480
550
  | `shamRecipient` | no | `null@knowless.invalid` | Where sham mail goes (your MTA must discard it). |
481
551
  | `sweepIntervalMs` | no | `300000` | Sweeper tick (5 min default). |
482
- | `failureRedirect` | no | (= `loginPath`) | Where /auth/callback failures redirect. |
552
+ | `failureRedirect` | no | (= `loginPath`) | Where /auth/callback failures redirect. **Mode-A adopters:** if you don't mount `loginForm`, set this to a route you actually serve (e.g. `/`) — otherwise expired/replayed magic-link clicks 302 to a 404. |
483
553
  | `store` | no | (built-in better-sqlite3) | Inject your own store implementation. |
484
554
  | `mailer` | no | (built-in nodemailer) | Inject your own mailer. |
485
555
 
package/OPS.md CHANGED
@@ -589,6 +589,54 @@ want for forward-auth-style deployments anyway.
589
589
 
590
590
  ---
591
591
 
592
+ ## 11b. Local development without a real Postfix
593
+
594
+ Spinning up Postfix on a developer laptop just to inspect what
595
+ knowless sends is heavy. Two leaner options:
596
+
597
+ ### Option A — `devLogMagicLinks: true` (no MTA needed)
598
+
599
+ knowless already supports this for the URL. When SMTP submission
600
+ fails AND `devLogMagicLinks: true` is set, the magic link is
601
+ printed to stderr tagged `[knowless dev:<from>]`. Sufficient for
602
+ smoke-testing the click flow, not the email content. AF-6.2.
603
+
604
+ ### Option B — MailHog (visual UI for the rendered mail)
605
+
606
+ For verifying subject/body/footer rendering or the timing of
607
+ sham-vs-real submissions, run [MailHog](https://github.com/mailhog/MailHog)
608
+ or any compatible test SMTP catcher (e.g. mailpit, smtp4dev) on
609
+ the dev machine and point knowless at it.
610
+
611
+ ```sh
612
+ # Docker one-liner for MailHog
613
+ docker run --rm -p 1025:1025 -p 8025:8025 mailhog/mailhog
614
+ ```
615
+
616
+ Then in your dev knowless config:
617
+
618
+ ```js
619
+ const auth = knowless({
620
+ secret: process.env.KNOWLESS_SECRET,
621
+ baseUrl: 'http://localhost:3000',
622
+ from: 'auth@dev.local',
623
+ smtpHost: 'localhost',
624
+ smtpPort: 1025, // MailHog's SMTP port
625
+ cookieSecure: false, // localhost-only dev
626
+ });
627
+ ```
628
+
629
+ Open `http://localhost:8025` in your browser; every magic-link mail
630
+ (including sham submissions to `null@knowless.invalid`) shows up in
631
+ the inbox UI with full subject/body/headers visible. Perfect for
632
+ verifying `bodyFooter`, `subjectOverride`, the URL line is intact
633
+ without QP soft-breaks, etc.
634
+
635
+ > Don't ship MailHog into production. It accepts mail from anywhere
636
+ > and stores it forever — defeats the entire knowless threat model.
637
+
638
+ ---
639
+
592
640
  ## 12. Backup and recovery
593
641
 
594
642
  The only stateful file is the SQLite database (`KNOWLESS_DB_PATH`,
package/README.md CHANGED
@@ -7,7 +7,7 @@ that don't need to email their users for anything but the sign-in link.
7
7
  npm install knowless
8
8
  ```
9
9
 
10
- > v0.1.8 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
10
+ > v0.1.10 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
11
11
 
12
12
  ## What this is
13
13
 
@@ -1,7 +1,7 @@
1
1
  # knowless -- Integration Guide
2
2
 
3
3
  > For AI assistants and developers wiring knowless into a project.
4
- > v0.1.0 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
4
+ > v0.1.10 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
5
5
 
6
6
  ## What this is
7
7
 
@@ -18,11 +18,13 @@ npm install knowless
18
18
 
19
19
  Two integration paths:
20
20
 
21
- 1. **Library mode (v0.1.0):** `import { knowless } from 'knowless'` --
22
- mount five handlers on Express / Fastify / Hono / `node:http`
23
- 2. **Standalone server (v0.2.0, in development):** `npx knowless-server` --
24
- forward-auth gateway for Caddy / nginx / Traefik in front of
25
- no-auth services like Uptime Kuma, AdGuard, Pi-hole
21
+ 1. **Library mode:** `import { knowless } from 'knowless'` --
22
+ mount five handlers on Express / Fastify / Hono / `node:http`,
23
+ gate your endpoints with `auth.handleFromRequest(req)`.
24
+ 2. **Standalone server:** `npx knowless-server` -- forward-auth
25
+ gateway for Caddy / nginx / Traefik in front of no-auth services
26
+ like Uptime Kuma, AdGuard, Pi-hole. Configured via `KNOWLESS_*`
27
+ env vars; see [`OPS.md`](OPS.md) for the full deployment guide.
26
28
 
27
29
  This document is the dense reference. For the why, see
28
30
  `docs/01-product/PRD.md`. For the wire formats, see
@@ -148,7 +150,7 @@ const auth = knowless({
148
150
  | `deleteHandle` | (handle: string) | void | Atomic delete of handle + tokens + sessions (FR-37a, GDPR) |
149
151
  | `revokeSessions` | (handle: string) | number | Drops every session for `handle` without deleting the account ("log out everywhere"). Returns rows removed. AF-6.1. |
150
152
  | `startLogin` | ({email, nextUrl?, sourceIp?, subjectOverride?, bypassRateLimit?}) | Promise\<{handle, submitted: true}\> | Programmatic magic-link send for "use first, claim later" flows. Same 12-step sham-work as form. `subjectOverride` (AF-9) replaces `cfg.subject` per call. `bypassRateLimit: true` (AF-10) opts trusted server-side callers (CLI, cron, worker) out of IP-rate-limit accounting. SPEC §7.3a. AF-7.3. |
151
- | `deriveHandle` | (email: string) | string | HMAC-SHA256(secret, normalize(email)) using the configured secret. Use to compute owner-handles outside HTTP context. AF-7.4. |
153
+ | `deriveHandle` | (email: string) | string | `HMAC-SHA256(secret, normalize(email))` using the configured secret. Normalizes input (lowercase + trim) so `Alice@X.com` and `alice@x.com` produce the same handle. Match what `startLogin` and `POST /login` compute. AF-7.4 / AF-13. |
152
154
  | `_sweep` | -- | void | Trigger one sweep tick on demand (tests, operator scripts). AF-5.3. |
153
155
  | `config` | -- | object | Merged effective config; safe to read (do not mutate) |
154
156
  | `close` | -- | void | Stops sweeper, closes mailer + store. Call on shutdown. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knowless",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Small, opinionated, full-stack passwordless auth for Node.js services that don't need to email their users for anything but the sign-in link.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createStore } from './store.js';
2
2
  import { createMailer, validateBodyFooter } from './mailer.js';
3
3
  import { createHandlers } from './handlers.js';
4
- import { deriveHandle as deriveHandleRaw } from './handle.js';
4
+ import { deriveHandle as deriveHandleRaw, normalize } from './handle.js';
5
5
 
6
6
  /** Default sweeper tick: 5 minutes. Per FR-13. */
7
7
  const DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
@@ -153,9 +153,12 @@ export function knowless(options = {}) {
153
153
  * See SPEC §7.3a. AF-7.3. */
154
154
  startLogin: handlers.startLogin,
155
155
  /** Derive the opaque handle for an email using the configured
156
- * secret. Lets adopters compute owner-handles outside HTTP context
157
- * without spreading the secret across modules. AF-7.4. */
158
- deriveHandle: (email) => deriveHandleRaw(email, options.secret),
156
+ * secret. Normalizes the email first (AF-13) so handles match
157
+ * what `auth.startLogin` and `POST /login` would compute for the
158
+ * same address typed with different casing or surrounding
159
+ * whitespace. Adopters should treat this as the canonical handle
160
+ * derivation. AF-7.4 / AF-13. */
161
+ deriveHandle: (email) => deriveHandleRaw(normalize(email), options.secret),
159
162
  /** Effective config (with defaults applied), useful for routing. */
160
163
  config: handlers._config,
161
164
  /** Run a sweep tick on demand. Useful for tests and operator scripts. */