knowless 1.1.6 → 1.1.8

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
@@ -30,6 +30,80 @@ v1.0.0 are:
30
30
  - Documentation corrections
31
31
  - Helper exports that pull existing mechanism back into the library
32
32
 
33
+ ## [1.1.8] — 2026-05-22
34
+
35
+ Documentation-only release. A security review found the forward-auth
36
+ proxy recipes told operators to copy an identity header
37
+ (`X-Knowless-Handle`) that knowless never sets — the handler, SPEC
38
+ §9.2, the `knowless.context.md` API table, and the integration test
39
+ all use `X-User-Handle`. The recipes were the lone outliers. Effect
40
+ in a real deployment: the verified handle never reaches the upstream
41
+ (broken identity propagation), and because the auth response carries
42
+ no header to copy, a client-supplied `X-User-Handle` is not
43
+ overwritten by the recipe — depending on the proxy's copy semantics
44
+ that is an impersonation path. Corrected the recipes to the canonical
45
+ name and documented the trust boundary so the class can't silently
46
+ reopen. No code changes; the public contract (`X-User-Handle`) was
47
+ already correct and stays unchanged.
48
+
49
+ ### Fixed
50
+
51
+ - `OPS.md` §7, `GUIDE.md` §forward-auth — corrected the identity
52
+ header in every Caddy / nginx / Traefik recipe from
53
+ `X-Knowless-Handle` to `X-User-Handle` (6 references, including the
54
+ nginx `$upstream_http_x_user_handle` variable). Now matches
55
+ `src/handlers.js`, `docs/02-design/SPEC.md` §9.2, and the
56
+ `knowless.context.md` API table.
57
+
58
+ ### Documented
59
+
60
+ - `OPS.md` §7, `knowless.context.md` § "Forward-auth" — added an
61
+ "Identity-header trust boundary" note: the proxy must overwrite or
62
+ strip any client-supplied `X-User-Handle` (never append), and the
63
+ upstream should reject the header from any source but the proxy.
64
+ This is the load-bearing forward-auth invariant; making it explicit
65
+ guards against a future name drift re-introducing the gap.
66
+ - `OPS.md` §1, `docs/01-product/PRD.md` §10.1 — corrected the runtime
67
+ floor from "Node.js ≥ 20" to "≥ 22.5". The `node:sqlite`
68
+ (`DatabaseSync`) import hard-crashes below 22.5; `engines.node`,
69
+ README, `knowless.context.md`, GUIDE, and PRD §16.4 already stated
70
+ 22.5, so §10.1 and the OPS prerequisite were the stale outliers.
71
+
72
+ ## [1.1.7] — 2026-05-11
73
+
74
+ Documentation-only release. Threat-model wording was being read as
75
+ an oversight rather than a principled boundary — adopters kept
76
+ returning to compromised-inbox scenarios because the README didn't
77
+ lead with the load-bearing fact (the inbox is the trust root, by
78
+ construction, in any email-based auth system). Tightened that
79
+ paragraph, recorded the same-browser-binding decision in the §16
80
+ decisions log so it doesn't have to be relitigated, and added a
81
+ fourth entry under `knowless.context.md` § "What's NOT" with a
82
+ ~15-line adopter recipe for adopters who genuinely want the
83
+ defense. No code changes.
84
+
85
+ ### Documented
86
+
87
+ - `README.md` — tightened the threat-model paragraph. Added a
88
+ leading sentence stating that email-based magic links are
89
+ exactly as strong as the user's mailbox (the inbox is the
90
+ trust root, by construction). Promoted "compromised email
91
+ accounts" to first position in "Does NOT defend against" with
92
+ a parenthetical explaining the magic link is a bearer token.
93
+ - `knowless.context.md` — added a fourth entry under "What's NOT
94
+ in knowless, and why": same-browser binding for magic links.
95
+ Documents why the library refuses to ship the defense
96
+ (§16.14 threat-model boundary, cross-device UX cost) and
97
+ provides a ~15-line adopter recipe using the `next_url`
98
+ round-trip + a state cookie. Mechanism in adopter, no library
99
+ change.
100
+ - `docs/01-product/PRD.md` §16.21 — recorded the same-browser-
101
+ binding decision in the §16 decisions log so the doctrine
102
+ survives the next agent who proposes it. Cross-references
103
+ §16.14 (honest threat model) as the load-bearing precedent.
104
+ - `CLAUDE.md` — bumped the §16 entry count (20 → 21) so the
105
+ pointer stays accurate.
106
+
33
107
  ## [1.1.6] — 2026-05-09
