@things-factory/notification 10.1.246 → 10.1.247

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.
@@ -0,0 +1,17 @@
1
+ export type AppBrand = {
2
+ name: string;
3
+ tagline?: string;
4
+ /** `#rrggbb`. */
5
+ color: string;
6
+ };
7
+ /** What the notice mail had before an application said its own. */
8
+ export declare const DEFAULT_BRAND: AppBrand;
9
+ /** The brand a manifest declares, each part it leaves out the default's. */
10
+ export declare function brandOf(manifest: any): AppBrand;
11
+ /**
12
+ * The text colour on `color`: white while it keeps 3:1 against it - the band and the button carry large bold text
13
+ * (WCAG 1.4.3) - black below that.
14
+ */
15
+ export declare function textColorOn(color: string): '#000000' | '#ffffff';
16
+ /** The running application's brand; the default when it ships no manifest or one that cannot be read. */
17
+ export declare function appBrand(): AppBrand;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_BRAND = void 0;
4
+ exports.brandOf = brandOf;
5
+ exports.textColorOn = textColorOn;
6
+ exports.appBrand = appBrand;
7
+ const tslib_1 = require("tslib");
8
+ /*
9
+ * Whose mail it is - the application's brand name, tagline and colour, read from its web manifest
10
+ * (`assets/manifest.json`), the one identity file every application ships and the image keeps. `name` is the brand
11
+ * name; `tagline` and `brand_color` are the application's own members (a browser passes over members it does not
12
+ * know); with no `brand_color` the manifest's `theme_color` stands in. Read once.
13
+ */
14
+ const fs_1 = tslib_1.__importDefault(require("fs"));
15
+ const path_1 = tslib_1.__importDefault(require("path"));
16
+ const env_1 = require("@things-factory/env");
17
+ /** What the notice mail had before an application said its own. */
18
+ exports.DEFAULT_BRAND = { name: 'Operato', color: '#22a6a7' };
19
+ const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
20
+ /** A 3-digit colour as its 6 digits; anything not a hex colour is not a colour. */
21
+ function hexOf(value) {
22
+ if (typeof value !== 'string' || !HEX.test(value.trim()))
23
+ return undefined;
24
+ const hex = value.trim().toLowerCase();
25
+ return hex.length === 4 ? `#${[...hex.slice(1)].map(digit => digit + digit).join('')}` : hex;
26
+ }
27
+ const textOf = (value) => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
28
+ /** The brand a manifest declares, each part it leaves out the default's. */
29
+ function brandOf(manifest) {
30
+ return {
31
+ name: textOf(manifest?.name) ?? exports.DEFAULT_BRAND.name,
32
+ tagline: textOf(manifest?.tagline),
33
+ color: hexOf(manifest?.brand_color) ?? hexOf(manifest?.theme_color) ?? exports.DEFAULT_BRAND.color
34
+ };
35
+ }
36
+ /**
37
+ * The text colour on `color`: white while it keeps 3:1 against it - the band and the button carry large bold text
38
+ * (WCAG 1.4.3) - black below that.
39
+ */
40
+ function textColorOn(color) {
41
+ const channel = (at) => {
42
+ const value = parseInt(color.slice(at, at + 2), 16) / 255;
43
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
44
+ };
45
+ const luminance = 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5);
46
+ return 1.05 / (luminance + 0.05) >= 3 ? '#ffffff' : '#000000';
47
+ }
48
+ let read;
49
+ /** The running application's brand; the default when it ships no manifest or one that cannot be read. */
50
+ function appBrand() {
51
+ if (!read) {
52
+ try {
53
+ read = brandOf(JSON.parse(fs_1.default.readFileSync(path_1.default.resolve(env_1.appRootPath, 'assets', 'manifest.json'), 'utf8')));
54
+ }
55
+ catch {
56
+ read = exports.DEFAULT_BRAND;
57
+ }
58
+ }
59
+ return read;
60
+ }
61
+ //# sourceMappingURL=app-brand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-brand.js","sourceRoot":"","sources":["../../../server/controllers/notice/app-brand.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Whose mail it is - the application's brand name, tagline and colour, read from its web manifest\n * (`assets/manifest.json`), the one identity file every application ships and the image keeps. `name` is the brand\n * name; `tagline` and `brand_color` are the application's own members (a browser passes over members it does not\n * know); with no `brand_color` the manifest's `theme_color` stands in. Read once.\n */\nimport fs from 'fs'\nimport path from 'path'\n\nimport { appRootPath } from '@things-factory/env'\n\nexport type AppBrand = {\n name: string\n tagline?: string\n /** `#rrggbb`. */\n color: string\n}\n\n/** What the notice mail had before an application said its own. */\nexport const DEFAULT_BRAND: AppBrand = { name: 'Operato', color: '#22a6a7' }\n\nconst HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i\n\n/** A 3-digit colour as its 6 digits; anything not a hex colour is not a colour. */\nfunction hexOf(value: unknown): string | undefined {\n if (typeof value !== 'string' || !HEX.test(value.trim())) return undefined\n const hex = value.trim().toLowerCase()\n return hex.length === 4 ? `#${[...hex.slice(1)].map(digit => digit + digit).join('')}` : hex\n}\n\nconst textOf = (value: unknown) => (typeof value === 'string' && value.trim() ? value.trim() : undefined)\n\n/** The brand a manifest declares, each part it leaves out the default's. */\nexport function brandOf(manifest: any): AppBrand {\n return {\n name: textOf(manifest?.name) ?? DEFAULT_BRAND.name,\n tagline: textOf(manifest?.tagline),\n color: hexOf(manifest?.brand_color) ?? hexOf(manifest?.theme_color) ?? DEFAULT_BRAND.color\n }\n}\n\n/**\n * The text colour on `color`: white while it keeps 3:1 against it - the band and the button carry large bold text\n * (WCAG 1.4.3) - black below that.\n */\nexport function textColorOn(color: string): '#000000' | '#ffffff' {\n const channel = (at: number) => {\n const value = parseInt(color.slice(at, at + 2), 16) / 255\n return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4\n }\n const luminance = 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5)\n return 1.05 / (luminance + 0.05) >= 3 ? '#ffffff' : '#000000'\n}\n\nlet read: AppBrand | undefined\n\n/** The running application's brand; the default when it ships no manifest or one that cannot be read. */\nexport function appBrand(): AppBrand {\n if (!read) {\n try {\n read = brandOf(JSON.parse(fs.readFileSync(path.resolve(appRootPath, 'assets', 'manifest.json'), 'utf8')))\n } catch {\n read = DEFAULT_BRAND\n }\n }\n return read\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { type NoticeWords } from './notice-channels.js';
2
+ import type { AppBrand } from './app-brand.js';
2
3
  export type NoticeReceiver = {
3
4
  id: string;
4
5
  email?: string | null;
@@ -69,6 +70,8 @@ export type NoticeChannelFunctions = {
69
70
  status?: number;
70
71
  }>;
71
72
  report: (message: string, error?: unknown) => void;
73
+ /** The application whose name, tagline and colour the mail carries; absent, the default. */
74
+ brand?: AppBrand;
72
75
  };
