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 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-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,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)** | Apache-2.0
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 email
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. Don't file feature requests
95
- for them the reasoning is locked in
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
- Reasoning: PRD §16.2 vendor relationships invite reusing the
103
- mailer for non-auth mail, which collapses the "one mail purpose"
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.** PRD §16.7 — that's the MTA's job;
110
- knowless emits clean RFC822 and your Postfix + opendkim signs it.
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.** No template overridesPRD §16.12.
117
- Fork, override the route entirely, or live with it.
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. (Operator-side observability is opt-in via hooks — see
120
- below.)
121
- - **Walks away at v1.0.0.** Maintenance mode after that — only
122
- security fixes.
123
-
124
- ## Observability (wire it or be silent)
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:** sophisticated bots that bypass the
186
- honeypot, distributed floods from many IPs, full server compromise,
187
- compromised email accounts, social engineering, insider threat at the
188
- operator. Layer-2 defences (Cloudflare, fail2ban, reverse-proxy
189
- rate-limits) belong above the library patterns in
190
- [`OPS.md`](OPS.md).
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
@@ -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
@@ -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 (PRD §16.2): vendor mailers invite "while we're
653
- at it, let's send a welcome email," which contradicts the
654
- philosophy. If you can't run Postfix, knowless isn't your
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 20+** -- targeting LTS; tested on Node 22
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.5",
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",