knowless 0.1.10 → 0.2.0

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
@@ -5,17 +5,74 @@ All notable changes to `knowless` are recorded here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  Versioning is [SemVer](https://semver.org/).
7
7
 
8
+ ## Milestones
9
+
10
+ - **2026-04-28 — First customer integration shipped.** addypin
11
+ merged its `try/knowless` branch and runs knowless as its
12
+ auth+mail layer. ~1,150 LOC of bespoke auth/mail code removed,
13
+ ~35 LOC of knowless wiring added (~33× reduction). Drove audit
14
+ findings AF-7 → AF-17 across v0.1.5–v0.1.10.
15
+
8
16
  ## [Unreleased]
9
17
 
10
18
  - **Turnkey Docker image** (`knowless/knowless-server:0.2.x`)
11
- bundling Postfix + null-route + the binary so a self-hoster
12
- runs `docker compose up` and has a working auth gateway in
13
- one step. Material UX win for the PRD §4.2 self-hoster
14
- audience. Targeted for v0.2.0.
19
+ bundling Postfix + null-route + the binary. Now meaningfully
20
+ smaller and faster to build because v0.2.0 dropped the native
21
+ compile dep. Targeted for v0.2.1.
15
22
  - Caddy forward-auth Docker integration test (TASKS.md 6.8).
16
23
  - `knowless-server --check-null-route`: CLI probe that submits a
17
24
  test message to `shamRecipient` and confirms the local MTA
18
- discarded it. Targeted for v0.2.0.
25
+ discarded it. Targeted for v0.2.1.
26
+
27
+ ## [0.2.0] — 2026-04-28
28
+
29
+ **No native compile. One production dep.** Drops `better-sqlite3`
30
+ in favour of `node:sqlite` (Node stdlib). Adopters on long-LTS
31
+ distros (RHEL 8/9, Alma, Rocky, Amazon Linux 2) no longer need a
32
+ C++20 toolchain to `npm install knowless`.
33
+
34
+ ### Breaking
35
+
36
+ - **Node floor bumped: `>=20.0.0` → `>=22.5.0`.** `node:sqlite`
37
+ requires Node 22.5+; unflagged stable on Node 24 LTS. Node 20
38
+ reaches EOL April 2026.
39
+ - **`better-sqlite3` removed from `dependencies`.** Down to one
40
+ production dep (`nodemailer`). Transitive package count goes
41
+ from ~40 to ~2. No `prebuild-install`, no `gcc`, no `make`,
42
+ no Python during install.
43
+ - **Storage internals changed**, public API unchanged. The
44
+ `createStore()` interface (SPEC §13) is byte-for-byte identical.
45
+ All 192 tests pass on first run after the swap.
46
+
47
+ ### Migration
48
+
49
+ - **For knowless library adopters:** ensure your runtime is
50
+ Node 22.5+. If you pinned `better-sqlite3` somewhere yourself
51
+ for unrelated reasons, that's now your call. Otherwise:
52
+ ```sh
53
+ npm install knowless@0.2.0
54
+ ```
55
+ No code changes on your side. Existing SQLite databases
56
+ continue to work — same schema, same WAL mode, same
57
+ prepared statements. Sessions and handles persist across
58
+ the upgrade.
59
+ - **For `knowless-server` operators:** ensure the host runs
60
+ Node 22.5+. If you ran `dnf install gcc-toolset-13` to get
61
+ v0.1.x to compile, you can remove it after the upgrade —
62
+ v0.2.0 doesn't need it. The systemd unit and env-var config
63
+ are unchanged.
64
+ - **You may see one `ExperimentalWarning` from `node:sqlite`
65
+ at first import** on Node 22.x. Suppress with `--no-warnings`
66
+ or run on Node 24 LTS where the API is fully stable.
67
+
68
+ ### Internal
69
+
70
+ - New `makeTransaction(db, fn)` adapter in `src/store.js`
71
+ replaces `better-sqlite3`'s `db.transaction()` wrapper. Uses
72
+ `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` directly — same
73
+ serialisation guarantee for the transactional cap-check
74
+ (SPEC §4.7) and account-deletion paths (FR-37a).
75
+ - Closes AF-18 (the addypin RHEL 8 deployment trap).
19
76
 
20
77
  ## [0.1.10] — 2026-04-28
21
78
 
package/GUIDE.md CHANGED
@@ -550,7 +550,7 @@ Full options table:
550
550
  | `shamRecipient` | no | `null@knowless.invalid` | Where sham mail goes (your MTA must discard it). |
551
551
  | `sweepIntervalMs` | no | `300000` | Sweeper tick (5 min default). |
552
552
  | `failureRedirect` | no | (= `loginPath`) | Where /auth/callback failures redirect. **Mode-A adopters:** if you don't mount `loginForm`, set this to a route you actually serve (e.g. `/`) — otherwise expired/replayed magic-link clicks 302 to a 404. |
553
- | `store` | no | (built-in better-sqlite3) | Inject your own store implementation. |
553
+ | `store` | no | (built-in `node:sqlite`) | Inject your own store implementation. |
554
554
  | `mailer` | no | (built-in nodemailer) | Inject your own mailer. |
555
555
 
556
556
  ## FAQ
@@ -659,15 +659,19 @@ mail account is later compromised.
659
659
 
660
660
  ## Constraints / install footprint
661
661
 
662
- - **Two direct dependencies.** `nodemailer` (SMTP submission) and
663
- `better-sqlite3` (storage). Both audited and pinned at major
664
- versions in `package.json`.
665
- - **~40 transitive packages** in a typical install. The bulk are
666
- `nodemailer`'s ecosystem deps (mostly idle in our usage) and
667
- `better-sqlite3`'s build-time prebuild fetcher. You may see one
668
- deprecation warning during install for `prebuild-install`
669
- build-chain noise, not runtime code.
670
- - **Node ≥ 20.** Uses `node:util parseArgs`, `node:net BlockList`
671
- for CIDR support, and `--env-file=` for the standalone server.
672
- - **No optional deps, no postinstall scripts** beyond `better-
673
- sqlite3`'s native binding fetch.
662
+ - **One direct dependency.** `nodemailer` (SMTP submission). Storage
663
+ is `node:sqlite` (Node stdlib, no native compile, no toolchain
664
+ required on the host).
665
+ - **~2 transitive packages** in a typical install (down from ~40 in
666
+ v0.1.x). No `prebuild-install`, no `gcc`, no `make`, no Python.
667
+ `npm ci` works on stock RHEL 8 / Alma / Rocky / Amazon Linux 2
668
+ with no extra packages. Self-hosters: `npm install knowless` is
669
+ done.
670
+ - **Node ≥ 22.5.** `node:sqlite` requires this floor (introduced
671
+ 22.5, unflagged in 22.13+, fully stable in 24 LTS). Drops Node
672
+ 20 about to EOL anyway.
673
+ - **No optional deps, no postinstall scripts.**
674
+
675
+ > `node:sqlite` may print one `ExperimentalWarning` to stderr on
676
+ > first import. Suppress with `--no-warnings` or by running on
677
+ > Node 24 LTS where the API is fully stable.
package/OPS.md CHANGED
@@ -548,13 +548,14 @@ CLI invoked by Postfix's `pipe` transport, or a web server plus a
548
548
  cron worker handling 48h reminders. Each process instantiates
549
549
  `knowless({...})` against the same `dbPath`.
550
550
 
551
- **Why this works.** `better-sqlite3` opens the database in WAL mode
552
- by default (knowless explicitly sets `journal_mode=WAL` at startup).
553
- WAL allows multiple readers and one writer concurrently, with the
554
- OS-level locking semantics needed for cross-process safety. Every
555
- write goes through a prepared statement under a SQLite transaction,
556
- so two processes inserting tokens or sessions at the same time can't
557
- corrupt the table.
551
+ **Why this works.** knowless opens the database in WAL mode at
552
+ startup (`PRAGMA journal_mode = WAL`). WAL allows multiple readers
553
+ and one writer concurrently, with the OS-level locking semantics
554
+ needed for cross-process safety. Every write goes through a prepared
555
+ statement under a SQLite transaction, so two processes inserting
556
+ tokens or sessions at the same time can't corrupt the table. Since
557
+ v0.2.0 the storage backend is `node:sqlite` (Node stdlib) — no
558
+ native compile, no `gcc` toolchain on the host.
558
559
 
559
560
  **What to know about each subsystem under multi-process:**
560
561
 
package/README.md CHANGED
@@ -7,35 +7,7 @@ that don't need to email their users for anything but the sign-in link.
7
7
  npm install knowless
8
8
  ```
9
9
 
10
- > v0.1.10 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
11
-
12
- ## What this is
13
-
14
- Magic-link auth + session cookie + nothing else. Six lines of
15
- operator code:
16
-
17
- ```js
18
- import express from 'express';
19
- import { knowless } from 'knowless';
20
-
21
- const app = express();
22
- const auth = knowless({
23
- secret: process.env.KNOWLESS_SECRET, // 64-char hex (32 bytes)
24
- baseUrl: 'https://app.example.com',
25
- from: 'auth@app.example.com',
26
- });
27
-
28
- app.use(express.urlencoded({ extended: false }));
29
- app.get('/login', auth.loginForm);
30
- app.post('/login', auth.login);
31
- app.get('/auth/callback', auth.callback);
32
- app.get('/verify', auth.verify);
33
- app.post('/logout', auth.logout);
34
- ```
35
-
36
- That's the entire integration. Users hit `/login`, type their email,
37
- click the magic link in their inbox, and are logged in for 30 days
38
- via a signed session cookie.
10
+ > v0.2.0 | Node.js >= 22.5 | **1 production dep (nodemailer)** | Apache-2.0
39
11
 
40
12
  ## Why this exists
41
13
 
@@ -44,114 +16,194 @@ maximum identity collection: full email stored in plaintext, profile
44
16
  fields, recovery email, federation. Even nominally privacy-focused
45
17
  options store enough that a breach is materially harmful.
46
18
 
47
- `knowless` is the simpler answer that always worked: magic link in,
48
- session cookie out, nothing else stored. The library refuses, by API
49
- shape, to send anything but the sign-in link or store anything
50
- identifying.
19
+ knowless is the simpler answer that always worked: **magic link in,
20
+ session cookie out, nothing else stored.** Email is HMAC-hashed at the
21
+ boundary and discarded. The library refuses, by API shape, to send
22
+ anything but the sign-in link or store anything identifying.
51
23
 
52
24
  The thesis: most services have ten layers of auth tooling where they
53
25
  need two.
54
26
 
55
- ## What it commits to
56
-
57
- - **Stores no plaintext email, ever.** Email is salted-hashed on the
58
- way in (`HMAC-SHA256(secret, normalized_email)`) and discarded.
59
- - **Sends no email except the magic link.** Not a welcome message,
60
- not a digest, not a notification. By API shape there is no
61
- `sendNotification()` method to be tempted by.
62
- - **Self-hostable end to end.** No vendor relationships. No
63
- telemetry. No phone-home of any kind.
64
- - **Walks away at v1.0.0.** Maintenance mode after that.
65
-
66
- ## What it deliberately doesn't do
67
-
68
- A short list (full table in [`docs/01-product/PRD.md`](docs/01-product/PRD.md) §14):
69
-
70
- - No remote SMTP / mail vendor support localhost Postfix is the
71
- only transport
72
- - No HTML email, no tracking pixels, no click-rewriting
73
- - No OAuth / OIDC / SAML — different audience
74
- - No 2FA / WebAuthn / TOTP — compose with a separate library if
75
- needed
76
- - No admin UI for handles or sessions `sqlite3 knowless.db` is
77
- the admin UI
78
- - No customisable login form templates — the page is hardcoded;
79
- fork or live with it
80
- - No telemetry, analytics, or error reporting
27
+ ## Two modes pick per action
28
+
29
+ Same library, two flows. They coexist in one app; choose per-endpoint
30
+ based on whether forcing a login *before* the action would harm UX.
31
+
32
+ ### Mode Bregister-first (the form)
33
+
34
+ User must log in before performing the action. Standard "sign in to
35
+ continue" flow.
36
+
37
+ - User hits `/login`, types email
38
+ - Magic link arrives, click → session cookie
39
+ - Your protected endpoints call `auth.handleFromRequest(req)` to gate
40
+ access
41
+
42
+ Use for: account settings, paid features, anything that requires an
43
+ identified user at the moment of the action.
44
+
45
+ ### Mode A use-first, claim-later (programmatic)
46
+
47
+ User performs the action *without* being logged in. You capture their
48
+ email along with the action, fire a magic link via
49
+ `auth.startLogin({email, nextUrl, ...})`, and clicking it opens a
50
+ session and "promotes" the deferred resource.
51
+
52
+ Use for: drop-a-pin / submit-a-paste / share-a-link / disposable
53
+ resources / anywhere logging in first kills the UX.
54
+
55
+ The same 12-step sham-work flow runs underneath either mode, so
56
+ unknown emails, rate-limit hits, and real sends look identical to an
57
+ external observer (the FR-6 timing-equivalence guarantee). Pick per
58
+ action; the two coexist.
59
+
60
+ Worked code for both modes is in [`GUIDE.md`](GUIDE.md). The dense
61
+ API reference is [`knowless.context.md`](knowless.context.md).
62
+
63
+ ## What's opinionated (locked by design)
64
+
65
+ These are deliberate trade-offs, documented as `NO-GO` in
66
+ [`docs/01-product/PRD.md`](docs/01-product/PRD.md) §14.
67
+ The library refuses, by API shape, to grow into them.
68
+
69
+ - **Localhost SMTP only.** No Mailgun/Postmark/SES/Resend. The
70
+ operator runs Postfix (or another MTA) on the same host, in
71
+ outbound-only mode.
72
+ - **One mail purpose: the sign-in link.** No welcome message, no
73
+ digest, no notification. There is no `sendNotification()` to be
74
+ tempted by.
75
+ - **Plain-text 7-bit email.** No HTML, no tracking pixels, no
76
+ click-rewriting, no read-receipts.
77
+ - **No OAuth / OIDC / SAML.** Different audience.
78
+ - **No 2FA / WebAuthn / TOTP / passkeys.** Compose with a separate
79
+ library if you need them.
80
+ - **No admin UI.** `sqlite3 knowless.db` is the admin UI.
81
+ - **Hardcoded login form.** No template overrides; fork or live
82
+ with it.
83
+ - **No telemetry, analytics, or error reporting.** Self-hostable end
84
+ to end. No phone-home of any kind.
85
+ - **Walks away at v1.0.0.** Maintenance mode after that — only
86
+ security fixes.
87
+
88
+ ## What's swappable
89
+
90
+ Everything that *isn't* identity-shape or threat-model essential is
91
+ config or injection.
92
+
93
+ | Knob | Default | Common reasons to change |
94
+ |---|---|---|
95
+ | `dbPath` | `./knowless.db` | Move to `/var/lib/knowless/...` for systemd; share across processes |
96
+ | `smtpHost`, `smtpPort` | `localhost`, `25` | Point at MailHog (`localhost:1025`) for dev mail inspection |
97
+ | `cookieDomain` | hostname of `baseUrl` | Set to your eTLD+1 for SSO across subdomains |
98
+ | `cookieSecure` | `true` | `false` only for `http://localhost` dev (logs a warning) |
99
+ | `tokenTtlSeconds`, `sessionTtlSeconds` | `900`, `2592000` | Tighten for high-security uses; loosen at your peril |
100
+ | `openRegistration` | `false` | `true` to let any new email auto-register on first link |
101
+ | `subject` | `Sign in` | Match your brand; per-call override on `startLogin` (`subjectOverride`) |
102
+ | `bodyFooter` | none | Append a constant brand/legal/feedback line to every magic-link mail |
103
+ | `confirmationMessage` | (default copy) | Replace the post-submit "we'll email you" text |
104
+ | `maxLoginRequestsPerIpPerHour`, `maxNewHandlesPerIpPerHour` | `30`, `3` | Raise for genuinely shared NATs; `0` to disable in dev |
105
+ | `trustedProxies` | `[127.0.0.1, ::1]` | Plain IPs **and** CIDRs (`10.0.0.0/8`) for k8s/docker/cgnat |
106
+ | `bypassRateLimit` (per-call) | `false` | Trusted CLI/cron callers via `auth.startLogin` |
107
+ | `store` | built-in `node:sqlite` | Inject your own store (Postgres, etc.) |
108
+ | `mailer` | built-in nodemailer | Inject your own mailer |
109
+ | `transportOverride` | none | Pass a custom `nodemailer.createTransport` |
110
+ | `onSweepError(err)` | none | Operator alerting hook for sweeper failures |
111
+ | `devLogMagicLinks` | `false` | `true` in dev: print magic-link URLs (or silent-miss hints) to stderr when SMTP fails |
112
+
113
+ Full table with defaults, types, and validation rules:
114
+ [`GUIDE.md`](GUIDE.md) → "Configuration reference."
81
115
 
82
116
  ## Two deployment shapes (one codebase)
83
117
 
84
118
  | Mode | Status | When |
85
119
  |---|---|---|
86
120
  | **Library mode** | shipped (v0.1.0) | Mount handlers in your existing Node app |
87
- | **Standalone server** (forward-auth) | shipped (v0.1.3) | Self-hosters gating Uptime Kuma / AdGuard / Pi-hole / Sonarr / etc. via Caddy or nginx |
121
+ | **Standalone server** (forward-auth) | shipped (v0.1.3) | Self-hosters gating Uptime Kuma / AdGuard / Pi-hole / Sonarr / etc. behind Caddy / nginx / Traefik |
122
+
123
+ Library mode is the six-line example in [`GUIDE.md`](GUIDE.md).
124
+ Standalone server is `npx knowless-server` — full Postfix + DNS +
125
+ reverse-proxy walkthrough in [`OPS.md`](OPS.md).
126
+
127
+ ## First customer: addypin
128
+
129
+ [`addypin`](https://github.com/hamr0/addypin) — location-sharing
130
+ service in the same hermit-architecture lineage — adopted knowless
131
+ as its auth+mail layer. The integration delta:
132
+
133
+ - **~1,150 lines of bespoke auth/mail code removed** (custom mailer,
134
+ inbound CLI, login plumbing, pin-confirmation state machine, email
135
+ fingerprinting helpers, the matching test files)
136
+ - **~35 lines of knowless wiring added**
137
+ - **~33× reduction** on the auth/mail surface
138
+ - **One production dep** (`nodemailer` only; v0.2.0 dropped
139
+ `better-sqlite3` for `node:sqlite`, the stdlib SQLite driver — no
140
+ C++ toolchain, no native compile, ~40 transitive packages → 2)
88
141
 
89
- Library mode is the six-line example above. Standalone server is
90
- `npx knowless-server` see [`OPS.md`](OPS.md) for the full Postfix +
91
- DNS + reverse-proxy walkthrough.
142
+ The integration round produced the audit findings AF-7 through AF-17
143
+ that drove v0.1.5 v0.1.10. See [`docs/01-product/PRD.md`](docs/01-product/PRD.md)
144
+ §17 for the full backlog.
92
145
 
93
146
  ## Operator commitments
94
147
 
95
148
  By choosing knowless, you commit to:
96
149
 
97
- - Running your own server with **Postfix installed and configured**
98
- for outbound-only mail (or another localhost MTA)
99
- - Setting up **SPF, DKIM, and PTR records** for your sending domain
100
- (one-time DNS setup)
150
+ - Running your own server with **Postfix** (or another MTA) installed
151
+ for outbound-only mail
152
+ - Setting up **SPF, DKIM, and PTR** for your sending domain
101
153
  - Verifying **outbound port 25** is open (some clouds block it)
102
- - A **null-route entry** in your MTA's `transport_maps` for the
103
- configured `shamRecipient` (default `null@knowless.invalid`) so
104
- silent-miss sham mail is dropped, not delivered to NXDOMAIN
105
- - Accepting that this is the **only email** your service ever sends
154
+ - A **null-route entry** for the configured `shamRecipient` so
155
+ silent-miss sham mail is dropped, not bounced
156
+ - Accepting that the magic link is the **only email** your service
157
+ ever sends
106
158
 
107
- These are documented in [`OPS.md`](OPS.md): Postfix install,
108
- null-route, SPF/DKIM/PTR, systemd unit, Caddy / nginx / Traefik
109
- forward-auth examples, Tailscale pattern, reverse-proxy rate limiting,
110
- and fail2ban / Turnstile references.
159
+ Step-by-step in [`OPS.md`](OPS.md): Postfix install, null-route,
160
+ SPF/DKIM/PTR/DMARC, systemd unit, Caddy / nginx / Traefik
161
+ forward-auth examples, Tailscale pattern, reverse-proxy rate
162
+ limiting, fail2ban / Turnstile, multi-process deployments, MailHog
163
+ dev workflow, backups.
111
164
 
112
165
  ## Documentation
113
166
 
114
- - [`README.md`](README.md) (this file) project pitch, six-line example
115
- - [`GUIDE.md`](GUIDE.md) adopter walkthrough: who it's for, who it
116
- isn't, how to integrate, configuration reference, FAQ
117
- - [`OPS.md`](OPS.md) — operator setup: Postfix, null-route, DNS,
118
- systemd, reverse-proxy forward-auth examples
119
- - [`knowless.context.md`](knowless.context.md) dense AI-agent
120
- integration guide (tables, gotchas, public API at a glance)
167
+ - [**`GUIDE.md`**](GUIDE.md) start here. Adopter walkthrough,
168
+ install, six-line example, both modes worked end-to-end,
169
+ configuration reference, FAQ, troubleshooting.
170
+ - [**`knowless.context.md`**](knowless.context.md) — dense reference
171
+ for AI agents and humans-in-a-hurry. Public API table, all options,
172
+ 18 gotchas, lifecycles, the sham-work pattern, threat model
173
+ summary.
174
+ - [`OPS.md`](OPS.md) — operator setup, fresh VPS to working forward-auth.
175
+ - [`CHANGELOG.md`](CHANGELOG.md) — version history.
121
176
  - [`docs/01-product/PRD.md`](docs/01-product/PRD.md) — product
122
- requirements: scope, threat model, decisions log, NO-GO table
177
+ requirements, threat model, decisions log, NO-GO table, audit
178
+ findings backlog.
123
179
  - [`docs/02-design/SPEC.md`](docs/02-design/SPEC.md) — wire formats,
124
- byte layouts, algorithms (reimplementation-grade)
125
- - [`docs/03-tasks/TASKS.md`](docs/03-tasks/TASKS.md) — implementation
126
- task list and phase plan
127
- - [`CHANGELOG.md`](CHANGELOG.md) — version history
180
+ algorithms, byte layouts (reimplementation-grade).
128
181
 
129
- ## Threat model — what this defends and what it doesn't
182
+ ## Threat model (one-paragraph)
130
183
 
131
- Honest version (full detail in [`docs/01-product/PRD.md`](docs/01-product/PRD.md) §12):
184
+ Honest version (full detail in [PRD §12](docs/01-product/PRD.md)):
132
185
 
133
- **Defends well:** database-only leaks, plaintext email exfiltration,
134
- password reuse / credential stuffing, silent email enumeration
135
- (timing-equivalent within 1ms locally), email-bombing a target,
136
- naive bot traffic, account-creation spam, replay attacks, open
137
- redirects.
186
+ **Defends well:** DB-only leaks, plaintext-email exfiltration, password
187
+ reuse / credential stuffing, silent email enumeration (timing-
188
+ equivalent within 1ms locally), email-bombing a target, naive bot
189
+ traffic, account-creation spam, replay attacks, open redirects, CSRF
190
+ on `POST /login` / `POST /logout` (Origin/Referer whitelist).
138
191
 
139
192
  **Defends partially:** HMAC-secret-only leak (allows targeted
140
- existence-checks but not session forgery), phishing (no password
141
- to type into a fake site, but a phished mailbox can still receive
142
- links).
193
+ existence checks but not session forgery), phishing (no password to
194
+ type into a fake site, but a phished mailbox still receives links).
143
195
 
144
196
  **Does NOT defend against:** sophisticated bots that bypass the
145
197
  honeypot, distributed floods from many IPs, full server compromise,
146
198
  compromised email accounts, social engineering, insider threat at
147
199
  the operator. Layer-2 defences (Cloudflare, fail2ban, reverse-proxy
148
- rate-limits) belong above the library; OPS.md will document the
149
- common patterns.
200
+ rate-limits) belong above the library; [`OPS.md`](OPS.md) §9–§10
201
+ covers the patterns.
150
202
 