73
76
  /**
74
77
  * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is
@@ -64,7 +64,7 @@ async function deliverNotice(notice, channels) {
64
64
  const sent = await channels.mail({
65
65
  receiver: receiver.email,
66
66
  subject: words.subject,
67
- content: (0, notice_email_js_1.noticeEmailForm)({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice) })
67
+ content: (0, notice_email_js_1.noticeEmailForm)({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })
68
68
  });
69
69
  if (sent) {
70
70
  result.email.sent++;
@@ -1 +1 @@
1
- {"version":3,"file":"deliver-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/deliver-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and\n * sees what each one was given; send-notice.ts hands in the real ones.\n */\nimport { languageOf, noticeChannels, teamsMessage, type NoticeWords } from './notice-channels.js'\nimport { noticeEmailForm } from './notice-email.js'\n\nexport type NoticeReceiver = { id: string; email?: string | null; locale?: string | null }\n\n/**\n * A notice, settled by the module whose event it tells of - who, what and where. The words are asked per language,\n * so each person reads their own.\n */\nexport type Notice = {\n /** What kind of thing it tells of - `approval`, `announcement`. The in-app notice carries it. */\n type: string\n level?: 'info' | 'warn'\n receivers: NoticeReceiver[]\n words: (lng: string) => NoticeWords\n /** The page, relative to the domain - what the in-app notice opens. */\n address: string\n /** The same page as a full address for mail and Teams; absent when no request said where we are. */\n link?: string\n /** The language of a person who has not chosen one. */\n defaultLanguage?: string\n /** Names the notice in the log. */\n label: string\n /** How `words().body` is written. Markdown is rendered in the mail; Teams and push take it as text. */\n format?: 'text' | 'markdown'\n /**\n * The channels this notice may take beyond the app, when its sender chooses per notice (an announcement's\n * \"send by mail too\"). Left out, it takes every channel the domain turned on; a channel the domain has not turned on\n * is never taken.\n */\n channels?: { email?: boolean; teams?: boolean }\n /**\n * Said to everyone in the domain (an announcement to the whole domain). The app, push and mail still go to each\n * person; Teams posts once to the domain's channel webhook, in the default language. Only a broadcast goes to Teams -\n * a notice to some people never lands where everyone reads.\n */\n broadcast?: boolean\n}\n\n/** What went out - mail counted per person, the Teams channel post once. A sender that keeps a record stores this. */\nexport type NoticeResult = {\n receivers: number\n email: { sent: number; failed: number }\n teams: { sent: number; failed: number }\n}\n\nexport type NoticeChannelFunctions = {\n /** The domain's attributes (EnvVar) - read when the notice goes. */\n attributes: () => Promise<Record<string, string | undefined>>\n /** notification's `notify` for the domain; absent, nothing goes in the app. */\n push?: (payload: any) => unknown\n /** email-base's `sendEmail` - answers the transport's receipt, nothing when it failed. */\n mail: (mail: { receiver: string; subject: string; content: string }) => Promise<unknown>\n /** Posts JSON to the Teams webhook and answers whether it took it. */\n teams: (url: string, body: unknown) => Promise<{ ok: boolean; status?: number }>\n report: (message: string, error?: unknown) => void\n}\n\n/** The domain's own address - the notice's full link without its page. */\nfunction baseOf(notice: Notice): string | undefined {\n return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined\n}\n\n/**\n * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is\n * also posted once to the domain's Teams channel. A channel that fails is reported with its name and the others still\n * go - nothing here throws, because what it tells of has already happened.\n */\nexport async function deliverNotice(notice: Notice, channels: NoticeChannelFunctions): Promise<NoticeResult> {\n const result: NoticeResult = { receivers: notice.receivers.length, email: { sent: 0, failed: 0 }, teams: { sent: 0, failed: 0 } }\n if (!notice.receivers.length) return result\n\n let turnedOn: ReturnType<typeof noticeChannels> = { email: false }\n try {\n turnedOn = noticeChannels(await channels.attributes())\n } catch (error) {\n channels.report(`${notice.label}: the domain's notice channels could not be read - in the app only`, error)\n }\n\n const email = turnedOn.email && notice.channels?.email !== false\n const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined\n\n for (const receiver of notice.receivers) {\n const lng = languageOf(receiver.locale, notice.defaultLanguage)\n let words: NoticeWords\n try {\n words = notice.words(lng)\n } catch (error) {\n channels.report(`${notice.label}: the words for ${receiver.id} could not be made`, error)\n if (email && receiver.email) result.email.failed++\n continue\n }\n\n if (channels.push) {\n for (const mode of ['foreground', 'background'] as const) {\n try {\n await channels.push({\n receivers: [receiver.id],\n type: notice.type,\n subject: notice.level || 'info',\n title: words.subject,\n body: words.summary ?? words.body,\n url: notice.address,\n mode\n })\n } catch (error) {\n channels.report(`${notice.label}: the ${mode} notice to ${receiver.id} failed`, error)\n }\n }\n }\n\n if (email && receiver.email) {\n try {\n const sent = await channels.mail({\n receiver: receiver.email,\n subject: words.subject,\n content: noticeEmailForm({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice) })\n })\n if (sent) {\n result.email.sent++\n } else {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} did not go out`)\n }\n } catch (error) {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} failed`, error)\n }\n }\n }\n\n if (teamsChannelWebhook) {\n try {\n const answer = await channels.teams(teamsChannelWebhook, teamsMessage({ words: notice.words(languageOf(undefined, notice.defaultLanguage)), link: notice.link }))\n if (answer.ok) {\n result.teams.sent++\n } else {\n result.teams.failed++\n channels.report(`${notice.label}: Teams refused the channel post (${answer.status ?? '-'})`)\n }\n } catch (error) {\n result.teams.failed++\n channels.report(`${notice.label}: the Teams channel post failed`, error)\n }\n }\n\n return result\n}\n"]}
1
+ {"version":3,"file":"deliver-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/deliver-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and\n * sees what each one was given; send-notice.ts hands in the real ones.\n */\nimport { languageOf, noticeChannels, teamsMessage, type NoticeWords } from './notice-channels.js'\nimport { noticeEmailForm } from './notice-email.js'\nimport type { AppBrand } from './app-brand.js'\n\nexport type NoticeReceiver = { id: string; email?: string | null; locale?: string | null }\n\n/**\n * A notice, settled by the module whose event it tells of - who, what and where. The words are asked per language,\n * so each person reads their own.\n */\nexport type Notice = {\n /** What kind of thing it tells of - `approval`, `announcement`. The in-app notice carries it. */\n type: string\n level?: 'info' | 'warn'\n receivers: NoticeReceiver[]\n words: (lng: string) => NoticeWords\n /** The page, relative to the domain - what the in-app notice opens. */\n address: string\n /** The same page as a full address for mail and Teams; absent when no request said where we are. */\n link?: string\n /** The language of a person who has not chosen one. */\n defaultLanguage?: string\n /** Names the notice in the log. */\n label: string\n /** How `words().body` is written. Markdown is rendered in the mail; Teams and push take it as text. */\n format?: 'text' | 'markdown'\n /**\n * The channels this notice may take beyond the app, when its sender chooses per notice (an announcement's\n * \"send by mail too\"). Left out, it takes every channel the domain turned on; a channel the domain has not turned on\n * is never taken.\n */\n channels?: { email?: boolean; teams?: boolean }\n /**\n * Said to everyone in the domain (an announcement to the whole domain). The app, push and mail still go to each\n * person; Teams posts once to the domain's channel webhook, in the default language. Only a broadcast goes to Teams -\n * a notice to some people never lands where everyone reads.\n */\n broadcast?: boolean\n}\n\n/** What went out - mail counted per person, the Teams channel post once. A sender that keeps a record stores this. */\nexport type NoticeResult = {\n receivers: number\n email: { sent: number; failed: number }\n teams: { sent: number; failed: number }\n}\n\nexport type NoticeChannelFunctions = {\n /** The domain's attributes (EnvVar) - read when the notice goes. */\n attributes: () => Promise<Record<string, string | undefined>>\n /** notification's `notify` for the domain; absent, nothing goes in the app. */\n push?: (payload: any) => unknown\n /** email-base's `sendEmail` - answers the transport's receipt, nothing when it failed. */\n mail: (mail: { receiver: string; subject: string; content: string }) => Promise<unknown>\n /** Posts JSON to the Teams webhook and answers whether it took it. */\n teams: (url: string, body: unknown) => Promise<{ ok: boolean; status?: number }>\n report: (message: string, error?: unknown) => void\n /** The application whose name, tagline and colour the mail carries; absent, the default. */\n brand?: AppBrand\n}\n\n/** The domain's own address - the notice's full link without its page. */\nfunction baseOf(notice: Notice): string | undefined {\n return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined\n}\n\n/**\n * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is\n * also posted once to the domain's Teams channel. A channel that fails is reported with its name and the others still\n * go - nothing here throws, because what it tells of has already happened.\n */\nexport async function deliverNotice(notice: Notice, channels: NoticeChannelFunctions): Promise<NoticeResult> {\n const result: NoticeResult = { receivers: notice.receivers.length, email: { sent: 0, failed: 0 }, teams: { sent: 0, failed: 0 } }\n if (!notice.receivers.length) return result\n\n let turnedOn: ReturnType<typeof noticeChannels> = { email: false }\n try {\n turnedOn = noticeChannels(await channels.attributes())\n } catch (error) {\n channels.report(`${notice.label}: the domain's notice channels could not be read - in the app only`, error)\n }\n\n const email = turnedOn.email && notice.channels?.email !== false\n const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined\n\n for (const receiver of notice.receivers) {\n const lng = languageOf(receiver.locale, notice.defaultLanguage)\n let words: NoticeWords\n try {\n words = notice.words(lng)\n } catch (error) {\n channels.report(`${notice.label}: the words for ${receiver.id} could not be made`, error)\n if (email && receiver.email) result.email.failed++\n continue\n }\n\n if (channels.push) {\n for (const mode of ['foreground', 'background'] as const) {\n try {\n await channels.push({\n receivers: [receiver.id],\n type: notice.type,\n subject: notice.level || 'info',\n title: words.subject,\n body: words.summary ?? words.body,\n url: notice.address,\n mode\n })\n } catch (error) {\n channels.report(`${notice.label}: the ${mode} notice to ${receiver.id} failed`, error)\n }\n }\n }\n\n if (email && receiver.email) {\n try {\n const sent = await channels.mail({\n receiver: receiver.email,\n subject: words.subject,\n content: noticeEmailForm({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })\n })\n if (sent) {\n result.email.sent++\n } else {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} did not go out`)\n }\n } catch (error) {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} failed`, error)\n }\n }\n }\n\n if (teamsChannelWebhook) {\n try {\n const answer = await channels.teams(teamsChannelWebhook, teamsMessage({ words: notice.words(languageOf(undefined, notice.defaultLanguage)), link: notice.link }))\n if (answer.ok) {\n result.teams.sent++\n } else {\n result.teams.failed++\n channels.report(`${notice.label}: Teams refused the channel post (${answer.status ?? '-'})`)\n }\n } catch (error) {\n result.teams.failed++\n channels.report(`${notice.label}: the Teams channel post failed`, error)\n }\n }\n\n return result\n}\n"]}
@@ -2,3 +2,4 @@ export * from './notice-channels.js';
2
2
  export * from './notice-email.js';
