knowless 0.1.6 → 0.1.7
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 +25 -0
- package/GUIDE.md +4 -1
- package/README.md +1 -1
- package/knowless.context.md +1 -1
- package/package.json +1 -1
- package/src/handlers.js +26 -4
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,31 @@ Versioning is [SemVer](https://semver.org/).
|
|
|
9
9
|
|
|
10
10
|
- Caddy forward-auth Docker integration test (TASKS.md 6.8).
|
|
11
11
|
|
|
12
|
+
## [0.1.7] — 2026-04-28
|
|
13
|
+
|
|
14
|
+
addypin integration round 3 — one small API addition.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`subjectOverride` arg on `auth.startLogin`.** Per-call subject
|
|
19
|
+
replaces the factory `subject` for that one mail. Validated by
|
|
20
|
+
the same rules (ASCII, ≤60 chars, no CR/LF) and throws on invalid
|
|
21
|
+
— programmer error, not silent miss. The subject is decided
|
|
22
|
+
before the hit/miss branch, so sham and real submissions carry
|
|
23
|
+
the same subject and no observer can distinguish outcomes by
|
|
24
|
+
subject. Spam-trigger warnings (`!!`, `FREE`, etc.) do not throw;
|
|
25
|
+
the caller has more context. Closes AF-9.1.
|
|
26
|
+
|
|
27
|
+
Example: addypin sends three magic-link variants with distinct
|
|
28
|
+
subjects:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
await auth.startLogin({
|
|
32
|
+
email, nextUrl, sourceIp,
|
|
33
|
+
subjectOverride: `Confirm your pin: ${shortcode}`,
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
12
37
|
## [0.1.6] — 2026-04-28
|
|
13
38
|
|
|
14
39
|
addypin integration round 2 — one correctness fix (HMAC key handling)
|
package/GUIDE.md
CHANGED
|
@@ -267,11 +267,14 @@ share link," "submit a paste" — patterns where forcing a login
|
|
|
267
267
|
app.post('/api/pins', async (req, res) => {
|
|
268
268
|
const { email, lat, lng } = await readJsonBody(req);
|
|
269
269
|
const owner = auth.deriveHandle(email); // AF-7.4
|
|
270
|
-
await db.insertPendingPin({ owner, lat, lng });
|
|
270
|
+
const shortcode = await db.insertPendingPin({ owner, lat, lng });
|
|
271
271
|
await auth.startLogin({ // AF-7.3
|
|
272
272
|
email,
|
|
273
273
|
nextUrl: 'https://app.example.com/manage',
|
|
274
274
|
sourceIp: req.socket.remoteAddress,
|
|
275
|
+
// Per-call subject so the user can tell at a glance this is a
|
|
276
|
+
// pin-confirmation, not a routine login. AF-9.
|
|
277
|
+
subjectOverride: `Confirm your pin: ${shortcode}`,
|
|
275
278
|
});
|
|
276
279
|
res.status(202).end(); // "we'll email you the link"
|
|
277
280
|
});
|
package/README.md
CHANGED
|
@@ -7,7 +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
|
+
> v0.1.7 | Node.js >= 20 | 2 deps (nodemailer, better-sqlite3) | Apache-2.0
|
|
11
11
|
|
|
12
12
|
## What this is
|
|
13
13
|
|
package/knowless.context.md
CHANGED
|
@@ -147,7 +147,7 @@ const auth = knowless({
|
|
|
147
147
|
| `handleFromRequest` | (req) | string \| null | Programmatic session resolver for in-process middleware. Returns the handle if the cookie is valid, else null. SPEC §9.4. |
|
|
148
148
|
| `deleteHandle` | (handle: string) | void | Atomic delete of handle + tokens + sessions (FR-37a, GDPR) |
|
|
149
149
|
| `revokeSessions` | (handle: string) | number | Drops every session for `handle` without deleting the account ("log out everywhere"). Returns rows removed. AF-6.1. |
|
|
150
|
-
| `startLogin` | ({email, nextUrl?, sourceIp?}) | Promise\<{handle, submitted: true}\> | Programmatic magic-link send for "use first, claim later" flows. Same 12-step sham-work as form. SPEC §7.3a. AF-7.3. |
|
|
150
|
+
| `startLogin` | ({email, nextUrl?, sourceIp?, subjectOverride?}) | Promise\<{handle, submitted: true}\> | Programmatic magic-link send for "use first, claim later" flows. Same 12-step sham-work as form. `subjectOverride` (AF-9) replaces `cfg.subject` for this call. SPEC §7.3a. AF-7.3. |
|
|
151
151
|
| `deriveHandle` | (email: string) | string | HMAC-SHA256(secret, normalize(email)) using the configured secret. Use to compute owner-handles outside HTTP context. AF-7.4. |
|
|
152
152
|
| `_sweep` | -- | void | Trigger one sweep tick on demand (tests, operator scripts). AF-5.3. |
|
|
153
153
|
| `config` | -- | object | Merged effective config; safe to read (do not mutate) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knowless",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { normalize, deriveHandle } from './handle.js';
|
|
3
3
|
import { issueToken, hashToken } from './token.js';
|
|
4
4
|
import { newSid, signSession, verifySessionSignature } from './session.js';
|
|
5
|
-
import { composeBody } from './mailer.js';
|
|
5
|
+
import { composeBody, validateSubject } from './mailer.js';
|
|
6
6
|
import { renderLoginForm } from './form.js';
|
|
7
7
|
import {
|
|
8
8
|
buildTrustedPeers,
|
|
@@ -243,7 +243,7 @@ export function createHandlers({ store, mailer, config }) {
|
|
|
243
243
|
* handle is null only when the email failed to normalize (programmer
|
|
244
244
|
* bug for startLogin; same-shape silent for /login).
|
|
245
245
|
*/
|
|
246
|
-
async function runSendLink({ emailRaw, nextRaw, sourceIp }) {
|
|
246
|
+
async function runSendLink({ emailRaw, nextRaw, sourceIp, subject }) {
|
|
247
247
|
// Step 1: parse + normalize
|
|
248
248
|
let emailNorm;
|
|
249
249
|
try {
|
|
@@ -322,8 +322,14 @@ export function createHandlers({ store, mailer, config }) {
|
|
|
322
322
|
bodyFooter: cfg.bodyFooter,
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
// AF-9: programmatic callers may override the subject per call
|
|
326
|
+
// (addypin sends confirmation / login / expiry-warning all via
|
|
327
|
+
// magic links and needs distinct subjects). Decision happens
|
|
328
|
+
// BEFORE the hit/miss branch — same subject for sham and real,
|
|
329
|
+
// so timing equivalence is preserved.
|
|
330
|
+
const effectiveSubject = subject ?? cfg.subject;
|
|
325
331
|
try {
|
|
326
|
-
await mailer.submit({ to: toAddress, subject:
|
|
332
|
+
await mailer.submit({ to: toAddress, subject: effectiveSubject, body: mailBody });
|
|
327
333
|
} catch (err) {
|
|
328
334
|
// Per NFR-10: SMTP failure logged, NEVER leaked to response shape.
|
|
329
335
|
console.error('[knowless] mail submit failed:', err.message);
|
|
@@ -393,7 +399,12 @@ export function createHandlers({ store, mailer, config }) {
|
|
|
393
399
|
sameResponse(res, result.emailNorm, result.nextValidated ?? '');
|
|
394
400
|
}
|
|
395
401
|
|
|
396
|
-
async function startLogin({
|
|
402
|
+
async function startLogin({
|
|
403
|
+
email,
|
|
404
|
+
nextUrl,
|
|
405
|
+
sourceIp = '',
|
|
406
|
+
subjectOverride,
|
|
407
|
+
} = {}) {
|
|
397
408
|
// Programmer-error guards (AF-7.3). These DO throw; they're not
|
|
398
409
|
// silent-miss conditions, they're "you called the API wrong."
|
|
399
410
|
if (typeof email !== 'string' || email.length === 0) {
|
|
@@ -405,10 +416,21 @@ export function createHandlers({ store, mailer, config }) {
|
|
|
405
416
|
if (typeof sourceIp !== 'string') {
|
|
406
417
|
throw new Error('startLogin: sourceIp must be a string when provided');
|
|
407
418
|
}
|
|
419
|
+
// AF-9: per-call subject override. Validated with the same rules as
|
|
420
|
+
// the factory subject (ASCII, ≤60 chars, no CR/LF). Throws on
|
|
421
|
+
// invalid — same "programmer-error" treatment as other startLogin
|
|
422
|
+
// arg validation. Spam-trigger warnings are NOT thrown for; the
|
|
423
|
+
// caller has more context than knowless about what's appropriate.
|
|
424
|
+
let subject;
|
|
425
|
+
if (subjectOverride !== undefined && subjectOverride !== null) {
|
|
426
|
+
validateSubject(subjectOverride); // throws on invalid
|
|
427
|
+
subject = subjectOverride;
|
|
428
|
+
}
|
|
408
429
|
const { handle } = await runSendLink({
|
|
409
430
|
emailRaw: email,
|
|
410
431
|
nextRaw: nextUrl ?? null,
|
|
411
432
|
sourceIp,
|
|
433
|
+
subject,
|
|
412
434
|
});
|
|
413
435
|
// Same-shape return: rate-limit / sham / real all collapse here.
|
|
414
436
|
// `handle` is the HMAC of the normalized email (or null if email
|