151
203
  ## Sibling projects
152
204
 
153
- - [`addypin`](https://github.com/hamr0/addypin) — location sharing
154
- with the same hermit philosophy
205
+ - [`addypin`](https://github.com/hamr0/addypin) — location sharing,
206
+ first knowless adopter
155
207
  - [`gitdone`](https://github.com/hamr0/gitdone) — verified email
156
208
  actions via DKIM/SPF inbound
157
209
 
@@ -160,8 +212,8 @@ common patterns.
160
212
  Issues and PRs welcome at <https://github.com/hamr0/knowless>.
161
213
 
162
214
  Per the v1.0.0 walk-away framing in PRD §6.3: feature requests after
163
- v1.0.0 ships will be deflected to the §14 NO-GO table or to sibling
164
- projects. The library being "done" is a feature.
215
+ v1.0.0 ships will be deflected to the [§14 NO-GO table](docs/01-product/PRD.md)
216
+ or to sibling projects. The library being "done" is a feature.
165
217
 
166
218
  ## License
167
219
 
@@ -1,7 +1,7 @@
1
1
  # knowless -- Integration Guide
2
2
 
3
3
  > For AI assistants and developers wiring knowless into a project.
4
- > v0.1.10 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
4
+ > v0.2.0 | Node.js >= 22.5 | 1 dep (nodemailer) | Apache-2.0
5
5
 
6
6
  ## What this is
7
7
 
@@ -333,7 +333,7 @@ URL/email -> handlers.js (login: 12-step sham-work flow per SPEC §7.3)
333
333
  -> handle.js (normalize ASCII-only, HMAC-SHA256)
334
334
  -> abuse.js (per-IP rate limit, per-handle token cap, honeypot)
335
335
  -> token.js (32 random bytes, base64url; SHA-256 at rest)
336
- -> store.js (better-sqlite3, transactional, prepared statements)
336
+ -> store.js (node:sqlite, transactional, prepared statements)
337
337
  -> mailer.js (raw RFC822 7bit; nodemailer for SMTP submission only)
338
338
  -> session.js (HMAC-signed cookie with "sess\\0" domain tag)
339
339
  -> form.js (hardcoded HTML5; no JS, no external resources)
@@ -344,7 +344,7 @@ URL/email -> handlers.js (login: 12-step sham-work flow per SPEC §7.3)
344
344
  |---|---|---|
345
345
  | `src/index.js` | ~140 | Public factory, sweeper, re-exports |
346
346
  | `src/handlers.js` | ~310 | login (sham), callback, verify, logout, loginForm, validateNextUrl |
347
- | `src/store.js` | ~210 | better-sqlite3 store; SPEC §13 interface |
347
+ | `src/store.js` | ~240 | node:sqlite store + transaction adapter; SPEC §13 interface |
348
348
  | `src/mailer.js` | ~120 | RFC822 raw composition + nodemailer SMTP submission |
349
349
  | `src/abuse.js` | ~95 | Source-IP determination, rate limits |
350
350
  | `src/handle.js` | ~50 | Email normalization, handle derivation |
@@ -433,7 +433,7 @@ rate-limits) belongs above the library.
433
433
  sweeper and closes the SQLite handle. Without it, your
434
434
  process won't exit cleanly. The sweeper timer is `unref()`d
435
435
  so it won't *prevent* exit, but the SQLite handle held by
436
- `better-sqlite3` will leave a finalizer warning.
436
+ `node:sqlite` will leave a finalizer warning.
437
437
 
438
438
  12. **CSRF defense is the Origin/Referer whitelist, not a token.**
439
439
  Modern browsers always emit `Origin` on cross-origin POSTs;
@@ -487,8 +487,9 @@ rate-limits) belongs above the library.
487
487
  - **Node 20+** -- targeting LTS; tested on Node 22
488
488
  - **Plain ES modules** -- no TypeScript source, no build step;
489
489
  ships JSDoc + (eventual) `.d.ts`
490
- - **Two production deps** -- `nodemailer` (SMTP submission) and
491
- `better-sqlite3` (storage). No third dep without revisiting
490
+ - **One production dep** -- `nodemailer` (SMTP submission). Storage
491
+ uses `node:sqlite` (stdlib, no native compile). No second runtime
492
+ dep without revisiting
492
493
  AGENT_RULES External Dependency Checklist.
493
494
  - **Localhost MTA only** -- no remote SMTP, no vendor SDKs.
494
495
  Operators run their own Postfix / OpenSMTPD / Exim.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knowless",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
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",
@@ -23,14 +23,13 @@
23
23
  "knowless.context.md"
24
24
  ],
