knowless 1.0.0 → 1.0.1
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 +51 -0
- package/knowless.context.md +5 -4
- package/package.json +1 -1
- package/src/handlers.js +5 -7
- package/src/mailer.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,57 @@ v1.0.0 are:
|
|
|
26
26
|
Feature requests are deflected to PRD §14 NO-GO, to sibling projects,
|
|
27
27
|
or to forking. The library being "done" is a feature.
|
|
28
28
|
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **XFF/X-Real-IP never honored through handler path (AF-28).**
|
|
32
|
+
`createHandlers` pre-built `trustedProxies` into a `{ has }` object
|
|
33
|
+
and passed it to `determineSourceIp`, which re-called
|
|
34
|
+
`buildTrustedPeers` on it. The pre-built object is not a `BlockList`,
|
|
35
|
+
array, or `Set`, so the peer list fell through to `[]`. Net effect:
|
|
36
|
+
trusted-proxy matching was silently empty in the handler code path —
|
|
37
|
+
rate limiting hit the reverse-proxy IP instead of the real client IP,
|
|
38
|
+
and XFF/X-Real-IP headers were ignored regardless of the
|
|
39
|
+
`trustedProxies` config. Abuse unit tests were passing because they
|
|
40
|
+
call `determineSourceIp` directly with raw arrays, bypassing the
|
|
41
|
+
handler path. Fix: removed the pre-build in `createHandlers`;
|
|
42
|
+
`determineSourceIp` now receives `cfg.trustedProxies` directly. Closes
|
|
43
|
+
AF-28.
|
|
44
|
+
|
|
45
|
+
- **`validateSubject` allowed CR/LF, enabling header injection in
|
|
46
|
+
standalone callers (AF-29).** The ASCII regex `/^[\x00-\x7f]*$/`
|
|
47
|
+
matched CR (0x0D) and LF (0x0A). `validateSubject` is re-exported as a
|
|
48
|
+
public validator (AF-9.1 / v0.1.7) so callers using it as a standalone
|
|
49
|
+
gate were unprotected. `composeRaw` caught the injection downstream, but
|
|
50
|
+
the validator is the authoritative public guard. Fix: added an explicit
|
|
51
|
+
`/[\r\n]/` check to `validateSubject`, consistent with the guards already
|
|
52
|
+
present in `validateFromName` and `validateBodyOverride`. Closes AF-29.
|
|
53
|
+
|
|
54
|
+
- **Factory `subject` not validated at startup (AF-30).** The factory
|
|
55
|
+
`subject` option was never passed to `validateSubject` during
|
|
56
|
+
`createHandlers` startup, breaking the fail-fast pattern that
|
|
57
|
+
`bodyFooter` (`validateBodyFooter` in `index.js`) and `fromName`
|
|
58
|
+
(`validateFromName` in `createMailer`) already followed. A non-ASCII
|
|
59
|
+
subject or empty string silently passed config time and would only fail
|
|
60
|
+
at first `mailer.submit()` — potentially hours into production. Fix:
|
|
61
|
+
added `validateSubject(cfg.subject)` to the config-validation block in
|
|
62
|
+
`createHandlers`. Closes AF-30.
|
|
63
|
+
|
|
64
|
+
- **`validateBodyFooter` rejected 4-line footers with a trailing newline
|
|
65
|
+
(AF-31).** `footer.split('\n').length > 4` counted 5 split parts for
|
|
66
|
+
`"a\nb\nc\nd\n"` (4 logical lines, trailing newline as is conventional
|
|
67
|
+
for multi-line strings). Fix: strip a single trailing newline before
|
|
68
|
+
counting: `footer.replace(/\n$/, '').split('\n').length > 4`. Closes
|
|
69
|
+
AF-31.
|
|
70
|
+
|
|
71
|
+
### Documentation
|
|
72
|
+
|
|
73
|
+
- **`runSendLink` JSDoc corrected: `handle` is null for both malformed
|
|
74
|
+
email and per-IP rate-limit short-circuit (AF-32).** The JSDoc stated
|
|
75
|
+
`handle` is null "only when the email failed to normalize." The per-IP
|
|
76
|
+
rate-limit early return also returns `handle: null` because `deriveHandle`
|
|
77
|
+
has not run at that point. No behavior change — documentation only. Closes
|
|
78
|
+
AF-32.
|
|
79
|
+
|
|
29
80
|
## [1.0.0] — 2026-04-29
|
|
30
81
|
|
|
31
82
|
**Walk-away release.** No new API surface vs v0.2.3 — v1.0.0 is the
|
package/knowless.context.md
CHANGED
|
@@ -659,10 +659,11 @@ rate-limits) belongs above the library.
|
|
|
659
659
|
who hold raw 32-byte keys.
|
|
660
660
|
|
|
661
661
|
18. **`bodyFooter` constraints (AF-8.2).** ASCII only — `·` is NOT
|
|
662
|
-
ASCII, use `|` or `-`. ≤ 240 chars, ≤ 4 lines
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
662
|
+
ASCII, use `|` or `-`. ≤ 240 chars, ≤ 4 lines (a single trailing
|
|
663
|
+
newline is allowed and not counted as an extra line), no
|
|
664
|
+
`http(s)://` URLs (would conflict with the magic-link line).
|
|
665
|
+
Validated at factory startup; fails fast. Goes after RFC 3676
|
|
666
|
+
`"-- "` delimiter so mail clients strip it from quoted replies.
|
|
666
667
|
|
|
667
668
|
19. **`startLogin` is silent at every layer (FR-6).** Returns
|
|
668
669
|
`{handle, submitted: true}` for *every* branch — real send, sham,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knowless",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
package/src/handlers.js
CHANGED
|
@@ -5,7 +5,6 @@ import { newSid, signSession, verifySessionSignature } from './session.js';
|
|
|
5
5
|
import { composeBody, validateSubject, validateBodyOverride } from './mailer.js';
|
|
6
6
|
import { renderLoginForm } from './form.js';
|
|
7
7
|
import {
|
|
8
|
-
buildTrustedPeers,
|
|
9
8
|
determineSourceIp,
|
|
10
9
|
rateLimitExceeded,
|
|
11
10
|
rateLimitIncrement,
|
|
@@ -191,9 +190,7 @@ export function createHandlers({ store, mailer, config, events }) {
|
|
|
191
190
|
throw new Error('config.baseUrl invalid');
|
|
192
191
|
}
|
|
193
192
|
}
|
|
194
|
-
|
|
195
|
-
// Build once at handler creation; supports plain IPs and CIDRs (AF-6.3).
|
|
196
|
-
const trustedProxies = buildTrustedPeers(cfg.trustedProxies);
|
|
193
|
+
validateSubject(cfg.subject);
|
|
197
194
|
|
|
198
195
|
// AF-7.1: emit at most one warning per handler instance about an
|
|
199
196
|
// upstream body parser swallowing the request body. Loud enough to
|
|
@@ -253,8 +250,9 @@ export function createHandlers({ store, mailer, config, events }) {
|
|
|
253
250
|
*
|
|
254
251
|
* @returns {Promise<{handle: string|null, isSham: boolean,
|
|
255
252
|
* emailNorm: string, nextValidated: string|null}>}
|
|
256
|
-
* handle is null
|
|
257
|
-
*
|
|
253
|
+
* handle is null when the email failed to normalize (programmer bug
|
|
254
|
+
* for startLogin) OR when per-IP rate-limit short-circuited before
|
|
255
|
+
* handle derivation; same-shape silent for /login.
|
|
258
256
|
*/
|
|
259
257
|
async function runSendLink({
|
|
260
258
|
emailRaw,
|
|
@@ -469,7 +467,7 @@ export function createHandlers({ store, mailer, config, events }) {
|
|
|
469
467
|
return;
|
|
470
468
|
}
|
|
471
469
|
|
|
472
|
-
const sourceIp = determineSourceIp(req, trustedProxies);
|
|
470
|
+
const sourceIp = determineSourceIp(req, cfg.trustedProxies);
|
|
473
471
|
const result = await runSendLink({ emailRaw, nextRaw, sourceIp });
|
|
474
472
|
sameResponse(res, result.emailNorm, result.nextValidated ?? '');
|
|
475
473
|
}
|
package/src/mailer.js
CHANGED
|
@@ -88,7 +88,7 @@ export function validateBodyFooter(footer) {
|
|
|
88
88
|
if (footer.length > 240) throw new Error('bodyFooter must be ≤ 240 chars');
|
|
89
89
|
if (!ASCII_RE.test(footer)) throw new Error('bodyFooter must be ASCII');
|
|
90
90
|
if (footer.includes('\r')) throw new Error('bodyFooter must not contain CR');
|
|
91
|
-
if (footer.split('\n').length > 4) {
|
|
91
|
+
if (footer.replace(/\n$/, '').split('\n').length > 4) {
|
|
92
92
|
throw new Error('bodyFooter must be ≤ 4 lines');
|
|
93
93
|
}
|
|
94
94
|
if (/https?:\/\//i.test(footer)) {
|
|
@@ -269,6 +269,7 @@ export function validateSubject(subject) {
|
|
|
269
269
|
}
|
|
270
270
|
if (subject.length > 60) throw new Error('subject longer than 60 chars');
|
|
271
271
|
if (!ASCII_RE.test(subject)) throw new Error('subject contains non-ASCII');
|
|
272
|
+
if (/[\r\n]/.test(subject)) throw new Error('subject must not contain CR/LF');
|
|
272
273
|
const warnings = [];
|
|
273
274
|
const triggers = ['!!', '$$', 'FREE', 'URGENT', 'WINNER'];
|
|
274
275
|
for (const t of triggers) {
|