@nxgt/janus 0.8.6 → 0.8.8
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/docs/guide/email-flows.md +37 -0
- package/docs/guide/sign-in-code.md +14 -0
- package/docs/guide/vocabulary.md +3 -1
- package/docs/roadmap.md +10 -21
- package/package.json +1 -1
|
@@ -29,6 +29,43 @@ const verified = await auth.verifyEmail.confirm(sent.token);
|
|
|
29
29
|
verified.emailVerified; // true
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Ready-made e-mails — `@nxgt/janus-mail`
|
|
33
|
+
|
|
34
|
+
`sendMail` above is yours to write, or
|
|
35
|
+
[`@nxgt/janus-mail`](https://www.npmjs.com/package/@nxgt/janus-mail)
|
|
36
|
+
writes it: it takes what `send` and `request` answer, as they answer it, and
|
|
37
|
+
sends the verification and reset e-mails — and the *password changed* and
|
|
38
|
+
*e-mail changed* notices — in English and French, over any `@nxgt/mail`
|
|
39
|
+
transport.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { janusMail } from '@nxgt/janus-mail';
|
|
43
|
+
|
|
44
|
+
const mail = janusMail({
|
|
45
|
+
mailer, // an @nxgt/mail transport
|
|
46
|
+
from: 'noreply@app.example',
|
|
47
|
+
brand: 'App',
|
|
48
|
+
links: {
|
|
49
|
+
verifyEmail: (token) => `https://app.example/verify?token=${token}`,
|
|
50
|
+
resetPassword: (token) => `https://app.example/reset?token=${token}`,
|
|
51
|
+
secureAccount: () => 'https://app.example/account/security',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await mail.verifyEmail(await auth.verifyEmail.send(user), { name: user.name });
|
|
56
|
+
|
|
57
|
+
const issued = await auth.resetPassword.request(email);
|
|
58
|
+
if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each e-mail goes to `issued.email`. A send that fails rejects with the
|
|
62
|
+
mailer's `MailFailure` — never report it as sent — and one refused, a link
|
|
63
|
+
that is not `http(s)` or `mailto:` or an address that is not one, with
|
|
64
|
+
`MailRefused`. A `mailto:` link is accepted on purpose —
|
|
65
|
+
`secureAccount: () => 'mailto:security@app.example'` points a user who made
|
|
66
|
+
no change at your support desk. Each link must answer a string at once: an
|
|
67
|
+
`async` function is a `TypeError` naming the call.
|
|
68
|
+
|
|
32
69
|
## Which types have these flows
|
|
33
70
|
|
|
34
71
|
`verifyEmail` exists on a type with an e-mail field: the one `email` names, or
|
|
@@ -160,6 +160,20 @@ sign-in completes in the browser that started it.
|
|
|
160
160
|
Send it to `issued.email`, not to what the visitor typed: it is the address
|
|
161
161
|
the user's field holds, as they registered it.
|
|
162
162
|
|
|
163
|
+
[`@nxgt/janus-mail`](https://www.npmjs.com/package/@nxgt/janus-mail)
|
|
164
|
+
sends this e-mail ready-made, in English and French, over any `@nxgt/mail`
|
|
165
|
+
transport. It takes the whole `IssuedCode` and reads only `code` and
|
|
166
|
+
`email`: the challenge never reaches the e-mail, nor a template of yours.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
if (issued !== null) await mail.signInCode(issued, { locale: 'fr-CA' }); // the recipient's locale: sent in fr
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Its send rejects with the mailer's `MailFailure` when the transport fails, so the `void sendMail(…)` above
|
|
173
|
+
does not carry over: to send after the answer, hand `mail.signInCode` to a
|
|
174
|
+
queue that awaits it and retries a `MailFailure` — a promise nobody awaits
|
|
175
|
+
turns an outage into an unhandled rejection.
|
|
176
|
+
|
|
163
177
|
## Keeping the challenge with the visitor
|
|
164
178
|
|
|
165
179
|
**The challenge is a secret.** The code alone signs nobody in: it is checked
|
package/docs/guide/vocabulary.md
CHANGED
|
@@ -56,6 +56,8 @@ either finds this row.
|
|
|
56
56
|
| **seal**, **sealing key** | To seal is to encrypt a TOTP secret — AES-256-GCM, bound to the user's id — before a store sees it. A sealing key is one `{ id, key }` of `secondFactor.keys`: the first seals, every one opens | "encrypt", "encryption key", "master key", "pepper" |
|
|
57
57
|
| **token** | Never alone in prose: a *session token* or a *one-time token*. The `tokens` store and the `TOKEN_*` codes are one-time tokens only | |
|
|
58
58
|
| **e-mail flow** | `verifyEmail` or `resetPassword`: send a one-time token, then confirm it. `signInCode` sends a one-time code instead | |
|
|
59
|
+
| **issued** | What a flow answers for the application to send: an `IssuedToken` from `verifyEmail.send` or `resetPassword.request`, an `IssuedCode` from `signInCode.request` — the address with it. `@nxgt/janus-mail` takes it as it is | "result", "payload" |
|
|
60
|
+
| **notice** | An e-mail that tells a user of a change and asks for nothing: *password changed*, *e-mail changed* — the latter sent to the former address. `@nxgt/janus-mail` sends both | "alert", "notification" |
|
|
59
61
|
| **user event** | What happened to a user, once it is written: `user.created`, `user.emailVerified`, `user.passwordReset`, `user.deleted` — a `UserEvent`, naming the user by id alone, with an `id` of its own. See [user events](events.md) | "webhook" — a webhook is one way to deliver it; "event" alone where it could be read as `@nxgt/janus-telemetry`'s audit log record — on a page about user events, "the event" is fine |
|
|
60
62
|
| **listener** | The one function `janus({ events })` hands every user event to, after the write and awaited | "handler", "hook", "subscriber" |
|
|
61
63
|
|
|
@@ -88,7 +90,7 @@ either finds this row.
|
|
|
88
90
|
| **store** | Where a side keeps its data, behind a port. The **identity stores** are `users`, `sessions` and `tokens`; the **relation store** holds the tuples | "database", "repository" |
|
|
89
91
|
| **port** | The interface a store implements: `JanusStores` for the identity stores — named after the package, not the side — and `RelationStore` | "driver" |
|
|
90
92
|
| **adapter** | A package implementing the ports for one database: `@nxgt/janus-mongo`, `@nxgt/janus-drizzle`, `@nxgt/janus-redis`. What its `create…Adapter(db)` answers is its stores, keyed as `janus()` takes them — `{ store, relations }` | "plugin", "connector" |
|
|
91
|
-
| **integration** | A package fitting Janus into one web framework or one
|
|
93
|
+
| **integration** | A package fitting Janus into one web framework, one observability library or one mail toolkit: `@nxgt/janus-hono`, `@nxgt/janus-telemetry`, `@nxgt/janus-mail`. It implements no port | "plugin", "adapter" |
|
|
92
94
|
| **kit** | A package that opens the connections and wires adapters and integrations into one object for an application: `@nxgt/janus-kit`'s `connectKit` answers `{ auth, access, db, redis, ping, close }`. It implements no port, and `janus()` and `permissions()` are still written by the application | "framework", "starter" |
|
|
93
95
|
|
|
94
96
|
### Answers
|
package/docs/roadmap.md
CHANGED
|
@@ -19,23 +19,6 @@ Nothing between releases.
|
|
|
19
19
|
- **Recovery codes** — single-use codes for the TOTP second factor, so a
|
|
20
20
|
user who loses their authenticator app can still sign in, without an
|
|
21
21
|
operator resetting the account.
|
|
22
|
-
- **Sending the e-mails** — in a package of its own, `@nxgt/janus-mail`, built
|
|
23
|
-
on a general mail toolkit shared with applications that are not about
|
|
24
|
-
sign-in: a `Mailer` port you plug your transport into (SMTP, Resend, SES…) —
|
|
25
|
-
a transport that fails throws, like a store — and default templates for
|
|
26
|
-
verification, password reset and one-time codes, in English and French. One
|
|
27
|
-
template per e-mail, never one HTML file per language: the layout is built
|
|
28
|
-
once with Maizzle and Tailwind CSS 4 — CSS inlined for mail clients — and
|
|
29
|
-
its text lives in ICU message catalogues, one per language, plurals and
|
|
30
|
-
dates included. Both are compiled when the package is built into typed
|
|
31
|
-
functions: `templates.verifyEmail({ locale: 'fr', link })` answers
|
|
32
|
-
`{ subject, html, text }`, every value escaped, a missing variable or an
|
|
33
|
-
unknown locale a compile error, a message that fails to format a throw —
|
|
34
|
-
never an e-mail sent with `{link}` in it. No template engine at run time.
|
|
35
|
-
The defaults are a starting point, not a requirement: add a language with a
|
|
36
|
-
catalogue, or replace any one template with your own function of the same
|
|
37
|
-
shape — built with the same toolkit, React Email or a plain string — and
|
|
38
|
-
keep the defaults for the rest.
|
|
39
22
|
|
|
40
23
|
## Later
|
|
41
24
|
|
|
@@ -87,6 +70,16 @@ Nothing between releases.
|
|
|
87
70
|
The last ten, newest first, each with the version it came in. Everything
|
|
88
71
|
before is in the [CHANGELOG](../CHANGELOG.md).
|
|
89
72
|
|
|
73
|
+
- **Sending the e-mails, `@nxgt/janus-mail` v0.1.0** — a package of its
|
|
74
|
+
own, built on the `@nxgt/mail` toolkit: `janusMail({ mailer, from, brand,
|
|
75
|
+
links })` takes what each flow answers —
|
|
76
|
+
`await mail.verifyEmail(await auth.verifyEmail.send(user), { name, locale })`
|
|
77
|
+
— and sends five e-mails, in English and French, over any `@nxgt/mail`
|
|
78
|
+
transport: e-mail verification, password reset, sign-in code, and the
|
|
79
|
+
notices *password changed* and *e-mail changed*. They are built once with
|
|
80
|
+
Maizzle when the package is built, and only filled in at send time, every
|
|
81
|
+
value escaped: no template engine in your server. A transport that fails
|
|
82
|
+
throws, like a store.
|
|
90
83
|
- **A permission id no store can keep is held by nobody** — an object or
|
|
91
84
|
subject id holding a NUL character or a lone surrogate answers `false` from
|
|
92
85
|
`can()` and an empty page from `list()`, before any store call, and a
|
|
@@ -157,7 +150,3 @@ before is in the [CHANGELOG](../CHANGELOG.md).
|
|
|
157
150
|
audit trail, never a login, a password, a session token or a one-time token;
|
|
158
151
|
and [`@nxgt/janus-kit`](https://www.npmjs.com/package/@nxgt/janus-kit), all
|
|
159
152
|
of it wired in one call.
|
|
160
|
-
- **A NUL character or a lone surrogate never reaches a store** — refused in
|
|
161
|
-
fields with `USER_INVALID` on every adapter, rather than `STORE_FAILED` on
|
|
162
|
-
PostgreSQL alone; a login holding one is nobody's. The conformance suite
|
|
163
|
-
holds every adapter to round-tripping every other character. — v0.2.1
|