25
25
  "engines": {
26
- "node": ">=20.0.0"
26
+ "node": ">=22.5.0"
27
27
  },
28
28
  "scripts": {
29
29
  "test": "node --test 'test/**/*.test.js'",
30
30
  "lint": "find src bin -type f \\( -name '*.js' -o -name 'knowless-server' \\) -exec node --check {} \\;"
31
31
  },
32
32
  "dependencies": {
33
- "better-sqlite3": "^11.0.0",
34
33
  "nodemailer": "^8.0.7"
35
34
  },
36
35
  "license": "Apache-2.0",
package/src/store.js CHANGED
@@ -1,4 +1,32 @@
1
- import Database from 'better-sqlite3';
1
+ import { DatabaseSync } from 'node:sqlite';
2
+
3
+ /**
4
+ * Wrap a function in a SQLite transaction. Mirrors better-sqlite3's
5
+ * `db.transaction(fn)` shape: returns a function that opens
6
+ * BEGIN IMMEDIATE, runs `fn`, COMMITs on success, ROLLBACKs on
7
+ * throw, and propagates the original error.
8
+ *
9
+ * BEGIN IMMEDIATE rather than BEGIN DEFERRED — knowless's
10
+ * transactional cap-check (SPEC §4.7) needs a write lock from the
11
+ * start to serialise concurrent issuance attempts.
12
+ *
13
+ * @param {DatabaseSync} db
14
+ * @param {Function} fn
15
+ */
16
+ function makeTransaction(db, fn) {
17
+ return (...args) => {
18
+ db.exec('BEGIN IMMEDIATE');
19
+ let result;
20
+ try {
21
+ result = fn(...args);
22
+ } catch (err) {
23
+ try { db.exec('ROLLBACK'); } catch { /* tolerate stack-unwind issues */ }
24
+ throw err;
25
+ }
26
+ db.exec('COMMIT');
27
+ return result;
28
+ };
29
+ }
2
30
 
