knowless 1.1.4 → 1.1.6

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,82 @@ 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.6] — 2026-05-09
34
+
35
+ Documentation-only release. README was routing adopters into
36
+ internal-only docs (PRD §16) for the "why we refuse X" rationale,
37
+ which collapsed two audiences (adopters reading the README;
38
+ agents/contributors litigating design decisions) into one trail.
39
+ PRD isn't shipped via npm anyway, so npm consumers clicking those
40
+ links got dead repo-relative paths. Restructured the README as a
41
+ self-contained explainer (philosophy → mechanism → modes →
42
+ deployment shapes → refusals → operator commitments → threat model
43
+ → adopters → going-further footer) and moved the deeper refusal
44
+ rationale into a new repo-root `CLAUDE.md` for future agents
45
+ working on the library. No code changes.
46
+
47
+ ### Documented
48
+
49
+ - `README.md` — dropped the "Required reading" routing table that
50
+ pointed adopters at internal docs (PRD, knowless.context.md).
51
+ Five `PRD §16.x` references in the refusals bullets replaced with
52
+ inline one-liner reasoning. "Hardcoded login form" bullet now
53
+ carries a brief inline reason ("templating is a slope") in place
54
+ of the `PRD §16.12` pointer. Detailed observability code block
55
+ removed — the philosophy stays as a refusals bullet ("wire it or
56
+ be silent"), the worked example lives in `GUIDE.md`. Threat model
57
+ paragraph trimmed (removed trailing `knowless.context.md`
58
+ pointer). New "Going further" footer at the end pointing at
59
+ `GUIDE.md`, `OPS.md`, `CHANGELOG.md`. Walk-away bullet now
60
+ mirrors the four PRD §6.3 carve-outs verbatim.
61
+ - `CLAUDE.md` (new, repo-root, **not shipped via npm**) — agent
62
+ context with the walk-away doctrine, two-test lens for "should X
63
+ go in knowless," README-discoverability triage for adopter
64
+ feature requests, the most-litigated refusals (each with its PRD
65
+ §16 anchor for long-form reasoning), pointers to PRD / SPEC /
66
+ TASKS / AGENT_RULES, the decision-revisit protocol, and a
67
+ condensed code-standards block.
68
+ - `knowless.context.md` — dropped the bare `(PRD §16.2)`
69
+ parenthetical from the Postfix-on-localhost gotcha. The inline
70
+ reasoning that follows ("vendor mailers invite 'while we're at
71
+ it'…") carries the same point without routing readers into a
72
+ doc that isn't shipped.
73
+
74
+ ## [1.1.5] — 2026-05-09
75
+
76
+ Documentation-only release. plato (Mode A adopter) hit a real seam
77
+ under `bodyOverride`: they want to reorder the body (expiry warning
78
+ before URL) AND keep the "Last sign-in" security signal. The current
79
+ contract drops the line under override — deliberately, since override
80
+ is full-content replacement — and that read as a missing capability.
81
+ It isn't: `auth.deriveHandle(email)` and a parallel `createStore`
82
+ handle calling `getLastLogin(handle)` already give adopters everything
83
+ they need to compose the signal themselves. The gap was discoverability,
84
+ not surface area. Considered exporting a `formatLastLogin` helper to
85
+ centralize the canonical wording across adopters; held off under the
86
+ walk-away default-no, since the population that hits the
87
+ override + reorder + want-signal intersection is narrow (most
88
+ `bodyOverride` uses are per-call subject branding without a sign-in
89
+ event), and the drift risk for the few who do is small and recoverable.
90
+ Cross-product wording consistency for operators running multiple
91
+ knowless adopters belongs in a shared module on the operator side, not
92
+ in knowless. Revisit if a second independent adopter asks for the
93
+ helper. No code changes.
94
+
95
+ ### Documented
96
+
97
+ - `GUIDE.md` Mode A walkthrough — added a "Composing the 'Last
98
+ sign-in' security signal under `bodyOverride`" callout right after
99
+ the `auth.deriveHandle` paragraph. Explains why the line doesn't
100
+ auto-append on overridden bodies (override is full-content
101
+ replacement, AF-26), points at the existing recipe
102
+ (`auth.deriveHandle` + parallel `createStore` + `getLastLogin`),
103
+ notes that `upsertLastLogin` only fires on callback consumption so
104
+ a pre-call read matches what knowless reads internally, and
105
+ includes a worked example. Targeted at adopters who reorder the
106
+ body and want the signal back; default-path adopters (no override)
107
+ are unaffected.
108
+
33
109
  ## [1.1.4] — 2026-05-08
34
110
 
35
111
  Documentation + small bug fix. Adopters (plato, addypin, bareagent,
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,21 +9,6 @@ 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
- ## 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. |
26
-
27
12
  ## What it does
28
13
 
29
14
  The simpler answer that always worked: **magic link in, session
@@ -31,8 +16,8 @@ cookie out, nothing else stored.** Email is HMAC-hashed at the
31
16
  boundary and discarded. The library refuses, by API shape, to send
32
17
  anything but the sign-in link or store anything identifying.
33
18
 
34
- Most auth libraries default to maximum identity collection: full email
35
- in plaintext, profile fields, recovery email, federation. Even
19
+ Most auth libraries default to maximum identity collection: full
20
+ email in plaintext, profile fields, recovery email, federation. Even
36
21
  nominally privacy-focused options store enough that a breach is
37
22
  materially harmful. knowless inverts the default.
38
23
 
@@ -80,8 +65,6 @@ The same sham-work flow runs underneath either mode, so unknown
80
65
  emails, rate-limit hits, and real sends look identical to an external
81
66
  observer.
82
67
 
83
- Worked code for both in [`GUIDE.md`](GUIDE.md).
84
-
85
68
  ## Two deployment shapes
86
69
 
87
70
  | Shape | When |
@@ -91,69 +74,35 @@ Worked code for both in [`GUIDE.md`](GUIDE.md).
91
74
 
92
75
  ## What knowless refuses (by design)
93
76
 
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
77
+ These are closed doors, not omissions. If any break your case,
78
+ knowless isn't the right tool look at
98
79
  [Lucia](https://lucia-auth.com/), [Auth.js](https://authjs.dev/),
99
80
  or commercial offerings.
100
81
 
101
82
  - **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.
83
+ Vendor relationships invite reusing the mailer for non-auth mail,
84
+ which collapses the "one mail purpose" invariant.
105
85
  - **One mail purpose: the sign-in link.** No `sendNotification()` to
106
86
  be tempted by.
107
87
  - **Plain-text 7-bit email.** No HTML, no tracking pixels, no
108
88
  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.
89
+ - **No DKIM/SPF in the library.** That's the MTA's job; knowless
90
+ emits clean RFC822 and your Postfix + opendkim signs it.
112
91
  - **No OAuth / OIDC / SAML.** Different audience.
113
92
  - **No 2FA / WebAuthn / TOTP / passkeys.** Compose with a separate
114
93
  library if you need them.
115
94
  - **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.
95
+ - **Hardcoded login form.** Templating is a slope today "let me
96
+ put my logo," tomorrow "let me theme the page," eventually "let me
97
+ embed a JS framework." Fork, override the route entirely, or live
98
+ with it.
118
99
  - **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.
100
+ any kind. Operator-side observability is opt-in via three hooks
101
+ (`onMailerSubmit` / `onTransportFailure` / `onSuppressionWindow`)
102
+ wire them or be silent.
103
+ - **Walks away at v1.0.0.** Maintenance mode after that — security
104
+ fixes, bug fixes that don't change the API surface, doc fixes,
105
+ helper exports.
157
106
 
158
107
  ## Operator commitments
159
108
 
@@ -165,8 +114,6 @@ By choosing knowless, you commit to running:
165
114
  - A **null-route** for the configured `shamRecipient` so silent-miss
166
115
  sham mail drops, not bounces
167
116
 
168
- Step-by-step in [`OPS.md`](OPS.md).
169
-
170
117
  ## Threat model — one paragraph
171
118
 
172
119
  **Defends well:** DB-only leaks (handles are HMAC-salted),
@@ -184,13 +131,9 @@ fake site, but a phished mailbox still receives links).
184
131
 
185
132
  **Does NOT defend against:** sophisticated bots that bypass the
186
133
  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."
134
+ compromised email accounts, social engineering, insider threat at
135
+ the operator. Layer-2 defences (Cloudflare, fail2ban, reverse-proxy
136
+ rate-limits) belong above the library.
194
137
 
195
138
  ## Adopters
196
139
 
@@ -208,6 +151,15 @@ If you're picking knowless up: the addypin and gitdone callsites are
208
151
  both Mode A and good worked references for the use-first / claim-later
209
152
  shape.
210
153
 
154
+ ## Going further
155
+
156
+ - [`GUIDE.md`](GUIDE.md) — integration walkthrough, observability
157
+ hooks, edge cases, FAQ, troubleshooting.
158
+ - [`OPS.md`](OPS.md) — operator setup (Postfix install, SPF/DKIM/PTR/
159
+ DMARC at your registrar, null-route, systemd, forward-auth wiring,
160
+ fail2ban).
161
+ - [`CHANGELOG.md`](CHANGELOG.md) — version history.
162
+
211
163
  ## License
212
164
 
213
165
  [Apache 2.0](LICENSE) with [`NOTICE`](NOTICE) preservation. Forks
@@ -649,10 +649,9 @@ rate-limits) belongs above the library.
649
649
 
650
650
  2. **Postfix on localhost is required.** No remote SMTP, no
651
651
  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.
652
+ intentional: vendor mailers invite "while we're at it, let's
653
+ send a welcome email," which contradicts the philosophy. If
654
+ you can't run Postfix, knowless isn't your library.
656
655
 
657
656
  3. **`shamRecipient` MUST be discarded without external delivery.**
658
657
  Default is `null@knowless.invalid`. With the default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knowless",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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",