@nxgt/janus-mail 0.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/generated/locales.d.ts +5 -0
- package/dist/generated/locales.d.ts.map +1 -0
- package/dist/generated/mail.d.ts +29 -0
- package/dist/generated/mail.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +14 -0
- package/dist/janus-mail.d.ts +24 -0
- package/dist/janus-mail.d.ts.map +1 -0
- package/dist/mails.d.ts +26 -0
- package/dist/mails.d.ts.map +1 -0
- package/dist/options.d.ts +27 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/templates.d.ts +15 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/README.md +34 -0
- package/docs/guide/building.md +130 -0
- package/docs/guide/locales.md +113 -0
- package/docs/guide/sending.md +242 -0
- package/docs/guide/templates.md +183 -0
- package/docs/roadmap.md +52 -0
- package/docs/troubleshooting.md +313 -0
- package/mails/en/email-changed.html +42 -0
- package/mails/en/email-changed.txt +21 -0
- package/mails/en/password-changed.html +42 -0
- package/mails/en/password-changed.txt +21 -0
- package/mails/en/reset-password.html +40 -0
- package/mails/en/reset-password.txt +22 -0
- package/mails/en/sign-in-code.html +36 -0
- package/mails/en/sign-in-code.txt +15 -0
- package/mails/en/verify-email.html +40 -0
- package/mails/en/verify-email.txt +22 -0
- package/mails/fr/email-changed.html +42 -0
- package/mails/fr/email-changed.txt +21 -0
- package/mails/fr/password-changed.html +41 -0
- package/mails/fr/password-changed.txt +21 -0
- package/mails/fr/reset-password.html +39 -0
- package/mails/fr/reset-password.txt +22 -0
- package/mails/fr/sign-in-code.html +36 -0
- package/mails/fr/sign-in-code.txt +15 -0
- package/mails/fr/verify-email.html +40 -0
- package/mails/fr/verify-email.txt +22 -0
- package/mails/mail-manifest.json +128 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Tsala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# @nxgt/janus-mail
|
|
2
|
+
|
|
3
|
+
The e-mails of [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus)'s
|
|
4
|
+
flows, ready to send: **e-mail verification, password reset, sign-in code,
|
|
5
|
+
password changed and e-mail changed**, in English and French, with your brand
|
|
6
|
+
in them. `@nxgt/janus` sends no e-mail — its flows answer what to send — and
|
|
7
|
+
this package turns that answer into an e-mail and hands it to any
|
|
8
|
+
[`@nxgt/mail`](https://www.npmjs.com/package/@nxgt/mail) transport.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createMemoryMailer } from '@nxgt/mail';
|
|
12
|
+
import { janusMail } from '@nxgt/janus-mail';
|
|
13
|
+
|
|
14
|
+
const mail = janusMail({
|
|
15
|
+
mailer: createMemoryMailer(), // in production, a transport: @nxgt/mail-smtp, @nxgt/mail-resend…
|
|
16
|
+
from: 'noreply@acme.example',
|
|
17
|
+
brand: 'Acme',
|
|
18
|
+
links: {
|
|
19
|
+
verifyEmail: (token) => `https://acme.example/verify?token=${token}`,
|
|
20
|
+
resetPassword: (token) => `https://acme.example/reset?token=${token}`,
|
|
21
|
+
secureAccount: () => 'https://acme.example/account/security',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await mail.verifyEmail(await auth.verifyEmail.send(user), { name: user.name, locale: user.locale });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The e-mails are built with [Maizzle](https://maizzle.com) when **this
|
|
29
|
+
package** is built — from
|
|
30
|
+
[`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets),
|
|
31
|
+
CSS inlined for mail clients — and shipped as HTML and text. At send time
|
|
32
|
+
they are only filled in: your brand, the recipient's name, your links, every
|
|
33
|
+
value escaped. No template engine and no Maizzle run in your server.
|
|
34
|
+
|
|
35
|
+
> **0.x.** A minor version may still change the surface; the changelog says how.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
bun add @nxgt/janus-mail @nxgt/mail @nxgt/janus
|
|
41
|
+
bun add -d typescript
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Three peers, all required: `@nxgt/mail` (0.1 or later, below 1 — the
|
|
45
|
+
transports of 0.4 included), which defines the `Mailer` port and the errors;
|
|
46
|
+
`@nxgt/janus` (0.8), whose flows' answers the methods take — types only,
|
|
47
|
+
nothing of it is loaded; and `typescript` (6). **No Maizzle, no Vue, no
|
|
48
|
+
Tailwind**: they run at this package's build, not in yours.
|
|
49
|
+
|
|
50
|
+
It reads its prebuilt e-mails with `node:fs`: **Node, Bun or Deno**, not an edge
|
|
51
|
+
runtime. Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`.
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
| Export | What it is |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `passwordChanged`, `emailChanged`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`), `locales?`, `fallbackLocale?`, `templates?`. A wrong option is a bare `TypeError`, thrown here |
|
|
58
|
+
| `janusTemplates()` | The five default templates alone, each `(variables & { locale }) => Rendered` |
|
|
59
|
+
| `JanusMail<L>`, `JanusMailOptions<L>` | What `janusMail()` answers and takes, for the locales `L` |
|
|
60
|
+
| `JanusMailTemplate<V, L>`, `JanusMailTemplates<L>`, `JanusMailTemplateName` | One template, the five of them, and their names |
|
|
61
|
+
| `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code` or `newEmail` as its e-mail needs |
|
|
62
|
+
| `JanusMailLocale` | `'en' \| 'fr'`: the locales the defaults are built in |
|
|
63
|
+
| `JanusMailLinks`, `Recipient` | The `links` option; who an e-mail is for — `{ name, locale? }` |
|
|
64
|
+
|
|
65
|
+
Each method answers the mailer's `SentMail` and **rejects with the mailer's
|
|
66
|
+
`MailFailure` or `MailRefused`, untouched**: this package defines no error
|
|
67
|
+
class.
|
|
68
|
+
|
|
69
|
+
| Method | Sends | To |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `verifyEmail(issued, to)` | `issued` from `auth.verifyEmail.send(user)`; a link from `links.verifyEmail(issued.token)` | `issued.email` |
|
|
72
|
+
| `resetPassword(issued, to)` | `issued` from `auth.resetPassword.request(email)`, checked for `null`; a link from `links.resetPassword(issued.token)` | `issued.email` |
|
|
73
|
+
| `signInCode(issued, to?)` | `issued.code` from `auth.signInCode.request(email)` — **never the challenge** | `issued.email` |
|
|
74
|
+
| `passwordChanged(to)` | A notice, with `links.secureAccount()` | `to.email` |
|
|
75
|
+
| `emailChanged(to)` | A notice naming `to.newEmail`, with `links.secureAccount()` | `to.formerEmail` |
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
The examples use `mail` from above, and an `auth` whose user schema has a
|
|
80
|
+
`name` and a `locale` beside its `email` — fields of yours, not `janus`'s: pass
|
|
81
|
+
whatever your users carry.
|
|
82
|
+
|
|
83
|
+
### Verifying an e-mail
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const issued = await auth.verifyEmail.send(user); // { token, email, expiresAt }
|
|
87
|
+
await mail.verifyEmail(issued, { name: user.name, locale: user.locale });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Resetting a password
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const issued = await auth.resetPassword.request(email);
|
|
94
|
+
if (issued !== null) {
|
|
95
|
+
await mail.resetPassword(issued, { name: issued.user.name, locale: issued.user.locale });
|
|
96
|
+
}
|
|
97
|
+
return new Response(null, { status: 202 }); // the same answer whether or not someone holds the address
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Signing in with a code
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { parseAcceptLanguage } from '@nxgt/mail';
|
|
104
|
+
|
|
105
|
+
const issued = await auth.signInCode.request(email);
|
|
106
|
+
if (issued !== null) {
|
|
107
|
+
const wanted = [issued.user.locale, ...parseAcceptLanguage(request.headers.get('accept-language'))];
|
|
108
|
+
await mail.signInCode(issued, { locale: wanted });
|
|
109
|
+
// issued.challenge stays with the visitor — a cookie or the form — never in the e-mail
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Telling a user their password changed
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const changed = await auth.changePassword(user, { current, next });
|
|
117
|
+
await mail.passwordChanged({ name: changed.name, locale: changed.locale, email: changed.email });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Telling a user their e-mail changed
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const before = user.email;
|
|
124
|
+
const updated = await auth.update(user, { email: next });
|
|
125
|
+
await mail.emailChanged({ name: updated.name, locale: updated.locale, formerEmail: before, newEmail: updated.email });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Replacing a template
|
|
129
|
+
|
|
130
|
+
Any template can be your own function — React Email, a string, another
|
|
131
|
+
build — and the defaults stay for the rest:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const mail = janusMail({
|
|
135
|
+
mailer, from: 'noreply@acme.example', brand: 'Acme', links,
|
|
136
|
+
templates: {
|
|
137
|
+
signInCode: ({ code, locale }) => ({
|
|
138
|
+
subject: locale === 'fr' ? `Votre code : ${code}` : `Your code: ${code}`,
|
|
139
|
+
html: `<p>${code}</p>`,
|
|
140
|
+
text: code,
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A locale beyond `en` and `fr` takes **every** template: see
|
|
147
|
+
[Templates](docs/guide/templates.md) and [Locales](docs/guide/locales.md).
|
|
148
|
+
|
|
149
|
+
## Traps
|
|
150
|
+
|
|
151
|
+
**Send to `issued.email`, which the methods do.** Never to what the visitor
|
|
152
|
+
typed: the flow answers the address the user's record holds, as they
|
|
153
|
+
registered it. `emailChanged` goes to `formerEmail` — the address a hijacker
|
|
154
|
+
just took the account from.
|
|
155
|
+
|
|
156
|
+
**Never fire and forget.** `void mail.signInCode(issued)` turns an outage
|
|
157
|
+
into an unhandled rejection and a user waiting for an e-mail that never
|
|
158
|
+
comes. `await` it, or hand it to a queue that retries — which is also how to
|
|
159
|
+
send after answering, as the sign-in code guide advises.
|
|
160
|
+
|
|
161
|
+
**`node:fs` means no edge runtime.** The default e-mails are read from the
|
|
162
|
+
package's `mails/` folder, on the first one sent. On an edge runtime, pass
|
|
163
|
+
every template yourself — or wait for the inlined build on the
|
|
164
|
+
[roadmap](docs/roadmap.md).
|
|
165
|
+
|
|
166
|
+
**Keep `@nxgt/janus-mail` out of your server bundle.** The default e-mails
|
|
167
|
+
are found beside the code, at `new URL('../mails/', import.meta.url)`.
|
|
168
|
+
Bundling the package into your own server file moves `import.meta.url` to
|
|
169
|
+
your bundle, where there is no `mails/`, and the first e-mail sent throws
|
|
170
|
+
`createMailRenderer: …/mails/mail-manifest.json cannot be read`. Mark it
|
|
171
|
+
external — `external: ['@nxgt/janus-mail']` in esbuild or `Bun.build`,
|
|
172
|
+
`ssr.external` in Vite — and deploy `node_modules/@nxgt/janus-mail/` whole,
|
|
173
|
+
`mails/` included. See
|
|
174
|
+
[troubleshooting](docs/troubleshooting.md#createmailrenderer-mailsmail-manifestjson-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder).
|
|
175
|
+
|
|
176
|
+
**A link answers a string, at once.** Each `links` function is called at
|
|
177
|
+
send time and must return an `http(s)` or `mailto:` URL as a string: an
|
|
178
|
+
`async` function or a `URL` object is a compile error, and in JavaScript a
|
|
179
|
+
`TypeError` naming the call.
|
|
180
|
+
`links`, `from` and `replyTo` are copied when `janusMail()` is called, so
|
|
181
|
+
changing them afterwards changes nothing.
|
|
182
|
+
|
|
183
|
+
**Templates are own properties.** A class's methods live on its prototype
|
|
184
|
+
and are refused with a `TypeError`; its fields (`signInCode = (variables) =>
|
|
185
|
+
…`) pass.
|
|
186
|
+
|
|
187
|
+
**A locale beyond `en` and `fr` needs every template.** The defaults are
|
|
188
|
+
built in those two only, so `locales: ['en', 'fr', 'de']` without all five
|
|
189
|
+
templates is a compile error, and a `TypeError` in JavaScript.
|
|
190
|
+
|
|
191
|
+
**The brand is text only.** `brand: 'Acme'` is written in the header, the
|
|
192
|
+
body and the footer, escaped: no logo, no link, no markup. For those, replace
|
|
193
|
+
the templates.
|
|
194
|
+
|
|
195
|
+
**The default e-mails do not state the expiry.** `@nxgt/mail-presets`' bodies
|
|
196
|
+
say nothing of how long a link or a code lasts. If yours must, replace the
|
|
197
|
+
template: `issued.expiresAt` is yours to format.
|
|
198
|
+
|
|
199
|
+
**A failed send is `MAIL_FAILED`; answer it as an outage.** In an HTTP
|
|
200
|
+
handler, a `MailFailure` is a `503` — nothing is known to have been sent —
|
|
201
|
+
and a `MailRefused` (a `javascript:` link, an address that is not one) is a
|
|
202
|
+
bug to fix, never retried:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { MailError } from '@nxgt/mail';
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await mail.verifyEmail(await auth.verifyEmail.send(user), { name: user.name });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof MailError && error.code === 'MAIL_FAILED') {
|
|
211
|
+
return Response.json({ code: error.code }, { status: 503 });
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The symptoms and fixes are in [troubleshooting](docs/troubleshooting.md).
|
|
218
|
+
|
|
219
|
+
## Documentation
|
|
220
|
+
|
|
221
|
+
- [Guides](docs/README.md) — sending, templates, locales and how the e-mails are built
|
|
222
|
+
- [Troubleshooting](docs/troubleshooting.md) — every message this package throws, and `MailRefused`
|
|
223
|
+
- [Roadmap](docs/roadmap.md) — what is next, and what is not planned
|
|
224
|
+
|
|
225
|
+
## Type safety, counted
|
|
226
|
+
|
|
227
|
+
**Twenty plausible mistakes, twenty refused at compile time.**
|
|
228
|
+
[`test/types/refusals.ts`](test/types/refusals.ts) holds one
|
|
229
|
+
`@ts-expect-error` per mistake, beside the calls that must keep compiling —
|
|
230
|
+
among them adding a language with every template:
|
|
231
|
+
|
|
232
|
+
1. A sign-in code given to `verifyEmail`: it has no token.
|
|
233
|
+
2. A one-time token given to `signInCode`: it has no code.
|
|
234
|
+
3. `resetPassword.request`'s answer, not checked for `null` first.
|
|
235
|
+
4. `verifyEmail` without the recipient.
|
|
236
|
+
5. A recipient without a name.
|
|
237
|
+
6. `passwordChanged` without the address to tell.
|
|
238
|
+
7. `emailChanged` without the former address.
|
|
239
|
+
8. A locale that is not a locale (a number).
|
|
240
|
+
9. `links` without `secureAccount`.
|
|
241
|
+
10. A link given as a URL rather than a function of the token.
|
|
242
|
+
11. `brand` given as an object (`{ name: 'Acme' }`) rather than its name.
|
|
243
|
+
12. A template that answers a string rather than `{ subject, html, text }`.
|
|
244
|
+
13. A template that reads a variable its e-mail does not have.
|
|
245
|
+
14. A template for an e-mail the package does not send.
|
|
246
|
+
15. A `fallbackLocale` outside `locales`.
|
|
247
|
+
16. A locale beyond `en` and `fr` with only some templates.
|
|
248
|
+
17. A default template reused for a locale it is not built in.
|
|
249
|
+
18. A template read from `mail.templates` that does not exist.
|
|
250
|
+
19. A link computed asynchronously.
|
|
251
|
+
20. A link answered as a `URL` object rather than its `href`.
|
|
252
|
+
|
|
253
|
+
In JavaScript, 19 and 20 are a `TypeError` at send time instead, naming the
|
|
254
|
+
call.
|
|
255
|
+
|
|
256
|
+
[`test/types/variables.ts`](test/types/variables.ts) also holds
|
|
257
|
+
`JanusMailVariables` equal to the variables of the build: an e-mail that
|
|
258
|
+
gains or loses one fails the typecheck.
|
|
259
|
+
|
|
260
|
+
## Licence
|
|
261
|
+
|
|
262
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The locales the default e-mails are built in, the fallback locale first. */
|
|
2
|
+
export declare const LOCALES: readonly ["en", "fr"];
|
|
3
|
+
/** A locale the default e-mails are built in. */
|
|
4
|
+
export type JanusMailLocale = (typeof LOCALES)[number];
|
|
5
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../src/generated/locales.ts"],"names":[],"mappings":"AAGA,+EAA+E;AAC/E,eAAO,MAAM,OAAO,uBAAwB,CAAC;AAE7C,iDAAiD;AACjD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The e-mails of the build, each with the variables it takes when it is sent. */
|
|
2
|
+
export interface MailEmails {
|
|
3
|
+
"email-changed": {
|
|
4
|
+
readonly brand: string | number;
|
|
5
|
+
readonly link: string;
|
|
6
|
+
readonly name: string | number;
|
|
7
|
+
readonly newEmail: string | number;
|
|
8
|
+
};
|
|
9
|
+
"password-changed": {
|
|
10
|
+
readonly brand: string | number;
|
|
11
|
+
readonly link: string;
|
|
12
|
+
readonly name: string | number;
|
|
13
|
+
};
|
|
14
|
+
"reset-password": {
|
|
15
|
+
readonly brand: string | number;
|
|
16
|
+
readonly link: string;
|
|
17
|
+
readonly name: string | number;
|
|
18
|
+
};
|
|
19
|
+
"sign-in-code": {
|
|
20
|
+
readonly brand: string | number;
|
|
21
|
+
readonly code: string | number;
|
|
22
|
+
};
|
|
23
|
+
"verify-email": {
|
|
24
|
+
readonly brand: string | number;
|
|
25
|
+
readonly link: string;
|
|
26
|
+
readonly name: string | number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=mail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../src/generated/mail.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,MAAM,WAAW,UAAU;IAC1B,eAAe,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAChJ,kBAAkB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/G,gBAAgB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC7G,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACpF,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CAC3G"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nxgt/janus-mail` — the e-mails of `@nxgt/janus`'s flows.
|
|
3
|
+
*
|
|
4
|
+
* `@nxgt/janus` sends no e-mail: its flows answer what to send — a one-time
|
|
5
|
+
* token, a sign-in code, the address — and this package turns that answer
|
|
6
|
+
* into an e-mail and hands it to an `@nxgt/mail` transport. Five e-mails, in
|
|
7
|
+
* English and French, prebuilt with Maizzle when this package is built, and
|
|
8
|
+
* filled at send time with your brand, the recipient's name and your links.
|
|
9
|
+
* Any of them can be replaced by a function of your own.
|
|
10
|
+
*
|
|
11
|
+
* ## The rule this package is built around
|
|
12
|
+
*
|
|
13
|
+
* **A send that fails throws.** Every method rejects with the mailer's
|
|
14
|
+
* `MailFailure` or `MailRefused`, untouched — never wrapped, never mapped to
|
|
15
|
+
* `false` — so a user is never told to check an inbox that will stay empty.
|
|
16
|
+
*/
|
|
17
|
+
export { janusMail } from './janus-mail';
|
|
18
|
+
export { janusTemplates } from './templates';
|
|
19
|
+
export type { JanusMail, JanusMailLinks, JanusMailLocale, JanusMailOptions, JanusMailTemplate, JanusMailTemplateName, JanusMailTemplates, JanusMailVariables, Recipient, } from './types';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EACX,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,GACT,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// src/janus-mail.ts
|
|
2
|
+
import { pickLocale } from "@nxgt/mail";
|
|
3
|
+
|
|
4
|
+
// src/generated/locales.ts
|
|
5
|
+
var LOCALES = ["en", "fr"];
|
|
6
|
+
|
|
7
|
+
// src/mails.ts
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createMailRenderer } from "@nxgt/mail/renderer";
|
|
10
|
+
var MAILS_DIR = fileURLToPath(new URL("../mails/", import.meta.url));
|
|
11
|
+
var renderer;
|
|
12
|
+
function defaultRenderer() {
|
|
13
|
+
renderer ??= createMailRenderer({ dir: MAILS_DIR });
|
|
14
|
+
return renderer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/templates.ts
|
|
18
|
+
function janusTemplates() {
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
verifyEmail: ({ brand, name, link, locale }) => defaultRenderer().render("verify-email", { brand, name, link }, { locale }),
|
|
21
|
+
resetPassword: ({ brand, name, link, locale }) => defaultRenderer().render("reset-password", { brand, name, link }, { locale }),
|
|
22
|
+
signInCode: ({ brand, code, locale }) => defaultRenderer().render("sign-in-code", { brand, code }, { locale }),
|
|
23
|
+
passwordChanged: ({ brand, name, link, locale }) => defaultRenderer().render("password-changed", { brand, name, link }, { locale }),
|
|
24
|
+
emailChanged: ({ brand, name, link, newEmail, locale }) => defaultRenderer().render("email-changed", { brand, name, link, newEmail }, { locale })
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/options.ts
|
|
29
|
+
var TEMPLATE_NAMES = [
|
|
30
|
+
"verifyEmail",
|
|
31
|
+
"resetPassword",
|
|
32
|
+
"signInCode",
|
|
33
|
+
"passwordChanged",
|
|
34
|
+
"emailChanged"
|
|
35
|
+
];
|
|
36
|
+
var LINK_NAMES = ["verifyEmail", "resetPassword", "secureAccount"];
|
|
37
|
+
function isObject(value) {
|
|
38
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function isAddress(value) {
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
return value.trim() !== "";
|
|
43
|
+
return isObject(value) && typeof value.name === "string" && typeof value.address === "string" && value.address.trim() !== "";
|
|
44
|
+
}
|
|
45
|
+
function refuse(message) {
|
|
46
|
+
throw new TypeError(`janusMail: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
function checkSending(options) {
|
|
49
|
+
const { mailer, from, replyTo, brand, links } = options;
|
|
50
|
+
if (!isObject(mailer) || typeof mailer.send !== "function") {
|
|
51
|
+
refuse("mailer must be a Mailer — an object with a send function");
|
|
52
|
+
}
|
|
53
|
+
if (!isAddress(from)) {
|
|
54
|
+
refuse("from must be an address, as 'noreply@example.com' or { name, address }");
|
|
55
|
+
}
|
|
56
|
+
if (replyTo !== undefined && !isAddress(replyTo)) {
|
|
57
|
+
refuse("replyTo must be an address, as 'support@example.com' or { name, address }");
|
|
58
|
+
}
|
|
59
|
+
if (typeof brand !== "string" || brand.trim() === "") {
|
|
60
|
+
refuse("brand must be the name the e-mails show, as 'Acme'");
|
|
61
|
+
}
|
|
62
|
+
if (!isObject(links)) {
|
|
63
|
+
refuse("links must be an object, as { verifyEmail, resetPassword, secureAccount }");
|
|
64
|
+
}
|
|
65
|
+
for (const name of LINK_NAMES) {
|
|
66
|
+
if (typeof links[name] !== "function") {
|
|
67
|
+
refuse(`links.${name} must be a function`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function resolveLocales(options) {
|
|
72
|
+
const wanted = options.locales ?? LOCALES;
|
|
73
|
+
if (!Array.isArray(wanted) || wanted.length === 0 || !wanted.every((locale) => typeof locale === "string" && locale !== "")) {
|
|
74
|
+
refuse("locales must list at least one locale, as ['en', 'fr']");
|
|
75
|
+
}
|
|
76
|
+
const locales = Object.freeze([...wanted]);
|
|
77
|
+
if (new Set(locales).size !== locales.length) {
|
|
78
|
+
refuse("locales holds the same locale twice");
|
|
79
|
+
}
|
|
80
|
+
const fallbackLocale = options.fallbackLocale ?? (locales.includes("en") ? "en" : locales[0]);
|
|
81
|
+
if (typeof fallbackLocale !== "string" || !locales.includes(fallbackLocale)) {
|
|
82
|
+
refuse("fallbackLocale must be one of locales");
|
|
83
|
+
}
|
|
84
|
+
return { locales, fallbackLocale };
|
|
85
|
+
}
|
|
86
|
+
function resolveTemplates(options, locales) {
|
|
87
|
+
const given = options.templates ?? {};
|
|
88
|
+
if (!isObject(given)) {
|
|
89
|
+
refuse("templates must be an object of functions, as { verifyEmail: (variables) => rendered }");
|
|
90
|
+
}
|
|
91
|
+
for (const [name, template] of Object.entries(given)) {
|
|
92
|
+
if (!TEMPLATE_NAMES.includes(name)) {
|
|
93
|
+
refuse(`templates has no template ${name} — name one of ${TEMPLATE_NAMES.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof template !== "function") {
|
|
96
|
+
refuse(`templates.${name} must be a function`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const passed = Object.keys(given);
|
|
100
|
+
for (const name of TEMPLATE_NAMES) {
|
|
101
|
+
if (name in given && !passed.includes(name)) {
|
|
102
|
+
refuse(`templates.${name} is not an own enumerable property — pass a plain object, as { ${name}: (variables) => rendered }`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const built = locales.every((locale) => LOCALES.includes(locale));
|
|
106
|
+
const missing = TEMPLATE_NAMES.filter((name) => !passed.includes(name));
|
|
107
|
+
if (!built && missing.length > 0) {
|
|
108
|
+
refuse(`the default templates are built in ${LOCALES.join(" and ")} only — with another ` + `locale in locales, pass every template in templates; ${missing.join(", ")} missing`);
|
|
109
|
+
}
|
|
110
|
+
const defaults = janusTemplates();
|
|
111
|
+
return Object.freeze({ ...defaults, ...given });
|
|
112
|
+
}
|
|
113
|
+
function frozenAddress(address) {
|
|
114
|
+
return typeof address === "string" ? address : Object.freeze({ name: address.name, address: address.address });
|
|
115
|
+
}
|
|
116
|
+
function frozenLinks(links) {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
verifyEmail: links.verifyEmail.bind(links),
|
|
119
|
+
resetPassword: links.resetPassword.bind(links),
|
|
120
|
+
secureAccount: links.secureAccount.bind(links)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function resolveOptions(options) {
|
|
124
|
+
if (!isObject(options)) {
|
|
125
|
+
refuse("options must be an object, as { mailer, from, brand, links }");
|
|
126
|
+
}
|
|
127
|
+
checkSending(options);
|
|
128
|
+
const { locales, fallbackLocale } = resolveLocales(options);
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
mailer: options.mailer,
|
|
131
|
+
from: frozenAddress(options.from),
|
|
132
|
+
replyTo: options.replyTo === undefined ? undefined : frozenAddress(options.replyTo),
|
|
133
|
+
brand: options.brand,
|
|
134
|
+
links: frozenLinks(options.links),
|
|
135
|
+
locales,
|
|
136
|
+
fallbackLocale,
|
|
137
|
+
templates: resolveTemplates(options, locales)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/janus-mail.ts
|
|
142
|
+
function field(method, value, key) {
|
|
143
|
+
const read = typeof value === "object" && value !== null ? value[key] : undefined;
|
|
144
|
+
if (typeof read !== "string") {
|
|
145
|
+
throw new TypeError(`janusMail.${method}: ${key} must be a string`);
|
|
146
|
+
}
|
|
147
|
+
return read;
|
|
148
|
+
}
|
|
149
|
+
function link(method, value, call) {
|
|
150
|
+
if (typeof value !== "string") {
|
|
151
|
+
throw new TypeError(`janusMail.${method}: links.${call} must answer a string`);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function localeFor(options, to) {
|
|
156
|
+
const wanted = typeof to === "object" && to !== null ? to.locale : undefined;
|
|
157
|
+
return pickLocale(wanted, options.locales, options.fallbackLocale);
|
|
158
|
+
}
|
|
159
|
+
async function send(options, name, address, variables) {
|
|
160
|
+
const template = options.templates[name];
|
|
161
|
+
const { subject, html, text } = await template(variables);
|
|
162
|
+
const replyTo = options.replyTo === undefined ? {} : { replyTo: options.replyTo };
|
|
163
|
+
return options.mailer.send({
|
|
164
|
+
to: address,
|
|
165
|
+
from: options.from,
|
|
166
|
+
...replyTo,
|
|
167
|
+
subject,
|
|
168
|
+
html,
|
|
169
|
+
text
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function methods(options) {
|
|
173
|
+
const { brand, links } = options;
|
|
174
|
+
return {
|
|
175
|
+
async verifyEmail(issued, to) {
|
|
176
|
+
const token = field("verifyEmail", issued, "token");
|
|
177
|
+
return send(options, "verifyEmail", field("verifyEmail", issued, "email"), {
|
|
178
|
+
brand,
|
|
179
|
+
name: field("verifyEmail", to, "name"),
|
|
180
|
+
link: link("verifyEmail", links.verifyEmail(token), "verifyEmail(token)"),
|
|
181
|
+
locale: localeFor(options, to)
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
async resetPassword(issued, to) {
|
|
185
|
+
const token = field("resetPassword", issued, "token");
|
|
186
|
+
return send(options, "resetPassword", field("resetPassword", issued, "email"), {
|
|
187
|
+
brand,
|
|
188
|
+
name: field("resetPassword", to, "name"),
|
|
189
|
+
link: link("resetPassword", links.resetPassword(token), "resetPassword(token)"),
|
|
190
|
+
locale: localeFor(options, to)
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async signInCode(issued, to) {
|
|
194
|
+
return send(options, "signInCode", field("signInCode", issued, "email"), {
|
|
195
|
+
brand,
|
|
196
|
+
code: field("signInCode", issued, "code"),
|
|
197
|
+
locale: localeFor(options, to)
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
async passwordChanged(to) {
|
|
201
|
+
return send(options, "passwordChanged", field("passwordChanged", to, "email"), {
|
|
202
|
+
brand,
|
|
203
|
+
name: field("passwordChanged", to, "name"),
|
|
204
|
+
link: link("passwordChanged", links.secureAccount(), "secureAccount()"),
|
|
205
|
+
locale: localeFor(options, to)
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
async emailChanged(to) {
|
|
209
|
+
return send(options, "emailChanged", field("emailChanged", to, "formerEmail"), {
|
|
210
|
+
brand,
|
|
211
|
+
name: field("emailChanged", to, "name"),
|
|
212
|
+
link: link("emailChanged", links.secureAccount(), "secureAccount()"),
|
|
213
|
+
newEmail: field("emailChanged", to, "newEmail"),
|
|
214
|
+
locale: localeFor(options, to)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function janusMail(options) {
|
|
220
|
+
const resolved = resolveOptions(options);
|
|
221
|
+
const mail = Object.freeze({
|
|
222
|
+
...methods(resolved),
|
|
223
|
+
templates: resolved.templates,
|
|
224
|
+
locales: resolved.locales
|
|
225
|
+
});
|
|
226
|
+
return mail;
|
|
227
|
+
}
|
|
228
|
+
export {
|
|
229
|
+
janusMail,
|
|
230
|
+
janusTemplates
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
//# debugId=B2A314C222070DD464756E2164756E21
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/janus-mail.ts", "../src/generated/locales.ts", "../src/mails.ts", "../src/templates.ts", "../src/options.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `janusMail()`: the e-mails of Janus's flows, rendered by the templates and\n * handed to the mailer. It sends what a flow answered, to the address the\n * flow answered, and nothing else: no retry, no queue, no error of its own —\n * the mailer's `MailFailure` and `MailRefused` reach the caller untouched.\n */\nimport type { Address, Rendered, SentMail, WantedLocales } from '@nxgt/mail';\nimport { pickLocale } from '@nxgt/mail';\nimport { type ResolvedOptions, resolveOptions } from './options';\nimport type {\n\tJanusMail,\n\tJanusMailLocale,\n\tJanusMailOptions,\n\tJanusMailTemplateName,\n\tJanusMailVariables,\n} from './types';\n\n/** A string field of an argument, or a `TypeError` naming the method and the field. */\nfunction field(method: string, value: unknown, key: string): string {\n\tconst read =\n\t\ttypeof value === 'object' && value !== null\n\t\t\t? (value as Record<string, unknown>)[key]\n\t\t\t: undefined;\n\tif (typeof read !== 'string') {\n\t\tthrow new TypeError(`janusMail.${method}: ${key} must be a string`);\n\t}\n\treturn read;\n}\n\n/**\n * What a `links` function answered, or a `TypeError` naming the method and\n * the call. Its scheme is the renderer's to check: `http(s)` and `mailto:`\n * pass, anything else is its `MailRefused`.\n */\nfunction link(method: string, value: unknown, call: string): string {\n\tif (typeof value !== 'string') {\n\t\tthrow new TypeError(\n\t\t\t`janusMail.${method}: links.${call} must answer a string`,\n\t\t);\n\t}\n\treturn value;\n}\n\n/** The locale the recipient gets: the first they want that is sent in, else the fallback. */\nfunction localeFor(options: ResolvedOptions, to: unknown): string {\n\tconst wanted =\n\t\ttypeof to === 'object' && to !== null\n\t\t\t? (to as { locale?: WantedLocales }).locale\n\t\t\t: undefined;\n\treturn pickLocale(wanted, options.locales, options.fallbackLocale);\n}\n\n/**\n * Renders one e-mail and hands it to the mailer. Only `subject`, `html` and\n * `text` are read from what the template answered: a template cannot add a\n * recipient or a header.\n */\nasync function send<K extends JanusMailTemplateName>(\n\toptions: ResolvedOptions,\n\tname: K,\n\taddress: string,\n\tvariables: JanusMailVariables[K] & { readonly locale: string },\n): Promise<SentMail> {\n\tconst template = options.templates[name] as (\n\t\tvariables: JanusMailVariables[K] & { readonly locale: string },\n\t) => Rendered | PromiseLike<Rendered>;\n\tconst { subject, html, text }: Rendered = await template(variables);\n\tconst replyTo: { replyTo?: Address } =\n\t\toptions.replyTo === undefined ? {} : { replyTo: options.replyTo };\n\treturn options.mailer.send({\n\t\tto: address,\n\t\tfrom: options.from,\n\t\t...replyTo,\n\t\tsubject,\n\t\thtml,\n\t\ttext,\n\t});\n}\n\n/** The five methods, over the resolved options. */\nfunction methods(\n\toptions: ResolvedOptions,\n): Omit<JanusMail<string>, 'templates' | 'locales'> {\n\tconst { brand, links } = options;\n\treturn {\n\t\tasync verifyEmail(issued, to) {\n\t\t\tconst token = field('verifyEmail', issued, 'token');\n\t\t\treturn send(\n\t\t\t\toptions,\n\t\t\t\t'verifyEmail',\n\t\t\t\tfield('verifyEmail', issued, 'email'),\n\t\t\t\t{\n\t\t\t\t\tbrand,\n\t\t\t\t\tname: field('verifyEmail', to, 'name'),\n\t\t\t\t\tlink: link(\n\t\t\t\t\t\t'verifyEmail',\n\t\t\t\t\t\tlinks.verifyEmail(token),\n\t\t\t\t\t\t'verifyEmail(token)',\n\t\t\t\t\t),\n\t\t\t\t\tlocale: localeFor(options, to),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\tasync resetPassword(issued, to) {\n\t\t\tconst token = field('resetPassword', issued, 'token');\n\t\t\treturn send(\n\t\t\t\toptions,\n\t\t\t\t'resetPassword',\n\t\t\t\tfield('resetPassword', issued, 'email'),\n\t\t\t\t{\n\t\t\t\t\tbrand,\n\t\t\t\t\tname: field('resetPassword', to, 'name'),\n\t\t\t\t\tlink: link(\n\t\t\t\t\t\t'resetPassword',\n\t\t\t\t\t\tlinks.resetPassword(token),\n\t\t\t\t\t\t'resetPassword(token)',\n\t\t\t\t\t),\n\t\t\t\t\tlocale: localeFor(options, to),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\tasync signInCode(issued, to) {\n\t\t\t// `code` and `email` only: the challenge is the visitor's secret.\n\t\t\treturn send(options, 'signInCode', field('signInCode', issued, 'email'), {\n\t\t\t\tbrand,\n\t\t\t\tcode: field('signInCode', issued, 'code'),\n\t\t\t\tlocale: localeFor(options, to),\n\t\t\t});\n\t\t},\n\t\tasync passwordChanged(to) {\n\t\t\treturn send(\n\t\t\t\toptions,\n\t\t\t\t'passwordChanged',\n\t\t\t\tfield('passwordChanged', to, 'email'),\n\t\t\t\t{\n\t\t\t\t\tbrand,\n\t\t\t\t\tname: field('passwordChanged', to, 'name'),\n\t\t\t\t\tlink: link(\n\t\t\t\t\t\t'passwordChanged',\n\t\t\t\t\t\tlinks.secureAccount(),\n\t\t\t\t\t\t'secureAccount()',\n\t\t\t\t\t),\n\t\t\t\t\tlocale: localeFor(options, to),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\tasync emailChanged(to) {\n\t\t\t// To the former address: the one a hijacker just took the account from.\n\t\t\treturn send(\n\t\t\t\toptions,\n\t\t\t\t'emailChanged',\n\t\t\t\tfield('emailChanged', to, 'formerEmail'),\n\t\t\t\t{\n\t\t\t\t\tbrand,\n\t\t\t\t\tname: field('emailChanged', to, 'name'),\n\t\t\t\t\tlink: link('emailChanged', links.secureAccount(), 'secureAccount()'),\n\t\t\t\t\tnewEmail: field('emailChanged', to, 'newEmail'),\n\t\t\t\t\tlocale: localeFor(options, to),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/**\n * The e-mails of Janus's flows, over your mailer:\n *\n * ```ts\n * const mail = janusMail({\n * \tmailer,\n * \tfrom: 'noreply@acme.example',\n * \tbrand: 'Acme',\n * \tlinks: {\n * \t\tverifyEmail: (token) => `https://acme.example/verify?token=${token}`,\n * \t\tresetPassword: (token) => `https://acme.example/reset?token=${token}`,\n * \t\tsecureAccount: () => 'https://acme.example/account/security',\n * \t},\n * });\n *\n * await mail.verifyEmail(await auth.verifyEmail.send(user), { name: user.name, locale: user.locale });\n * ```\n *\n * Reads nothing when called: the default e-mails are read on the first one\n * sent. A wrong option is a bare `TypeError`, thrown here.\n */\nexport function janusMail<const L extends string = JanusMailLocale>(\n\toptions: JanusMailOptions<L>,\n): JanusMail<L> {\n\tconst resolved = resolveOptions(options);\n\tconst mail: JanusMail<string> = Object.freeze({\n\t\t...methods(resolved),\n\t\ttemplates: resolved.templates,\n\t\tlocales: resolved.locales,\n\t});\n\t// The one cast at the boundary: the mirror above works on `string` locales.\n\treturn mail as unknown as JanusMail<L>;\n}\n",
|
|
6
|
+
"// Generated by scripts/build-mail.ts from mails/mail-manifest.json, after\n// each build of the default e-mails. Never edited; committed.\n\n/** The locales the default e-mails are built in, the fallback locale first. */\nexport const LOCALES = ['en', 'fr'] as const;\n\n/** A locale the default e-mails are built in. */\nexport type JanusMailLocale = (typeof LOCALES)[number];\n",
|
|
7
|
+
"/**\n * Where the default e-mails are, and the one renderer that reads them.\n *\n * The package ships the built e-mails in `mails/`, beside `dist/` — not in\n * it, because the root `build.ts` removes from `dist/` every file it did not\n * write itself.\n */\nimport { fileURLToPath } from 'node:url';\nimport { createMailRenderer, type MailRenderer } from '@nxgt/mail/renderer';\nimport type { MailEmails } from './generated/mail';\n\n/**\n * The folder of the built e-mails, resolved from this module rather than from\n * the working directory, which is the consumer's.\n *\n * **This file must stay directly under `src/`.** `../mails/` is right from\n * `src/mails.ts` in the specs, and from `dist/index.js` in the tarball,\n * because the build bundles this module into the single entry\n * `dist/index.js`, one level under the package as `src/` is. Moved into a\n * subfolder, or bundled into `dist/chunks/` by a second entry point, it would\n * resolve a folder that does not exist — which the artifact spec, rendering\n * from `../dist`, would catch.\n */\nexport const MAILS_DIR = fileURLToPath(new URL('../mails/', import.meta.url));\n\nlet renderer: MailRenderer<MailEmails> | undefined;\n\n/**\n * The renderer of the default e-mails, created on first use and shared by\n * every `janusMail()` and `janusTemplates()` after it.\n *\n * Lazy, so importing the package reads nothing: an application that\n * overrides every template never opens `mails/`. The first default template\n * rendered reads the whole build once — a missing `mails/` throws there, with\n * `createMailRenderer`'s own message.\n */\nexport function defaultRenderer(): MailRenderer<MailEmails> {\n\trenderer ??= createMailRenderer<MailEmails>({ dir: MAILS_DIR });\n\treturn renderer;\n}\n",
|
|
8
|
+
"/**\n * The default templates: the prebuilt e-mails, filled by `@nxgt/mail`'s\n * renderer. Each picks from its variables exactly what its e-mail takes — the\n * renderer refuses a variable too many — so the `locale` beside them goes to\n * the renderer's options, and nowhere else.\n */\nimport type { Rendered } from '@nxgt/mail';\nimport { defaultRenderer } from './mails';\nimport type { JanusMailTemplates } from './types';\n\n/**\n * The five default templates, in the locales they are built in. The\n * renderer behind them is created on the first render, not here.\n *\n * ```ts\n * const defaults = janusTemplates();\n * const { subject } = await defaults.verifyEmail({ brand: 'Acme', name: 'Ada', link, locale: 'fr' });\n * ```\n *\n * A URL that is not `http:`, `https:` or `mailto:` throws `MailRefused`; a\n * missing `mails/` folder throws when the first one renders.\n */\nexport function janusTemplates(): JanusMailTemplates {\n\treturn Object.freeze({\n\t\tverifyEmail: ({ brand, name, link, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'verify-email',\n\t\t\t\t{ brand, name, link },\n\t\t\t\t{ locale },\n\t\t\t),\n\t\tresetPassword: ({ brand, name, link, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'reset-password',\n\t\t\t\t{ brand, name, link },\n\t\t\t\t{ locale },\n\t\t\t),\n\t\tsignInCode: ({ brand, code, locale }): Rendered =>\n\t\t\tdefaultRenderer().render('sign-in-code', { brand, code }, { locale }),\n\t\tpasswordChanged: ({ brand, name, link, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'password-changed',\n\t\t\t\t{ brand, name, link },\n\t\t\t\t{ locale },\n\t\t\t),\n\t\temailChanged: ({ brand, name, link, newEmail, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'email-changed',\n\t\t\t\t{ brand, name, link, newEmail },\n\t\t\t\t{ locale },\n\t\t\t),\n\t});\n}\n",
|
|
9
|
+
"/**\n * `janusMail()`'s options, checked once when it is called. A mistake is a\n * wiring mistake, so a bare `TypeError` naming the option — never its value —\n * thrown before anything is sent.\n */\nimport type { Address, Mailer } from '@nxgt/mail';\nimport { LOCALES } from './generated/locales';\nimport { janusTemplates } from './templates';\nimport type {\n\tJanusMailLinks,\n\tJanusMailTemplateName,\n\tJanusMailTemplates,\n} from './types';\n\n/** The templates in the order the documentation lists them. */\nexport const TEMPLATE_NAMES: readonly JanusMailTemplateName[] = [\n\t'verifyEmail',\n\t'resetPassword',\n\t'signInCode',\n\t'passwordChanged',\n\t'emailChanged',\n];\n\nconst LINK_NAMES = ['verifyEmail', 'resetPassword', 'secureAccount'] as const;\n\n/** The options once checked, with `string` for the locales: the degenericised mirror. */\nexport interface ResolvedOptions {\n\treadonly mailer: Mailer;\n\treadonly from: Address;\n\treadonly replyTo: Address | undefined;\n\treadonly brand: string;\n\treadonly links: JanusMailLinks;\n\treadonly locales: readonly string[];\n\treadonly fallbackLocale: string;\n\treadonly templates: JanusMailTemplates<string>;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isAddress(value: unknown): value is Address {\n\tif (typeof value === 'string') return value.trim() !== '';\n\treturn (\n\t\tisObject(value) &&\n\t\ttypeof value.name === 'string' &&\n\t\ttypeof value.address === 'string' &&\n\t\tvalue.address.trim() !== ''\n\t);\n}\n\nfunction refuse(message: string): never {\n\tthrow new TypeError(`janusMail: ${message}`);\n}\n\n/** `mailer`, `from`, `replyTo`, `brand` and `links`. */\nfunction checkSending(options: Record<string, unknown>): void {\n\tconst { mailer, from, replyTo, brand, links } = options;\n\tif (!isObject(mailer) || typeof mailer.send !== 'function') {\n\t\trefuse('mailer must be a Mailer — an object with a send function');\n\t}\n\tif (!isAddress(from)) {\n\t\trefuse(\n\t\t\t\"from must be an address, as 'noreply@example.com' or { name, address }\",\n\t\t);\n\t}\n\tif (replyTo !== undefined && !isAddress(replyTo)) {\n\t\trefuse(\n\t\t\t\"replyTo must be an address, as 'support@example.com' or { name, address }\",\n\t\t);\n\t}\n\tif (typeof brand !== 'string' || brand.trim() === '') {\n\t\trefuse(\"brand must be the name the e-mails show, as 'Acme'\");\n\t}\n\tif (!isObject(links)) {\n\t\trefuse(\n\t\t\t'links must be an object, as { verifyEmail, resetPassword, secureAccount }',\n\t\t);\n\t}\n\tfor (const name of LINK_NAMES) {\n\t\tif (typeof links[name] !== 'function') {\n\t\t\trefuse(`links.${name} must be a function`);\n\t\t}\n\t}\n}\n\n/** `locales` and `fallbackLocale`, with their defaults. */\nfunction resolveLocales(options: Record<string, unknown>): {\n\tlocales: readonly string[];\n\tfallbackLocale: string;\n} {\n\tconst wanted = options.locales ?? LOCALES;\n\tif (\n\t\t!Array.isArray(wanted) ||\n\t\twanted.length === 0 ||\n\t\t!wanted.every((locale) => typeof locale === 'string' && locale !== '')\n\t) {\n\t\trefuse(\"locales must list at least one locale, as ['en', 'fr']\");\n\t}\n\tconst locales: readonly string[] = Object.freeze([...wanted]);\n\tif (new Set(locales).size !== locales.length) {\n\t\trefuse('locales holds the same locale twice');\n\t}\n\tconst fallbackLocale =\n\t\toptions.fallbackLocale ??\n\t\t(locales.includes('en') ? 'en' : (locales[0] as string));\n\tif (typeof fallbackLocale !== 'string' || !locales.includes(fallbackLocale)) {\n\t\trefuse('fallbackLocale must be one of locales');\n\t}\n\treturn { locales, fallbackLocale };\n}\n\n/** `templates` over the defaults — the defaults only while every locale is built. */\nfunction resolveTemplates(\n\toptions: Record<string, unknown>,\n\tlocales: readonly string[],\n): JanusMailTemplates<string> {\n\tconst given = options.templates ?? {};\n\tif (!isObject(given)) {\n\t\trefuse(\n\t\t\t'templates must be an object of functions, as { verifyEmail: (variables) => rendered }',\n\t\t);\n\t}\n\tfor (const [name, template] of Object.entries(given)) {\n\t\tif (!(TEMPLATE_NAMES as readonly string[]).includes(name)) {\n\t\t\trefuse(\n\t\t\t\t`templates has no template ${name} — name one of ${TEMPLATE_NAMES.join(', ')}`,\n\t\t\t);\n\t\t}\n\t\tif (typeof template !== 'function') {\n\t\t\trefuse(`templates.${name} must be a function`);\n\t\t}\n\t}\n\t// Own enumerable keys, the ones the spread below copies. A template on a\n\t// prototype — a class instance's method — or an own property that is not\n\t// enumerable would be dropped without a word, the default sent in its\n\t// place: `in` sees it and `passed` does not, so it is refused instead.\n\tconst passed = Object.keys(given);\n\tfor (const name of TEMPLATE_NAMES) {\n\t\tif (name in given && !passed.includes(name)) {\n\t\t\trefuse(\n\t\t\t\t`templates.${name} is not an own enumerable property — pass a plain object, as { ${name}: (variables) => rendered }`,\n\t\t\t);\n\t\t}\n\t}\n\tconst built = locales.every((locale) =>\n\t\t(LOCALES as readonly string[]).includes(locale),\n\t);\n\tconst missing = TEMPLATE_NAMES.filter((name) => !passed.includes(name));\n\tif (!built && missing.length > 0) {\n\t\trefuse(\n\t\t\t`the default templates are built in ${LOCALES.join(' and ')} only — with another ` +\n\t\t\t\t`locale in locales, pass every template in templates; ${missing.join(', ')} missing`,\n\t\t);\n\t}\n\t// The one cast: a default template takes a built locale only, and the check\n\t// above proves every locale is one whenever a default is kept.\n\tconst defaults = janusTemplates() as unknown as JanusMailTemplates<string>;\n\treturn Object.freeze({ ...defaults, ...given }) as JanusMailTemplates<string>;\n}\n\n/** A frozen copy of a checked address: a later change to the caller's object is not seen. */\nfunction frozenAddress(address: Address): Address {\n\treturn typeof address === 'string'\n\t\t? address\n\t\t: Object.freeze({ name: address.name, address: address.address });\n}\n\n/**\n * A frozen copy of the checked links, each bound to the caller's object — a\n * class instance's methods keep their `this` — so replacing a link after\n * `janusMail()` cannot undo the check.\n */\nfunction frozenLinks(links: JanusMailLinks): JanusMailLinks {\n\treturn Object.freeze({\n\t\tverifyEmail: links.verifyEmail.bind(links),\n\t\tresetPassword: links.resetPassword.bind(links),\n\t\tsecureAccount: links.secureAccount.bind(links),\n\t});\n}\n\n/**\n * Checks `janusMail()`'s options and answers them resolved, or throws a\n * `TypeError`. What was checked is copied and frozen, so the guarantee made\n * here holds at every send.\n */\nexport function resolveOptions(options: unknown): ResolvedOptions {\n\tif (!isObject(options)) {\n\t\trefuse('options must be an object, as { mailer, from, brand, links }');\n\t}\n\tcheckSending(options);\n\tconst { locales, fallbackLocale } = resolveLocales(options);\n\treturn Object.freeze({\n\t\tmailer: options.mailer as Mailer,\n\t\tfrom: frozenAddress(options.from as Address),\n\t\treplyTo:\n\t\t\toptions.replyTo === undefined\n\t\t\t\t? undefined\n\t\t\t\t: frozenAddress(options.replyTo as Address),\n\t\tbrand: options.brand as string,\n\t\tlinks: frozenLinks(options.links as unknown as JanusMailLinks),\n\t\tlocales,\n\t\tfallbackLocale,\n\t\ttemplates: resolveTemplates(options, locales),\n\t});\n}\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "B2A314C222070DD464756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|