3
31
  /**
4
32
  * Default token-sweeper grace: keep used tokens for 24h after redemption
@@ -76,11 +104,11 @@ const DDL = `
76
104
  * @returns {Store}
77
105
  */
78
106
  export function createStore(dbPath = ':memory:') {
79
- const db = new Database(dbPath);
80
- db.pragma('journal_mode = WAL');
81
- db.pragma('synchronous = NORMAL');
82
- db.pragma('foreign_keys = OFF');
83
- db.pragma('temp_store = MEMORY');
107
+ const db = new DatabaseSync(dbPath);
108
+ db.exec('PRAGMA journal_mode = WAL');
109
+ db.exec('PRAGMA synchronous = NORMAL');
110
+ db.exec('PRAGMA foreign_keys = OFF');
111
+ db.exec('PRAGMA temp_store = MEMORY');
84
112
  db.exec(DDL);
85
113
 
86
114
  const existing = db
@@ -166,7 +194,7 @@ export function createStore(dbPath = ':memory:') {
166
194
  };
167
195
 
168
196
  // Transactional cap-check + insert per SPEC §4.7.
169
- const insertTokenAtomic = db.transaction(
197
+ const insertTokenAtomic = makeTransaction(db,
170
198
  (tokenHash, handle, expiresAt, nextUrl, isSham, maxActive, now) => {
171
199
  if (maxActive > 0) {
172
200
  const { n: count } = stmt.countActiveTokens.get(handle, now);
@@ -181,7 +209,7 @@ export function createStore(dbPath = ':memory:') {
181
209
  );
182
210
 
183
211
  // Transactional account deletion per FR-37a.
184
- const deleteHandleAtomic = db.transaction((handle) => {
212
+ const deleteHandleAtomic = makeTransaction(db, (handle) => {
185
213
  stmt.deleteHandleSessions.run(handle);
186
214
  stmt.deleteHandleTokens.run(handle);
187
215
  stmt.deleteHandleRow.run(handle);