knowless 1.1.1 → 1.1.3
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 +56 -0
- package/GUIDE.md +62 -8
- package/knowless.context.md +26 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,62 @@ 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.3] — 2026-05-03
|
|
34
|
+
|
|
35
|
+
Documentation-only release. Surfaces a partial enumeration leak
|
|
36
|
+
in the default `failureRedirect` fallback that previously read as
|
|
37
|
+
"Mode-A only" guidance but actually applies to every adopter.
|
|
38
|
+
POST /login is built to make valid/invalid/rate-limited responses
|
|
39
|
+
indistinguishable (timing equivalence, sham tokens, sham-recipient
|
|
40
|
+
mailing, identical response shapes); the link-click stage extends
|
|
41
|
+
that contract, but the default `failureRedirect` cascade points at
|
|
42
|
+
`loginPath` (typically `/login`) — so a user clicking a sham link
|
|
43
|
+
lands on a "Sign in" page and immediately knows the link was
|
|
44
|
+
rejected, defeating the POST-stage work. plato wraps knowless with
|
|
45
|
+
`failureRedirect: '/'` for exactly this reason. No code changes.
|
|
46
|
+
|
|
47
|
+
### Documented
|
|
48
|
+
|
|
49
|
+
- `GUIDE.md` Step-4 callout — replaced the narrow "Mode-A
|
|
50
|
+
heads-up" with a broader "set `failureRedirect: '/'` — it's
|
|
51
|
+
part of the silent-miss contract, not just a UX knob"
|
|
52
|
+
guidance, with the reasoning chain (POST-stage work, link-
|
|
53
|
+
click extension, leak-free landing) and the explicit opt-in
|
|
54
|
+
for adopters who want "try again" UX (`failureRedirect:
|
|
55
|
+
cfg.loginPath`). plato cited as the reference adopter.
|
|
56
|
+
- `GUIDE.md` config-table row for `failureRedirect` —
|
|
57
|
+
expanded the cell to lead with the silent-miss framing
|
|
58
|
+
rather than the Mode-A 404 framing.
|
|
59
|
+
- `knowless.context.md` `failureRedirect` comment in the
|
|
60
|
+
options block — flagged the default as a leak, pointer to
|
|
61
|
+
the new gotcha.
|
|
62
|
+
- `knowless.context.md` gotcha #20 — full silent-miss
|
|
63
|
+
contract write-up with the trade-off and reference adopter.
|
|
64
|
+
|
|
65
|
+
## [1.1.2] — 2026-05-03
|
|
66
|
+
|
|
67
|
+
Documentation-only release. Adopters were repeatedly missing that
|
|
68
|
+
`auth.loginForm` is an unstyled contract-minimal fallback they
|
|
69
|
+
should override, and that `/login` is also where every silent-miss
|
|
70
|
+
failure (used / expired / sham / malformed token) redirects — so
|
|
71
|
+
the page deserves first-class UI in the host app, not the bundled
|
|
72
|
+
default. No code changes.
|
|
73
|
+
|
|
74
|
+
### Documented
|
|
75
|
+
|
|
76
|
+
- `GUIDE.md` FAQ — added section "Branding the GET /login page
|
|
77
|
+
(you almost certainly want to override it)" with the three
|
|
78
|
+
reasons (brand consistency, silent-miss redirect target, opt-in
|
|
79
|
+
fallback) and the override pattern. Extended the existing
|
|
80
|
+
custom-form contract with `next` validation against
|
|
81
|
+
`baseUrl` + `cookieDomain` and the optional honeypot field name
|
|
82
|
+
from `cfg.honeypotFieldName`.
|
|
83
|
+
- `knowless.context.md` gotcha #10 — expanded the "operators
|
|
84
|
+
wanting branding fork the project" note to surface the more
|
|
85
|
+
common path (skip mounting `auth.loginForm`, serve your own
|
|
86
|
+
`GET /login`) and call out that `/login` is the silent-miss
|
|
87
|
+
redirect target.
|
|
88
|
+
|
|
33
89
|
## [1.1.1] — 2026-05-02
|
|
34
90
|
|
|
35
91
|
Documentation-only release. Adds the wrong-shape-integration
|
package/GUIDE.md
CHANGED
|
@@ -384,12 +384,25 @@ and `startLogin` would compute. The bare `deriveHandle` re-export
|
|
|
384
384
|
takes pre-normalized input; use the instance method unless you
|
|
385
385
|
have a specific reason to call the lower-level primitive.
|
|
386
386
|
|
|
387
|
-
> **
|
|
388
|
-
>
|
|
389
|
-
> `
|
|
390
|
-
>
|
|
391
|
-
>
|
|
392
|
-
>
|
|
387
|
+
> **Set `failureRedirect: '/'` — it's part of the silent-miss
|
|
388
|
+
> contract, not just a UX knob.** The default falls back to
|
|
389
|
+
> `loginPath` (typically `/login`). That's a partial enumeration
|
|
390
|
+
> leak: `POST /login` goes to significant lengths to make
|
|
391
|
+
> valid/invalid/rate-limited submissions indistinguishable
|
|
392
|
+
> (timing equivalence, sham tokens, sham-recipient mailing,
|
|
393
|
+
> identical response shapes) — but if a user clicks a sham or
|
|
394
|
+
> expired magic link and lands on a "Sign in" page, they
|
|
395
|
+
> immediately know the link was rejected, defeating the work the
|
|
396
|
+
> POST stage did. The leak-free landing is the same page any
|
|
397
|
+
> logged-out visitor sees on first arrival — usually `/`. Mode-A
|
|
398
|
+
> adopters have an extra reason: the default `failureRedirect`
|
|
399
|
+
> cascade points at `/login`, a route Mode A doesn't serve, so
|
|
400
|
+
> expired/replayed clicks 302 to a 404. Reference: plato wraps
|
|
401
|
+
> knowless with `failureRedirect: '/'` for this reason.
|
|
402
|
+
> Adopters who genuinely want a "try again" UX after failure
|
|
403
|
+
> can opt in explicitly with `failureRedirect: cfg.loginPath`,
|
|
404
|
+
> but understand you're trading the silent-miss guarantee at
|
|
405
|
+
> the link-click stage for that UX.
|
|
393
406
|
|
|
394
407
|
### Step 5: Pre-seed users (closed-registration mode, default)
|
|
395
408
|
|
|
@@ -808,7 +821,7 @@ Full options table:
|
|
|
808
821
|
| `trustedProxies` | no | `['127.0.0.1', '::1']` | IPs allowed to set `X-Forwarded-For`. |
|
|
809
822
|
| `shamRecipient` | no | `null@knowless.invalid` | Where sham mail goes (your MTA must discard it). |
|
|
810
823
|
| `sweepIntervalMs` | no | `300000` | Sweeper tick (5 min default). |
|
|
811
|
-
| `failureRedirect` | no | (= `loginPath`) | Where /auth/callback failures redirect. **Mode-A adopters
|
|
824
|
+
| `failureRedirect` | no | (= `loginPath`) | Where /auth/callback failures (expired / used / sham / malformed token) redirect. **Set this to `'/'` (or any logged-out landing).** Default falls back to `loginPath`, which is a silent-miss leak: a user who clicks a sham/expired link and lands on a "Sign in" page knows the link was rejected, defeating the anti-enumeration work done at POST /login. Part of the silent-miss contract, not a UX knob. Mode-A adopters who don't mount `loginForm` *also* hit a 404 with the default. Opt back into the "try again" UX by passing `loginPath` explicitly if you understand the trade. |
|
|
812
825
|
| `store` | no | (built-in `node:sqlite`) | Inject your own store implementation. |
|
|
813
826
|
| `mailer` | no | (built-in nodemailer) | Inject your own mailer. |
|
|
814
827
|
|
|
@@ -929,7 +942,48 @@ own form, provided it satisfies the handler contract:
|
|
|
929
942
|
stream itself. A body-parser middleware mounted before `auth.login`
|
|
930
943
|
will silently steal the data (see gotcha #15 in
|
|
931
944
|
[`knowless.context.md`](knowless.context.md)).
|
|
932
|
-
- Optional: include a `next` field for the post-callback redirect URL
|
|
945
|
+
- Optional: include a `next` field for the post-callback redirect URL
|
|
946
|
+
(knowless validates `next` against `baseUrl` + `cookieDomain`).
|
|
947
|
+
- Optional but recommended: include the honeypot field using the name
|
|
948
|
+
from `cfg.honeypotFieldName`.
|
|
949
|
+
|
|
950
|
+
### Branding the GET /login page (you almost certainly want to override it)
|
|
951
|
+
|
|
952
|
+
Knowless ships `auth.loginForm(req, res)` as a turnkey fallback so
|
|
953
|
+
adopters can wire `GET /login` in one line and have a working magic-link
|
|
954
|
+
form. The page is intentionally unstyled — it's the contract-minimal
|
|
955
|
+
renderer needed to make the flow demonstrable, not a UI you ship to
|
|
956
|
+
end users. Three reasons most adopters override it:
|
|
957
|
+
|
|
958
|
+
1. **Brand consistency.** The fallback page has no header, footer, nav,
|
|
959
|
+
or styling, so users redirected here from elsewhere in your app land
|
|
960
|
+
on what looks like a different site. That's especially jarring after
|
|
961
|
+
a sham-token failure (a deliberate part of the silent-miss design —
|
|
962
|
+
see "Silent miss" in [`knowless.context.md`](knowless.context.md)),
|
|
963
|
+
where a user clicked a magic link and ended up on a "Sign in" page
|
|
964
|
+
that looks unrelated to where they started.
|
|
965
|
+
2. **`/login` is load-bearing in the silent-miss contract.** Knowless
|
|
966
|
+
redirects to `loginPath` on every failure mode that must be
|
|
967
|
+
indistinguishable from success — used token, expired token, sham
|
|
968
|
+
token, malformed token. That redirect is correct and required. But
|
|
969
|
+
it means `/login` is the page users actually land on during
|
|
970
|
+
anti-enumeration failures, not just the page they navigate to
|
|
971
|
+
deliberately. It deserves first-class UI in your app.
|
|
972
|
+
3. **`auth.loginForm` is opt-in, not opt-out.** Adopters who never wire
|
|
973
|
+
the GET route still get a working app — just without a friendly
|
|
974
|
+
`/login` page. Override it whenever you want your app's chrome on
|
|
975
|
+
the page. The POST handler can stay as-is (or also be wrapped — for
|
|
976
|
+
example, plato wraps it for the "we sent a link" confirmation).
|
|
977
|
+
|
|
978
|
+
Override pattern (mount your own handler instead of `auth.loginForm`):
|
|
979
|
+
|
|
980
|
+
```js
|
|
981
|
+
app.get('/login', (req, res) => renderMyOwnLogin(req, res));
|
|
982
|
+
app.post('/login', auth.login); // unchanged
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
The form just needs to satisfy the contract listed in the previous
|
|
986
|
+
question.
|
|
933
987
|
|
|
934
988
|
### How do I add 2FA / WebAuthn / TOTP?
|
|
935
989
|
|
package/knowless.context.md
CHANGED
|
@@ -114,7 +114,7 @@ const auth = knowless({
|
|
|
114
114
|
linkPath: '/auth/callback',
|
|
115
115
|
verifyPath: '/verify',
|
|
116
116
|
logoutPath: '/logout',
|
|
117
|
-
failureRedirect: null, // null → loginPath
|
|
117
|
+
failureRedirect: null, // null → loginPath (LEAK — set '/'; see gotcha #20)
|
|
118
118
|
|
|
119
119
|
// --- Mail / SMTP ---
|
|
120
120
|
smtpHost: 'localhost',
|
|
@@ -699,7 +699,13 @@ rate-limits) belongs above the library.
|
|
|
699
699
|
10. **No JavaScript in any HTML page.** The login form, the
|
|
700
700
|
confirmation page, error pages — all static HTML5. Works in
|
|
701
701
|
text-mode browsers (Lynx, w3m). Operators wanting branding
|
|
702
|
-
fork the project
|
|
702
|
+
fork the project — **or, more commonly, skip mounting
|
|
703
|
+
`auth.loginForm` and serve their own `GET /login`**. The
|
|
704
|
+
fallback is intentionally a contract-minimal renderer, not a
|
|
705
|
+
UI to ship. `/login` is also where every silent-miss failure
|
|
706
|
+
redirects (used / expired / sham / malformed token), so it
|
|
707
|
+
deserves first-class chrome in the host app. See GUIDE.md
|
|
708
|
+
§ "Branding the GET /login page".
|
|
703
709
|
|
|
704
710
|
11. **Process cleanup matters.** `auth.close()` stops the
|
|
705
711
|
sweeper and closes the SQLite handle. Without it, your
|
|
@@ -764,6 +770,24 @@ rate-limits) belongs above the library.
|
|
|
764
770
|
return shape. Don't wrap `startLogin` in something that surfaces
|
|
765
771
|
the branch to the caller; that re-opens the enumeration oracle.
|
|
766
772
|
|
|
773
|
+
20. **`failureRedirect` is part of the silent-miss contract.**
|
|
774
|
+
Default falls back to `loginPath` — a partial enumeration
|
|
775
|
+
leak. POST /login goes to significant lengths to keep
|
|
776
|
+
valid/invalid/rate-limited submissions indistinguishable
|
|
777
|
+
(timing, sham tokens, sham-recipient mailing, identical
|
|
778
|
+
response shapes), and the link-click stage extends that
|
|
779
|
+
contract: real tokens issue a session + redirect to
|
|
780
|
+
`nextUrl`; failures (expired, used, sham, malformed) go to
|
|
781
|
+
`failureRedirect`. If that's `/login`, a user who clicks a
|
|
782
|
+
sham link lands on a "Sign in" page and immediately knows
|
|
783
|
+
the link was rejected — defeating the POST-stage work. Set
|
|
784
|
+
`failureRedirect: '/'` (or any route a logged-out visitor
|
|
785
|
+
would see on first arrival) so a sham click is
|
|
786
|
+
indistinguishable from a never-clicked link. Adopters who
|
|
787
|
+
genuinely want a "try again" UX after failure opt back in
|
|
788
|
+
explicitly with `failureRedirect: cfg.loginPath`. plato is
|
|
789
|
+
the reference adopter for the leak-free wiring.
|
|
790
|
+
|
|
767
791
|
## Constraints
|
|
768
792
|
|
|
769
793
|
- **Node 20+** -- targeting LTS; tested on Node 22
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knowless",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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",
|