@wizeworks/lens-cloud 0.3.0-alpha.1 → 0.3.0-alpha.2

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 (275) hide show
  1. package/README.md +57 -5
  2. package/dist/alerts/evaluate.d.ts.map +1 -1
  3. package/dist/alerts/evaluate.js +17 -2
  4. package/dist/alerts/evaluate.js.map +1 -1
  5. package/dist/alerts/notify.d.ts +13 -2
  6. package/dist/alerts/notify.d.ts.map +1 -1
  7. package/dist/alerts/notify.js +22 -16
  8. package/dist/alerts/notify.js.map +1 -1
  9. package/dist/app.d.ts +10 -2
  10. package/dist/app.d.ts.map +1 -1
  11. package/dist/auth/auth.d.ts +51 -0
  12. package/dist/auth/auth.d.ts.map +1 -0
  13. package/dist/auth/auth.js +63 -0
  14. package/dist/auth/auth.js.map +1 -0
  15. package/dist/auth/bridge.d.ts +7 -0
  16. package/dist/auth/bridge.d.ts.map +1 -0
  17. package/dist/auth/bridge.js +39 -0
  18. package/dist/auth/bridge.js.map +1 -0
  19. package/dist/auth/crypto.d.ts +0 -5
  20. package/dist/auth/crypto.d.ts.map +1 -1
  21. package/dist/auth/crypto.js +3 -20
  22. package/dist/auth/crypto.js.map +1 -1
  23. package/dist/auth/delete-user.d.ts +16 -0
  24. package/dist/auth/delete-user.d.ts.map +1 -0
  25. package/dist/auth/delete-user.js +21 -0
  26. package/dist/auth/delete-user.js.map +1 -0
  27. package/dist/auth/deps.d.ts +27 -0
  28. package/dist/auth/deps.d.ts.map +1 -0
  29. package/dist/auth/deps.js +3 -0
  30. package/dist/auth/deps.js.map +1 -0
  31. package/dist/auth/email-token.d.ts +8 -0
  32. package/dist/auth/email-token.d.ts.map +1 -0
  33. package/dist/auth/email-token.js +15 -0
  34. package/dist/auth/email-token.js.map +1 -0
  35. package/dist/auth/hooks.d.ts +4 -0
  36. package/dist/auth/hooks.d.ts.map +1 -0
  37. package/dist/auth/hooks.js +143 -0
  38. package/dist/auth/hooks.js.map +1 -0
  39. package/dist/auth/options.d.ts +17 -0
  40. package/dist/auth/options.d.ts.map +1 -0
  41. package/dist/auth/options.js +98 -0
  42. package/dist/auth/options.js.map +1 -0
  43. package/dist/auth/password.d.ts +10 -0
  44. package/dist/auth/password.d.ts.map +1 -0
  45. package/dist/auth/password.js +22 -0
  46. package/dist/auth/password.js.map +1 -0
  47. package/dist/auth/plugins.d.ts +4 -0
  48. package/dist/auth/plugins.d.ts.map +1 -0
  49. package/dist/auth/plugins.js +45 -0
  50. package/dist/auth/plugins.js.map +1 -0
  51. package/dist/auth/principal.d.ts +8 -3
  52. package/dist/auth/principal.d.ts.map +1 -1
  53. package/dist/auth/principal.js +13 -8
  54. package/dist/auth/principal.js.map +1 -1
  55. package/dist/auth/roles.d.ts +43 -3
  56. package/dist/auth/roles.d.ts.map +1 -1
  57. package/dist/auth/roles.js +15 -6
  58. package/dist/auth/roles.js.map +1 -1
  59. package/dist/auth/schema-names.d.ts +130 -0
  60. package/dist/auth/schema-names.d.ts.map +1 -0
  61. package/dist/auth/schema-names.js +53 -0
  62. package/dist/auth/schema-names.js.map +1 -0
  63. package/dist/auth/secret.d.ts +12 -0
  64. package/dist/auth/secret.d.ts.map +1 -0
  65. package/dist/auth/secret.js +32 -0
  66. package/dist/auth/secret.js.map +1 -0
  67. package/dist/auth/security-notices.d.ts +34 -0
  68. package/dist/auth/security-notices.d.ts.map +1 -0
  69. package/dist/auth/security-notices.js +81 -0
  70. package/dist/auth/security-notices.js.map +1 -0
  71. package/dist/auth/signup.d.ts +23 -0
  72. package/dist/auth/signup.d.ts.map +1 -0
  73. package/dist/auth/signup.js +61 -0
  74. package/dist/auth/signup.js.map +1 -0
  75. package/dist/auth/sso.d.ts +21 -0
  76. package/dist/auth/sso.d.ts.map +1 -0
  77. package/dist/auth/sso.js +35 -0
  78. package/dist/auth/sso.js.map +1 -0
  79. package/dist/auth/wire.d.ts +16 -0
  80. package/dist/auth/wire.d.ts.map +1 -0
  81. package/dist/auth/wire.js +47 -0
  82. package/dist/auth/wire.js.map +1 -0
  83. package/dist/cli-args.d.ts +1 -1
  84. package/dist/cli-args.d.ts.map +1 -1
  85. package/dist/cli-args.js +55 -14
  86. package/dist/cli-args.js.map +1 -1
  87. package/dist/cli.js +18 -2
  88. package/dist/cli.js.map +1 -1
  89. package/dist/config.d.ts +26 -4
  90. package/dist/config.d.ts.map +1 -1
  91. package/dist/config.js +35 -0
  92. package/dist/config.js.map +1 -1
  93. package/dist/db/db.d.ts +5 -2
  94. package/dist/db/db.d.ts.map +1 -1
  95. package/dist/db/db.js +15 -8
  96. package/dist/db/db.js.map +1 -1
  97. package/dist/db/migrate.d.ts +2 -2
  98. package/dist/db/migrate.d.ts.map +1 -1
  99. package/dist/db/migrate.js +15 -5
  100. package/dist/db/migrate.js.map +1 -1
  101. package/dist/db/schema-auth.d.ts +2 -0
  102. package/dist/db/schema-auth.d.ts.map +1 -0
  103. package/dist/db/schema-auth.js +93 -0
  104. package/dist/db/schema-auth.js.map +1 -0
  105. package/dist/db/schema-mail.d.ts +2 -0
  106. package/dist/db/schema-mail.d.ts.map +1 -0
  107. package/dist/db/schema-mail.js +8 -0
  108. package/dist/db/schema-mail.js.map +1 -0
  109. package/dist/db/schema-notify.d.ts +2 -0
  110. package/dist/db/schema-notify.d.ts.map +1 -0
  111. package/dist/db/schema-notify.js +26 -0
  112. package/dist/db/schema-notify.js.map +1 -0
  113. package/dist/db/schema-sso.d.ts +2 -0
  114. package/dist/db/schema-sso.d.ts.map +1 -0
  115. package/dist/db/schema-sso.js +17 -0
  116. package/dist/db/schema-sso.js.map +1 -0
  117. package/dist/db/schema.d.ts.map +1 -1
  118. package/dist/db/schema.js +8 -0
  119. package/dist/db/schema.js.map +1 -1
  120. package/dist/http/http.d.ts +2 -0
  121. package/dist/http/http.d.ts.map +1 -1
  122. package/dist/http/http.js +7 -3
  123. package/dist/http/http.js.map +1 -1
  124. package/dist/jobs.d.ts +1 -1
  125. package/dist/jobs.d.ts.map +1 -1
  126. package/dist/jobs.js +14 -3
  127. package/dist/jobs.js.map +1 -1
  128. package/dist/mail/blocks.d.ts +14 -0
  129. package/dist/mail/blocks.d.ts.map +1 -0
  130. package/dist/mail/blocks.js +35 -0
  131. package/dist/mail/blocks.js.map +1 -0
  132. package/dist/mail/content.d.ts +68 -0
  133. package/dist/mail/content.d.ts.map +1 -0
  134. package/dist/mail/content.js +6 -0
  135. package/dist/mail/content.js.map +1 -0
  136. package/dist/mail/emails-digest.d.ts +10 -0
  137. package/dist/mail/emails-digest.d.ts.map +1 -0
  138. package/dist/mail/emails-digest.js +55 -0
  139. package/dist/mail/emails-digest.js.map +1 -0
  140. package/dist/mail/emails-health.d.ts +6 -0
  141. package/dist/mail/emails-health.d.ts.map +1 -0
  142. package/dist/mail/emails-health.js +36 -0
  143. package/dist/mail/emails-health.js.map +1 -0
  144. package/dist/mail/emails-security.d.ts +14 -0
  145. package/dist/mail/emails-security.d.ts.map +1 -0
  146. package/dist/mail/emails-security.js +27 -0
  147. package/dist/mail/emails-security.js.map +1 -0
  148. package/dist/mail/emails-team.d.ts +11 -0
  149. package/dist/mail/emails-team.d.ts.map +1 -0
  150. package/dist/mail/emails-team.js +45 -0
  151. package/dist/mail/emails-team.js.map +1 -0
  152. package/dist/mail/emails.d.ts +9 -0
  153. package/dist/mail/emails.d.ts.map +1 -0
  154. package/dist/mail/emails.js +49 -0
  155. package/dist/mail/emails.js.map +1 -0
  156. package/dist/mail/layout.d.ts +6 -0
  157. package/dist/mail/layout.d.ts.map +1 -0
  158. package/dist/mail/layout.js +10 -0
  159. package/dist/mail/layout.js.map +1 -0
  160. package/dist/mail/mailer.d.ts +25 -0
  161. package/dist/mail/mailer.d.ts.map +1 -0
  162. package/dist/mail/mailer.js +49 -0
  163. package/dist/mail/mailer.js.map +1 -0
  164. package/dist/mail/preview.d.ts +6 -0
  165. package/dist/mail/preview.d.ts.map +1 -0
  166. package/dist/mail/preview.js +26 -0
  167. package/dist/mail/preview.js.map +1 -0
  168. package/dist/mail/resend.d.ts +11 -0
  169. package/dist/mail/resend.d.ts.map +1 -0
  170. package/dist/mail/resend.js +34 -0
  171. package/dist/mail/resend.js.map +1 -0
  172. package/dist/mail/samples.d.ts +3 -0
  173. package/dist/mail/samples.d.ts.map +1 -0
  174. package/dist/mail/samples.js +63 -0
  175. package/dist/mail/samples.js.map +1 -0
  176. package/dist/mail/stop-route.d.ts +4 -0
  177. package/dist/mail/stop-route.d.ts.map +1 -0
  178. package/dist/mail/stop-route.js +41 -0
  179. package/dist/mail/stop-route.js.map +1 -0
  180. package/dist/mail/svix.d.ts +8 -0
  181. package/dist/mail/svix.d.ts.map +1 -0
  182. package/dist/mail/svix.js +17 -0
  183. package/dist/mail/svix.js.map +1 -0
  184. package/dist/mail/templates.d.ts +12 -0
  185. package/dist/mail/templates.d.ts.map +1 -0
  186. package/dist/mail/templates.js +18 -0
  187. package/dist/mail/templates.js.map +1 -0
  188. package/dist/mail/text.d.ts +4 -0
  189. package/dist/mail/text.d.ts.map +1 -0
  190. package/dist/mail/text.js +17 -0
  191. package/dist/mail/text.js.map +1 -0
  192. package/dist/mail/theme.d.ts +26 -0
  193. package/dist/mail/theme.d.ts.map +1 -0
  194. package/dist/mail/theme.js +21 -0
  195. package/dist/mail/theme.js.map +1 -0
  196. package/dist/mail/unsubscribe.d.ts +14 -0
  197. package/dist/mail/unsubscribe.d.ts.map +1 -0
  198. package/dist/mail/unsubscribe.js +39 -0
  199. package/dist/mail/unsubscribe.js.map +1 -0
  200. package/dist/mail/webhook.d.ts +5 -0
  201. package/dist/mail/webhook.d.ts.map +1 -0
  202. package/dist/mail/webhook.js +41 -0
  203. package/dist/mail/webhook.js.map +1 -0
  204. package/dist/notify/digest-data.d.ts +31 -0
  205. package/dist/notify/digest-data.d.ts.map +1 -0
  206. package/dist/notify/digest-data.js +37 -0
  207. package/dist/notify/digest-data.js.map +1 -0
  208. package/dist/notify/digest.d.ts +17 -0
  209. package/dist/notify/digest.d.ts.map +1 -0
  210. package/dist/notify/digest.js +76 -0
  211. package/dist/notify/digest.js.map +1 -0
  212. package/dist/notify/health.d.ts +6 -0
  213. package/dist/notify/health.d.ts.map +1 -0
  214. package/dist/notify/health.js +43 -0
  215. package/dist/notify/health.js.map +1 -0
  216. package/dist/notify/team-describe.d.ts +22 -0
  217. package/dist/notify/team-describe.d.ts.map +1 -0
  218. package/dist/notify/team-describe.js +67 -0
  219. package/dist/notify/team-describe.js.map +1 -0
  220. package/dist/notify/team.d.ts +6 -0
  221. package/dist/notify/team.d.ts.map +1 -0
  222. package/dist/notify/team.js +56 -0
  223. package/dist/notify/team.js.map +1 -0
  224. package/dist/repo/alerts.d.ts +6 -2
  225. package/dist/repo/alerts.d.ts.map +1 -1
  226. package/dist/repo/alerts.js.map +1 -1
  227. package/dist/repo/auth-prune.d.ts +4 -0
  228. package/dist/repo/auth-prune.d.ts.map +1 -0
  229. package/dist/repo/auth-prune.js +8 -0
  230. package/dist/repo/auth-prune.js.map +1 -0
  231. package/dist/repo/mail-prefs.d.ts +35 -0
  232. package/dist/repo/mail-prefs.d.ts.map +1 -0
  233. package/dist/repo/mail-prefs.js +34 -0
  234. package/dist/repo/mail-prefs.js.map +1 -0
  235. package/dist/repo/orgs.d.ts +15 -25
  236. package/dist/repo/orgs.d.ts.map +1 -1
  237. package/dist/repo/orgs.js +18 -60
  238. package/dist/repo/orgs.js.map +1 -1
  239. package/dist/repo/suppressions.d.ts +8 -0
  240. package/dist/repo/suppressions.d.ts.map +1 -0
  241. package/dist/repo/suppressions.js +16 -0
  242. package/dist/repo/suppressions.js.map +1 -0
  243. package/dist/repo/users.d.ts +5 -16
  244. package/dist/repo/users.d.ts.map +1 -1
  245. package/dist/repo/users.js +2 -60
  246. package/dist/repo/users.js.map +1 -1
  247. package/dist/routes/alerts.d.ts.map +1 -1
  248. package/dist/routes/alerts.js +23 -6
  249. package/dist/routes/alerts.js.map +1 -1
  250. package/dist/routes/index.d.ts.map +1 -1
  251. package/dist/routes/index.js +4 -2
  252. package/dist/routes/index.js.map +1 -1
  253. package/dist/routes/me.d.ts +4 -0
  254. package/dist/routes/me.d.ts.map +1 -0
  255. package/dist/routes/me.js +37 -0
  256. package/dist/routes/me.js.map +1 -0
  257. package/dist/routes/notify.d.ts +4 -0
  258. package/dist/routes/notify.d.ts.map +1 -0
  259. package/dist/routes/notify.js +55 -0
  260. package/dist/routes/notify.js.map +1 -0
  261. package/dist/routes/orgs.d.ts.map +1 -1
  262. package/dist/routes/orgs.js +30 -53
  263. package/dist/routes/orgs.js.map +1 -1
  264. package/dist/routes/projects.d.ts.map +1 -1
  265. package/dist/routes/projects.js +4 -3
  266. package/dist/routes/projects.js.map +1 -1
  267. package/dist/server.d.ts +2 -0
  268. package/dist/server.d.ts.map +1 -1
  269. package/dist/server.js +45 -14
  270. package/dist/server.js.map +1 -1
  271. package/package.json +19 -6
  272. package/dist/routes/auth.d.ts +0 -4
  273. package/dist/routes/auth.d.ts.map +0 -1
  274. package/dist/routes/auth.js +0 -106
  275. package/dist/routes/auth.js.map +0 -1