3
3
  export * from './deliver-notice.js';
4
4
  export * from './send-notice.js';
5
+ export * from './app-brand.js';
@@ -5,4 +5,5 @@ tslib_1.__exportStar(require("./notice-channels.js"), exports);
5
5
  tslib_1.__exportStar(require("./notice-email.js"), exports);
6
6
  tslib_1.__exportStar(require("./deliver-notice.js"), exports);
7
7
  tslib_1.__exportStar(require("./send-notice.js"), exports);
8
+ tslib_1.__exportStar(require("./app-brand.js"), exports);
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/controllers/notice/index.ts"],"names":[],"mappings":";;;AAAA,+DAAoC;AACpC,4DAAiC;AACjC,8DAAmC;AACnC,2DAAgC","sourcesContent":["export * from './notice-channels.js'\nexport * from './notice-email.js'\nexport * from './deliver-notice.js'\nexport * from './send-notice.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/controllers/notice/index.ts"],"names":[],"mappings":";;;AAAA,+DAAoC;AACpC,4DAAiC;AACjC,8DAAmC;AACnC,2DAAgC;AAChC,yDAA8B","sourcesContent":["export * from './notice-channels.js'\nexport * from './notice-email.js'\nexport * from './deliver-notice.js'\nexport * from './send-notice.js'\nexport * from './app-brand.js'\n"]}
@@ -1,3 +1,4 @@
1
+ import { type AppBrand } from './app-brand.js';
1
2
  import type { NoticeWords } from './notice-channels.js';
