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 +74 -0
- package/GUIDE.md +2 -2
- package/OPS.md +19 -5
- package/README.md +16 -5
- package/knowless.context.md +57 -2
- package/package.json +1 -1
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-
|
|
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-
|
|
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 ≥
|
|
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-
|
|
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 $
|
|
367
|
-
proxy_set_header X-
|
|
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-
|
|
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)**
|
|
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:**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
package/knowless.context.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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",
|