@@ -0,0 +1,41 @@
1
+ import { readRawBody } from "../http/http.js";
2
+ import { applyStop } from "../repo/mail-prefs.js";
3
+ import { theme } from "./theme.js";
4
+ import { readStop } from "./unsubscribe.js";
5
+ const escape = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
6
+ async function labelOf(ctx, scope) {
7
+ if (scope === "team")
8
+ return "emails about team changes";
9
+ if (scope === "health")
10
+ return "emails when a project goes quiet";
11
+ if (scope === "digests")
12
+ return "every digest";
13
+ const [p] = await ctx.app.db.query("SELECT name FROM projects WHERE id = $1", [scope.projectId]);
14
+ return `the ${scope.period} digest for ${p?.name ?? "this project"}`;
15
+ }
16
+ function page(ctx, status, title, body) {
17
+ const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex">
18
+ <title>${escape(title)}</title><style>body{margin:0;padding:48px 16px;background:${theme.page};font-family:${theme.sans};color:${theme.body}}
19
+ main{max-width:480px;margin:0 auto;background:${theme.card};border:1px solid ${theme.line};border-radius:${theme.radius};padding:32px}
20
+ h1{margin:0 0 12px;font-size:24px;letter-spacing:-0.02em;color:${theme.ink}}button{background:${theme.accent};color:#fff;border:0;border-radius:8px;padding:12px 20px;font:600 15px ${theme.sans};cursor:pointer}
21
+ a{color:${theme.accent}}</style></head><body><main><h1>${escape(title)}</h1>${body}</main></body></html>`;
22
+ ctx.res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'" });
23
+ ctx.res.end(html);
24
+ }
25
+ /** GET shows a button (mail scanners open links, so a GET changes nothing). POST turns the mail off; it is also the RFC 8058 one-click call. */
26
+ export async function stopRoute(ctx) {
27
+ const token = ctx.url.searchParams.get("token") ?? "";
28
+ const stop = readStop(ctx.app.secrets, token);
29
+ if (!stop)
30
+ return page(ctx, 400, "This link does not work.", "<p>Change your emails on the Account page in WizeLens instead.</p>");
31
+ const label = await labelOf(ctx, stop.scope);
32
+ const account = `${ctx.app.baseUrl()}/_wizelens/#/account`;
33
+ if (ctx.req.method !== "POST") {
34
+ const form = `<form method="post" action="?token=${escape(encodeURIComponent(token))}"><button type="submit">Stop ${escape(label)}</button></form>`;
35
+ return page(ctx, 200, "Stop these emails?", `<p>You will not get ${escape(label)} any more.</p>${form}`);
36
+ }
37
+ await readRawBody(ctx.req);
38
+ await applyStop(ctx.app.db, stop.userId, stop.scope);
39
+ page(ctx, 200, "Done.", `<p>You will not get ${escape(label)} any more. Turn it back on any time on <a href="${escape(account)}">your Account page</a>.</p>`);
40
+ }
41
+ //# sourceMappingURL=stop-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stop-route.js","sourceRoot":"","sources":["../../src/mail/stop-route.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export interface SvixHeaders {
2
+ id: string | undefined;
3
+ timestamp: string | undefined;
4
+ signature: string | undefined;
5
+ }
6
+ /** True when a webhook body was signed with this `whsec_` secret (Svix, used by Resend) within the last 5 minutes. */
7
+ export declare function verifySvix(secret: string, h: SvixHeaders, rawBody: string, nowMs: number): boolean;
8
+ //# sourceMappingURL=svix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svix.d.ts","sourceRoot":"","sources":["../../src/mail/svix.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,sHAAsH;AACtH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAUlG"}
@@ -0,0 +1,17 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ const TOLERANCE_S = 5 * 60;
3
+ /** True when a webhook body was signed with this `whsec_` secret (Svix, used by Resend) within the last 5 minutes. */
4
+ export function verifySvix(secret, h, rawBody, nowMs) {
5
+ if (!h.id || !h.timestamp || !h.signature || !/^\d+$/.test(h.timestamp))
6
+ return false;
7
+ if (Math.abs(nowMs / 1000 - Number(h.timestamp)) > TOLERANCE_S)
8
+ return false;
9
+ const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
10
+ const expected = createHmac("sha256", key).update(`${h.id}.${h.timestamp}.${rawBody}`).digest();
11
+ return h.signature.split(" ").some((entry) => {
12
+ const [version, sig = ""] = entry.split(",");
13
+ const given = Buffer.from(sig, "base64");
14
+ return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
15
+ });
16
+ }
17
+ //# sourceMappingURL=svix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svix.js","sourceRoot":"","sources":["../../src/mail/svix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC;AAQ3B,sHAAsH;AACtH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,CAAc,EAAE,OAAe,EAAE,KAAa;IACvF,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IACtF,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,WAAW;QAAE,OAAO,KAAK,CAAC;IAC7E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;IAChG,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QAC3C,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACzC,OAAO,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Notice } from "../alerts/notify.js";
2
+ import type { EmailContent } from "./content.js";
3
+ import type { Mail } from "./mailer.js";
4
+ /** One email for one address: the layout as HTML, and the same content as plain text. */
5
+ export declare function renderMail(to: string, content: EmailContent): Promise<Mail>;
6
+ export declare const verifyEmailMail: (to: string, url: string) => Promise<Mail>;
7
+ export declare const resetPasswordMail: (to: string, url: string) => Promise<Mail>;
8
+ export declare const changeEmailMail: (to: string, newEmail: string, url: string) => Promise<Mail>;
9
+ export declare const deleteAccountMail: (to: string, url: string) => Promise<Mail>;
10
+ export declare const inviteMail: (to: string, inviter: string, org: string, role: string, url: string) => Promise<Mail>;
11
+ export declare const alertMail: (to: string, notice: Notice) => Promise<Mail>;
12
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/mail/templates.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAGxC,yFAAyF;AACzF,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;AAED,eAAO,MAAM,eAAe,OAAQ,MAAM,OAAO,MAAM,kBAA4C,CAAC;AACpG,eAAO,MAAM,iBAAiB,OAAQ,MAAM,OAAO,MAAM,kBAA8C,CAAC;AACxG,eAAO,MAAM,eAAe,OAAQ,MAAM,YAAY,MAAM,OAAO,MAAM,kBAAsD,CAAC;AAChI,eAAO,MAAM,iBAAiB,OAAQ,MAAM,OAAO,MAAM,kBAA8C,CAAC;AACxG,eAAO,MAAM,UAAU,OAAQ,MAAM,WAAW,MAAM,OAAO,MAAM,QAAQ,MAAM,OAAO,MAAM,kBAA2D,CAAC;AAC1J,eAAO,MAAM,SAAS,OAAQ,MAAM,UAAU,MAAM,kBAAyC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { render } from "@react-email/components";
2
+ import { createElement } from "react";
3
+ import * as emails from "./emails.js";
4
+ import { EmailLayout } from "./layout.js";
5
+ import { textOf } from "./text.js";
6
+ /** One email for one address: the layout as HTML, and the same content as plain text. */
7
+ export async function renderMail(to, content) {
8
+ const html = await render(createElement(EmailLayout, { content }));
9
+ const headers = content.unsubscribe && { "List-Unsubscribe": `<${content.unsubscribe.url}>`, "List-Unsubscribe-Post": "List-Unsubscribe=One-Click" };
10
+ return { kind: content.kind, to, subject: content.subject, html, text: textOf(content), ...(headers ? { headers } : {}) };
11
+ }
12
+ export const verifyEmailMail = (to, url) => renderMail(to, emails.verifyEmail(url));
13
+ export const resetPasswordMail = (to, url) => renderMail(to, emails.resetPassword(url));
14
+ export const changeEmailMail = (to, newEmail, url) => renderMail(to, emails.changeEmail(newEmail, url));
15
+ export const deleteAccountMail = (to, url) => renderMail(to, emails.deleteAccount(url));
16
+ export const inviteMail = (to, inviter, org, role, url) => renderMail(to, emails.invite(inviter, org, role, url));
17
+ export const alertMail = (to, notice) => renderMail(to, emails.alert(notice));
18
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/mail/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,EAAU,EAAE,OAAqB;IAChE,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,kBAAkB,EAAE,IAAI,OAAO,CAAC,WAAW,CAAC,GAAG,GAAG,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;IACrJ,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC5H,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAU,EAAE,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;AACpG,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAU,EAAE,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;AACxG,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAU,EAAE,QAAgB,EAAE,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;AAChI,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAU,EAAE,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;AACxG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,OAAe,EAAE,GAAW,EAAE,IAAY,EAAE,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAC1J,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EAAU,EAAE,MAAc,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { EmailContent } from "./content.js";
2
+ /** The plain-text version. Each link sits on its own line, so mail apps keep it whole. */
3
+ export declare function textOf(c: EmailContent): string;
4
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/mail/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,YAAY,EAAE,MAAM,cAAc,CAAC;AAUxD,0FAA0F;AAC1F,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAI9C"}
@@ -0,0 +1,17 @@
1
+ import { theme } from "./theme.js";
2
+ const block = (b) => {
3
+ if (b.kind === "facts")
4
+ return b.rows.map((r) => `${r.label}: ${r.value}`).join("\n");
5
+ if (b.kind === "stats")
6
+ return b.items.map((s) => `${s.label}: ${s.value}${s.change ? ` (${s.change})` : ""}`).join("\n");
7
+ if (b.kind === "list")
8
+ return [b.title, ...b.items.map((i) => `- ${i.text}${i.meta ? ` (${i.meta})` : ""}`)].join("\n");
9
+ return b.text;
10
+ };
11
+ /** The plain-text version. Each link sits on its own line, so mail apps keep it whole. */
12
+ export function textOf(c) {
13
+ const action = c.action ? [`${c.action.label}:`, c.action.url, ""] : [];
14
+ const stop = c.unsubscribe ? [`${c.unsubscribe.label}:`, c.unsubscribe.url] : [];
15
+ return [c.heading, "", ...c.blocks.flatMap((b) => [block(b), ""]), ...action, "--", c.footer, ...stop, theme.company, ""].join("\n");
16
+ }
17
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/mail/text.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,KAAK,GAAG,CAAC,CAAQ,EAAU,EAAE;IACjC,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtF,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1H,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxH,OAAO,CAAC,CAAC,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF,0FAA0F;AAC1F,MAAM,UAAU,MAAM,CAAC,CAAe;IACpC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjF,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvI,CAAC"}
@@ -0,0 +1,26 @@
1
+ export declare const theme: {
2
+ readonly page: "#f6f7f9";
3
+ readonly card: "#ffffff";
4
+ readonly line: "#e5e8ee";
5
+ readonly ink: "#1f1f1f";
6
+ readonly body: "#3d4350";
7
+ readonly muted: "#586273";
8
+ readonly accent: "#2563eb";
9
+ readonly accentText: "#ffffff";
10
+ readonly factsBg: "#f6f7f9";
11
+ readonly tone: {
12
+ readonly neutral: "#2563eb";
13
+ readonly good: "#22c55e";
14
+ readonly warn: "#f4b400";
15
+ readonly bad: "#e63946";
16
+ };
17
+ readonly sans: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
18
+ readonly mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
19
+ readonly radius: "12px";
20
+ readonly width: 560;
21
+ readonly logoUrl: "https://wizelens.com/brand/wizelens-mark-512.png";
22
+ readonly homeUrl: "https://wizelens.com";
23
+ readonly company: "WizeLens by WizeWorks LLC, Visalia, California";
24
+ };
25
+ export type Tone = keyof typeof theme.tone;
26
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/mail/theme.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,KAAK;aAChB,IAAI,EAAE,SAAS;aACf,IAAI,EAAE,SAAS;aACf,IAAI,EAAE,SAAS;aACf,GAAG,EAAE,SAAS;aACd,IAAI,EAAE,SAAS;aACf,KAAK,EAAE,SAAS;aAChB,MAAM,EAAE,SAAS;aACjB,UAAU,EAAE,SAAS;aACrB,OAAO,EAAE,SAAS;aAClB,IAAI;iBAAI,OAAO,EAAE,SAAS;iBAAE,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,SAAS;iBAAE,GAAG,EAAE,SAAS;;aAC5E,IAAI,EAAE,kGAAkG;aACxG,IAAI,EAAE,6EAA6E;aACnF,MAAM,EAAE,MAAM;aACd,KAAK,EAAE,GAAG;aACV,OAAO,EAAE,kDAAkD;aAC3D,OAAO,EAAE,sBAAsB;aAC/B,OAAO,EAAE,gDAAgD;CACjD,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,MAAM,OAAO,KAAK,CAAC,IAAI,CAAC"}
@@ -0,0 +1,21 @@
1
+ // The look of every email, from the WizeLens brand guide. Solid colors only. A new design changes this file and layout.tsx.
2
+ export const theme = {
3
+ page: "#f6f7f9",
4
+ card: "#ffffff",
5
+ line: "#e5e8ee",
6
+ ink: "#1f1f1f",
7
+ body: "#3d4350",
8
+ muted: "#586273",
9
+ accent: "#2563eb",
10
+ accentText: "#ffffff",
11
+ factsBg: "#f6f7f9",
12
+ tone: { neutral: "#2563eb", good: "#22c55e", warn: "#f4b400", bad: "#e63946" },
13
+ sans: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
14
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
15
+ radius: "12px",
16
+ width: 560,
17
+ logoUrl: "https://wizelens.com/brand/wizelens-mark-512.png",
18
+ homeUrl: "https://wizelens.com",
19
+ company: "WizeLens by WizeWorks LLC, Visalia, California",
20
+ };
21
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/mail/theme.ts"],"names":[],"mappings":"AAAA,4HAA4H;AAC5H,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,SAAS;IACd,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,SAAS;IACrB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE;IAC9E,IAAI,EAAE,kGAAkG;IACxG,IAAI,EAAE,6EAA6E;IACnF,MAAM,EAAE,MAAM;IACd,KAAK,EAAE,GAAG;IACV,OAAO,EAAE,kDAAkD;IAC3D,OAAO,EAAE,sBAAsB;IAC/B,OAAO,EAAE,gDAAgD;CACjD,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { CloudApp } from "../app.js";
2
+ import { type StopScope } from "../repo/mail-prefs.js";
3
+ export declare const UNSUBSCRIBE_PATH = "api/mail/unsubscribe";
4
+ export declare function signStop(secret: string, userId: string, scope: StopScope): string;
5
+ /** Who and what a stop token names, when any of the secrets signed it. */
6
+ export declare function readStop(secrets: readonly {
7
+ value: string;
8
+ }[], token: string): {
9
+ userId: string;
10
+ scope: StopScope;
11
+ } | null;
12
+ /** The stop link for one person and one kind of mail. */
13
+ export declare const stopUrl: (app: CloudApp, userId: string, scope: StopScope) => string;
14
+ //# sourceMappingURL=unsubscribe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/mail/unsubscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE1F,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAcvD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAGjF;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAAG,IAAI,CAe1H;AAED,yDAAyD;AACzD,eAAO,MAAM,OAAO,QAAS,QAAQ,UAAU,MAAM,SAAS,SAAS,KAAG,MACgC,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { DIGEST_PERIODS } from "../repo/mail-prefs.js";
3
+ export const UNSUBSCRIBE_PATH = "api/mail/unsubscribe";
4
+ // Stop links carry who and what, signed with the auth secret. They never expire: a stop link must keep working.
5
+ const keyOf = (secret) => createHmac("sha256", secret).update("wizelens-unsubscribe").digest();
6
+ const mac = (secret, payload) => createHmac("sha256", keyOf(secret)).update(payload).digest();
7
+ const scopeText = (s) => (typeof s === "object" ? `digest:${s.projectId}:${s.period}` : s);
8
+ function scopeOf(text) {
9
+ if (text === "team" || text === "health" || text === "digests")
10
+ return text;
11
+ const m = /^digest:([^:]+):([a-z]+)$/.exec(text);
12
+ return m && DIGEST_PERIODS.includes(m[2]) ? { projectId: m[1], period: m[2] } : null;
13
+ }
14
+ export function signStop(secret, userId, scope) {
15
+ const payload = Buffer.from(JSON.stringify([userId, scopeText(scope)])).toString("base64url");
16
+ return `${payload}.${mac(secret, payload).toString("base64url")}`;
17
+ }
18
+ /** Who and what a stop token names, when any of the secrets signed it. */
19
+ export function readStop(secrets, token) {
20
+ const [payload = "", sig = ""] = token.split(".");
21
+ const given = Buffer.from(sig, "base64url");
22
+ const ok = secrets.some((s) => {
23
+ const want = mac(s.value, payload);
24
+ return given.length === want.length && timingSafeEqual(given, want);
25
+ });
26
+ if (!ok)
27
+ return null;
28
+ try {
29
+ const [userId, scope] = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
30
+ const parsed = typeof scope === "string" ? scopeOf(scope) : null;
31
+ return typeof userId === "string" && parsed ? { userId, scope: parsed } : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** The stop link for one person and one kind of mail. */
38
+ export const stopUrl = (app, userId, scope) => `${app.baseUrl()}/_wizelens/${UNSUBSCRIBE_PATH}?token=${signStop(app.secrets[0].value, userId, scope)}`;
39
+ //# sourceMappingURL=unsubscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../../src/mail/unsubscribe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAqC,MAAM,uBAAuB,CAAC;AAE1F,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,gHAAgH;AAChH,MAAM,KAAK,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC,MAAM,EAAE,CAAC;AACvG,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,OAAe,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAE9G,MAAM,SAAS,GAAG,CAAC,CAAY,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEtG,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5E,MAAM,CAAC,GAAG,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAiB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACxH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,MAAc,EAAE,KAAgB;IACvE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC9F,OAAO,GAAG,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,OAAqC,EAAE,KAAa;IAC3E,MAAM,CAAC,OAAO,GAAG,EAAE,EAAE,GAAG,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACnC,OAAO,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAuB,CAAC;QAC7G,MAAM,MAAM,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACjE,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAa,EAAE,MAAc,EAAE,KAAgB,EAAU,EAAE,CACjF,GAAG,GAAG,CAAC,OAAO,EAAE,cAAc,gBAAgB,UAAU,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Ctx } from "../app.js";
2
+ export declare const RESEND_WEBHOOK_PATH = "api/mail/resend";
3
+ /** POST from Resend's webhook, signed with the endpoint's secret. Hard bounces and complaints go on the suppression list. */
4
+ export declare function resendWebhook(ctx: Ctx): Promise<void>;
5
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/mail/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAKrC,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAoBrD,6HAA6H;AAC7H,wBAAsB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB3D"}
@@ -0,0 +1,41 @@
1
+ import { fail, readRawBody } from "../http/http.js";
2
+ import { suppress } from "../repo/suppressions.js";
3
+ import { verifySvix } from "./svix.js";
4
+ export const RESEND_WEBHOOK_PATH = "api/mail/resend";
5
+ const header = (ctx, name) => {
6
+ const v = ctx.req.headers[name];
7
+ return Array.isArray(v) ? v[0] : v;
8
+ };
9
+ /** What to do with an event: only hard bounces and spam complaints change anything. */
10
+ function suppressionFor(e) {
11
+ if (e.type === "email.complained")
12
+ return { reason: "complaint", detail: "Marked as spam." };
13
+ if (e.type !== "email.bounced" || e.data?.bounce?.type !== "Permanent")
14
+ return null;
15
+ const b = e.data.bounce;
16
+ return { reason: "bounce", detail: [b.subType, b.message].filter((x) => typeof x === "string" && x).join(": ") || "Hard bounce." };
17
+ }
18
+ /** POST from Resend's webhook, signed with the endpoint's secret. Hard bounces and complaints go on the suppression list. */
19
+ export async function resendWebhook(ctx) {
20
+ const secret = ctx.app.config.resendWebhookSecret;
21
+ if (!secret)
22
+ fail(404, "Not found.");
23
+ const raw = await readRawBody(ctx.req);
24
+ const signed = { id: header(ctx, "svix-id"), timestamp: header(ctx, "svix-timestamp"), signature: header(ctx, "svix-signature") };
25
+ if (!verifySvix(secret, signed, raw, ctx.app.config.now()))
26
+ fail(401, "The webhook signature does not match.");
27
+ let event;
28
+ try {
29
+ event = JSON.parse(raw);
30
+ }
31
+ catch {
32
+ return fail(400, "The body is not valid JSON.");
33
+ }
34
+ const action = suppressionFor(event);
35
+ const to = Array.isArray(event.data?.to) ? event.data.to.filter((x) => typeof x === "string") : [];
36
+ if (action)
37
+ for (const email of to)
38
+ await suppress(ctx.app.db, email, action.reason, action.detail, ctx.app.config.now());
39
+ ctx.res.writeHead(204).end();
40
+ }
41
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/mail/webhook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type Headline, type TimeRange } from "@wizeworks/lens-core";
2
+ import type { Queryable } from "../db/db.js";
3
+ import type { DigestPeriod } from "../repo/mail-prefs.js";
4
+ export declare const RANGE_OF: Record<DigestPeriod, TimeRange>;
5
+ /** One project's numbers for one digest: the same models the dashboard pages use, plus the slowest endpoints and alerts. */
6
+ export interface DigestData {
7
+ empty: boolean;
8
+ requests: Headline;
9
+ errorRate: Headline;
10
+ avgMs: Headline;
11
+ sqlQueries: Headline;
12
+ p95Ms: number | null;
13
+ errors: {
14
+ type: string;
15
+ message: string;
16
+ count: number;
17
+ }[];
18
+ slowest: {
19
+ endpoint: string;
20
+ p95Ms: number;
21
+ count: number;
22
+ }[];
23
+ insights: {
24
+ title: string;
25
+ occurrences: number;
26
+ }[];
27
+ alertsFired: number;
28
+ }
29
+ /** The digest's numbers, or null when the organization keeps data for less time than the digest covers. */
30
+ export declare function digestData(q: Queryable, projectId: string, retentionDays: number, period: DigestPeriod, now: number): Promise<DigestData | null>;
31
+ //# sourceMappingURL=digest-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest-data.d.ts","sourceRoot":"","sources":["../../src/notify/digest-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,QAAQ,EAAqC,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtI,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAI1D,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,YAAY,EAAE,SAAS,CAAkD,CAAC;AAExG,4HAA4H;AAC5H,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,QAAQ,CAAC;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,UAAU,EAAE,QAAQ,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3D,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,2GAA2G;AAC3G,wBAAsB,UAAU,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAoBtJ"}
@@ -0,0 +1,37 @@
1
+ import { alignNow, FACT_SLOW_QUERY_MS, pageContext, runPage, TIME_RANGES } from "@wizeworks/lens-core";
2
+ import { cloudExecutor, cloudSettling } from "../scale-exec.js";
3
+ const DAY_MS = 86_400_000;
4
+ export const RANGE_OF = { daily: "24h", weekly: "7d", monthly: "30d" };
5
+ async function slowest(q, projectId, from, to) {
6
+ const rows = await q.query(`SELECT endpoint, percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms) AS p95, count(*)::int AS n FROM fact_requests
7
+ WHERE project_id = $1 AND start_ms >= $2 AND start_ms < $3 GROUP BY endpoint HAVING count(*) >= 5 ORDER BY p95 DESC LIMIT 5`, [projectId, from, to]);
8
+ return rows.map((r) => ({ endpoint: r.endpoint, p95Ms: Number(r.p95), count: r.n }));
9
+ }
10
+ async function alertsFired(q, projectId, from, to) {
11
+ const [row] = await q.query("SELECT count(*)::int AS n FROM alert_events WHERE project_id = $1 AND kind = 'fired' AND time_ms >= $2 AND time_ms < $3", [projectId, from, to]);
12
+ return row?.n ?? 0;
13
+ }
14
+ /** The digest's numbers, or null when the organization keeps data for less time than the digest covers. */
15
+ export async function digestData(q, projectId, retentionDays, period, now) {
16
+ const range = RANGE_OF[period];
17
+ if (TIME_RANGES[range].ms > retentionDays * DAY_MS)
18
+ return null;
19
+ const pc = pageContext(range, alignNow(now), {}, { slowQueryMs: FACT_SLOW_QUERY_MS }, await cloudSettling(q, projectId));
20
+ const ex = cloudExecutor(q, projectId, retentionDays * DAY_MS);
21
+ const [overview, errors] = await Promise.all([runPage("overview", pc, ex), runPage("errors", pc, ex)]);
22
+ const { from, to } = pc.window;
23
+ const h = overview.headlines;
24
+ return {
25
+ empty: !h.total.value && !h.total.previous,
26
+ requests: h.total,
27
+ errorRate: h.errorRate,
28
+ avgMs: h.avgDuration,
29
+ sqlQueries: h.sqlQueries,
30
+ p95Ms: overview.timing.find((t) => t.key === "request")?.p95 ?? null,
31
+ errors: errors.groups.slice(0, 5).map((g) => ({ type: g.type, message: g.message, count: g.count })),
32
+ slowest: await slowest(q, projectId, from, to),
33
+ insights: overview.insights.map((i) => ({ title: i.title, occurrences: i.occurrences })),
34
+ alertsFired: await alertsFired(q, projectId, from, to),
35
+ };
36
+ }
37
+ //# sourceMappingURL=digest-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest-data.js","sourceRoot":"","sources":["../../src/notify/digest-data.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { CloudApp } from "../app.js";
2
+ import { type DigestPeriod } from "../repo/mail-prefs.js";
3
+ interface Local {
4
+ date: string;
5
+ month: string;
6
+ weekday: number;
7
+ day: number;
8
+ hour: number;
9
+ }
10
+ /** The date and hour in a time zone. Unknown zones count as UTC. */
11
+ export declare function localOf(ms: number, timeZone: string): Local;
12
+ /** Digests due at this local time, each with the key that makes it go out once: every day, on Mondays, on the 1st. */
13
+ export declare function dueKeys(l: Local, hour: number): [DigestPeriod, string][];
14
+ /** Send the digests that are due now. People with digests off, and unconfirmed addresses when mail is real, get none. */
15
+ export declare function sendDigests(app: CloudApp): Promise<void>;
16
+ export {};
17
+ //# sourceMappingURL=digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/notify/digest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C,OAAO,EAAkB,KAAK,YAAY,EAAiB,MAAM,uBAAuB,CAAC;AAGzF,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,oEAAoE;AACpE,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAY3D;AAED,sHAAsH;AACtH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,CAMxE;AAwDD,yHAAyH;AACzH,wBAAsB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9D"}
@@ -0,0 +1,76 @@
1
+ import { digest } from "../mail/emails-digest.js";
2
+ import { renderMail } from "../mail/templates.js";
3
+ import { stopUrl } from "../mail/unsubscribe.js";
4
+ import { DEFAULT_DIGEST, digestPrefsOf } from "../repo/mail-prefs.js";
5
+ import { digestData } from "./digest-data.js";
6
+ /** The date and hour in a time zone. Unknown zones count as UTC. */
7
+ export function localOf(ms, timeZone) {
8
+ const fmt = (tz) => new Intl.DateTimeFormat("en-CA", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", hourCycle: "h23", weekday: "short" }).formatToParts(new Date(ms));
9
+ let parts;
10
+ try {
11
+ parts = fmt(timeZone);
12
+ }
13
+ catch {
14
+ parts = fmt("UTC");
15
+ }
16
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
17
+ const weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].indexOf(get("weekday")) + 1;
18
+ return { date: `${get("year")}-${get("month")}-${get("day")}`, month: `${get("year")}-${get("month")}`, weekday, day: Number(get("day")), hour: Number(get("hour")) };
19
+ }
20
+ /** Digests due at this local time, each with the key that makes it go out once: every day, on Mondays, on the 1st. */
21
+ export function dueKeys(l, hour) {
22
+ if (l.hour < hour)
23
+ return [];
24
+ const due = [["daily", `D:${l.date}`]];
25
+ if (l.weekday === 1)
26
+ due.push(["weekly", `W:${l.date}`]);
27
+ if (l.day === 1)
28
+ due.push(["monthly", `M:${l.month}`]);
29
+ return due;
30
+ }
31
+ /** Claim one send. Only the instance whose row went in sends, so a digest goes out once. */
32
+ const claim = async (app, userId, projectId, period, key) => (await app.db.query("INSERT INTO digest_sends (user_id, project_id, period, period_key, sent_ms) VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING RETURNING period", [
33
+ userId,
34
+ projectId,
35
+ period,
36
+ key,
37
+ app.config.now(),
38
+ ])).length > 0;
39
+ async function sendOne(app, who, p, period, key, cache) {
40
+ if (!(await claim(app, who.id, p.id, period, key)))
41
+ return;
42
+ try {
43
+ const id = `${p.id}:${period}`;
44
+ if (!cache.has(id))
45
+ cache.set(id, digestData(app.db, p.id, p.retention_days, period, app.config.now()));
46
+ const data = await cache.get(id);
47
+ if (!data || data.empty)
48
+ return;
49
+ const links = { project: `${app.baseUrl()}/_wizelens/#/?project=${encodeURIComponent(p.id)}`, stop: stopUrl(app, who.id, { projectId: p.id, period }) };
50
+ app.mailer.send(renderMail(who.email, digest(p.name, p.org, period, data, links)));
51
+ }
52
+ catch (err) {
53
+ await app.db.query("DELETE FROM digest_sends WHERE user_id = $1 AND project_id = $2 AND period = $3 AND period_key = $4", [who.id, p.id, period, key]);
54
+ throw err;
55
+ }
56
+ }
57
+ async function digestsFor(app, who, cache) {
58
+ const due = dueKeys(localOf(app.config.now(), who.time_zone ?? "UTC"), who.digest_hour ?? 8);
59
+ if (due.length === 0)
60
+ return;
61
+ const projects = await app.db.query("SELECT p.id, p.name, o.name AS org, o.retention_days FROM members m JOIN orgs o ON o.id = m.org_id JOIN projects p ON p.org_id = o.id WHERE m.user_id = $1 ORDER BY p.name", [who.id]);
62
+ const prefs = await digestPrefsOf(app.db, who.id);
63
+ for (const p of projects)
64
+ for (const [period, key] of due)
65
+ if ((prefs.get(p.id) ?? DEFAULT_DIGEST)[period])
66
+ await sendOne(app, who, p, period, key, cache);
67
+ }
68
+ /** Send the digests that are due now. People with digests off, and unconfirmed addresses when mail is real, get none. */
69
+ export async function sendDigests(app) {
70
+ const people = await app.db.query(`SELECT u.id, u.email, p.time_zone, p.digest_hour FROM users u LEFT JOIN mail_prefs p ON p.user_id = u.id
71
+ WHERE COALESCE(p.digests, TRUE) AND (u.email_verified OR NOT $1)`, [app.mailer.live]);
72
+ const cache = new Map();
73
+ for (const who of people)
74
+ await digestsFor(app, who, cache).catch((err) => console.error(`[wizelens-cloud] digest for ${who.id} failed:`, err));
75
+ }
76
+ //# sourceMappingURL=digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/notify/digest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { CloudApp } from "../app.js";
2
+ /** Silence this long counts as quiet, for a project that sends around the clock. */
3
+ export declare const QUIET_MS: number;
4
+ /** Tell owners and admins when a busy project stops sending data, and when it starts again. */
5
+ export declare function checkHealth(app: CloudApp): Promise<void>;
6
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/notify/health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAQ1C,oFAAoF;AACpF,eAAO,MAAM,QAAQ,QAAW,CAAC;AA0CjC,+FAA+F;AAC/F,wBAAsB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9D"}
@@ -0,0 +1,43 @@
1
+ import { projectBack, projectQuiet } from "../mail/emails-health.js";
2
+ import { renderMail } from "../mail/templates.js";
3
+ import { stopUrl } from "../mail/unsubscribe.js";
4
+ import { managersOf } from "../repo/mail-prefs.js";
5
+ const HOUR = 3_600_000;
6
+ /** Silence this long counts as quiet, for a project that sends around the clock. */
7
+ export const QUIET_MS = 2 * HOUR;
8
+ /** "Around the clock": data in at least 58 of the 72 hours before the last data. */
9
+ const BUSY_HOURS = 58;
10
+ async function busy(app, projectId, lastMs) {
11
+ const [row] = await app.db.query("SELECT count(DISTINCT floor(start_ms / 3600000))::int AS n FROM traces WHERE project_id = $1 AND start_ms > $2 AND start_ms <= $3", [projectId, lastMs - 72 * HOUR, lastMs]);
12
+ return (row?.n ?? 0) >= BUSY_HOURS;
13
+ }
14
+ async function tell(app, p, make) {
15
+ for (const m of await managersOf(app.db, p.org_id, "health", app.mailer.live))
16
+ app.mailer.send(renderMail(m.email, make(stopUrl(app, m.id, "health"))));
17
+ }
18
+ /** Check one project. The first look only records its state, so an upgrade sends nothing for projects already quiet. */
19
+ async function checkProject(app, p, now) {
20
+ if (p.last_ms === null)
21
+ return;
22
+ const quiet = now - p.last_ms > QUIET_MS && (await busy(app, p.id, p.last_ms));
23
+ if (p.quiet === quiet)
24
+ return;
25
+ const saved = await app.db.query(`INSERT INTO project_health (project_id, quiet, changed_ms) VALUES ($1, $2, $3)
26
+ ON CONFLICT (project_id) DO UPDATE SET quiet = $2, changed_ms = $3 WHERE project_health.quiet <> $2 RETURNING project_id`, [p.id, quiet, now]);
27
+ if (p.quiet === null || saved.length === 0)
28
+ return;
29
+ const url = `${app.baseUrl()}/_wizelens/#/?project=${encodeURIComponent(p.id)}`;
30
+ if (quiet)
31
+ await tell(app, p, (stop) => projectQuiet(p.name, p.org, p.last_ms, now, url, stop));
32
+ else
33
+ await tell(app, p, (stop) => projectBack(p.name, p.org, now - (p.changed_ms ?? now) + QUIET_MS, url, stop));
34
+ }
35
+ /** Tell owners and admins when a busy project stops sending data, and when it starts again. */
36
+ export async function checkHealth(app) {
37
+ const now = app.config.now();
38
+ const projects = await app.db.query(`SELECT p.id, p.org_id, p.name, o.name AS org, (SELECT max(t.start_ms) FROM traces t WHERE t.project_id = p.id) AS last_ms, h.quiet, h.changed_ms
39
+ FROM projects p JOIN orgs o ON o.id = p.org_id LEFT JOIN project_health h ON h.project_id = p.id`);
40
+ for (const p of projects)
41
+ await checkProject(app, p, now);
42
+ }
43
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/notify/health.ts"],"names":[],"mappings":"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"}