@wizeworks/lens-cloud 0.0.0-stage → 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 (351) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -2
  3. package/dist/alerts/evaluate.d.ts +10 -0
  4. package/dist/alerts/evaluate.d.ts.map +1 -0
  5. package/dist/alerts/evaluate.js +81 -0
  6. package/dist/alerts/evaluate.js.map +1 -0
  7. package/dist/alerts/notify.d.ts +28 -0
  8. package/dist/alerts/notify.d.ts.map +1 -0
  9. package/dist/alerts/notify.js +52 -0
  10. package/dist/alerts/notify.js.map +1 -0
  11. package/dist/app.d.ts +26 -0
  12. package/dist/app.d.ts.map +1 -0
  13. package/dist/app.js +2 -0
  14. package/dist/app.js.map +1 -0
  15. package/dist/auth/auth.d.ts +51 -0
  16. package/dist/auth/auth.d.ts.map +1 -0
  17. package/dist/auth/auth.js +63 -0
  18. package/dist/auth/auth.js.map +1 -0
  19. package/dist/auth/bridge.d.ts +7 -0
  20. package/dist/auth/bridge.d.ts.map +1 -0
  21. package/dist/auth/bridge.js +39 -0
  22. package/dist/auth/bridge.js.map +1 -0
  23. package/dist/auth/crypto.d.ts +14 -0
  24. package/dist/auth/crypto.d.ts.map +1 -0
  25. package/dist/auth/crypto.js +16 -0
  26. package/dist/auth/crypto.js.map +1 -0
  27. package/dist/auth/delete-user.d.ts +16 -0
  28. package/dist/auth/delete-user.d.ts.map +1 -0
  29. package/dist/auth/delete-user.js +21 -0
  30. package/dist/auth/delete-user.js.map +1 -0
  31. package/dist/auth/deps.d.ts +27 -0
  32. package/dist/auth/deps.d.ts.map +1 -0
  33. package/dist/auth/deps.js +3 -0
  34. package/dist/auth/deps.js.map +1 -0
  35. package/dist/auth/email-token.d.ts +8 -0
  36. package/dist/auth/email-token.d.ts.map +1 -0
  37. package/dist/auth/email-token.js +15 -0
  38. package/dist/auth/email-token.js.map +1 -0
  39. package/dist/auth/hooks.d.ts +4 -0
  40. package/dist/auth/hooks.d.ts.map +1 -0
  41. package/dist/auth/hooks.js +143 -0
  42. package/dist/auth/hooks.js.map +1 -0
  43. package/dist/auth/options.d.ts +17 -0
  44. package/dist/auth/options.d.ts.map +1 -0
  45. package/dist/auth/options.js +98 -0
  46. package/dist/auth/options.js.map +1 -0
  47. package/dist/auth/password.d.ts +10 -0
  48. package/dist/auth/password.d.ts.map +1 -0
  49. package/dist/auth/password.js +22 -0
  50. package/dist/auth/password.js.map +1 -0
  51. package/dist/auth/plugins.d.ts +4 -0
  52. package/dist/auth/plugins.d.ts.map +1 -0
  53. package/dist/auth/plugins.js +45 -0
  54. package/dist/auth/plugins.js.map +1 -0
  55. package/dist/auth/principal.d.ts +38 -0
  56. package/dist/auth/principal.d.ts.map +1 -0
  57. package/dist/auth/principal.js +69 -0
  58. package/dist/auth/principal.js.map +1 -0
  59. package/dist/auth/roles.d.ts +50 -0
  60. package/dist/auth/roles.d.ts.map +1 -0
  61. package/dist/auth/roles.js +18 -0
  62. package/dist/auth/roles.js.map +1 -0
  63. package/dist/auth/schema-names.d.ts +130 -0
  64. package/dist/auth/schema-names.d.ts.map +1 -0
  65. package/dist/auth/schema-names.js +53 -0
  66. package/dist/auth/schema-names.js.map +1 -0
  67. package/dist/auth/secret.d.ts +12 -0
  68. package/dist/auth/secret.d.ts.map +1 -0
  69. package/dist/auth/secret.js +32 -0
  70. package/dist/auth/secret.js.map +1 -0
  71. package/dist/auth/security-notices.d.ts +34 -0
  72. package/dist/auth/security-notices.d.ts.map +1 -0
  73. package/dist/auth/security-notices.js +81 -0
  74. package/dist/auth/security-notices.js.map +1 -0
  75. package/dist/auth/signup.d.ts +23 -0
  76. package/dist/auth/signup.d.ts.map +1 -0
  77. package/dist/auth/signup.js +61 -0
  78. package/dist/auth/signup.js.map +1 -0
  79. package/dist/auth/sso.d.ts +21 -0
  80. package/dist/auth/sso.d.ts.map +1 -0
  81. package/dist/auth/sso.js +35 -0
  82. package/dist/auth/sso.js.map +1 -0
  83. package/dist/auth/wire.d.ts +16 -0
  84. package/dist/auth/wire.d.ts.map +1 -0
  85. package/dist/auth/wire.js +47 -0
  86. package/dist/auth/wire.js.map +1 -0
  87. package/dist/cli-args.d.ts +5 -0
  88. package/dist/cli-args.d.ts.map +1 -0
  89. package/dist/cli-args.js +86 -0
  90. package/dist/cli-args.js.map +1 -0
  91. package/dist/cli.d.ts +3 -0
  92. package/dist/cli.d.ts.map +1 -0
  93. package/dist/cli.js +44 -0
  94. package/dist/cli.js.map +1 -0
  95. package/dist/config.d.ts +55 -0
  96. package/dist/config.d.ts.map +1 -0
  97. package/dist/config.js +55 -0
  98. package/dist/config.js.map +1 -0
  99. package/dist/db/db.d.ts +22 -0
  100. package/dist/db/db.d.ts.map +1 -0
  101. package/dist/db/db.js +64 -0
  102. package/dist/db/db.js.map +1 -0
  103. package/dist/db/migrate.d.ts +6 -0
  104. package/dist/db/migrate.d.ts.map +1 -0
  105. package/dist/db/migrate.js +35 -0
  106. package/dist/db/migrate.js.map +1 -0
  107. package/dist/db/schema-auth.d.ts +2 -0
  108. package/dist/db/schema-auth.d.ts.map +1 -0
  109. package/dist/db/schema-auth.js +93 -0
  110. package/dist/db/schema-auth.js.map +1 -0
  111. package/dist/db/schema-mail.d.ts +2 -0
  112. package/dist/db/schema-mail.d.ts.map +1 -0
  113. package/dist/db/schema-mail.js +8 -0
  114. package/dist/db/schema-mail.js.map +1 -0
  115. package/dist/db/schema-notify.d.ts +2 -0
  116. package/dist/db/schema-notify.d.ts.map +1 -0
  117. package/dist/db/schema-notify.js +26 -0
  118. package/dist/db/schema-notify.js.map +1 -0
  119. package/dist/db/schema-sso.d.ts +2 -0
  120. package/dist/db/schema-sso.d.ts.map +1 -0
  121. package/dist/db/schema-sso.js +17 -0
  122. package/dist/db/schema-sso.js.map +1 -0
  123. package/dist/db/schema.d.ts +3 -0
  124. package/dist/db/schema.d.ts.map +1 -0
  125. package/dist/db/schema.js +208 -0
  126. package/dist/db/schema.js.map +1 -0
  127. package/dist/http/http.d.ts +19 -0
  128. package/dist/http/http.d.ts.map +1 -0
  129. package/dist/http/http.js +66 -0
  130. package/dist/http/http.js.map +1 -0
  131. package/dist/http/limiter.d.ts +11 -0
  132. package/dist/http/limiter.d.ts.map +1 -0
  133. package/dist/http/limiter.js +31 -0
  134. package/dist/http/limiter.js.map +1 -0
  135. package/dist/http/router.d.ts +21 -0
  136. package/dist/http/router.d.ts.map +1 -0
  137. package/dist/http/router.js +28 -0
  138. package/dist/http/router.js.map +1 -0
  139. package/dist/index.d.ts +7 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +4 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/jobs.d.ts +4 -0
  144. package/dist/jobs.d.ts.map +1 -0
  145. package/dist/jobs.js +53 -0
  146. package/dist/jobs.js.map +1 -0
  147. package/dist/mail/blocks.d.ts +14 -0
  148. package/dist/mail/blocks.d.ts.map +1 -0
  149. package/dist/mail/blocks.js +35 -0
  150. package/dist/mail/blocks.js.map +1 -0
  151. package/dist/mail/content.d.ts +68 -0
  152. package/dist/mail/content.d.ts.map +1 -0
  153. package/dist/mail/content.js +6 -0
  154. package/dist/mail/content.js.map +1 -0
  155. package/dist/mail/emails-digest.d.ts +10 -0
  156. package/dist/mail/emails-digest.d.ts.map +1 -0
  157. package/dist/mail/emails-digest.js +55 -0
  158. package/dist/mail/emails-digest.js.map +1 -0
  159. package/dist/mail/emails-health.d.ts +6 -0
  160. package/dist/mail/emails-health.d.ts.map +1 -0
  161. package/dist/mail/emails-health.js +36 -0
  162. package/dist/mail/emails-health.js.map +1 -0
  163. package/dist/mail/emails-security.d.ts +14 -0
  164. package/dist/mail/emails-security.d.ts.map +1 -0
  165. package/dist/mail/emails-security.js +27 -0
  166. package/dist/mail/emails-security.js.map +1 -0
  167. package/dist/mail/emails-team.d.ts +11 -0
  168. package/dist/mail/emails-team.d.ts.map +1 -0
  169. package/dist/mail/emails-team.js +45 -0
  170. package/dist/mail/emails-team.js.map +1 -0
  171. package/dist/mail/emails.d.ts +9 -0
  172. package/dist/mail/emails.d.ts.map +1 -0
  173. package/dist/mail/emails.js +49 -0
  174. package/dist/mail/emails.js.map +1 -0
  175. package/dist/mail/layout.d.ts +6 -0
  176. package/dist/mail/layout.d.ts.map +1 -0
  177. package/dist/mail/layout.js +10 -0
  178. package/dist/mail/layout.js.map +1 -0
  179. package/dist/mail/mailer.d.ts +25 -0
  180. package/dist/mail/mailer.d.ts.map +1 -0
  181. package/dist/mail/mailer.js +49 -0
  182. package/dist/mail/mailer.js.map +1 -0
  183. package/dist/mail/preview.d.ts +6 -0
  184. package/dist/mail/preview.d.ts.map +1 -0
  185. package/dist/mail/preview.js +26 -0
  186. package/dist/mail/preview.js.map +1 -0
  187. package/dist/mail/resend.d.ts +11 -0
  188. package/dist/mail/resend.d.ts.map +1 -0
  189. package/dist/mail/resend.js +34 -0
  190. package/dist/mail/resend.js.map +1 -0
  191. package/dist/mail/samples.d.ts +3 -0
  192. package/dist/mail/samples.d.ts.map +1 -0
  193. package/dist/mail/samples.js +63 -0
  194. package/dist/mail/samples.js.map +1 -0
  195. package/dist/mail/stop-route.d.ts +4 -0
  196. package/dist/mail/stop-route.d.ts.map +1 -0
  197. package/dist/mail/stop-route.js +41 -0
  198. package/dist/mail/stop-route.js.map +1 -0
  199. package/dist/mail/svix.d.ts +8 -0
  200. package/dist/mail/svix.d.ts.map +1 -0
  201. package/dist/mail/svix.js +17 -0
  202. package/dist/mail/svix.js.map +1 -0
  203. package/dist/mail/templates.d.ts +12 -0
  204. package/dist/mail/templates.d.ts.map +1 -0
  205. package/dist/mail/templates.js +18 -0
  206. package/dist/mail/templates.js.map +1 -0
  207. package/dist/mail/text.d.ts +4 -0
  208. package/dist/mail/text.d.ts.map +1 -0
  209. package/dist/mail/text.js +17 -0
  210. package/dist/mail/text.js.map +1 -0
  211. package/dist/mail/theme.d.ts +26 -0
  212. package/dist/mail/theme.d.ts.map +1 -0
  213. package/dist/mail/theme.js +21 -0
  214. package/dist/mail/theme.js.map +1 -0
  215. package/dist/mail/unsubscribe.d.ts +14 -0
  216. package/dist/mail/unsubscribe.d.ts.map +1 -0
  217. package/dist/mail/unsubscribe.js +39 -0
  218. package/dist/mail/unsubscribe.js.map +1 -0
  219. package/dist/mail/webhook.d.ts +5 -0
  220. package/dist/mail/webhook.d.ts.map +1 -0
  221. package/dist/mail/webhook.js +41 -0
  222. package/dist/mail/webhook.js.map +1 -0
  223. package/dist/notify/digest-data.d.ts +31 -0
  224. package/dist/notify/digest-data.d.ts.map +1 -0
  225. package/dist/notify/digest-data.js +37 -0
  226. package/dist/notify/digest-data.js.map +1 -0
  227. package/dist/notify/digest.d.ts +17 -0
  228. package/dist/notify/digest.d.ts.map +1 -0
  229. package/dist/notify/digest.js +76 -0
  230. package/dist/notify/digest.js.map +1 -0
  231. package/dist/notify/health.d.ts +6 -0
  232. package/dist/notify/health.d.ts.map +1 -0
  233. package/dist/notify/health.js +43 -0
  234. package/dist/notify/health.js.map +1 -0
  235. package/dist/notify/team-describe.d.ts +22 -0
  236. package/dist/notify/team-describe.d.ts.map +1 -0
  237. package/dist/notify/team-describe.js +67 -0
  238. package/dist/notify/team-describe.js.map +1 -0
  239. package/dist/notify/team.d.ts +6 -0
  240. package/dist/notify/team.d.ts.map +1 -0
  241. package/dist/notify/team.js +56 -0
  242. package/dist/notify/team.js.map +1 -0
  243. package/dist/repo/alerts.d.ts +52 -0
  244. package/dist/repo/alerts.d.ts.map +1 -0
  245. package/dist/repo/alerts.js +38 -0
  246. package/dist/repo/alerts.js.map +1 -0
  247. package/dist/repo/audit.d.ts +11 -0
  248. package/dist/repo/audit.d.ts.map +1 -0
  249. package/dist/repo/audit.js +17 -0
  250. package/dist/repo/audit.js.map +1 -0
  251. package/dist/repo/auth-prune.d.ts +4 -0
  252. package/dist/repo/auth-prune.d.ts.map +1 -0
  253. package/dist/repo/auth-prune.js +8 -0
  254. package/dist/repo/auth-prune.js.map +1 -0
  255. package/dist/repo/mail-prefs.d.ts +35 -0
  256. package/dist/repo/mail-prefs.d.ts.map +1 -0
  257. package/dist/repo/mail-prefs.js +34 -0
  258. package/dist/repo/mail-prefs.js.map +1 -0
  259. package/dist/repo/metric-read.d.ts +12 -0
  260. package/dist/repo/metric-read.d.ts.map +1 -0
  261. package/dist/repo/metric-read.js +11 -0
  262. package/dist/repo/metric-read.js.map +1 -0
  263. package/dist/repo/metric-write.d.ts +9 -0
  264. package/dist/repo/metric-write.d.ts.map +1 -0
  265. package/dist/repo/metric-write.js +53 -0
  266. package/dist/repo/metric-write.js.map +1 -0
  267. package/dist/repo/orgs.d.ts +37 -0
  268. package/dist/repo/orgs.d.ts.map +1 -0
  269. package/dist/repo/orgs.js +33 -0
  270. package/dist/repo/orgs.js.map +1 -0
  271. package/dist/repo/projects.d.ts +37 -0
  272. package/dist/repo/projects.d.ts.map +1 -0
  273. package/dist/repo/projects.js +57 -0
  274. package/dist/repo/projects.js.map +1 -0
  275. package/dist/repo/suppressions.d.ts +8 -0
  276. package/dist/repo/suppressions.d.ts.map +1 -0
  277. package/dist/repo/suppressions.js +16 -0
  278. package/dist/repo/suppressions.js.map +1 -0
  279. package/dist/repo/trace-read.d.ts +28 -0
  280. package/dist/repo/trace-read.d.ts.map +1 -0
  281. package/dist/repo/trace-read.js +76 -0
  282. package/dist/repo/trace-read.js.map +1 -0
  283. package/dist/repo/trace-write.d.ts +9 -0
  284. package/dist/repo/trace-write.d.ts.map +1 -0
  285. package/dist/repo/trace-write.js +82 -0
  286. package/dist/repo/trace-write.js.map +1 -0
  287. package/dist/repo/users.d.ts +13 -0
  288. package/dist/repo/users.d.ts.map +1 -0
  289. package/dist/repo/users.js +7 -0
  290. package/dist/repo/users.js.map +1 -0
  291. package/dist/routes/alerts.d.ts +4 -0
  292. package/dist/routes/alerts.d.ts.map +1 -0
  293. package/dist/routes/alerts.js +116 -0
  294. package/dist/routes/alerts.js.map +1 -0
  295. package/dist/routes/data.d.ts +7 -0
  296. package/dist/routes/data.d.ts.map +1 -0
  297. package/dist/routes/data.js +74 -0
  298. package/dist/routes/data.js.map +1 -0
  299. package/dist/routes/index.d.ts +5 -0
  300. package/dist/routes/index.d.ts.map +1 -0
  301. package/dist/routes/index.js +25 -0
  302. package/dist/routes/index.js.map +1 -0
  303. package/dist/routes/ingest.d.ts +8 -0
  304. package/dist/routes/ingest.d.ts.map +1 -0
  305. package/dist/routes/ingest.js +38 -0
  306. package/dist/routes/ingest.js.map +1 -0
  307. package/dist/routes/mcp.d.ts +4 -0
  308. package/dist/routes/mcp.d.ts.map +1 -0
  309. package/dist/routes/mcp.js +52 -0
  310. package/dist/routes/mcp.js.map +1 -0
  311. package/dist/routes/me.d.ts +4 -0
  312. package/dist/routes/me.d.ts.map +1 -0
  313. package/dist/routes/me.js +37 -0
  314. package/dist/routes/me.js.map +1 -0
  315. package/dist/routes/metrics.d.ts +13 -0
  316. package/dist/routes/metrics.d.ts.map +1 -0
  317. package/dist/routes/metrics.js +29 -0
  318. package/dist/routes/metrics.js.map +1 -0
  319. package/dist/routes/models.d.ts +4 -0
  320. package/dist/routes/models.d.ts.map +1 -0
  321. package/dist/routes/models.js +71 -0
  322. package/dist/routes/models.js.map +1 -0
  323. package/dist/routes/notify.d.ts +4 -0
  324. package/dist/routes/notify.d.ts.map +1 -0
  325. package/dist/routes/notify.js +55 -0
  326. package/dist/routes/notify.js.map +1 -0
  327. package/dist/routes/orgs.d.ts +4 -0
  328. package/dist/routes/orgs.d.ts.map +1 -0
  329. package/dist/routes/orgs.js +73 -0
  330. package/dist/routes/orgs.js.map +1 -0
  331. package/dist/routes/projects.d.ts +4 -0
  332. package/dist/routes/projects.d.ts.map +1 -0
  333. package/dist/routes/projects.js +55 -0
  334. package/dist/routes/projects.js.map +1 -0
  335. package/dist/routes/shares.d.ts +11 -0
  336. package/dist/routes/shares.d.ts.map +1 -0
  337. package/dist/routes/shares.js +61 -0
  338. package/dist/routes/shares.js.map +1 -0
  339. package/dist/scale-exec.d.ts +11 -0
  340. package/dist/scale-exec.d.ts.map +1 -0
  341. package/dist/scale-exec.js +26 -0
  342. package/dist/scale-exec.js.map +1 -0
  343. package/dist/server.d.ts +15 -0
  344. package/dist/server.d.ts.map +1 -0
  345. package/dist/server.js +126 -0
  346. package/dist/server.js.map +1 -0
  347. package/dist/settle.d.ts +26 -0
  348. package/dist/settle.d.ts.map +1 -0
  349. package/dist/settle.js +85 -0
  350. package/dist/settle.js.map +1 -0
  351. package/package.json +76 -3
