@nxgt/janus-mail 0.1.0 → 0.2.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/README.md +56 -15
- package/dist/expiring-mails.d.ts +7 -0
- package/dist/expiring-mails.d.ts.map +1 -0
- package/dist/expiry.d.ts +29 -0
- package/dist/expiry.d.ts.map +1 -0
- package/dist/generated/mail.d.ts +3 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +152 -84
- package/dist/index.js.map +10 -6
- package/dist/janus-mail.d.ts +6 -0
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/notice-mails.d.ts +12 -0
- package/dist/notice-mails.d.ts.map +1 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/send.d.ts +27 -0
- package/dist/send.d.ts.map +1 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +36 -7
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +2 -1
- package/docs/guide/building.md +1 -1
- package/docs/guide/locales.md +6 -4
- package/docs/guide/sending.md +65 -11
- package/docs/guide/templates.md +16 -14
- package/docs/roadmap.md +9 -2
- package/docs/troubleshooting.md +97 -4
- package/mails/en/reset-password.html +2 -2
- package/mails/en/reset-password.txt +3 -2
- package/mails/en/sign-in-code.html +1 -1
- package/mails/en/sign-in-code.txt +2 -0
- package/mails/en/verify-email.html +2 -2
- package/mails/en/verify-email.txt +3 -2
- package/mails/fr/reset-password.html +2 -2
- package/mails/fr/reset-password.txt +3 -2
- package/mails/fr/sign-in-code.html +1 -1
- package/mails/fr/sign-in-code.txt +2 -0
- package/mails/fr/verify-email.html +2 -2
- package/mails/fr/verify-email.txt +3 -2
- package/mails/mail-manifest.json +4 -1
- package/package.json +6 -6
package/dist/index.js.map
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
3
|
+
"sources": ["../src/expiry.ts", "../src/send.ts", "../src/expiring-mails.ts", "../src/notice-mails.ts", "../src/generated/locales.ts", "../src/mails.ts", "../src/templates.ts", "../src/options.ts", "../src/janus-mail.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `
|
|
5
|
+
"/**\n * `expiresIn`: how long the link or the code of an e-mail stays valid, as\n * the text the e-mail shows — \"1 hour\", \"1 heure\". Derived from the\n * `expiresAt` the flow answered and the locale the recipient gets, unless the\n * send passes its own.\n */\nimport type { Clock } from '@nxgt/janus';\n\nconst MINUTE_MS = 60_000;\nconst MINUTES_IN_HOUR = 60;\nconst MINUTES_IN_DAY = 24 * MINUTES_IN_HOUR;\n\n/**\n * A duration in the locale, in the largest unit it holds at least once:\n * rounded to the minute first — the flow set `expiresAt` a moment before the\n * send, so an hour is 59 minutes and 59 seconds by then — and then down to a\n * whole unit: 90 minutes is \"1 hour\", 36 hours \"1 day\". At 30 seconds or\n * more, the e-mail never promises more than half a minute beyond what is\n * left; under that, it says \"1 minute\".\n *\n * ```ts\n * formatExpiry(3_600_000, 'fr'); // '1 heure'\n * ```\n */\nexport function formatExpiry(ms: number, locale: string): string {\n\tconst minutes = Math.max(1, Math.round(ms / MINUTE_MS));\n\tconst [value, unit] =\n\t\tminutes >= MINUTES_IN_DAY\n\t\t\t? [Math.floor(minutes / MINUTES_IN_DAY), 'day']\n\t\t\t: minutes >= MINUTES_IN_HOUR\n\t\t\t\t? [Math.floor(minutes / MINUTES_IN_HOUR), 'hour']\n\t\t\t\t: [minutes, 'minute'];\n\treturn new Intl.NumberFormat(locale, {\n\t\tstyle: 'unit',\n\t\tunit,\n\t\tunitDisplay: 'long',\n\t}).format(value);\n}\n\n/** The per-send `expiresIn`, when one was passed, or `undefined`; a `TypeError` when it is not a string. */\nfunction passedExpiresIn(method: string, options: unknown): string | undefined {\n\tconst read =\n\t\ttypeof options === 'object' && options !== null\n\t\t\t? (options as { expiresIn?: unknown }).expiresIn\n\t\t\t: undefined;\n\tif (read !== undefined && typeof read !== 'string') {\n\t\tthrow new TypeError(`janusMail.${method}: expiresIn must be a string`);\n\t}\n\treturn read;\n}\n\n/**\n * The `expiresIn` of one send: the one passed, as is, or the time left\n * until the flow's `expiresAt` by `clock` — the one given to `janus()`, when\n * it was given to `janusMail()` too — in `locale`. A `TypeError` naming the method\n * when `expiresAt` is not a valid `Date` — a flow's answer that went through\n * JSON holds a string there — or is already past.\n */\nexport function expiresInFor(\n\tmethod: string,\n\tissued: unknown,\n\tlocale: string,\n\toptions: unknown,\n\tclock: Clock,\n): string {\n\tconst passed = passedExpiresIn(method, options);\n\tif (passed !== undefined) return passed;\n\tconst expiresAt =\n\t\ttypeof issued === 'object' && issued !== null\n\t\t\t? (issued as { expiresAt?: unknown }).expiresAt\n\t\t\t: undefined;\n\tif (!(expiresAt instanceof Date) || Number.isNaN(expiresAt.getTime())) {\n\t\tthrow new TypeError(`janusMail.${method}: expiresAt must be a Date`);\n\t}\n\tconst now = clock.now();\n\tif (!(now instanceof Date) || Number.isNaN(now.getTime())) {\n\t\tthrow new TypeError(`janusMail.${method}: clock.now() must answer a Date`);\n\t}\n\tconst left = expiresAt.getTime() - now.getTime();\n\tif (left <= 0) {\n\t\tthrow new TypeError(\n\t\t\t`janusMail.${method}: expiresAt is past — the link or code would not work`,\n\t\t);\n\t}\n\treturn formatExpiry(left, locale);\n}\n",
|
|
6
|
+
"/**\n * What every method shares: reading an argument's fields, a link, the\n * recipient's locale, and the send itself. A wrong argument is a `TypeError`\n * naming the method and the field — never its value.\n */\nimport type { Address, Rendered, SentMail, WantedLocales } from '@nxgt/mail';\nimport { pickLocale } from '@nxgt/mail';\nimport type { ResolvedOptions } from './options';\nimport type { JanusMailTemplateName, JanusMailVariables } from './types';\n\n/** A string field of an argument, or a `TypeError` naming the method and the field. */\nexport function 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 */\nexport function 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. */\nexport function 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 */\nexport async 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",
|
|
7
|
+
"/**\n * The three e-mails that carry what a flow issued — a one-time link or a\n * sign-in code — and say how long it lasts: `verifyEmail`, `resetPassword`\n * and `signInCode`.\n */\nimport { expiresInFor } from './expiry';\nimport type { ResolvedOptions } from './options';\nimport { field, link, localeFor, send } from './send';\nimport type { JanusMail } from './types';\n\ntype ExpiringMails = Pick<\n\tJanusMail<string>,\n\t'verifyEmail' | 'resetPassword' | 'signInCode'\n>;\n\n/** The two e-mails of a one-time token, which link to your page with it. */\ntype LinkMailName = 'verifyEmail' | 'resetPassword';\n\n/** One e-mail of a one-time token: to `issued.email`, its link built from `issued.token`. */\nfunction linkMail(options: ResolvedOptions, name: LinkMailName) {\n\treturn async (issued: unknown, to: unknown, sendOptions?: unknown) => {\n\t\tconst token = field(name, issued, 'token');\n\t\tconst locale = localeFor(options, to);\n\t\treturn send(options, name, field(name, issued, 'email'), {\n\t\t\tbrand: options.brand,\n\t\t\tname: field(name, to, 'name'),\n\t\t\tlink: link(name, options.links[name](token), `${name}(token)`),\n\t\t\texpiresIn: expiresInFor(name, issued, locale, sendOptions, options.clock),\n\t\t\tlocale,\n\t\t});\n\t};\n}\n\n/** `verifyEmail`, `resetPassword` and `signInCode`, over the resolved options. */\nexport function expiringMails(options: ResolvedOptions): ExpiringMails {\n\treturn {\n\t\tverifyEmail: linkMail(options, 'verifyEmail'),\n\t\tresetPassword: linkMail(options, 'resetPassword'),\n\t\tasync signInCode(issued, to, sendOptions) {\n\t\t\t// `code`, `email` and `expiresAt` only: the challenge is the visitor's secret.\n\t\t\tconst locale = localeFor(options, to);\n\t\t\treturn send(options, 'signInCode', field('signInCode', issued, 'email'), {\n\t\t\t\tbrand: options.brand,\n\t\t\t\tcode: field('signInCode', issued, 'code'),\n\t\t\t\texpiresIn: expiresInFor(\n\t\t\t\t\t'signInCode',\n\t\t\t\t\tissued,\n\t\t\t\t\tlocale,\n\t\t\t\t\tsendOptions,\n\t\t\t\t\toptions.clock,\n\t\t\t\t),\n\t\t\t\tlocale,\n\t\t\t});\n\t\t},\n\t};\n}\n",
|
|
8
|
+
"/**\n * The two notices of a change the user made — or someone else did:\n * `passwordChanged` and `emailChanged`. Each links to where the account is\n * secured.\n */\nimport type { ResolvedOptions } from './options';\nimport { field, link, localeFor, send } from './send';\nimport type { JanusMail } from './types';\n\ntype NoticeMails = Pick<JanusMail<string>, 'passwordChanged' | 'emailChanged'>;\n\n/** `passwordChanged` and `emailChanged`, over the resolved options. */\nexport function noticeMails(options: ResolvedOptions): NoticeMails {\n\tconst { brand, links } = options;\n\treturn {\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",
|
|
6
9
|
"// 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
10
|
"/**\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'
|
|
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"
|
|
11
|
+
"/**\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, expiresIn, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'verify-email',\n\t\t\t\t{ brand, name, link, expiresIn },\n\t\t\t\t{ locale },\n\t\t\t),\n\t\tresetPassword: ({ brand, name, link, expiresIn, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'reset-password',\n\t\t\t\t{ brand, name, link, expiresIn },\n\t\t\t\t{ locale },\n\t\t\t),\n\t\tsignInCode: ({ brand, code, expiresIn, locale }): Rendered =>\n\t\t\tdefaultRenderer().render(\n\t\t\t\t'sign-in-code',\n\t\t\t\t{ brand, code, expiresIn },\n\t\t\t\t{ locale },\n\t\t\t),\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",
|
|
12
|
+
"/**\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 { Clock } from '@nxgt/janus';\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\treadonly clock: Clock;\n}\n\n/** The system clock, written here: `@nxgt/janus` is a peer for its types only. */\nconst SYSTEM_CLOCK: Clock = Object.freeze({ now: () => new Date() });\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/** `clock`, bound to the caller's object, or the system clock. */\nfunction resolveClock(clock: unknown): Clock {\n\tif (clock === undefined) return SYSTEM_CLOCK;\n\tif (!isObject(clock) || typeof clock.now !== 'function') {\n\t\trefuse('clock must be a Clock — an object with a now function');\n\t}\n\treturn Object.freeze({ now: (clock.now as () => Date).bind(clock) });\n}\n\n/** Whether `Intl` takes the locale — the expiry is formatted in it — as `fr-CA`, not `fr_CA`. */\nfunction isLanguageTag(locale: string): boolean {\n\ttry {\n\t\tIntl.getCanonicalLocales(locale);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\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 (!locales.every(isLanguageTag)) {\n\t\trefuse(\"locales must be BCP 47 language tags, with hyphens, as 'fr-CA'\");\n\t}\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\tclock: resolveClock(options.clock),\n\t});\n}\n",
|
|
13
|
+
"/**\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 */\n\nimport { expiringMails } from './expiring-mails';\nimport { noticeMails } from './notice-mails';\nimport { resolveOptions } from './options';\nimport type { JanusMail, JanusMailLocale, JanusMailOptions } from './types';\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...expiringMails(resolved),\n\t\t...noticeMails(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"
|
|
10
14
|
],
|
|
11
|
-
"mappings": ";
|
|
12
|
-
"debugId": "
|
|
15
|
+
"mappings": "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",
|
|
16
|
+
"debugId": "941D325BF4DF9C0164756E2164756E21",
|
|
13
17
|
"names": []
|
|
14
18
|
}
|
package/dist/janus-mail.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `janusMail()`: the e-mails of Janus's flows, rendered by the templates and
|
|
3
|
+
* handed to the mailer. It sends what a flow answered, to the address the
|
|
4
|
+
* flow answered, and nothing else: no retry, no queue, no error of its own —
|
|
5
|
+
* the mailer's `MailFailure` and `MailRefused` reach the caller untouched.
|
|
6
|
+
*/
|
|
1
7
|
import type { JanusMail, JanusMailLocale, JanusMailOptions } from './types';
|
|
2
8
|
/**
|
|
3
9
|
* The e-mails of Janus's flows, over your mailer:
|
package/dist/janus-mail.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"janus-mail.d.ts","sourceRoot":"","sources":["../src/janus-mail.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"janus-mail.d.ts","sourceRoot":"","sources":["../src/janus-mail.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EACjE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC1B,SAAS,CAAC,CAAC,CAAC,CAUd"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two notices of a change the user made — or someone else did:
|
|
3
|
+
* `passwordChanged` and `emailChanged`. Each links to where the account is
|
|
4
|
+
* secured.
|
|
5
|
+
*/
|
|
6
|
+
import type { ResolvedOptions } from './options';
|
|
7
|
+
import type { JanusMail } from './types';
|
|
8
|
+
type NoticeMails = Pick<JanusMail<string>, 'passwordChanged' | 'emailChanged'>;
|
|
9
|
+
/** `passwordChanged` and `emailChanged`, over the resolved options. */
|
|
10
|
+
export declare function noticeMails(options: ResolvedOptions): NoticeMails;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=notice-mails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notice-mails.d.ts","sourceRoot":"","sources":["../src/notice-mails.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,KAAK,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,cAAc,CAAC,CAAC;AAE/E,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,CAoCjE"}
|
package/dist/options.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* wiring mistake, so a bare `TypeError` naming the option — never its value —
|
|
4
4
|
* thrown before anything is sent.
|
|
5
5
|
*/
|
|
6
|
+
import type { Clock } from '@nxgt/janus';
|
|
6
7
|
import type { Address, Mailer } from '@nxgt/mail';
|
|
7
8
|
import type { JanusMailLinks, JanusMailTemplateName, JanusMailTemplates } from './types';
|
|
8
9
|
/** The templates in the order the documentation lists them. */
|
|
@@ -17,6 +18,7 @@ export interface ResolvedOptions {
|
|
|
17
18
|
readonly locales: readonly string[];
|
|
18
19
|
readonly fallbackLocale: string;
|
|
19
20
|
readonly templates: JanusMailTemplates<string>;
|
|
21
|
+
readonly clock: Clock;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Checks `janusMail()`'s options and answers them resolved, or throws a
|
package/dist/options.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGlD,OAAO,KAAK,EACX,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,MAAM,SAAS,CAAC;AAEjB,+DAA+D;AAC/D,eAAO,MAAM,cAAc,EAAE,SAAS,qBAAqB,EAM1D,CAAC;AAIF,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGlD,OAAO,KAAK,EACX,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,MAAM,SAAS,CAAC;AAEjB,+DAA+D;AAC/D,eAAO,MAAM,cAAc,EAAE,SAAS,qBAAqB,EAM1D,CAAC;AAIF,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACtB;AA2KD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAoBhE"}
|
package/dist/send.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every method shares: reading an argument's fields, a link, the
|
|
3
|
+
* recipient's locale, and the send itself. A wrong argument is a `TypeError`
|
|
4
|
+
* naming the method and the field — never its value.
|
|
5
|
+
*/
|
|
6
|
+
import type { SentMail } from '@nxgt/mail';
|
|
7
|
+
import type { ResolvedOptions } from './options';
|
|
8
|
+
import type { JanusMailTemplateName, JanusMailVariables } from './types';
|
|
9
|
+
/** A string field of an argument, or a `TypeError` naming the method and the field. */
|
|
10
|
+
export declare function field(method: string, value: unknown, key: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* What a `links` function answered, or a `TypeError` naming the method and
|
|
13
|
+
* the call. Its scheme is the renderer's to check: `http(s)` and `mailto:`
|
|
14
|
+
* pass, anything else is its `MailRefused`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function link(method: string, value: unknown, call: string): string;
|
|
17
|
+
/** The locale the recipient gets: the first they want that is sent in, else the fallback. */
|
|
18
|
+
export declare function localeFor(options: ResolvedOptions, to: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Renders one e-mail and hands it to the mailer. Only `subject`, `html` and
|
|
21
|
+
* `text` are read from what the template answered: a template cannot add a
|
|
22
|
+
* recipient or a header.
|
|
23
|
+
*/
|
|
24
|
+
export declare function send<K extends JanusMailTemplateName>(options: ResolvedOptions, name: K, address: string, variables: JanusMailVariables[K] & {
|
|
25
|
+
readonly locale: string;
|
|
26
|
+
}): Promise<SentMail>;
|
|
27
|
+
//# sourceMappingURL=send.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAqB,QAAQ,EAAiB,MAAM,YAAY,CAAC;AAE7E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEzE,uFAAuF;AACvF,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CASzE;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAOzE;AAED,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,OAAO,GAAG,MAAM,CAMvE;AAED;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,CAAC,SAAS,qBAAqB,EACzD,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5D,OAAO,CAAC,QAAQ,CAAC,CAenB"}
|
package/dist/templates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,IAAI,kBAAkB,
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,IAAI,kBAAkB,CAiCnD"}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
* implementation works on a degenericised mirror (`string` locales) and casts
|
|
4
4
|
* once, in `janusMail()`.
|
|
5
5
|
*/
|
|
6
|
-
import type { IssuedCode, IssuedToken } from '@nxgt/janus';
|
|
6
|
+
import type { Clock, IssuedCode, IssuedToken } from '@nxgt/janus';
|
|
7
7
|
import type { Address, Mailer, Rendered, SentMail, WantedLocales } from '@nxgt/mail';
|
|
8
8
|
import type { JanusMailLocale } from './generated/locales';
|
|
9
9
|
export type { JanusMailLocale } from './generated/locales';
|
|
10
10
|
/**
|
|
11
11
|
* What each e-mail's template is given, by template name. Every value is
|
|
12
12
|
* text: `brand` is the name `janusMail({ brand })` was given, `name` the
|
|
13
|
-
* recipient's, `link` an absolute URL from `links
|
|
13
|
+
* recipient's, `link` an absolute URL from `links`, and `expiresIn` how long
|
|
14
|
+
* the link or the code stays valid, in the recipient's locale — "1 hour",
|
|
15
|
+
* "1 heure" — or the send's own `expiresIn`.
|
|
14
16
|
*
|
|
15
17
|
* Held equal to the build's variables by `test/types/variables.ts`: an e-mail
|
|
16
18
|
* of the build that gains or loses a variable fails the typecheck there.
|
|
@@ -20,16 +22,19 @@ export interface JanusMailVariables {
|
|
|
20
22
|
readonly brand: string;
|
|
21
23
|
readonly name: string;
|
|
22
24
|
readonly link: string;
|
|
25
|
+
readonly expiresIn: string;
|
|
23
26
|
};
|
|
24
27
|
readonly resetPassword: {
|
|
25
28
|
readonly brand: string;
|
|
26
29
|
readonly name: string;
|
|
27
30
|
readonly link: string;
|
|
31
|
+
readonly expiresIn: string;
|
|
28
32
|
};
|
|
29
33
|
/** No `challenge`, ever: it is the visitor's secret, and never goes in an e-mail. */
|
|
30
34
|
readonly signInCode: {
|
|
31
35
|
readonly brand: string;
|
|
32
36
|
readonly code: string;
|
|
37
|
+
readonly expiresIn: string;
|
|
33
38
|
};
|
|
34
39
|
readonly passwordChanged: {
|
|
35
40
|
readonly brand: string;
|
|
@@ -102,6 +107,25 @@ interface JanusMailBaseOptions<L extends string> {
|
|
|
102
107
|
readonly locales?: readonly L[];
|
|
103
108
|
/** The locale when the recipient wants none of `locales`. Default `en`, else the first of `locales`. */
|
|
104
109
|
readonly fallbackLocale?: NoInfer<L>;
|
|
110
|
+
/**
|
|
111
|
+
* What the time left until a flow's `expiresAt` is measured against: pass
|
|
112
|
+
* the clock given to `janus({ clock })` — a `fixedClock` in tests. Default
|
|
113
|
+
* the system clock.
|
|
114
|
+
*/
|
|
115
|
+
readonly clock?: Clock;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* What one send of `verifyEmail`, `resetPassword` or `signInCode` may pass
|
|
119
|
+
* besides the flow's answer and the recipient.
|
|
120
|
+
*/
|
|
121
|
+
export interface JanusMailSendOptions {
|
|
122
|
+
/**
|
|
123
|
+
* How long the link or the code stays valid, as the e-mail shows it —
|
|
124
|
+
* plain text, already in the recipient's language: `'24 heures'`. Default:
|
|
125
|
+
* the time left until the flow's `expiresAt`, in the largest whole unit,
|
|
126
|
+
* in the recipient's locale.
|
|
127
|
+
*/
|
|
128
|
+
readonly expiresIn?: string;
|
|
105
129
|
}
|
|
106
130
|
/**
|
|
107
131
|
* `janusMail()`'s options. `templates` replaces any of the defaults — and
|
|
@@ -119,15 +143,20 @@ export type JanusMailOptions<L extends string = JanusMailLocale> = JanusMailBase
|
|
|
119
143
|
* rejects with the mailer's `MailFailure` or `MailRefused`, untouched.
|
|
120
144
|
*/
|
|
121
145
|
export interface JanusMail<L extends string = JanusMailLocale> {
|
|
122
|
-
/**
|
|
123
|
-
|
|
146
|
+
/**
|
|
147
|
+
* What `auth.verifyEmail.send(user)` answered, to `issued.email`, saying
|
|
148
|
+
* how long the link stays valid — from `issued.expiresAt`, unless
|
|
149
|
+
* `options.expiresIn` says it.
|
|
150
|
+
*/
|
|
151
|
+
verifyEmail(issued: IssuedToken, to: Recipient, options?: JanusMailSendOptions): Promise<SentMail>;
|
|
124
152
|
/** What `auth.resetPassword.request(email)` answered, once checked for `null`, to `issued.email`. */
|
|
125
|
-
resetPassword(issued: IssuedToken, to: Recipient): Promise<SentMail>;
|
|
153
|
+
resetPassword(issued: IssuedToken, to: Recipient, options?: JanusMailSendOptions): Promise<SentMail>;
|
|
126
154
|
/**
|
|
127
155
|
* The code `auth.signInCode.request(email)` answered, to `issued.email`.
|
|
128
|
-
* Reads `code` and `
|
|
156
|
+
* Reads `code`, `email` and `expiresAt` only: the challenge never reaches
|
|
157
|
+
* the e-mail.
|
|
129
158
|
*/
|
|
130
|
-
signInCode(issued: Pick<IssuedCode<unknown>, 'code' | 'email'>, to?: Pick<Recipient, 'locale'
|
|
159
|
+
signInCode(issued: Pick<IssuedCode<unknown>, 'code' | 'email' | 'expiresAt'>, to?: Pick<Recipient, 'locale'>, options?: JanusMailSendOptions): Promise<SentMail>;
|
|
131
160
|
/** Tells `to.email` their password was changed, with a link to secure the account. */
|
|
132
161
|
passwordChanged(to: Recipient & {
|
|
133
162
|
readonly email: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/docs/README.md
CHANGED
|
@@ -5,7 +5,7 @@ the detail.
|
|
|
5
5
|
|
|
6
6
|
| Page | Read it when |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options
|
|
8
|
+
| [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options — `clock` among them — each method, the expiry an e-mail states and how to set it, where each e-mail goes, what a failure looks like, and a test |
|
|
9
9
|
| [Templates](guide/templates.md) | You want an e-mail of your own: what a template takes and answers, a partial override, `janusTemplates()`, and what the defaults say |
|
|
10
10
|
| [Locales](guide/locales.md) | You send in more than one language: how the locale is picked, `fallbackLocale`, fewer locales, and adding one |
|
|
11
11
|
| [Building](guide/building.md) | You work on this package: the Maizzle project in `mail/`, `mails/`, the generated files, and why none of it reaches a consumer's build |
|
|
@@ -30,5 +30,6 @@ They mean the same here. These pages add:
|
|
|
30
30
|
| **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send` or `resetPassword.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
|
|
31
31
|
| **recipient** | Who an e-mail is for besides the address: a `Recipient`, `{ name, locale? }` | "user" — a user is a record of `@nxgt/janus`; "to" is the address |
|
|
32
32
|
| **notice** | An e-mail that tells, and asks for nothing: `passwordChanged`, `emailChanged`. It links to `links.secureAccount()` | "alert", "notification" |
|
|
33
|
+
| **expiry** | How long a link or a code lasts, as the e-mail says it: `expiresIn`, derived from the flow's `expiresAt` in the recipient's locale — "1 hour", "1 heure" — or passed per send | "TTL", "lifetime" — the `tokens` TTL is `@nxgt/janus`'s setting, which sets `expiresAt` |
|
|
33
34
|
| **brand** | The name the e-mails show, `janusMail({ brand })`: text, filled at send time | "logo", "company" |
|
|
34
35
|
| **the build** | The e-mails as `scripts/build-mail.ts` writes them to `mails/`: HTML and text per locale, and the manifest | "dist" — `mails/` is not `dist/` |
|
package/docs/guide/building.md
CHANGED
|
@@ -56,7 +56,7 @@ The package peers `@nxgt/mail` at `>=0.1.0 <1`, so the build it ships must
|
|
|
56
56
|
be readable by `@nxgt/mail` 0.1.0. The manifest says which format it is in —
|
|
57
57
|
`formatVersion`, its first key, `MANIFEST_FORMAT` of the `@nxgt/mail-i18n`
|
|
58
58
|
that built it — and within 0.x a renderer reads every format up to its own.
|
|
59
|
-
`@nxgt/mail` 0.1.0 through 0.
|
|
59
|
+
`@nxgt/mail` 0.1.0 through 0.6.0 read format 1, and a manifest without the
|
|
60
60
|
field is format 1.
|
|
61
61
|
|
|
62
62
|
`formatProblem` in `scripts/build-mail.ts` (spec'd beside it) fails the build
|
package/docs/guide/locales.md
CHANGED
|
@@ -74,9 +74,9 @@ import { janusMail, type JanusMailTemplates } from '@nxgt/janus-mail';
|
|
|
74
74
|
type Locale = 'en' | 'fr' | 'de';
|
|
75
75
|
|
|
76
76
|
const templates: JanusMailTemplates<Locale> = {
|
|
77
|
-
verifyEmail: ({ name, link, locale }) => render('verify-email', locale, { name, link }),
|
|
78
|
-
resetPassword: ({ name, link, locale }) => render('reset-password', locale, { name, link }),
|
|
79
|
-
signInCode: ({ code, locale }) => render('sign-in-code', locale, { code }),
|
|
77
|
+
verifyEmail: ({ name, link, expiresIn, locale }) => render('verify-email', locale, { name, link, expiresIn }),
|
|
78
|
+
resetPassword: ({ name, link, expiresIn, locale }) => render('reset-password', locale, { name, link, expiresIn }),
|
|
79
|
+
signInCode: ({ code, expiresIn, locale }) => render('sign-in-code', locale, { code, expiresIn }),
|
|
80
80
|
passwordChanged: ({ name, link, locale }) => render('password-changed', locale, { name, link }),
|
|
81
81
|
emailChanged: ({ name, link, newEmail, locale }) => render('email-changed', locale, { name, link, newEmail }),
|
|
82
82
|
};
|
|
@@ -85,7 +85,9 @@ const mail = janusMail({ mailer, from, brand: 'Acme', links, locales: ['en', 'fr
|
|
|
85
85
|
mail.locales; // readonly ('en' | 'fr' | 'de')[]
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
`render` here is yours
|
|
88
|
+
`render` here is yours; `expiresIn` is the expiry, already formatted in
|
|
89
|
+
`locale` — "1 Stunde" for `de`, where the runtime's `Intl` has German. A
|
|
90
|
+
renderer over a Maizzle build of your own, with
|
|
89
91
|
`@nxgt/mail-presets` and a `locales/de.json`, is the closest to the defaults.
|
|
90
92
|
With only some templates, the call does not compile — the error lands on
|
|
91
93
|
`templates`:
|
package/docs/guide/sending.md
CHANGED
|
@@ -48,6 +48,7 @@ time one is sent, and kept.
|
|
|
48
48
|
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
49
49
|
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
50
50
|
| `templates` | `Partial<JanusMailTemplates<L>>` | none | Your own templates, over the defaults — see [Templates](templates.md) |
|
|
51
|
+
| `clock` | `Clock` from `@nxgt/janus` | the system clock | What the time left until a flow's `expiresAt` is measured against — pass the one `janus({ clock })` was given; see [The expiry](#the-expiry) |
|
|
51
52
|
|
|
52
53
|
Every link is called at send time, and must answer an absolute `http:`,
|
|
53
54
|
`https:` or `mailto:` URL: a default template refuses anything else with
|
|
@@ -72,16 +73,17 @@ option and never its value — every message is in
|
|
|
72
73
|
Each method renders its e-mail with its template, hands it to the mailer,
|
|
73
74
|
and answers the mailer's `SentMail` (`{ messageId }`).
|
|
74
75
|
|
|
75
|
-
### `verifyEmail(issued, to)`
|
|
76
|
+
### `verifyEmail(issued, to, options?)`
|
|
76
77
|
|
|
77
78
|
```ts
|
|
78
79
|
const issued = await auth.verifyEmail.send(user); // IssuedToken: { token, email, expiresAt }
|
|
79
80
|
await mail.verifyEmail(issued, { name: user.name, locale: user.locale });
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
Sent to `issued.email`, with the link `links.verifyEmail(issued.token)
|
|
83
|
+
Sent to `issued.email`, with the link `links.verifyEmail(issued.token)` and
|
|
84
|
+
how long it lasts — see [The expiry](#the-expiry).
|
|
83
85
|
|
|
84
|
-
### `resetPassword(issued, to)`
|
|
86
|
+
### `resetPassword(issued, to, options?)`
|
|
85
87
|
|
|
86
88
|
```ts
|
|
87
89
|
const issued = await auth.resetPassword.request(email); // (IssuedToken & { user }) | null
|
|
@@ -92,16 +94,18 @@ if (issued !== null) {
|
|
|
92
94
|
|
|
93
95
|
`request` answers `null` for an address nobody holds, and the compiler
|
|
94
96
|
refuses `null` here: check it first, and answer the visitor the same either
|
|
95
|
-
way. Sent to `issued.email`, with `links.resetPassword(issued.token)
|
|
97
|
+
way. Sent to `issued.email`, with `links.resetPassword(issued.token)` and how
|
|
98
|
+
long it lasts.
|
|
96
99
|
|
|
97
|
-
### `signInCode(issued, to?)`
|
|
100
|
+
### `signInCode(issued, to?, options?)`
|
|
98
101
|
|
|
99
102
|
```ts
|
|
100
103
|
const issued = await auth.signInCode.request(email); // IssuedCode: { code, challenge, email, expiresAt, user } | null
|
|
101
104
|
if (issued !== null) await mail.signInCode(issued, { locale: issued.user.locale });
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
It reads `issued.code` and `issued.
|
|
107
|
+
It reads `issued.code`, `issued.email` and `issued.expiresAt`, **and nothing
|
|
108
|
+
else**: the
|
|
105
109
|
challenge is the visitor's secret, and neither the default template nor an
|
|
106
110
|
override is ever given it. `to` is optional — the e-mail greets nobody by
|
|
107
111
|
name — and only its `locale` is read.
|
|
@@ -130,6 +134,50 @@ since whoever changed it already controls the new one. It names `newEmail`,
|
|
|
130
134
|
and links to `links.secureAccount()`. Read the former address before the
|
|
131
135
|
update: the user `update` answers already holds the new one.
|
|
132
136
|
|
|
137
|
+
## The expiry
|
|
138
|
+
|
|
139
|
+
`verifyEmail`, `resetPassword` and `signInCode` say how long the link or the
|
|
140
|
+
code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
141
|
+
10 minutes." The text is `expiresIn`, one of the template's variables,
|
|
142
|
+
derived at send time from the flow's `issued.expiresAt`:
|
|
143
|
+
|
|
144
|
+
1. the time left until `expiresAt`, rounded to the minute — the flow set it a
|
|
145
|
+
moment ago, so an hour is still "1 hour";
|
|
146
|
+
2. then **down** to the largest whole unit it holds — days, hours or minutes
|
|
147
|
+
— so, at 30 seconds or more, the e-mail never promises more than half a
|
|
148
|
+
minute beyond what is left; under that, it says "1 minute". 90 minutes is
|
|
149
|
+
"1 hour", 36 hours "1 day";
|
|
150
|
+
3. formatted by `Intl.NumberFormat` with `style: 'unit'` and
|
|
151
|
+
`unitDisplay: 'long'`, in the recipient's locale: "3 hours", "3 heures".
|
|
152
|
+
|
|
153
|
+
With `@nxgt/janus`'s defaults, a verification link says "1 day", a reset
|
|
154
|
+
link "1 hour" and a sign-in code "10 minutes".
|
|
155
|
+
|
|
156
|
+
The time is `janusMail({ clock })`'s — pass the clock `janus({ clock })` was
|
|
157
|
+
given, a `fixedClock` in tests — else the system clock:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { fixedClock } from '@nxgt/janus';
|
|
161
|
+
|
|
162
|
+
const clock = fixedClock(Date.UTC(2026, 0, 1));
|
|
163
|
+
const auth = janus({ /* … */ clock });
|
|
164
|
+
const mail = janusMail({ mailer, from, brand: 'Acme', links, clock });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
To say it yourself — another wording, or a locale the runtime's `Intl` has no
|
|
168
|
+
data for — pass `expiresIn` as the third argument, plain text already in the
|
|
169
|
+
recipient's language. `expiresAt` is then not read:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
await mail.verifyEmail(issued, { name: user.name, locale: 'fr' }, { expiresIn: '24 heures' });
|
|
173
|
+
await mail.signInCode(issued, undefined, { expiresIn: 'ten minutes' });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A send whose `expiresAt` is not a valid `Date` — a flow's answer that went
|
|
177
|
+
through JSON on its way to a queue holds a string — or is already past, is
|
|
178
|
+
a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
179
|
+
`new Date(issued.expiresAt)` when the job runs, or pass `expiresIn`.
|
|
180
|
+
|
|
133
181
|
## Where each e-mail goes
|
|
134
182
|
|
|
135
183
|
| Method | To | Never to |
|
|
@@ -157,7 +205,7 @@ defines no error class and wraps nothing:
|
|
|
157
205
|
| --- | --- | --- | --- |
|
|
158
206
|
| `MailFailure` (`MAIL_FAILED`) | the mailer | The transport could not hand the e-mail over. Nothing is known to have been sent | A `503`, or a retry from a queue |
|
|
159
207
|
| `MailRefused` (`MAIL_REFUSED`) | the renderer, or the mailer | The e-mail itself is wrong: a link that is not `http:`, `https:` or `mailto:`, an address that is not one | A bug to fix; sending it again fails again |
|
|
160
|
-
| `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string` | A bug to fix |
|
|
208
|
+
| `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string`. For [the expiry](#the-expiry): `janusMail.<method>: expiresAt must be a Date`, `janusMail.<method>: expiresAt is past — the link or code would not work`, `janusMail.<method>: expiresIn must be a string`, `janusMail.<method>: clock.now() must answer a Date` | A bug to fix — for a past `expiresAt`, issue a new token or code and send that |
|
|
161
209
|
| `Error` from `createMailRenderer` | the renderer | `mails/` is missing where the package runs: a bundler inlined `@nxgt/janus-mail`, or a deploy kept `dist/` only | Keep `@nxgt/janus-mail` external to your bundle and deploy its `mails/` with it; see [troubleshooting](../troubleshooting.md#createmailrenderer-mailsmail-manifestjson-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder) |
|
|
162
210
|
|
|
163
211
|
`instanceof` holds against the classes of your own `@nxgt/mail`, since it is
|
|
@@ -197,9 +245,14 @@ fail:
|
|
|
197
245
|
|
|
198
246
|
```ts
|
|
199
247
|
import { expect, it } from 'bun:test';
|
|
248
|
+
import { fixedClock } from '@nxgt/janus';
|
|
200
249
|
import { createMemoryMailer, MailFailure } from '@nxgt/mail';
|
|
201
250
|
import { janusMail } from '@nxgt/janus-mail';
|
|
202
251
|
|
|
252
|
+
// The clock given to janus({ clock }) too: the codes it issues expire ten minutes on.
|
|
253
|
+
const clock = fixedClock(Date.UTC(2026, 0, 1, 9));
|
|
254
|
+
const expiresAt = new Date(Date.UTC(2026, 0, 1, 9, 10));
|
|
255
|
+
|
|
203
256
|
const links = {
|
|
204
257
|
verifyEmail: (token: string) => `https://acme.example/verify?token=${token}`,
|
|
205
258
|
resetPassword: (token: string) => `https://acme.example/reset?token=${token}`,
|
|
@@ -208,24 +261,25 @@ const links = {
|
|
|
208
261
|
|
|
209
262
|
it('sends the code, and never the challenge', async () => {
|
|
210
263
|
const mailer = createMemoryMailer();
|
|
211
|
-
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links });
|
|
212
|
-
const issued = { code: '042817', challenge: 'secret', email: 'ada@example.com' };
|
|
264
|
+
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links, clock });
|
|
265
|
+
const issued = { code: '042817', challenge: 'secret', email: 'ada@example.com', expiresAt };
|
|
213
266
|
|
|
214
267
|
await mail.signInCode(issued, { locale: 'fr-CA' });
|
|
215
268
|
|
|
216
269
|
const [sent] = mailer.sent;
|
|
217
270
|
expect(sent?.to).toBe('ada@example.com');
|
|
218
271
|
expect(sent?.subject).toBe('Votre code de connexion : 042817');
|
|
272
|
+
expect(sent?.text).toContain('10 minutes.');
|
|
219
273
|
expect(`${sent?.html}${sent?.text}`).not.toContain('secret');
|
|
220
274
|
});
|
|
221
275
|
|
|
222
276
|
it('says so when the mailer is down', async () => {
|
|
223
277
|
const mailer = createMemoryMailer();
|
|
224
278
|
mailer.failNext();
|
|
225
|
-
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links });
|
|
279
|
+
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links, clock });
|
|
226
280
|
|
|
227
281
|
const error = await mail
|
|
228
|
-
.signInCode({ code: '042817', email: 'ada@example.com' })
|
|
282
|
+
.signInCode({ code: '042817', email: 'ada@example.com', expiresAt })
|
|
229
283
|
.then(() => null, (e: unknown) => e); // settled where it is created
|
|
230
284
|
|
|
231
285
|
expect(error).toBeInstanceOf(MailFailure);
|