@nxgt/janus 0.8.6 → 0.8.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.
@@ -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://github.com/softistx/nxgt-janus/blob/develop/packages/janus-mail/README.md)
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. In progress, and private until its first release.
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,21 @@ 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://github.com/softistx/nxgt-janus/blob/develop/packages/janus-mail/README.md)
164
+ sends this e-mail ready-made, in English and French, over any `@nxgt/mail`
165
+ transport — in progress, and private until its first release. It takes the
166
+ whole `IssuedCode` and reads only `code` and `email`: the challenge never
167
+ reaches the e-mail, nor a template of yours.
168
+
169
+ ```ts
170
+ if (issued !== null) await mail.signInCode(issued, { locale: 'fr-CA' }); // the recipient's locale: sent in fr
171
+ ```
172
+
173
+ Its send rejects with the mailer's `MailFailure` when the transport fails, so the `void sendMail(…)` above
174
+ does not carry over: to send after the answer, hand `mail.signInCode` to a
175
+ queue that awaits it and retries a `MailFailure` — a promise nobody awaits
176
+ turns an outage into an unhandled rejection.
177
+
163
178
  ## Keeping the challenge with the visitor
164
179
 
165
180
  **The challenge is a secret.** The code alone signs nobody in: it is checked
@@ -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 observability library: `@nxgt/janus-hono`, `@nxgt/janus-telemetry`. It implements no port | "plugin", "adapter" |
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
@@ -5,7 +5,21 @@ dates here, and the version something shipped in is the only number.
5
5
 
6
6
  ## Now
7
7
 
8
- Nothing between releases.
8
+ - **Sending the e-mails — in progress, private** — in a package of its own,
9
+ `@nxgt/janus-mail`, built on the `@nxgt/mail` toolkit shared with
10
+ applications that are not about sign-in: its `Mailer` port takes your
11
+ transport (SMTP, Resend…), and a transport that fails throws, like a store.
12
+ `janusMail({ mailer, from, brand, links })` takes what each flow answers —
13
+ `await mail.verifyEmail(await auth.verifyEmail.send(user), { name, locale })`
14
+ — and sends five e-mails, in English and French: e-mail verification,
15
+ password reset, sign-in code, and the notices *password changed* and
16
+ *e-mail changed*. They are built once, when the package is built, with
17
+ Maizzle and Tailwind CSS — CSS inlined for mail clients, the text in ICU
18
+ catalogues — and only filled at send time, every value escaped, your brand
19
+ included: no template engine in your server. Any one of them can be your
20
+ own function — React Email, a string — and a language beyond the two takes
21
+ all five. Built and tested in this repository, not yet published; see its
22
+ [README](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus-mail/README.md).
9
23
 
10
24
  ## Next
11
25
 
@@ -19,23 +33,6 @@ Nothing between releases.
19
33
  - **Recovery codes** — single-use codes for the TOTP second factor, so a
20
34
  user who loses their authenticator app can still sign in, without an
21
35
  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
36
 
40
37
  ## Later
41
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Embeddable, type-safe identities and permissions: bring your own database",
5
5
  "license": "MIT",
6
6
  "type": "module",