@@ -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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"}
@@ -0,0 +1,22 @@
1
+ import type { Queryable } from "../db/db.js";
2
+ /** Audit actions that become team notices. MCP calls and the first organization are not news. */
3
+ export declare const NOTICED: Set<string>;
4
+ export interface AuditRow {
5
+ id: string;
6
+ org_id: string;
7
+ actor: string;
8
+ action: string;
9
+ detail: string;
10
+ time_ms: number;
11
+ }
12
+ /** Names by id, each looked up once per run. */
13
+ export declare function namesOf(q: Queryable): {
14
+ user: (id: string) => Promise<string>;
15
+ member: (id: string) => Promise<string>;
16
+ project: (id: string) => Promise<string>;
17
+ key: (id: string) => Promise<string>;
18
+ };
19
+ export type Names = ReturnType<typeof namesOf>;
20
+ /** One sentence per change, naming who did it. */
21
+ export declare function describe(row: AuditRow, names: Names): Promise<string>;
22
+ //# sourceMappingURL=team-describe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-describe.d.ts","sourceRoot":"","sources":["../../src/notify/team-describe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,iGAAiG;AACjG,eAAO,MAAM,OAAO,aAmBlB,CAAC;AAEH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,wBAAgB,OAAO,CAAC,CAAC,EAAE,SAAS;IAKhC,IAAI,OAAO,MAAM;IACjB,MAAM,OAAO,MAAM;IACnB,OAAO,OAAO,MAAM;IACpB,GAAG,OAAO,MAAM;EAEnB;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAS/C,kDAAkD;AAClD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B3E"}
@@ -0,0 +1,67 @@
1
+ /** Audit actions that become team notices. MCP calls and the first organization are not news. */
2
+ export const NOTICED = new Set([
3
+ "member.invite",
4
+ "invite.cancel",
5
+ "member.join",
6
+ "member.leave",
7
+ "member.remove",
8
+ "member.role",
9
+ "key.create",
10
+ "key.revoke",
11
+ "sso.create",
12
+ "sso.update",
13
+ "sso.delete",
14
+ "sso.verify",
15
+ "project.create",
16
+ "project.delete",
17
+ "project.clear",
18
+ "share.create",
19
+ "share.revoke",
20
+ "org.update",
21
+ ]);
22
+ /** Names by id, each looked up once per run. */
23
+ export function namesOf(q) {
24
+ const cache = new Map();
25
+ const once = (key, load) => cache.get(key) ?? (cache.set(key, load()), cache.get(key));
26
+ const one = async (sql, id, fallback) => (await q.query(sql, [id]))[0]?.v ?? fallback;
27
+ return {
28
+ user: (id) => once(`u:${id}`, () => one("SELECT name || ' (' || email || ')' AS v FROM users WHERE id = $1", id, "Someone")),
29
+ member: (id) => once(`m:${id}`, () => one("SELECT u.email AS v FROM members m JOIN users u ON u.id = m.user_id WHERE m.id = $1", id, "a member")),
30
+ project: (id) => once(`p:${id}`, () => one("SELECT name AS v FROM projects WHERE id = $1", id, "a project")),
31
+ key: (id) => once(`k:${id}`, () => one("SELECT name AS v FROM api_keys WHERE id = $1", id, "")),
32
+ };
33
+ }
34
+ const s = (v) => (typeof v === "string" ? v : "");
35
+ async function projectsText(names, ids) {
36
+ const list = Array.isArray(ids) ? await Promise.all(ids.filter((x) => typeof x === "string").map(names.project)) : [];
37
+ return list.length ? list.join(", ") : "no project";
38
+ }
39
+ /** One sentence per change, naming who did it. */
40
+ export async function describe(row, names) {
41
+ const d = JSON.parse(row.detail);
42
+ const who = row.actor.startsWith("user:") ? await names.user(row.actor.slice(5)) : "WizeLens";
43
+ const sso = `${s(d.type).toUpperCase() || "single sign-on"} for ${s(d.domain) || s(d.providerId)}`;
44
+ const project = async () => s(d.name) || (await names.project(s(d.projectId)));
45
+ const say = {
46
+ "member.invite": () => `${who} invited ${s(d.email)} as ${s(d.role) || "member"}.`,
47
+ "invite.cancel": () => `${who} canceled an invite.`,
48
+ "member.join": () => `${who} joined${d.via === "sso" ? " through single sign-on" : ""}.`,
49
+ "member.leave": () => `${who} left.`,
50
+ "member.remove": () => `${who} removed ${s(d.memberEmail) || s(d.memberIdOrEmail) || "a member"}.`,
51
+ "member.role": async () => `${who} made ${await names.member(s(d.memberId))} ${s(d.role)}.`,
52
+ "key.create": async () => `${who} made ${d.kind === "read" ? "a read key" : "an ingest key"} for ${await projectsText(names, d.projectIds)}.`,
53
+ "key.revoke": async () => `${who} revoked the key ${(await names.key(s(d.keyId))) || s(d.keyId)}.`,
54
+ "sso.create": () => `${who} added ${sso}.`,
55
+ "sso.update": () => `${who} changed ${sso}.`,
56
+ "sso.delete": () => `${who} removed ${sso}.`,
57
+ "sso.verify": () => `${who} proved the domain for ${sso}.`,
58
+ "project.create": async () => `${who} made the project ${await project()}.`,
59
+ "project.delete": async () => `${who} deleted the project ${await project()}.`,
60
+ "project.clear": async () => `${who} deleted all data of ${await project()}.`,
61
+ "share.create": async () => `${who} shared a trace from ${await project()} for ${Number(d.days) || 7} days.`,
62
+ "share.revoke": () => `${who} revoked a share link.`,
63
+ "org.update": () => `${who} changed the organization settings${typeof d.retentionDays === "number" ? ` (keep data ${d.retentionDays} days)` : ""}.`,
64
+ };
65
+ return (await say[row.action]?.()) ?? `${who}: ${row.action}.`;
66
+ }
67
+ //# sourceMappingURL=team-describe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-describe.js","sourceRoot":"","sources":["../../src/notify/team-describe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { CloudApp } from "../app.js";
2
+ /** Send team notices for new audit lines, grouped by organization. Returns how many lines it took. */
3
+ export declare function sendTeamNotices(app: CloudApp): Promise<number>;
4
+ /** Before an organization is deleted: who to tell, since members are gone after. */
5
+ export declare function orgDeletionNotice(app: CloudApp, orgId: string, actorId: string): Promise<() => void>;
6
+ //# sourceMappingURL=team.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../src/notify/team.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AA0C1C,sGAAsG;AACtG,wBAAsB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAOpE;AAED,oFAAoF;AACpF,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAO1G"}
@@ -0,0 +1,56 @@
1
+ import { orgDeleted, roleChanged, teamChanges } from "../mail/emails-team.js";
2
+ import { renderMail } from "../mail/templates.js";
3
+ import { stopUrl } from "../mail/unsubscribe.js";
4
+ import { managersOf } from "../repo/mail-prefs.js";
5
+ import { describe, NOTICED, namesOf } from "./team-describe.js";
6
+ const BATCH = 200;
7
+ /** Take audit lines nobody was told about yet. SKIP LOCKED lets several instances share the work without sending twice. */
8
+ async function claim(app) {
9
+ return app.db.query(`UPDATE audit SET noticed = TRUE WHERE id IN (SELECT id FROM audit WHERE NOT noticed ORDER BY time_ms LIMIT ${BATCH} FOR UPDATE SKIP LOCKED)
10
+ RETURNING id, org_id, actor, action, detail, time_ms`);
11
+ }
12
+ const auditLink = (app, orgId) => `${app.baseUrl()}/_wizelens/#/team?org=${encodeURIComponent(orgId)}`;
13
+ /** A member whose role changed hears it from us too, even when they are not a manager. */
14
+ async function tellRoleChange(app, row, org) {
15
+ const d = JSON.parse(row.detail);
16
+ const [m] = await app.db.query("SELECT m.user_id, u.email, p.team FROM members m JOIN users u ON u.id = m.user_id LEFT JOIN mail_prefs p ON p.user_id = u.id WHERE m.id = $1", [d.memberId ?? ""]);
17
+ if (!m || m.team === false || row.actor === `user:${m.user_id}`)
18
+ return;
19
+ app.mailer.send(renderMail(m.email, roleChanged(org, d.role ?? "member", auditLink(app, row.org_id), stopUrl(app, m.user_id, "team"))));
20
+ }
21
+ /** One email per manager with the changes others made in that organization. */
22
+ async function noticeOrg(app, orgId, rows, names) {
23
+ const [o] = await app.db.query("SELECT name FROM orgs WHERE id = $1", [orgId]);
24
+ if (!o)
25
+ return;
26
+ for (const row of rows.filter((r) => r.action === "member.role"))
27
+ await tellRoleChange(app, row, o.name);
28
+ const changes = await Promise.all(rows.map(async (r) => ({ actor: r.actor, text: await describe(r, names), timeMs: r.time_ms })));
29
+ for (const m of await managersOf(app.db, orgId, "team", app.mailer.live)) {
30
+ const theirs = changes.filter((c) => c.actor !== `user:${m.id}`);
31
+ if (theirs.length)
32
+ app.mailer.send(renderMail(m.email, teamChanges(o.name, theirs, auditLink(app, orgId), stopUrl(app, m.id, "team"))));
33
+ }
34
+ }
35
+ /** Send team notices for new audit lines, grouped by organization. Returns how many lines it took. */
36
+ export async function sendTeamNotices(app) {
37
+ const rows = (await claim(app)).sort((a, b) => a.time_ms - b.time_ms);
38
+ const byOrg = new Map();
39
+ for (const r of rows.filter((r) => NOTICED.has(r.action)))
40
+ byOrg.set(r.org_id, [...(byOrg.get(r.org_id) ?? []), r]);
41
+ const names = namesOf(app.db);
42
+ for (const [orgId, list] of byOrg)
43
+ await noticeOrg(app, orgId, list, names);
44
+ return rows.length;
45
+ }
46
+ /** Before an organization is deleted: who to tell, since members are gone after. */
47
+ export async function orgDeletionNotice(app, orgId, actorId) {
48
+ const [o] = await app.db.query("SELECT name FROM orgs WHERE id = $1", [orgId]);
49
+ const people = await app.db.query("SELECT u.email FROM members m JOIN users u ON u.id = m.user_id WHERE m.org_id = $1 AND u.id <> $2", [orgId, actorId]);
50
+ const by = await namesOf(app.db).user(actorId);
51
+ return () => {
52
+ for (const p of people)
53
+ app.mailer.send(renderMail(p.email, orgDeleted(o?.name ?? "An organization", by, `${app.baseUrl()}/_wizelens/`)));
54
+ };
55
+ }
56
+ //# sourceMappingURL=team.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/notify/team.ts"],"names":[],"mappings":"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"}