34
108
 
35
109
  Documentation-only release. README was routing adopters into
package/GUIDE.md CHANGED
@@ -799,7 +799,7 @@ auth.example.com {
799
799
  kuma.example.com {
800
800
  forward_auth localhost:8080 {
801
801
  uri /verify
802
- copy_headers X-Knowless-Handle
802
+ copy_headers X-User-Handle
803
803
  }
804
804
  reverse_proxy localhost:3001 # Uptime Kuma
805
805
  }
@@ -807,7 +807,7 @@ kuma.example.com {
807
807
  adguard.example.com {
808
808
  forward_auth localhost:8080 {
809
809
  uri /verify
810
- copy_headers X-Knowless-Handle
810
+ copy_headers X-User-Handle
811
811
  }
812
812
  reverse_proxy localhost:3000 # AdGuard Home
813
813
  }
package/OPS.md CHANGED
@@ -19,7 +19,7 @@ delegate email to a SaaS, knowless is the wrong tool.
19
19
  - send outbound TCP/25 (verify before going further — see §3)
20
20
  - have a working PTR record for its public IPv4 (and IPv6 if used)
21
21
  - A domain with control of its DNS records
22
- - Node.js ≥ 20 installed
22
+ - Node.js ≥ 22.5 installed (`node:sqlite` requires this floor)
23
23
  - A reverse proxy in front of HTTP (Caddy, nginx, or Traefik)
24
24
 
25
25
  knowless does not handle TLS termination. Your reverse proxy does.
@@ -304,6 +304,20 @@ knowless does not terminate TLS. Your proxy fronts it on `:443`,
304
304
  forwards `Host` and `X-Forwarded-For`, and (for forward-auth
305
305
  deployments) routes protected upstreams through `/verify`.
306
306
 
307
+ **Identity-header trust boundary.** On success `/verify` returns
308
+ `200` with `X-User-Handle: <handle>`. The proxy copies that header
309
+ onto the request it forwards to the protected upstream, and the
310
+ upstream trusts it as the authenticated identity. This only holds if
311
+ the value the upstream sees is the one knowless set — so the proxy
312
+ **must overwrite (or strip) any client-supplied `X-User-Handle`**, never
313
+ merely append to it. The recipes below do this (`proxy_set_header` in
314
+ nginx replaces; `copy_headers` / `authResponseHeaders` replace from
315
+ the auth response). If you adapt them, confirm a request sent with a
316
+ forged `X-User-Handle:` header reaches the upstream with the value
317
+ *replaced*, not duplicated — a duplicate the upstream reads first is a
318
+ full impersonation bypass. Defense in depth: have the upstream reject
319
+ the header from any source other than the proxy.
320
+
307
321
  ### 7.1 Caddy
308
322
 
309
323
  ```caddy
@@ -315,7 +329,7 @@ auth.example.com {
315
329
  kuma.example.com {
316
330
  forward_auth 127.0.0.1:8080 {
317
331
  uri /verify
318
- copy_headers X-Knowless-Handle
332
+ copy_headers X-User-Handle
319
333
  }
320
334
  reverse_proxy 127.0.0.1:3001
321
335
  }
@@ -363,8 +377,8 @@ server {
363
377
 
364
378
  location / {
365
379
  auth_request /_knowless_verify;
366
- auth_request_set $handle $upstream_http_x_knowless_handle;
367
- proxy_set_header X-Knowless-Handle $handle;
380
+ auth_request_set $handle $upstream_http_x_user_handle;
381
+ proxy_set_header X-User-Handle $handle;
368
382
  proxy_pass http://127.0.0.1:3001;
369
383
  }
370
384
  }
@@ -379,7 +393,7 @@ http:
379
393
  knowless:
380
394
  forwardAuth:
381
395
  address: "http://127.0.0.1:8080/verify"
382
- authResponseHeaders: [ "X-Knowless-Handle" ]
396
+ authResponseHeaders: [ "X-User-Handle" ]
383
397
 
384
398
  routers:
385
399
  auth:
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # knowless
2
2
 
3
+ <p align="center">
4
+ <img src="https://img.shields.io/github/package-json/v/hamr0/knowless?label=version&color=2a4f8c" alt="version (auto from package.json)">
5
+ <img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
6
+ </p>
7
+
3
8
  Small, opinionated, full-stack passwordless auth for Node.js services
4
9
  that don't need to email their users for anything but the sign-in link.
5
10
 
@@ -7,7 +12,7 @@ that don't need to email their users for anything but the sign-in link.
7
12
  npm install knowless
8
13
  ```
9
14
 
10
- > v1.0.0 (walk-away release) | Node.js >= 22.5 | **1 production dep (nodemailer)** | Apache-2.0
15
+ > v1.0.0 (walk-away release) | Node.js >= 22.5 | **1 production dep (nodemailer)**
11
16
 
12
17
  ## What it does
13
18
 
@@ -116,6 +121,10 @@ By choosing knowless, you commit to running:
116
121
 
117
122
  ## Threat model — one paragraph
118
123
 
124
+ Email-based magic links are exactly as strong as the user's mailbox.
125
+ knowless can harden the auth flow; it cannot harden an inbox the user
126
+ has already lost. The list below reflects that boundary.
127
+
119
128
  **Defends well:** DB-only leaks (handles are HMAC-salted),
120
129
  plaintext-email exfiltration (none persisted), password reuse (no
121
130
  passwords), silent email enumeration via the login form (timing-
@@ -129,10 +138,12 @@ whitelist).
129
138
  checks but not session forgery), phishing (no password to type into a
130
139
  fake site, but a phished mailbox still receives links).
131
140
 
132
- **Does NOT defend against:** sophisticated bots that bypass the
133
- honeypot, distributed floods from many IPs, full server compromise,
134
- compromised email accounts, social engineering, insider threat at
135
- the operator. Layer-2 defences (Cloudflare, fail2ban, reverse-proxy
141
+ **Does NOT defend against:** compromised email accounts (the magic
142
+ link is a bearer token anyone who can read the inbox can use it;
143
+ defense lives at the email provider, not in this library),
144
+ sophisticated bots that bypass the honeypot, distributed floods from
145
+ many IPs, full server compromise, social engineering, insider threat
146
+ at the operator. Layer-2 defences (Cloudflare, fail2ban, reverse-proxy
136
147
  rate-limits) belong above the library.
137
148
 
138
149
  ## Adopters
@@ -483,6 +483,18 @@ which requires the operator secret.
483
483
  [Caddy verifies cookie via /verify, proxies to Uptime Kuma]
484
484
  ```
485
485
 
486
+ **Identity-header trust boundary.** `/verify` returns `200` +
487
+ `X-User-Handle: <handle>` on success; the proxy copies that header
488
+ onto the request forwarded to the protected upstream. The header is
489
+ trustworthy *only* because the proxy sets it from the `/verify`
490
+ response — so your proxy config must **overwrite or strip any
491
+ client-supplied `X-User-Handle`**, never append. nginx
492
+ `proxy_set_header`, Caddy `copy_headers`, and Traefik
493
+ `authResponseHeaders` all replace; if you hand-roll a config, confirm
494
+ a request carrying a forged `X-User-Handle:` reaches the upstream with
495
+ the value replaced (not duplicated), and have the upstream reject the
496
+ header from any source but the proxy. Exact recipes: `OPS.md` §7.
497
+
486
498
  ## Custom mailer contract
487
499
 
488
500
  When you inject `options.mailer`, knowless hands off five obligations.
@@ -562,7 +574,7 @@ URL/email -> handlers.js (login: 12-step sham-work flow per SPEC §7.3)
562
574
 
563
575
  ## What's NOT in knowless, and why
564
576
 
565
- Three capabilities that look like they belong here but don't, listed
577
+ Four capabilities that look like they belong here but don't, listed
566
578
  because the "why not" needs to outlast walk-away-at-v1.0.0. When future
567
579
  contributors propose adding any of these back, point them here.
568
580
 
@@ -611,6 +623,49 @@ requires JS in the login form (the only zero-JS exception we'd carry),
611
623
  per-IP signup actually saturating the cap, Caddy (or another perimeter
612
624
  layer) can run hashcash off-the-shelf without making knowless carry it.
613
625
 
626
+ ### Same-browser binding — adopter / landing route
627
+
628
+ Refuse the magic-link click unless it lands in the browser that
629
+ requested it. Defends against compromised inboxes (attacker reading
630
+ the mail is by definition in a different browser → click fails).
631
+
632
+ The library refuses this because §16.14 records inbox compromise as
633
+ out-of-scope, and adding the defense would expand the threat model
634
+ knowless promises. It also breaks legitimate cross-device flows
635
+ (request on phone, click on desktop) that many users rely on. PRD
636
+ §16.21 has the full reasoning.
637
+
638
+ Adopters who genuinely want it can do it in ~15 lines, no knowless
639
+ change:
640
+
641
+ ```js
642
+ // At /login submission, before forwarding to knowless:
643
+ const bind = crypto.randomBytes(16).toString('hex');
644
+ res.setHeader('Set-Cookie',
645
+ `app_bind=${bind}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900`);
646
+ const bindHash = crypto.createHash('sha256').update(bind).digest('hex');
647
+ req.body.next = `${baseUrl}/post-login?b=${bindHash}`;
648
+ // → forward to knowless's loginHandler; next_url round-trips through
649
+ // the token row and lands at /post-login after consume.
650
+
651
+ // At /post-login (your route, after knowless set the session cookie):
652
+ const cookieBind = getCookie(req, 'app_bind');
653
+ const expected = req.query.b;
654
+ if (!cookieBind ||
655
+ crypto.createHash('sha256').update(cookieBind).digest('hex') !== expected) {
656
+ // Different browser → kill the session knowless just minted.
657
+ res.setHeader('Set-Cookie', 'knowless_session=; Max-Age=0; Path=/');
658
+ return res.redirect('/login?error=different_browser');
659
+ }
660
+ res.redirect('/');
661
+ ```
662
+
663
+ Mechanism (cookie + hash round-trip) is generic. Policy (refuse, warn,
664
+ require step-up) is the adopter's call. UX recovery ("click the link
665
+ in the same browser you requested it from") is adopter copy.
666
+ Splitting it this way keeps the knowless contract honest while letting
667
+ adopters who accept the cross-device UX cost have the defense.
668
+
614
669
  ### The deciding lens
615
670
 
616
671
  knowless walks away at v1.0.0 (PRD §6.3). Every config option carried
@@ -789,7 +844,7 @@ rate-limits) belongs above the library.
789
844
 
790
845
  ## Constraints
791
846
 
792
- - **Node 20+** -- targeting LTS; tested on Node 22
847
+ - **Node 22.5+** -- `node:sqlite` (`DatabaseSync`) floor; tested on Node 22
793
848
  - **Plain ES modules** -- no TypeScript source, no build step;
794
849
  ships JSDoc + (eventual) `.d.ts`
795
850
  - **One production dep** -- `nodemailer` (SMTP submission). Storage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knowless",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
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",