@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.
Files changed (43) hide show
  1. package/README.md +56 -15
  2. package/dist/expiring-mails.d.ts +7 -0
  3. package/dist/expiring-mails.d.ts.map +1 -0
  4. package/dist/expiry.d.ts +29 -0
  5. package/dist/expiry.d.ts.map +1 -0
  6. package/dist/generated/mail.d.ts +3 -0
  7. package/dist/generated/mail.d.ts.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +152 -84
  11. package/dist/index.js.map +10 -6
  12. package/dist/janus-mail.d.ts +6 -0
  13. package/dist/janus-mail.d.ts.map +1 -1
  14. package/dist/notice-mails.d.ts +12 -0
  15. package/dist/notice-mails.d.ts.map +1 -0
  16. package/dist/options.d.ts +2 -0
  17. package/dist/options.d.ts.map +1 -1
  18. package/dist/send.d.ts +27 -0
  19. package/dist/send.d.ts.map +1 -0
  20. package/dist/templates.d.ts.map +1 -1
  21. package/dist/types.d.ts +36 -7
  22. package/dist/types.d.ts.map +1 -1
  23. package/docs/README.md +2 -1
  24. package/docs/guide/building.md +1 -1
  25. package/docs/guide/locales.md +6 -4
  26. package/docs/guide/sending.md +65 -11
  27. package/docs/guide/templates.md +16 -14
  28. package/docs/roadmap.md +9 -2
  29. package/docs/troubleshooting.md +97 -4
  30. package/mails/en/reset-password.html +2 -2
  31. package/mails/en/reset-password.txt +3 -2
  32. package/mails/en/sign-in-code.html +1 -1
  33. package/mails/en/sign-in-code.txt +2 -0
  34. package/mails/en/verify-email.html +2 -2
  35. package/mails/en/verify-email.txt +3 -2
  36. package/mails/fr/reset-password.html +2 -2
  37. package/mails/fr/reset-password.txt +3 -2
  38. package/mails/fr/sign-in-code.html +1 -1
  39. package/mails/fr/sign-in-code.txt +2 -0
  40. package/mails/fr/verify-email.html +2 -2
  41. package/mails/fr/verify-email.txt +3 -2
  42. package/mails/mail-manifest.json +4 -1
  43. package/package.json +6 -6
package/dist/index.js.map CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/janus-mail.ts", "../src/generated/locales.ts", "../src/mails.ts", "../src/templates.ts", "../src/options.ts"],
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 * `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",
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', { 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"
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": "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",
12
- "debugId": "B2A314C222070DD464756E2164756E21",
15
+ "mappings": "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",
16
+ "debugId": "941D325BF4DF9C0164756E2164756E21",
13
17
  "names": []
14
18
  }
@@ -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:
@@ -1 +1 @@
1
- {"version":3,"file":"janus-mail.d.ts","sourceRoot":"","sources":["../src/janus-mail.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,SAAS,EACT,eAAe,EACf,gBAAgB,EAGhB,MAAM,SAAS,CAAC;AAqJjB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EACjE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC1B,SAAS,CAAC,CAAC,CAAC,CASd"}
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
@@ -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;CAC/C;AAkJD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAmBhE"}
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"}
@@ -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,CA6BnD"}
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
- /** What `auth.verifyEmail.send(user)` answered, to `issued.email`. */
123
- verifyEmail(issued: IssuedToken, to: Recipient): Promise<SentMail>;
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 `email` only: the challenge never reaches the e-mail.
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'>): Promise<SentMail>;
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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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, each method, where each e-mail goes, what a failure looks like, and a test |
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/` |
@@ -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.5.0 read format 1, and a manifest without the
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
@@ -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 — a renderer over a Maizzle build of your own, with
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`:
@@ -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.email`, **and nothing else**: the
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);