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 +76 -0
- package/GUIDE.md +33 -0
- package/README.md +30 -78
- package/knowless.context.md +3 -4
- package/package.json +1 -1
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
|
|
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.
|
|
95
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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.**
|
|
110
|
-
|
|
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.**
|
|
117
|
-
|
|
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.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
188
|
-
operator. Layer-2 defences (Cloudflare, fail2ban, reverse-proxy
|
|
189
|
-
rate-limits) belong above the library
|
|
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
|
package/knowless.context.md
CHANGED
|
@@ -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
|
|
653
|
-
|
|
654
|
-
|
|
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.
|
|
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",
|