knowless 1.1.5 → 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 +115 -0
- package/GUIDE.md +2 -2
- package/OPS.md +19 -5
- package/README.md +44 -81
- package/knowless.context.md +60 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,121 @@ 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
|
+
|
|
107
|
+
## [1.1.6] — 2026-05-09
|
|
108
|
+
|
|
109
|
+
Documentation-only release. README was routing adopters into
|
|
110
|
+
internal-only docs (PRD §16) for the "why we refuse X" rationale,
|
|
111
|
+
which collapsed two audiences (adopters reading the README;
|
|
112
|
+
agents/contributors litigating design decisions) into one trail.
|
|
113
|
+
PRD isn't shipped via npm anyway, so npm consumers clicking those
|
|
114
|
+
links got dead repo-relative paths. Restructured the README as a
|
|
115
|
+
self-contained explainer (philosophy → mechanism → modes →
|
|
116
|
+
deployment shapes → refusals → operator commitments → threat model
|
|
117
|
+
→ adopters → going-further footer) and moved the deeper refusal
|
|
118
|
+
rationale into a new repo-root `CLAUDE.md` for future agents
|
|
119
|
+
working on the library. No code changes.
|
|
120
|
+
|
|
121
|
+
### Documented
|
|
122
|
+
|
|
123
|
+
- `README.md` — dropped the "Required reading" routing table that
|
|
124
|
+
pointed adopters at internal docs (PRD, knowless.context.md).
|
|
125
|
+
Five `PRD §16.x` references in the refusals bullets replaced with
|
|
126
|
+
inline one-liner reasoning. "Hardcoded login form" bullet now
|
|
127
|
+
carries a brief inline reason ("templating is a slope") in place
|
|
128
|
+
of the `PRD §16.12` pointer. Detailed observability code block
|
|
129
|
+
removed — the philosophy stays as a refusals bullet ("wire it or
|
|
130
|
+
be silent"), the worked example lives in `GUIDE.md`. Threat model
|
|
131
|
+
paragraph trimmed (removed trailing `knowless.context.md`
|
|
132
|
+
pointer). New "Going further" footer at the end pointing at
|
|
133
|
+
`GUIDE.md`, `OPS.md`, `CHANGELOG.md`. Walk-away bullet now
|
|
134
|
+
mirrors the four PRD §6.3 carve-outs verbatim.
|
|
135
|
+
- `CLAUDE.md` (new, repo-root, **not shipped via npm**) — agent
|
|
136
|
+
context with the walk-away doctrine, two-test lens for "should X
|
|
137
|
+
go in knowless," README-discoverability triage for adopter
|
|
138
|
+
feature requests, the most-litigated refusals (each with its PRD
|
|
139
|
+
§16 anchor for long-form reasoning), pointers to PRD / SPEC /
|
|
140
|
+
TASKS / AGENT_RULES, the decision-revisit protocol, and a
|
|
141
|
+
condensed code-standards block.
|
|
142
|
+
- `knowless.context.md` — dropped the bare `(PRD §16.2)`
|
|
143
|
+
parenthetical from the Postfix-on-localhost gotcha. The inline
|
|
144
|
+
reasoning that follows ("vendor mailers invite 'while we're at
|
|
145
|
+
it'…") carries the same point without routing readers into a
|
|
146
|
+
doc that isn't shipped.
|
|
147
|
+
|
|
33
148
|
## [1.1.5] — 2026-05-09
|
|
34
149
|
|
|
35
150
|
Documentation-only release. plato (Mode A adopter) hit a real seam
|
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,22 +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)**
|
|
11
|
-
|
|
12
|
-
## Required reading (before integrating or filing an issue)
|
|
13
|
-
|
|
14
|
-
This README is the front door, not the manual. Most "missing feature"
|
|
15
|
-
questions about knowless turn out to be answered in the docs below —
|
|
16
|
-
hooks that already exist, refusals that are deliberate, operator
|
|
17
|
-
setup steps already documented. **Read these before assuming a gap.**
|
|
18
|
-
|
|
19
|
-
| Read this | Why you need it |
|
|
20
|
-
|---|---|
|
|
21
|
-
| [`GUIDE.md`](GUIDE.md) | Integration walkthrough, **observability hooks** (`onMailerSubmit` / `onTransportFailure` / `onSuppressionWindow`), edge cases, FAQ, troubleshooting. |
|
|
22
|
-
| [`OPS.md`](OPS.md) | Operator setup — Postfix install, **SPF / DKIM / PTR / DMARC at your domain registrar** (§5), null-route, systemd, Caddy/nginx/Traefik forward-auth, MailHog dev, fail2ban. |
|
|
23
|
-
| [`docs/01-product/PRD.md`](docs/01-product/PRD.md) §16 | Why knowless refuses what it refuses. Decisions log — read §16.2 before asking for vendor SMTP, §16.7 before asking for built-in DKIM, §16.12 before asking for a templatable login form. |
|
|
24
|
-
| [`knowless.context.md`](knowless.context.md) | Dense single-file reference for AI agents and quick lookups. Public API table, every option with defaults, 19 gotchas, threat model. Fits one context window. |
|
|
25
|
-
| [`CHANGELOG.md`](CHANGELOG.md) | Version history. |
|
|
15
|
+
> v1.0.0 (walk-away release) | Node.js >= 22.5 | **1 production dep (nodemailer)**
|
|
26
16
|
|
|
27
17
|
## What it does
|
|
28
18
|
|
|
@@ -31,8 +21,8 @@ cookie out, nothing else stored.** Email is HMAC-hashed at the
|
|
|
31
21
|
boundary and discarded. The library refuses, by API shape, to send
|
|
32
22
|
anything but the sign-in link or store anything identifying.
|
|
33
23
|
|
|
34
|
-
Most auth libraries default to maximum identity collection: full
|
|
35
|
-
in plaintext, profile fields, recovery email, federation. Even
|
|
24
|
+
Most auth libraries default to maximum identity collection: full
|
|
25
|
+
email in plaintext, profile fields, recovery email, federation. Even
|
|
36
26
|
nominally privacy-focused options store enough that a breach is
|
|
37
27
|
materially harmful. knowless inverts the default.
|
|
38
28
|
|
|
@@ -80,8 +70,6 @@ The same sham-work flow runs underneath either mode, so unknown
|
|
|
80
70
|
emails, rate-limit hits, and real sends look identical to an external
|
|
81
71
|
observer.
|
|
82
72
|
|
|
83
|
-
Worked code for both in [`GUIDE.md`](GUIDE.md).
|
|
84
|
-
|
|
85
73
|
## Two deployment shapes
|
|
86
74
|
|
|
87
75
|
| Shape | When |
|
|
@@ -91,69 +79,35 @@ Worked code for both in [`GUIDE.md`](GUIDE.md).
|
|
|
91
79
|
|
|
92
80
|
## What knowless refuses (by design)
|
|
93
81
|
|
|
94
|
-
These are closed doors, not omissions.
|
|
95
|
-
|
|
96
|
-
[`docs/01-product/PRD.md`](docs/01-product/PRD.md) §16. If any
|
|
97
|
-
break your case, knowless isn't the right tool — look at
|
|
82
|
+
These are closed doors, not omissions. If any break your case,
|
|
83
|
+
knowless isn't the right tool — look at
|
|
98
84
|
[Lucia](https://lucia-auth.com/), [Auth.js](https://authjs.dev/),
|
|
99
85
|
or commercial offerings.
|
|
100
86
|
|
|
101
87
|
- **Localhost SMTP only.** No Mailgun / Postmark / SES / Resend.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
invariant.
|
|
88
|
+
Vendor relationships invite reusing the mailer for non-auth mail,
|
|
89
|
+
which collapses the "one mail purpose" invariant.
|
|
105
90
|
- **One mail purpose: the sign-in link.** No `sendNotification()` to
|
|
106
91
|
be tempted by.
|
|
107
92
|
- **Plain-text 7-bit email.** No HTML, no tracking pixels, no
|
|
108
93
|
click-rewriting, no read-receipts.
|
|
109
|
-
- **No DKIM/SPF in the library.**
|
|
110
|
-
|
|
111
|
-
Setup steps in [`OPS.md`](OPS.md) §5.
|
|
94
|
+
- **No DKIM/SPF in the library.** That's the MTA's job; knowless
|
|
95
|
+
emits clean RFC822 and your Postfix + opendkim signs it.
|
|
112
96
|
- **No OAuth / OIDC / SAML.** Different audience.
|
|
113
97
|
- **No 2FA / WebAuthn / TOTP / passkeys.** Compose with a separate
|
|
114
98
|
library if you need them.
|
|
115
99
|
- **No admin UI.** `sqlite3 knowless.db` is the admin UI.
|
|
116
|
-
- **Hardcoded login form.**
|
|
117
|
-
|
|
100
|
+
- **Hardcoded login form.** Templating is a slope — today "let me
|
|
101
|
+
put my logo," tomorrow "let me theme the page," eventually "let me
|
|
102
|
+
embed a JS framework." Fork, override the route entirely, or live
|
|
103
|
+
with it.
|
|
118
104
|
- **No telemetry, analytics, or error reporting.** No phone-home of
|
|
119
|
-
any kind.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
knowless emits **three operator-visibility hooks** on the mail-send
|
|
127
|
-
path. They're the only API for SMTP outcomes — there is no internal
|
|
128
|
-
logging the library does on your behalf beyond an unwired-default
|
|
129
|
-
stderr line on transport failure. If you want metrics, alerting, or
|
|
130
|
-
an admin UI showing send results, you wire these.
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
const auth = knowless({
|
|
134
|
-
secret, baseUrl, from,
|
|
135
|
-
|
|
136
|
-
onMailerSubmit: ({ messageId, handle, timestamp }) => {
|
|
137
|
-
// Real (non-sham) submission succeeded. Safe per-event — fires
|
|
138
|
-
// ONLY on registered handles, so no enumeration oracle.
|
|
139
|
-
},
|
|
140
|
-
onTransportFailure: ({ error, timestamp }) => {
|
|
141
|
-
// SMTP submission failed. Carries no identity data. Wire to
|
|
142
|
-
// your alerting / admin "last 10 sends" panel.
|
|
143
|
-
},
|
|
144
|
-
onSuppressionWindow: ({ sham, rateLimited, windowMs }) => {
|
|
145
|
-
// Aggregate counters for the silent-202 branches (sham + rate-
|
|
146
|
-
// limit hits). Windowed, NOT per-event — per-event would reopen
|
|
147
|
-
// the enumeration oracle that sham-work exists to prevent.
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Threat-model reasoning for why three hooks (and not a fourth
|
|
153
|
-
per-event sham hook) lives in [`GUIDE.md`](GUIDE.md) Step 8 and
|
|
154
|
-
`knowless.context.md` § "Why three hooks, not four". **Read it
|
|
155
|
-
before logging payloads** — careless aggregation can leak handles
|
|
156
|
-
into log lines.
|
|
105
|
+
any kind. Operator-side observability is opt-in via three hooks
|
|
106
|
+
(`onMailerSubmit` / `onTransportFailure` / `onSuppressionWindow`) —
|
|
107
|
+
wire them or be silent.
|
|
108
|
+
- **Walks away at v1.0.0.** Maintenance mode after that — security
|
|
109
|
+
fixes, bug fixes that don't change the API surface, doc fixes,
|
|
110
|
+
helper exports.
|
|
157
111
|
|
|
158
112
|
## Operator commitments
|
|
159
113
|
|
|
@@ -165,10 +119,12 @@ By choosing knowless, you commit to running:
|
|
|
165
119
|
- A **null-route** for the configured `shamRecipient` so silent-miss
|
|
166
120
|
sham mail drops, not bounces
|
|
167
121
|
|
|
168
|
-
Step-by-step in [`OPS.md`](OPS.md).
|
|
169
|
-
|
|
170
122
|
## Threat model — one paragraph
|
|
171
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
|
+
|
|
172
128
|
**Defends well:** DB-only leaks (handles are HMAC-salted),
|
|
173
129
|
plaintext-email exfiltration (none persisted), password reuse (no
|
|
174
130
|
passwords), silent email enumeration via the login form (timing-
|
|
@@ -182,15 +138,13 @@ whitelist).
|
|
|
182
138
|
checks but not session forgery), phishing (no password to type into a
|
|
183
139
|
fake site, but a phished mailbox still receives links).
|
|
184
140
|
|
|
185
|
-
**Does NOT defend against:**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
Full detail in [`knowless.context.md`](knowless.context.md) §
|
|
193
|
-
"Threat model summary."
|
|
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
|
|
147
|
+
rate-limits) belong above the library.
|
|
194
148
|
|
|
195
149
|
## Adopters
|
|
196
150
|
|
|
@@ -208,6 +162,15 @@ If you're picking knowless up: the addypin and gitdone callsites are
|
|
|
208
162
|
both Mode A and good worked references for the use-first / claim-later
|
|
209
163
|
shape.
|
|
210
164
|
|
|
165
|
+
## Going further
|
|
166
|
+
|
|
167
|
+
- [`GUIDE.md`](GUIDE.md) — integration walkthrough, observability
|
|
168
|
+
hooks, edge cases, FAQ, troubleshooting.
|
|
169
|
+
- [`OPS.md`](OPS.md) — operator setup (Postfix install, SPF/DKIM/PTR/
|
|
170
|
+
DMARC at your registrar, null-route, systemd, forward-auth wiring,
|
|
171
|
+
fail2ban).
|
|
172
|
+
- [`CHANGELOG.md`](CHANGELOG.md) — version history.
|
|
173
|
+
|
|
211
174
|
## License
|
|
212
175
|
|
|
213
176
|
[Apache 2.0](LICENSE) with [`NOTICE`](NOTICE) preservation. Forks
|
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
|
|
@@ -649,10 +704,9 @@ rate-limits) belongs above the library.
|
|
|
649
704
|
|
|
650
705
|
2. **Postfix on localhost is required.** No remote SMTP, no
|
|
651
706
|
Mailgun / Postmark / SES. The localhost requirement is
|
|
652
|
-
intentional
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
library.
|
|
707
|
+
intentional: vendor mailers invite "while we're at it, let's
|
|
708
|
+
send a welcome email," which contradicts the philosophy. If
|
|
709
|
+
you can't run Postfix, knowless isn't your library.
|
|
656
710
|
|
|
657
711
|
3. **`shamRecipient` MUST be discarded without external delivery.**
|
|
658
712
|
Default is `null@knowless.invalid`. With the default
|
|
@@ -790,7 +844,7 @@ rate-limits) belongs above the library.
|
|
|
790
844
|
|
|
791
845
|
## Constraints
|
|
792
846
|
|
|
793
|
-
- **Node
|
|
847
|
+
- **Node 22.5+** -- `node:sqlite` (`DatabaseSync`) floor; tested on Node 22
|
|
794
848
|
- **Plain ES modules** -- no TypeScript source, no build step;
|
|
795
849
|
ships JSDoc + (eventual) `.d.ts`
|
|
796
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",
|