2
3
  /** Every value goes into the HTML through this - a subject like `<b>` or `A&B` stays text. */
3
4
  export declare function escapeHtml(value: unknown): string;
@@ -14,10 +15,11 @@ export declare function safeNoticeHref(href: string | null | undefined, base?: s
14
15
  */
15
16
  export declare function renderNoticeMarkdown(markdown: string, base?: string): string;
16
17
  /**
17
- * The notice mail (ADR-0164) - the head, band, button and foot of the auth mails, so a person knows the sender.
18
+ * The notice mail (ADR-0164) - the head, band, button and foot of the auth mails, so a person knows the sender. The head
19
+ * names the application that sends it with its tagline, and the band and button are in its brand colour (`appBrand`).
18
20
  * Without a link (a notice sent with no request to take the address from) the button gives way to the note.
19
21
  */
20
- export declare function noticeEmailForm({ lang, words, link, format, base }: {
22
+ export declare function noticeEmailForm({ lang, words, link, format, base, brand }: {
21
23
  lang: string;
22
24
  words: NoticeWords;
23
25
  link?: string;
@@ -25,4 +27,6 @@ export declare function noticeEmailForm({ lang, words, link, format, base }: {
25
27
  format?: 'text' | 'markdown';
26
28
  /** The domain's address, under which an app address in a Markdown body is resolved. */
27
29
  base?: string;
30
+ /** The application sending it. */
31
+ brand?: AppBrand;
28
32
  }): string;
@@ -5,6 +5,7 @@ exports.safeNoticeHref = safeNoticeHref;
5
5
  exports.renderNoticeMarkdown = renderNoticeMarkdown;
6
6
  exports.noticeEmailForm = noticeEmailForm;
7
7
  const marked_1 = require("marked");
8
+ const app_brand_js_1 = require("./app-brand.js");
8
9
  /** Every value goes into the HTML through this - a subject like `<b>` or `A&B` stays text. */
9
10
  function escapeHtml(value) {
10
11
  return String(value ?? '')
@@ -67,14 +68,17 @@ function renderNoticeMarkdown(markdown, base) {
67
68
  return marked.parse(String(markdown ?? ''));
68
69
  }
69
70
  /**
70
- * The notice mail (ADR-0164) - the head, band, button and foot of the auth mails, so a person knows the sender.
71
+ * The notice mail (ADR-0164) - the head, band, button and foot of the auth mails, so a person knows the sender. The head
72
+ * names the application that sends it with its tagline, and the band and button are in its brand colour (`appBrand`).
71
73
  * Without a link (a notice sent with no request to take the address from) the button gives way to the note.
72
74
  */
73
- function noticeEmailForm({ lang, words, link, format = 'text', base }) {
75
+ function noticeEmailForm({ lang, words, link, format = 'text', base, brand = app_brand_js_1.DEFAULT_BRAND }) {
76
+ const color = escapeHtml(brand.color);
77
+ const onColor = (0, app_brand_js_1.textColorOn)(brand.color);
74
78
  const action = link
75
79
  ? `<a
76
80
  href="${escapeHtml(link)}"
77
- style="display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:#22a6a7;padding:7px 15px;color:#fff;font-size:18px;text-decoration:none;"
81
+ style="display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:${color};padding:7px 15px;color:${onColor};font-size:18px;font-weight:bold;text-decoration:none;"
78
82
  >${escapeHtml(words.action)}</a
79
83
  >`
80
84
  : `<span style="display:block;color:#555">${escapeHtml(words.note)}</span>`;
@@ -90,14 +94,14 @@ function noticeEmailForm({ lang, words, link, format = 'text', base }) {
90
94
  <body>
91
95
  <div style="background-color:#f6f6f6">
92
96
  <!--header begin-->
93
- <div style="background-color:#fff;padding:0 10px;border-top: 2px solid #394e64;">
94
- <img src="http://www.hatiolab.com/assets/img/logo-operato.png" style="max-height:50px" />
97
+ <div style="background-color:#fff;padding:10px 20px;border-top:2px solid ${color};">
98
+ <span style="font-size:20px;font-weight:bold;color:#1f2933">${escapeHtml(brand.name)}</span>${brand.tagline ? `<span style="margin-left:10px;font-size:13px;color:#6b7280">${escapeHtml(brand.tagline)}</span>` : ''}
95
99
  </div>
96
100
  <!--header end-->
97
101
 
98
102
  <!--title begin-->
99
- <div style="background-color:#22a6a7;padding:12px 10px 10px 10px;min-height:50px;">
100
- <span style="display:block;color:#fff;font-size:24px;font-weight:bold;margin-left:40px"
103
+ <div style="background-color:${color};padding:12px 10px 10px 10px;min-height:50px;">
104
+ <span style="display:block;color:${onColor};font-size:24px;font-weight:bold;margin-left:40px"
101
105
  >${escapeHtml(words.subject)}</span
102
106
  >
103
107
  </div>
@@ -1 +1 @@
1
- {"version":3,"file":"notice-email.js","sourceRoot":"","sources":["../../../server/controllers/notice/notice-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { Marked } from 'marked'\n\nimport type { NoticeWords } from './notice-channels.js'\n\n/** Every value goes into the HTML through this - a subject like `<b>` or `A&B` stays text. */\nexport function escapeHtml(value: unknown): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\n/** The schemes a link in a notice may take; anything else is shown as its text. */\nconst SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:'])\nconst NO_BASE = 'https://no-base.invalid/'\n\n/**\n * Where a link in a notice may go: http(s) and mailto as written, and an address of the app (no scheme) under `base`,\n * the domain's own address. With no base an app address has nowhere to go and is dropped, as is every other scheme -\n * `javascript:` in any spelling the URL parser reads as one.\n */\nexport function safeNoticeHref(href: string | null | undefined, base?: string): string | undefined {\n const raw = String(href ?? '').trim()\n if (!raw) return undefined\n\n let url: URL\n try {\n url = new URL(raw, base || NO_BASE)\n } catch {\n return undefined\n }\n\n if (!SAFE_PROTOCOLS.has(url.protocol)) return undefined\n if (url.origin === new URL(NO_BASE).origin) return undefined\n return url.href\n}\n\n/**\n * Markdown as mail HTML, rendered here on the server. Raw HTML in the text is shown as text, never as markup; a link or\n * an image becomes a link only to an address `safeNoticeHref` allows, and an image is not loaded - it is shown as a\n * link with its alt text, so a mail does not fetch from wherever its writer pointed.\n */\nexport function renderNoticeMarkdown(markdown: string, base?: string): string {\n const marked = new Marked({\n gfm: true,\n breaks: true,\n async: false,\n renderer: {\n html({ text }) {\n return escapeHtml(text)\n },\n link({ href, title, tokens }) {\n const label = this.parser.parseInline(tokens)\n const safe = safeNoticeHref(href, base)\n return safe ? `<a href=\"${escapeHtml(safe)}\"${title ? ` title=\"${escapeHtml(title)}\"` : ''}>${label}</a>` : label\n },\n image({ href, text }) {\n const safe = safeNoticeHref(href, base)\n return safe ? `<a href=\"${escapeHtml(safe)}\">${escapeHtml(text || safe)}</a>` : escapeHtml(text)\n }\n }\n })\n\n return marked.parse(String(markdown ?? '')) as string\n}\n\n/**\n * The notice mail (ADR-0164) - the head, band, button and foot of the auth mails, so a person knows the sender.\n * Without a link (a notice sent with no request to take the address from) the button gives way to the note.\n */\nexport function noticeEmailForm({\n lang,\n words,\n link,\n format = 'text',\n base\n}: {\n lang: string\n words: NoticeWords\n link?: string\n /** How the body is written - an announcement's is Markdown. */\n format?: 'text' | 'markdown'\n /** The domain's address, under which an app address in a Markdown body is resolved. */\n base?: string\n}) {\n const action = link\n ? `<a\n href=\"${escapeHtml(link)}\"\n style=\"display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:#22a6a7;padding:7px 15px;color:#fff;font-size:18px;text-decoration:none;\"\n >${escapeHtml(words.action)}</a\n >`\n : `<span style=\"display:block;color:#555\">${escapeHtml(words.note)}</span>`\n\n return `\n <html lang=\"${escapeHtml(lang)}\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${escapeHtml(words.subject)}</title>\n <meta name=\"author\" content=\"hatiolab\" />\n <meta name=\"google\" content=\"notranslate\"/>\n </head>\n\n <body>\n <div style=\"background-color:#f6f6f6\">\n <!--header begin-->\n <div style=\"background-color:#fff;padding:0 10px;border-top: 2px solid #394e64;\">\n <img src=\"http://www.hatiolab.com/assets/img/logo-operato.png\" style=\"max-height:50px\" />\n </div>\n <!--header end-->\n\n <!--title begin-->\n <div style=\"background-color:#22a6a7;padding:12px 10px 10px 10px;min-height:50px;\">\n <span style=\"display:block;color:#fff;font-size:24px;font-weight:bold;margin-left:40px\"\n >${escapeHtml(words.subject)}</span\n >\n </div>\n <!--title end-->\n\n <!--body begin-->\n ${\n format === 'markdown'\n ? `<div style=\"padding:10px 20px;line-height:1.5;font-size:16px\">${renderNoticeMarkdown(words.body, base)}</div>`\n : `<p style=\"padding:10px 20px;line-height:1.5;font-size:16px;white-space:pre-line\">${escapeHtml(words.body)}</p>`\n }\n <p style=\"padding:0 20px 10px 20px;font-size:16px\">\n ${action}\n </p>\n <!--body end-->\n\n <!--footer begin-->\n <div style=\"background-color:#3d5874;padding:7px 20px 5px 20px;font-size:12px;color:#efefef\">\n © Hatio, Lab. Inc. All rights reserved.\n </div>\n <!--footer end-->\n </div>\n </body>\n </html>\n `\n}\n"]}
1
+ {"version":3,"file":"notice-email.js","sourceRoot":"","sources":["../../../server/controllers/notice/notice-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { Marked } from 'marked'\n\nimport { DEFAULT_BRAND, textColorOn, type AppBrand } from './app-brand.js'\nimport type { NoticeWords } from './notice-channels.js'\n\n/** Every value goes into the HTML through this - a subject like `<b>` or `A&B` stays text. */\nexport function escapeHtml(value: unknown): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\n/** The schemes a link in a notice may take; anything else is shown as its text. */\nconst SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:'])\nconst NO_BASE = 'https://no-base.invalid/'\n\n/**\n * Where a link in a notice may go: http(s) and mailto as written, and an address of the app (no scheme) under `base`,\n * the domain's own address. With no base an app address has nowhere to go and is dropped, as is every other scheme -\n * `javascript:` in any spelling the URL parser reads as one.\n */\nexport function safeNoticeHref(href: string | null | undefined, base?: string): string | undefined {\n const raw = String(href ?? '').trim()\n if (!raw) return undefined\n\n let url: URL\n try {\n url = new URL(raw, base || NO_BASE)\n } catch {\n return undefined\n }\n\n if (!SAFE_PROTOCOLS.has(url.protocol)) return undefined\n if (url.origin === new URL(NO_BASE).origin) return undefined\n return url.href\n}\n\n/**\n * Markdown as mail HTML, rendered here on the server. Raw HTML in the text is shown as text, never as markup; a link or\n * an image becomes a link only to an address `safeNoticeHref` allows, and an image is not loaded - it is shown as a\n * link with its alt text, so a mail does not fetch from wherever its writer pointed.\n */\nexport function renderNoticeMarkdown(markdown: string, base?: string): string {\n const marked = new Marked({\n gfm: true,\n breaks: true,\n async: false,\n renderer: {\n html({ text }) {\n return escapeHtml(text)\n },\n link({ href, title, tokens }) {\n const label = this.parser.parseInline(tokens)\n const safe = safeNoticeHref(href, base)\n return safe ? `<a href=\"${escapeHtml(safe)}\"${title ? ` title=\"${escapeHtml(title)}\"` : ''}>${label}</a>` : label\n },\n image({ href, text }) {\n const safe = safeNoticeHref(href, base)\n return safe ? `<a href=\"${escapeHtml(safe)}\">${escapeHtml(text || safe)}</a>` : escapeHtml(text)\n }\n }\n })\n\n return marked.parse(String(markdown ?? '')) as string\n}\n\n/**\n * The notice mail (ADR-0164) - the head, band, button and foot of the auth mails, so a person knows the sender. The head\n * names the application that sends it with its tagline, and the band and button are in its brand colour (`appBrand`).\n * Without a link (a notice sent with no request to take the address from) the button gives way to the note.\n */\nexport function noticeEmailForm({\n lang,\n words,\n link,\n format = 'text',\n base,\n brand = DEFAULT_BRAND\n}: {\n lang: string\n words: NoticeWords\n link?: string\n /** How the body is written - an announcement's is Markdown. */\n format?: 'text' | 'markdown'\n /** The domain's address, under which an app address in a Markdown body is resolved. */\n base?: string\n /** The application sending it. */\n brand?: AppBrand\n}) {\n const color = escapeHtml(brand.color)\n const onColor = textColorOn(brand.color)\n const action = link\n ? `<a\n href=\"${escapeHtml(link)}\"\n style=\"display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:${color};padding:7px 15px;color:${onColor};font-size:18px;font-weight:bold;text-decoration:none;\"\n >${escapeHtml(words.action)}</a\n >`\n : `<span style=\"display:block;color:#555\">${escapeHtml(words.note)}</span>`\n\n return `\n <html lang=\"${escapeHtml(lang)}\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${escapeHtml(words.subject)}</title>\n <meta name=\"author\" content=\"hatiolab\" />\n <meta name=\"google\" content=\"notranslate\"/>\n </head>\n\n <body>\n <div style=\"background-color:#f6f6f6\">\n <!--header begin-->\n <div style=\"background-color:#fff;padding:10px 20px;border-top:2px solid ${color};\">\n <span style=\"font-size:20px;font-weight:bold;color:#1f2933\">${escapeHtml(brand.name)}</span>${\n brand.tagline ? `<span style=\"margin-left:10px;font-size:13px;color:#6b7280\">${escapeHtml(brand.tagline)}</span>` : ''\n }\n </div>\n <!--header end-->\n\n <!--title begin-->\n <div style=\"background-color:${color};padding:12px 10px 10px 10px;min-height:50px;\">\n <span style=\"display:block;color:${onColor};font-size:24px;font-weight:bold;margin-left:40px\"\n >${escapeHtml(words.subject)}</span\n >\n </div>\n <!--title end-->\n\n <!--body begin-->\n ${\n format === 'markdown'\n ? `<div style=\"padding:10px 20px;line-height:1.5;font-size:16px\">${renderNoticeMarkdown(words.body, base)}</div>`\n : `<p style=\"padding:10px 20px;line-height:1.5;font-size:16px;white-space:pre-line\">${escapeHtml(words.body)}</p>`\n }\n <p style=\"padding:0 20px 10px 20px;font-size:16px\">\n ${action}\n </p>\n <!--body end-->\n\n <!--footer begin-->\n <div style=\"background-color:#3d5874;padding:7px 20px 5px 20px;font-size:12px;color:#efefef\">\n © Hatio, Lab. Inc. All rights reserved.\n </div>\n <!--footer end-->\n </div>\n </body>\n </html>\n `\n}\n"]}
@@ -15,6 +15,7 @@ const shell_1 = require("@things-factory/shell");
15
15
  const notification_middleware_js_1 = require("../../middlewares/notification-middleware.js");
16
16
  const deliver_notice_js_1 = require("./deliver-notice.js");
17
17
  const notice_channels_js_1 = require("./notice-channels.js");
18
+ const app_brand_js_1 = require("./app-brand.js");
18
19
  const report = (message, error) => env_1.logger.error(`[notification] ${message}`, error);
19
20
  /** The real channels of a domain. */
20
21
  function noticeChannelsOf(domain, push) {
@@ -26,7 +27,8 @@ function noticeChannelsOf(domain, push) {
26
27
  const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
27
28
  return { ok: response.ok, status: response.status };
28
29
  },
29
- report
30
+ report,
31
+ brand: (0, app_brand_js_1.appBrand)()
30
32
  };
31
33
  }
32
34
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"send-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/send-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The one way a module tells people of something (ADR-0164): in the app and by web push always, by mail and Teams\n * when the domain turned them on. The module settles who and what; this sends it.\n */\nimport { buildAuthURL } from '@things-factory/auth-base'\nimport { sendEmail } from '@things-factory/email-base'\nimport { config, logger } from '@things-factory/env'\nimport { getContextPath, loadDomainAttributes, whenCommitted } from '@things-factory/shell'\n\nimport { notifierOf } from '../../middlewares/notification-middleware.js'\nimport { deliverNotice, type Notice, type NoticeChannelFunctions, type NoticeResult } from './deliver-notice.js'\nimport { NOTICE_ATTRIBUTE_PREFIX } from './notice-channels.js'\n\nconst report = (message: string, error?: unknown) => logger.error(`[notification] ${message}`, error)\n\n/** The real channels of a domain. */\nexport function noticeChannelsOf(domain: any, push?: (payload: any) => unknown): NoticeChannelFunctions {\n return {\n attributes: () => loadDomainAttributes(domain, NOTICE_ATTRIBUTE_PREFIX),\n push: push ?? notifierOf(domain),\n mail: mail => sendEmail(mail) as Promise<unknown>,\n teams: async (url, body) => {\n const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })\n return { ok: response.ok, status: response.status }\n },\n report\n }\n}\n\n/**\n * Sends now and answers what went out - for work with no request (a scheduled publication), or a sender that records\n * the result. Never throws; a notice that could not be sent at all answers undefined.\n */\nexport async function sendNotice(domain: any, notice: Notice, push?: (payload: any) => unknown): Promise<NoticeResult | undefined> {\n try {\n return await deliverNotice({ defaultLanguage: config.get('i18n/defaultLanguage', 'en'), ...notice }, noticeChannelsOf(domain, push))\n } catch (error) {\n report(`${notice.label}: sending failed`, error)\n return undefined\n }\n}\n\n/**\n * Sends once the request's transaction has committed, and does not make the request wait for it - a slow mail server\n * does not hold the button that caused the notice. Rolled back, nothing goes. With no transaction list, it goes now.\n * `sent` hears what went out, for a sender that records it (in its own transaction - the request's is over).\n */\nexport function sendNoticeWhenCommitted(\n context: ResolverContext,\n notice: Notice,\n sent?: (result: NoticeResult | undefined) => unknown | Promise<unknown>\n): void {\n const { domain } = context.state\n const push = (context.state as any).notify\n const send = () => {\n void sendNotice(domain, notice, push)\n .then(result => sent?.(result))\n .catch(error => report(`${notice.label}: recording what was sent failed`, error))\n }\n\n if (!whenCommitted((context.state as any).afterCommit, notice.label, send)) send()\n}\n\n/** A page of the request's domain as a full address - where this request came in. None with no request. */\nexport function noticeLinkOf(context: ResolverContext, address: string): string | undefined {\n try {\n if (!(context as any)?.request) return undefined\n return `${buildAuthURL('/', context)}${getContextPath(context.state.domain)}${address}`\n } catch {\n return undefined\n }\n}\n"]}
1
+ {"version":3,"file":"send-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/send-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The one way a module tells people of something (ADR-0164): in the app and by web push always, by mail and Teams\n * when the domain turned them on. The module settles who and what; this sends it.\n */\nimport { buildAuthURL } from '@things-factory/auth-base'\nimport { sendEmail } from '@things-factory/email-base'\nimport { config, logger } from '@things-factory/env'\nimport { getContextPath, loadDomainAttributes, whenCommitted } from '@things-factory/shell'\n\nimport { notifierOf } from '../../middlewares/notification-middleware.js'\nimport { deliverNotice, type Notice, type NoticeChannelFunctions, type NoticeResult } from './deliver-notice.js'\nimport { NOTICE_ATTRIBUTE_PREFIX } from './notice-channels.js'\nimport { appBrand } from './app-brand.js'\n\nconst report = (message: string, error?: unknown) => logger.error(`[notification] ${message}`, error)\n\n/** The real channels of a domain. */\nexport function noticeChannelsOf(domain: any, push?: (payload: any) => unknown): NoticeChannelFunctions {\n return {\n attributes: () => loadDomainAttributes(domain, NOTICE_ATTRIBUTE_PREFIX),\n push: push ?? notifierOf(domain),\n mail: mail => sendEmail(mail) as Promise<unknown>,\n teams: async (url, body) => {\n const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })\n return { ok: response.ok, status: response.status }\n },\n report,\n brand: appBrand()\n }\n}\n\n/**\n * Sends now and answers what went out - for work with no request (a scheduled publication), or a sender that records\n * the result. Never throws; a notice that could not be sent at all answers undefined.\n */\nexport async function sendNotice(domain: any, notice: Notice, push?: (payload: any) => unknown): Promise<NoticeResult | undefined> {\n try {\n return await deliverNotice({ defaultLanguage: config.get('i18n/defaultLanguage', 'en'), ...notice }, noticeChannelsOf(domain, push))\n } catch (error) {\n report(`${notice.label}: sending failed`, error)\n return undefined\n }\n}\n\n/**\n * Sends once the request's transaction has committed, and does not make the request wait for it - a slow mail server\n * does not hold the button that caused the notice. Rolled back, nothing goes. With no transaction list, it goes now.\n * `sent` hears what went out, for a sender that records it (in its own transaction - the request's is over).\n */\nexport function sendNoticeWhenCommitted(\n context: ResolverContext,\n notice: Notice,\n sent?: (result: NoticeResult | undefined) => unknown | Promise<unknown>\n): void {\n const { domain } = context.state\n const push = (context.state as any).notify\n const send = () => {\n void sendNotice(domain, notice, push)\n .then(result => sent?.(result))\n .catch(error => report(`${notice.label}: recording what was sent failed`, error))\n }\n\n if (!whenCommitted((context.state as any).afterCommit, notice.label, send)) send()\n}\n\n/** A page of the request's domain as a full address - where this request came in. None with no request. */\nexport function noticeLinkOf(context: ResolverContext, address: string): string | undefined {\n try {\n if (!(context as any)?.request) return undefined\n return `${buildAuthURL('/', context)}${getContextPath(context.state.domain)}${address}`\n } catch {\n return undefined\n }\n}\n"]}