knowless 0.1.10 → 0.2.1
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 +148 -5
- package/GUIDE.md +148 -13
- package/OPS.md +8 -7
- package/README.md +155 -103
- package/knowless.context.md +173 -6
- package/package.json +2 -3
- package/src/handlers.js +46 -3
- package/src/index.js +98 -1
- package/src/mailer.js +21 -0
- package/src/store.js +50 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,17 +5,160 @@ 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
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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.
|
|
25
|
+
discarded it.
|
|
26
|
+
|
|
27
|
+
## [0.2.1] — 2026-04-29
|
|
28
|
+
|
|
29
|
+
**Operator visibility, opt-in.** Three event hooks + one method,
|
|
30
|
+
the full surface forum + addypin asked for during the v0.2.0
|
|
31
|
+
integration spike. The shape was negotiated against
|
|
32
|
+
`walk-away-at-v1.0.0` (PRD §6.3): every "obvious" addition was
|
|
33
|
+
deliberately rejected if it could be done in adopter or perimeter
|
|
34
|
+
code. See `knowless.context.md` § "What's NOT in knowless, and why"
|
|
35
|
+
for the rejected-by-design list (disposable-domain check, account-age
|
|
36
|
+
accessor, hashcash, `lookupMessageId()`, `onShamHit`).
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`onMailerSubmit({messageId, handle, timestamp})` (AF-19).**
|
|
41
|
+
Per-event hook fired on successful SMTP submission for *real*
|
|
42
|
+
(non-sham) sends only. Adopters log it, build msg_id ↔ handle
|
|
43
|
+
correlation maps, or pipe to structured logging. Knowless never
|
|
44
|
+
stores the mapping. Sham branches deliberately do NOT fire this
|
|
45
|
+
hook — that's the load-bearing NFR-10 invariant (would let a
|
|
46
|
+
careless adopter log per-handle data and reopen the enumeration
|
|
47
|
+
oracle that sham-work was designed to prevent).
|
|
48
|
+
- **`onTransportFailure({error, timestamp}) (AF-19).** Per-event
|
|
49
|
+
hook fired on SMTP errors. No identity data — safe per-event,
|
|
50
|
+
safe to alert on.
|
|
51
|
+
- **`onSuppressionWindow({sham, rateLimited, windowMs})` (AF-19).**
|
|
52
|
+
Heartbeat hook fired every `suppressionWindowMs` (default 60s)
|
|
53
|
+
with aggregate counters across all silent-202 branches: sham
|
|
54
|
+
hits, `login_ip` cap, `create_ip` cap (counted both as sham and
|
|
55
|
+
rate-limited when fall-through happens), and per-handle token-cap
|
|
56
|
+
rotation. Heartbeats fire even when both counters are zero — a
|
|
57
|
+
missing emission is itself diagnostic. Replaces a per-event
|
|
58
|
+
`onShamHit` / `onRateLimitHit` design that would have leaked
|
|
59
|
+
per-handle data through log lines; the windowed aggregate
|
|
60
|
+
preserves the spike signal without per-call distinguishability.
|
|
61
|
+
- **`auth.verifyTransport()` method (AF-20).** Wraps
|
|
62
|
+
`transport.verify()`. Resolves `Promise<true>` on non-rejection,
|
|
63
|
+
rejects with the underlying error. Adopters call this explicitly
|
|
64
|
+
when they want fail-fast on misconfigured SMTP at boot. **No
|
|
65
|
+
auto-on-boot variant by design (AF-21).** Deployments where
|
|
66
|
+
knowless starts before Postfix (docker-compose ordering, k8s
|
|
67
|
+
readiness probes) would fail boot for the wrong reason.
|
|
68
|
+
- **`startLogin` silent-202 documented (AF-22).** New gotcha #19 in
|
|
69
|
+
`knowless.context.md` and a Mode-A pointer in GUIDE.md make
|
|
70
|
+
explicit that `startLogin` returns `{handle, submitted: true}`
|
|
71
|
+
for every branch (real, sham, rate-limited, missing handle) by
|
|
72
|
+
design. Operators who need branch visibility wire the v0.2.1
|
|
73
|
+
hooks; the per-call return shape never reveals which branch ran.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`store.insertToken` returns the eviction count.** Internal
|
|
78
|
+
store-interface change: `insertToken` now returns the number of
|
|
79
|
+
tokens evicted to make room for the new one (always `0` when
|
|
80
|
+
`maxActive` is `0`). Used by `runSendLink` to count per-handle
|
|
81
|
+
cap rotation events into the `rateLimited` counter. Adopters
|
|
82
|
+
with custom stores implementing the SPEC §13 interface should
|
|
83
|
+
update accordingly; the change is forward-compatible (returning
|
|
84
|
+
`undefined` is treated as zero evictions).
|
|
85
|
+
|
|
86
|
+
### Documentation (forum + addypin negotiation outcome)
|
|
87
|
+
|
|
88
|
+
- **knowless.context.md § "What's NOT in knowless, and why"** —
|
|
89
|
+
permanent record of three rejected-by-design additions
|
|
90
|
+
(disposable-domain check, account-age accessor, per-IP hashcash)
|
|
91
|
+
with the seam argument and walk-away-at-v1.0.0 framing. Future
|
|
92
|
+
contributors evaluating "should X go in knowless?" run two tests
|
|
93
|
+
before answering yes: identity layer vs behavior layer; mechanism
|
|
94
|
+
living with policy.
|
|
95
|
+
- **GUIDE.md FAQ** — "Why doesn't knowless block disposable email
|
|
96
|
+
domains?" + "How do I check how old a user is?" with adopter-side
|
|
97
|
+
code patterns for both. Closes the most likely "but-can-it" requests.
|
|
98
|
+
|
|
99
|
+
### Internal
|
|
100
|
+
|
|
101
|
+
- Hook errors are caught and swallowed via a single `safeHook()`
|
|
102
|
+
wrapper, matching the existing `onSweepError` contract. Knowless
|
|
103
|
+
never crashes because an operator's observability sink threw.
|
|
104
|
+
- Suppression-window timer is `unref()`'d and only started when
|
|
105
|
+
`onSuppressionWindow` is wired — adopters not using the hook
|
|
106
|
+
spend zero `setInterval` slots on it.
|
|
107
|
+
- 16 new tests in `test/integration/v021-hooks.test.js` covering
|
|
108
|
+
payload shapes, the sham-no-fire invariant, aggregation
|
|
109
|
+
semantics, heartbeat behavior, counter reset, hook-error
|
|
110
|
+
containment, and `verifyTransport()` resolve/reject paths.
|
|
111
|
+
Test count: 192 → 207.
|
|
112
|
+
|
|
113
|
+
## [0.2.0] — 2026-04-28
|
|
114
|
+
|
|
115
|
+
**No native compile. One production dep.** Drops `better-sqlite3`
|
|
116
|
+
in favour of `node:sqlite` (Node stdlib). Adopters on long-LTS
|
|
117
|
+
distros (RHEL 8/9, Alma, Rocky, Amazon Linux 2) no longer need a
|
|
118
|
+
C++20 toolchain to `npm install knowless`.
|
|
119
|
+
|
|
120
|
+
### Breaking
|
|
121
|
+
|
|
122
|
+
- **Node floor bumped: `>=20.0.0` → `>=22.5.0`.** `node:sqlite`
|
|
123
|
+
requires Node 22.5+; unflagged stable on Node 24 LTS. Node 20
|
|
124
|
+
reaches EOL April 2026.
|
|
125
|
+
- **`better-sqlite3` removed from `dependencies`.** Down to one
|
|
126
|
+
production dep (`nodemailer`). Transitive package count goes
|
|
127
|
+
from ~40 to ~2. No `prebuild-install`, no `gcc`, no `make`,
|
|
128
|
+
no Python during install.
|
|
129
|
+
- **Storage internals changed**, public API unchanged. The
|
|
130
|
+
`createStore()` interface (SPEC §13) is byte-for-byte identical.
|
|
131
|
+
All 192 tests pass on first run after the swap.
|
|
132
|
+
|
|
133
|
+
### Migration
|
|
134
|
+
|
|
135
|
+
- **For knowless library adopters:** ensure your runtime is
|
|
136
|
+
Node 22.5+. If you pinned `better-sqlite3` somewhere yourself
|
|
137
|
+
for unrelated reasons, that's now your call. Otherwise:
|
|
138
|
+
```sh
|
|
139
|
+
npm install knowless@0.2.0
|
|
140
|
+
```
|
|
141
|
+
No code changes on your side. Existing SQLite databases
|
|
142
|
+
continue to work — same schema, same WAL mode, same
|
|
143
|
+
prepared statements. Sessions and handles persist across
|
|
144
|
+
the upgrade.
|
|
145
|
+
- **For `knowless-server` operators:** ensure the host runs
|
|
146
|
+
Node 22.5+. If you ran `dnf install gcc-toolset-13` to get
|
|
147
|
+
v0.1.x to compile, you can remove it after the upgrade —
|
|
148
|
+
v0.2.0 doesn't need it. The systemd unit and env-var config
|
|
149
|
+
are unchanged.
|
|
150
|
+
- **You may see one `ExperimentalWarning` from `node:sqlite`
|
|
151
|
+
at first import** on Node 22.x. Suppress with `--no-warnings`
|
|
152
|
+
or run on Node 24 LTS where the API is fully stable.
|
|
153
|
+
|
|
154
|
+
### Internal
|
|
155
|
+
|
|
156
|
+
- New `makeTransaction(db, fn)` adapter in `src/store.js`
|
|
157
|
+
replaces `better-sqlite3`'s `db.transaction()` wrapper. Uses
|
|
158
|
+
`BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` directly — same
|
|
159
|
+
serialisation guarantee for the transactional cap-check
|
|
160
|
+
(SPEC §4.7) and account-deletion paths (FR-37a).
|
|
161
|
+
- Closes AF-18 (the addypin RHEL 8 deployment trap).
|
|
19
162
|
|
|
20
163
|
## [0.1.10] — 2026-04-28
|
|
21
164
|
|
package/GUIDE.md
CHANGED
|
@@ -295,6 +295,11 @@ return identical shapes — the caller can't observe which happened.
|
|
|
295
295
|
This preserves FR-6 timing equivalence even for programmatic
|
|
296
296
|
callers. See SPEC §7.3a for the full contract.
|
|
297
297
|
|
|
298
|
+
If you need *operator* visibility (not per-call: aggregate counts and
|
|
299
|
+
real-send confirmation), wire the v0.2.1 hooks documented in
|
|
300
|
+
[Step 8](#step-8-optional-operator-monitoring-via-event-hooks-v021)
|
|
301
|
+
below — they emit without breaking the per-call silent-202 contract.
|
|
302
|
+
|
|
298
303
|
`auth.deriveHandle(email)` returns the same opaque HMAC handle
|
|
299
304
|
that the form path uses, without you having to import the helper
|
|
300
305
|
or pass the secret around. The instance method **normalizes the
|
|
@@ -469,6 +474,76 @@ Library doesn't ship a built-in HTTP endpoint for this — operator
|
|
|
469
474
|
chooses the UX (admin CLI, in-app self-service, ticket-driven
|
|
470
475
|
support).
|
|
471
476
|
|
|
477
|
+
### Step 8 (optional): Operator monitoring via event hooks (v0.2.1+)
|
|
478
|
+
|
|
479
|
+
Three event hooks + one opt-in method. All optional, all opt-in. None
|
|
480
|
+
are required for correct operation; the library is fully functional
|
|
481
|
+
with zero hooks wired. They exist so an operator can wire knowless to
|
|
482
|
+
their existing observability stack (Prometheus, statsd, structured
|
|
483
|
+
logs, on-call paging) without knowless curating its own metrics shape.
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
const auth = knowless({
|
|
487
|
+
// ...required + existing options...
|
|
488
|
+
|
|
489
|
+
onMailerSubmit: ({messageId, handle, timestamp}) => {
|
|
490
|
+
log.info('knowless.dispatch', { messageId, handle, ts: timestamp });
|
|
491
|
+
},
|
|
492
|
+
onTransportFailure: ({error, timestamp}) => {
|
|
493
|
+
log.error('knowless.smtp_failed', { err: error.message, ts: timestamp });
|
|
494
|
+
pager.notify('SMTP transport failed');
|
|
495
|
+
},
|
|
496
|
+
onSuppressionWindow: ({sham, rateLimited, windowMs}) => {
|
|
497
|
+
metrics.gauge('knowless.sham_count', sham);
|
|
498
|
+
metrics.gauge('knowless.rate_limited', rateLimited);
|
|
499
|
+
if (sham > BASELINE * 10) pager.notify('possible enumeration attack');
|
|
500
|
+
},
|
|
501
|
+
// suppressionWindowMs: 60_000, // default; configurable
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Optional: explicit transport probe at boot. No auto-probe by design.
|
|
505
|
+
try {
|
|
506
|
+
await auth.verifyTransport();
|
|
507
|
+
} catch (err) {
|
|
508
|
+
console.error('SMTP unreachable at boot:', err);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### Why three hooks, not four
|
|
514
|
+
|
|
515
|
+
The two silent-202 branches — sham hits (handle does not exist) and
|
|
516
|
+
rate-limit hits (any of the three caps) — are bundled into one *windowed
|
|
517
|
+
aggregate* (`onSuppressionWindow`) rather than per-event hooks. Per-event
|
|
518
|
+
hooks would let a careless adopter log per-handle data, which is the
|
|
519
|
+
enumeration oracle that sham-work exists to prevent. The HTTP response
|
|
520
|
+
is silent on these branches; the log file must be silent too.
|
|
521
|
+
|
|
522
|
+
Operators still get the spike signal — a 10× jump in `sham` count over
|
|
523
|
+
the window is the enumeration-attack alarm. They don't get per-call
|
|
524
|
+
correlation to a specific handle, and they shouldn't have it.
|
|
525
|
+
|
|
526
|
+
`onMailerSubmit` is per-event because it fires *only* on real
|
|
527
|
+
submissions, where the handle was already disclosed by the form
|
|
528
|
+
input. `onTransportFailure` is per-event because it carries no
|
|
529
|
+
identity data.
|
|
530
|
+
|
|
531
|
+
> **Don't log `onSuppressionWindow` payloads in a way that distinguishes
|
|
532
|
+
> them from `onMailerSubmit` at the log-line level.** The aggregate
|
|
533
|
+
> count is fine; the line itself should be cleanly labeled as a periodic
|
|
534
|
+
> counter emission, not "a sham just happened." If your log shipper or
|
|
535
|
+
> dashboard groups them differently, you've put back the per-event
|
|
536
|
+
> distinguishability the bundling was meant to remove.
|
|
537
|
+
|
|
538
|
+
#### Why `verifyTransport()` is opt-in
|
|
539
|
+
|
|
540
|
+
No auto-on-boot variant exists by design. Deployments where knowless
|
|
541
|
+
starts before Postfix (docker-compose ordering, k8s readiness probes
|
|
542
|
+
that run knowless before the SMTP container is healthy) would fail
|
|
543
|
+
boot for the wrong reason. Adopters who want fail-fast call
|
|
544
|
+
`verifyTransport()` explicitly; everyone else gets eventually-consistent
|
|
545
|
+
SMTP startup.
|
|
546
|
+
|
|
472
547
|
## Walkthrough: standalone server mode
|
|
473
548
|
|
|
474
549
|
Run `npx knowless-server`, point Caddy / nginx / Traefik at it for
|
|
@@ -550,11 +625,67 @@ Full options table:
|
|
|
550
625
|
| `shamRecipient` | no | `null@knowless.invalid` | Where sham mail goes (your MTA must discard it). |
|
|
551
626
|
| `sweepIntervalMs` | no | `300000` | Sweeper tick (5 min default). |
|
|
552
627
|
| `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
|
|
628
|
+
| `store` | no | (built-in `node:sqlite`) | Inject your own store implementation. |
|
|
554
629
|
| `mailer` | no | (built-in nodemailer) | Inject your own mailer. |
|
|
555
630
|
|
|
556
631
|
## FAQ
|
|
557
632
|
|
|
633
|
+
### Why doesn't knowless block disposable email domains?
|
|
634
|
+
|
|
635
|
+
Disposable-domain blocking (mailinator.com, throwaway.email, etc.) is
|
|
636
|
+
adopter policy, not identity layer. The blocklist is a public GitHub
|
|
637
|
+
repo, the override list is operator-specific, and the cron to refresh
|
|
638
|
+
it lives in your ops layer. Putting the *mechanism* in knowless while
|
|
639
|
+
the *list curation* and *overrides* live in the adopter is the wrong
|
|
640
|
+
seam — both stay together in your form handler.
|
|
641
|
+
|
|
642
|
+
```js
|
|
643
|
+
// In your /login form handler, before calling auth.login:
|
|
644
|
+
import { DISPOSABLE_DOMAINS, ADOPTER_OVERRIDES } from './disposable-domains.js';
|
|
645
|
+
|
|
646
|
+
app.post('/login', async (req, res) => {
|
|
647
|
+
const email = /* parse from body */;
|
|
648
|
+
const domain = email.split('@')[1]?.toLowerCase();
|
|
649
|
+
if (domain && DISPOSABLE_DOMAINS.has(domain) && !ADOPTER_OVERRIDES.has(domain)) {
|
|
650
|
+
// Reply with the same shape as auth.login's success/sham response
|
|
651
|
+
// to preserve FR-6-equivalent timing at your layer too. Match
|
|
652
|
+
// status, headers, and body that auth.login would emit.
|
|
653
|
+
return res.status(200).type('html').send(/* same confirmation HTML */);
|
|
654
|
+
}
|
|
655
|
+
return auth.login(req, res);
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The same argument applies to per-IP hashcash / proof-of-work: if
|
|
660
|
+
`maxNewHandlesPerIpPerHour: 3` isn't enough for your threat model,
|
|
661
|
+
run hashcash at Caddy or your edge layer — knowless's login form
|
|
662
|
+
deliberately stays zero-JS.
|
|
663
|
+
|
|
664
|
+
### How do I check how old a handle / user is?
|
|
665
|
+
|
|
666
|
+
knowless deliberately doesn't expose handle creation dates. The reason:
|
|
667
|
+
"first time this email hit knowless" is rarely the trust signal you
|
|
668
|
+
actually want — you want "first time this user did something meaningful
|
|
669
|
+
in *my app*." A six-month-old knowless handle that has never posted
|
|
670
|
+
has zero application tenure.
|
|
671
|
+
|
|
672
|
+
Pattern: track `(handle, first_seen_at)` in your own table the first
|
|
673
|
+
time a handle performs the action you care about (first post, first
|
|
674
|
+
purchase, first non-trivial API call). Bucket by your tenure, not
|
|
675
|
+
knowless's.
|
|
676
|
+
|
|
677
|
+
```js
|
|
678
|
+
// On the action you care about:
|
|
679
|
+
const handle = auth.handleFromRequest(req);
|
|
680
|
+
db.recordFirstSeen(handle, Date.now()); // INSERT OR IGNORE
|
|
681
|
+
const age = db.ageBucketFor(handle); // 'new' | '1mo' | '1y' | '5y+'
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
This is also safer: returning a `Date | null` keyed by handle is itself
|
|
685
|
+
an enumeration oracle (null leaks "this handle doesn't exist"). Bucket
|
|
686
|
+
on your side from a table that only knows about handles that have
|
|
687
|
+
already acted.
|
|
688
|
+
|
|
558
689
|
### My users say magic links land in spam.
|
|
559
690
|
|
|
560
691
|
This is operator infrastructure, not the library. The library
|
|
@@ -659,15 +790,19 @@ mail account is later compromised.
|
|
|
659
790
|
|
|
660
791
|
## Constraints / install footprint
|
|
661
792
|
|
|
662
|
-
- **
|
|
663
|
-
`
|
|
664
|
-
|
|
665
|
-
- **~
|
|
666
|
-
|
|
667
|
-
`
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
- **Node ≥
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
793
|
+
- **One direct dependency.** `nodemailer` (SMTP submission). Storage
|
|
794
|
+
is `node:sqlite` (Node stdlib, no native compile, no toolchain
|
|
795
|
+
required on the host).
|
|
796
|
+
- **~2 transitive packages** in a typical install (down from ~40 in
|
|
797
|
+
v0.1.x). No `prebuild-install`, no `gcc`, no `make`, no Python.
|
|
798
|
+
`npm ci` works on stock RHEL 8 / Alma / Rocky / Amazon Linux 2
|
|
799
|
+
with no extra packages. Self-hosters: `npm install knowless` is
|
|
800
|
+
done.
|
|
801
|
+
- **Node ≥ 22.5.** `node:sqlite` requires this floor (introduced
|
|
802
|
+
22.5, unflagged in 22.13+, fully stable in 24 LTS). Drops Node
|
|
803
|
+
20 — about to EOL anyway.
|
|
804
|
+
- **No optional deps, no postinstall scripts.**
|
|
805
|
+
|
|
806
|
+
> `node:sqlite` may print one `ExperimentalWarning` to stderr on
|
|
807
|
+
> first import. Suppress with `--no-warnings` or by running on
|
|
808
|
+
> 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.**
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|