knowless 1.1.3 → 1.1.5
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 +87 -0
- package/GUIDE.md +33 -0
- package/README.md +62 -18
- package/package.json +1 -1
- package/src/handlers.js +8 -4
- package/src/index.js +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,93 @@ 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.5] — 2026-05-09
|
|
34
|
+
|
|
35
|
+
Documentation-only release. plato (Mode A adopter) hit a real seam
|
|
36
|
+
under `bodyOverride`: they want to reorder the body (expiry warning
|
|
37
|
+
before URL) AND keep the "Last sign-in" security signal. The current
|
|
38
|
+
contract drops the line under override — deliberately, since override
|
|
39
|
+
is full-content replacement — and that read as a missing capability.
|
|
40
|
+
It isn't: `auth.deriveHandle(email)` and a parallel `createStore`
|
|
41
|
+
handle calling `getLastLogin(handle)` already give adopters everything
|
|
42
|
+
they need to compose the signal themselves. The gap was discoverability,
|
|
43
|
+
not surface area. Considered exporting a `formatLastLogin` helper to
|
|
44
|
+
centralize the canonical wording across adopters; held off under the
|
|
45
|
+
walk-away default-no, since the population that hits the
|
|
46
|
+
override + reorder + want-signal intersection is narrow (most
|
|
47
|
+
`bodyOverride` uses are per-call subject branding without a sign-in
|
|
48
|
+
event), and the drift risk for the few who do is small and recoverable.
|
|
49
|
+
Cross-product wording consistency for operators running multiple
|
|
50
|
+
knowless adopters belongs in a shared module on the operator side, not
|
|
51
|
+
in knowless. Revisit if a second independent adopter asks for the
|
|
52
|
+
helper. No code changes.
|
|
53
|
+
|
|
54
|
+
### Documented
|
|
55
|
+
|
|
56
|
+
- `GUIDE.md` Mode A walkthrough — added a "Composing the 'Last
|
|
57
|
+
sign-in' security signal under `bodyOverride`" callout right after
|
|
58
|
+
the `auth.deriveHandle` paragraph. Explains why the line doesn't
|
|
59
|
+
auto-append on overridden bodies (override is full-content
|
|
60
|
+
replacement, AF-26), points at the existing recipe
|
|
61
|
+
(`auth.deriveHandle` + parallel `createStore` + `getLastLogin`),
|
|
62
|
+
notes that `upsertLastLogin` only fires on callback consumption so
|
|
63
|
+
a pre-call read matches what knowless reads internally, and
|
|
64
|
+
includes a worked example. Targeted at adopters who reorder the
|
|
65
|
+
body and want the signal back; default-path adopters (no override)
|
|
66
|
+
are unaffected.
|
|
67
|
+
|
|
68
|
+
## [1.1.4] — 2026-05-08
|
|
69
|
+
|
|
70
|
+
Documentation + small bug fix. Adopters (plato, addypin, bareagent,
|
|
71
|
+
bareguard) repeatedly filed feature requests for things knowless
|
|
72
|
+
already shipped (`onTransportFailure` since v0.2.1) or deliberately
|
|
73
|
+
refuses (vendor SMTP, built-in DKIM). Triage showed a single root
|
|
74
|
+
cause: `README.md` under-routed adopters to `GUIDE.md`, `OPS.md`,
|
|
75
|
+
and PRD §16, so adopters read the README and assumed gaps. Pair
|
|
76
|
+
with one bug fix: the legacy `console.error('[knowless] mail submit
|
|
77
|
+
failed:', ...)` in `src/handlers.js` fired alongside
|
|
78
|
+
`onTransportFailure` and actively misled adopters into thinking no
|
|
79
|
+
callback existed.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- `src/handlers.js` — removed the duplicate `console.error` on SMTP
|
|
84
|
+
submission failure. The `onTransportFailure` hook is now the only
|
|
85
|
+
reporting path, with the default impl preserving stderr behaviour
|
|
86
|
+
(see below). Adopters who wired `onTransportFailure` previously
|
|
87
|
+
saw the same failure logged twice (once via stderr, once via
|
|
88
|
+
their hook); they now see it once via their hook.
|
|
89
|
+
- `src/index.js` — `onTransportFailure` default changed from a
|
|
90
|
+
no-op to a stderr printer (`[knowless] mail submit failed:
|
|
91
|
+
<message>`). Preserves NFR-10 ("SMTP delivery failures MUST be
|
|
92
|
+
logged") for adopters who don't wire the hook. Adopters who
|
|
93
|
+
explicitly want silence pass `onTransportFailure: () => {}`.
|
|
94
|
+
*Behavioural note:* adopters who were relying on the implicit
|
|
95
|
+
no-op default to suppress stderr will now see lines on transport
|
|
96
|
+
failure. Pass an explicit no-op to restore prior behaviour.
|
|
97
|
+
|
|
98
|
+
### Documented
|
|
99
|
+
|
|
100
|
+
- `README.md` "Where to go next" → "Required reading (before
|
|
101
|
+
integrating or filing an issue)". Reframed the routing block
|
|
102
|
+
from an optional menu to required reading, with each linked
|
|
103
|
+
doc's hook spelled out (hooks live in `GUIDE.md`, registrar
|
|
104
|
+
setup in `OPS.md` §5, refusal reasoning in PRD §16). Goal:
|
|
105
|
+
reduce the recurring "missing feature" filings that turn out to
|
|
106
|
+
be already-shipped or deliberately-refused.
|
|
107
|
+
- `README.md` "What's opinionated (locked by design)" → "What
|
|
108
|
+
knowless refuses (by design)". Sharper framing — closed doors,
|
|
109
|
+
not omissions — with PRD §16.2 / §16.7 / §16.12 anchors inline so
|
|
110
|
+
adopters who want to litigate a refusal land on the reasoning,
|
|
111
|
+
not the bullet. Added the "No DKIM/SPF in the library" line
|
|
112
|
+
explicitly (was implicit before; PRD §16.7 reasoning); pointed
|
|
113
|
+
at `OPS.md` §5 for the operator-side setup.
|
|
114
|
+
- `README.md` new section "Observability (wire it or be silent)" —
|
|
115
|
+
worked example of the three hooks (`onMailerSubmit`,
|
|
116
|
+
`onTransportFailure`, `onSuppressionWindow`) with the per-hook
|
|
117
|
+
threat-model framing. Previously surfaced only in `GUIDE.md`
|
|
118
|
+
Step 8, which adopters reliably missed.
|
|
119
|
+
|
|
33
120
|
## [1.1.3] — 2026-05-03
|
|
34
121
|
|
|
35
122
|
Documentation-only release. Surfaces a partial enumeration leak
|
package/GUIDE.md
CHANGED
|
@@ -384,6 +384,39 @@ 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
|
+
> **Composing the "Last sign-in" security signal under
|
|
388
|
+
> `bodyOverride`.** The default last-sign-in line lives in
|
|
389
|
+
> `composeBody` and does **not** auto-append on overridden bodies —
|
|
390
|
+
> override is full-content replacement (handlers.js AF-26). If your
|
|
391
|
+
> override needs the same signal, look up the timestamp before
|
|
392
|
+
> calling `startLogin` and interpolate it yourself. Everything you
|
|
393
|
+
> need is already exported: `auth.deriveHandle(email)` returns the
|
|
394
|
+
> handle, and a parallel read-only `createStore(dbPath)` exposes
|
|
395
|
+
> `getLastLogin(handle)` (Unix ms when the handle exists, `null` for
|
|
396
|
+
> sham / new / opted-out). `upsertLastLogin` only fires on callback
|
|
397
|
+
> consumption, so a pre-call read returns the same value knowless
|
|
398
|
+
> reads internally. Wording stays your responsibility under override —
|
|
399
|
+
> small drift cost for taking the wheel.
|
|
400
|
+
>
|
|
401
|
+
> ```js
|
|
402
|
+
> import { createStore } from 'knowless';
|
|
403
|
+
> const store = createStore(process.env.KNOWLESS_DB_PATH);
|
|
404
|
+
> // ... in your request handler ...
|
|
405
|
+
> const handle = auth.deriveHandle(email);
|
|
406
|
+
> const lastLoginAt = store.getLastLogin(handle); // null for sham/new
|
|
407
|
+
> await auth.startLogin({
|
|
408
|
+
> email,
|
|
409
|
+
> bodyOverride: ({ url }) =>
|
|
410
|
+
> `Click to sign in:\n\n${url}\n\n` +
|
|
411
|
+
> `This link expires in 15 minutes. If you didn't request this,\n` +
|
|
412
|
+
> `ignore this email.\n` +
|
|
413
|
+
> (lastLoginAt != null
|
|
414
|
+
> ? `\nLast sign-in: ${new Date(lastLoginAt).toISOString()}.\n` +
|
|
415
|
+
> `If that wasn't you, do not click the link above.\n`
|
|
416
|
+
> : ''),
|
|
417
|
+
> });
|
|
418
|
+
> ```
|
|
419
|
+
|
|
387
420
|
> **Set `failureRedirect: '/'` — it's part of the silent-miss
|
|
388
421
|
> contract, not just a UX knob.** The default falls back to
|
|
389
422
|
> `loginPath` (typically `/login`). That's a partial enumeration
|
package/README.md
CHANGED
|
@@ -9,17 +9,20 @@ npm install knowless
|
|
|
9
9
|
|
|
10
10
|
> v1.0.0 (walk-away release) | Node.js >= 22.5 | **1 production dep (nodemailer)** | Apache-2.0
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Required reading (before integrating or filing an issue)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
the
|
|
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.**
|
|
16
18
|
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
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. |
|
|
23
26
|
|
|
24
27
|
## What it does
|
|
25
28
|
|
|
@@ -86,30 +89,71 @@ Worked code for both in [`GUIDE.md`](GUIDE.md).
|
|
|
86
89
|
| **Library mode** | Mount the five handlers (`login`, `callback`, `verify`, `logout`, `loginForm`) in your existing Node app. |
|
|
87
90
|
| **Standalone server** (`npx knowless-server`) | Forward-auth gateway behind Caddy / nginx / Traefik for self-hosters gating Uptime Kuma / AdGuard / Pi-hole / Sonarr / Jellyfin / etc. One auth subdomain, SSO across services via the parent-domain cookie. |
|
|
88
91
|
|
|
89
|
-
## What
|
|
92
|
+
## What knowless refuses (by design)
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
98
|
+
[Lucia](https://lucia-auth.com/), [Auth.js](https://authjs.dev/),
|
|
99
|
+
or commercial offerings.
|
|
93
100
|
|
|
94
101
|
- **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.
|
|
95
105
|
- **One mail purpose: the sign-in link.** No `sendNotification()` to
|
|
96
106
|
be tempted by.
|
|
97
107
|
- **Plain-text 7-bit email.** No HTML, no tracking pixels, no
|
|
98
108
|
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.
|
|
99
112
|
- **No OAuth / OIDC / SAML.** Different audience.
|
|
100
113
|
- **No 2FA / WebAuthn / TOTP / passkeys.** Compose with a separate
|
|
101
114
|
library if you need them.
|
|
102
115
|
- **No admin UI.** `sqlite3 knowless.db` is the admin UI.
|
|
103
|
-
- **Hardcoded login form.** No template overrides
|
|
104
|
-
it.
|
|
116
|
+
- **Hardcoded login form.** No template overrides — PRD §16.12.
|
|
117
|
+
Fork, override the route entirely, or live with it.
|
|
105
118
|
- **No telemetry, analytics, or error reporting.** No phone-home of
|
|
106
|
-
any kind.
|
|
119
|
+
any kind. (Operator-side observability is opt-in via hooks — see
|
|
120
|
+
below.)
|
|
107
121
|
- **Walks away at v1.0.0.** Maintenance mode after that — only
|
|
108
122
|
security fixes.
|
|
109
123
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
113
157
|
|
|
114
158
|
## Operator commitments
|
|
115
159
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knowless",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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
|
@@ -399,10 +399,14 @@ export function createHandlers({ store, mailer, config, events }) {
|
|
|
399
399
|
});
|
|
400
400
|
}
|
|
401
401
|
} catch (err) {
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
//
|
|
402
|
+
// NFR-10: SMTP failure must be reported to the operator but
|
|
403
|
+
// MUST NOT leak to response shape. Reporting goes through the
|
|
404
|
+
// onTransportFailure hook only — the default impl (see
|
|
405
|
+
// index.js safeHook) writes to stderr when the adopter hasn't
|
|
406
|
+
// wired their own; an adopter who wires the hook owns the
|
|
407
|
+
// policy. The previous explicit console.error fired alongside
|
|
408
|
+
// the hook and misled adopters into thinking no callback
|
|
409
|
+
// existed.
|
|
406
410
|
ev.onTransportFailure({ error: err, timestamp: Date.now() });
|
|
407
411
|
// AF-6.2: dev-mode fallback. When SMTP is unreachable in local
|
|
408
412
|
// development the operator otherwise has no way to obtain the magic
|
package/src/index.js
CHANGED
|
@@ -172,7 +172,19 @@ export function knowless(options = {}) {
|
|
|
172
172
|
let shamCount = 0;
|
|
173
173
|
let rateLimitedCount = 0;
|
|
174
174
|
const onMailerSubmit = safeHook(options.onMailerSubmit, 'onMailerSubmit');
|
|
175
|
-
|
|
175
|
+
// NFR-10: SMTP failures must be reported to the operator. When the
|
|
176
|
+
// adopter doesn't wire onTransportFailure, default to a stderr
|
|
177
|
+
// printer so the unwired case still satisfies NFR-10. Adopters who
|
|
178
|
+
// want programmatic alerting override; adopters who want silence
|
|
179
|
+
// pass `() => {}` explicitly.
|
|
180
|
+
const onTransportFailure = safeHook(
|
|
181
|
+
options.onTransportFailure ??
|
|
182
|
+
((p) =>
|
|
183
|
+
process.stderr.write(
|
|
184
|
+
`[knowless] mail submit failed: ${p?.error?.message ?? p?.error ?? 'unknown'}\n`,
|
|
185
|
+
)),
|
|
186
|
+
'onTransportFailure',
|
|
187
|
+
);
|
|
176
188
|
const onSuppressionWindow = safeHook(options.onSuppressionWindow, 'onSuppressionWindow');
|
|
177
189
|
|
|
178
190
|
const events = {
|