knowless 1.1.3 → 1.1.4

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,58 @@ 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.4] — 2026-05-08
34
+
35
+ Documentation + small bug fix. Adopters (plato, addypin, bareagent,
36
+ bareguard) repeatedly filed feature requests for things knowless
37
+ already shipped (`onTransportFailure` since v0.2.1) or deliberately
38
+ refuses (vendor SMTP, built-in DKIM). Triage showed a single root
39
+ cause: `README.md` under-routed adopters to `GUIDE.md`, `OPS.md`,
40
+ and PRD §16, so adopters read the README and assumed gaps. Pair
41
+ with one bug fix: the legacy `console.error('[knowless] mail submit
42
+ failed:', ...)` in `src/handlers.js` fired alongside
43
+ `onTransportFailure` and actively misled adopters into thinking no
44
+ callback existed.
45
+
46
+ ### Fixed
47
+
48
+ - `src/handlers.js` — removed the duplicate `console.error` on SMTP
49
+ submission failure. The `onTransportFailure` hook is now the only
50
+ reporting path, with the default impl preserving stderr behaviour
51
+ (see below). Adopters who wired `onTransportFailure` previously
52
+ saw the same failure logged twice (once via stderr, once via
53
+ their hook); they now see it once via their hook.
54
+ - `src/index.js` — `onTransportFailure` default changed from a
55
+ no-op to a stderr printer (`[knowless] mail submit failed:
56
+ <message>`). Preserves NFR-10 ("SMTP delivery failures MUST be
57
+ logged") for adopters who don't wire the hook. Adopters who
58
+ explicitly want silence pass `onTransportFailure: () => {}`.
59
+ *Behavioural note:* adopters who were relying on the implicit
60
+ no-op default to suppress stderr will now see lines on transport
61
+ failure. Pass an explicit no-op to restore prior behaviour.
62
+
63
+ ### Documented
64
+
65
+ - `README.md` "Where to go next" → "Required reading (before
66
+ integrating or filing an issue)". Reframed the routing block
67
+ from an optional menu to required reading, with each linked
68
+ doc's hook spelled out (hooks live in `GUIDE.md`, registrar
69
+ setup in `OPS.md` §5, refusal reasoning in PRD §16). Goal:
70
+ reduce the recurring "missing feature" filings that turn out to
71
+ be already-shipped or deliberately-refused.
72
+ - `README.md` "What's opinionated (locked by design)" → "What
73
+ knowless refuses (by design)". Sharper framing — closed doors,
74
+ not omissions — with PRD §16.2 / §16.7 / §16.12 anchors inline so
75
+ adopters who want to litigate a refusal land on the reasoning,
76
+ not the bullet. Added the "No DKIM/SPF in the library" line
77
+ explicitly (was implicit before; PRD §16.7 reasoning); pointed
78
+ at `OPS.md` §5 for the operator-side setup.
79
+ - `README.md` new section "Observability (wire it or be silent)" —
80
+ worked example of the three hooks (`onMailerSubmit`,
81
+ `onTransportFailure`, `onSuppressionWindow`) with the per-hook
82
+ threat-model framing. Previously surfaced only in `GUIDE.md`
83
+ Step 8, which adopters reliably missed.
84
+
33
85
  ## [1.1.3] — 2026-05-03
34
86
 
35
87
  Documentation-only release. Surfaces a partial enumeration leak
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
- ## Where to go next
12
+ ## Required reading (before integrating or filing an issue)
13
13
 
14
- Two docs live alongside this README. They serve different readers; pick
15
- the one that matches yours.
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
- | You are | Read this | What's there |
18
- |---|---|---|
19
- | **A human integrating for the first time** | [`GUIDE.md`](GUIDE.md) | Step-by-step walkthrough — install, generate the secret, set up Postfix, mount handlers, both modes worked end-to-end. Configuration reference, FAQ, troubleshooting. |
20
- | **An AI agent, or reading in a hurry** | [`knowless.context.md`](knowless.context.md) | Dense single-file reference. Public API table, every option with defaults, 19 gotchas, lifecycle diagrams, the sham-work pattern, threat model, "what's NOT in knowless and why." Designed to fit one context window. |
21
- | **Deploying to a real server** | [`OPS.md`](OPS.md) | Postfix install, SPF/DKIM/PTR/DMARC, null-route, systemd, Caddy/nginx/Traefik forward-auth, MailHog dev, fail2ban, multi-process. |
22
- | **Tracking what changed** | [`CHANGELOG.md`](CHANGELOG.md) | Version history. |
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's opinionated (locked by design)
92
+ ## What knowless refuses (by design)
90
93
 
91
- Deliberate trade-offs. The library refuses, by API shape, to grow
92
- into them.
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; fork or live with
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
- If any of those break your case, knowless isn't the right tool. Look
111
- at [Lucia](https://lucia-auth.com/), [Auth.js](https://authjs.dev/),
112
- or commercial offerings.
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",
3
+ "version": "1.1.4",
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
- // Per NFR-10: SMTP failure logged, NEVER leaked to response shape.
403
- console.error('[knowless] mail submit failed:', err.message);
404
- // v0.2.1: per-event hook for SMTP failures. Carries no identity
405
- // data, safe per-event. Operator wires this to alerting.
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
- const onTransportFailure = safeHook(options.onTransportFailure, 'onTransportFailure');
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 = {