@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.
- package/LICENSE +21 -0
- package/README.md +115 -2
- package/dist/alerts/evaluate.d.ts +10 -0
- package/dist/alerts/evaluate.d.ts.map +1 -0
- package/dist/alerts/evaluate.js +81 -0
- package/dist/alerts/evaluate.js.map +1 -0
- package/dist/alerts/notify.d.ts +28 -0
- package/dist/alerts/notify.d.ts.map +1 -0
- package/dist/alerts/notify.js +52 -0
- package/dist/alerts/notify.js.map +1 -0
- package/dist/app.d.ts +26 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +2 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/auth.d.ts +51 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +63 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/bridge.d.ts +7 -0
- package/dist/auth/bridge.d.ts.map +1 -0
- package/dist/auth/bridge.js +39 -0
- package/dist/auth/bridge.js.map +1 -0
- package/dist/auth/crypto.d.ts +14 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +16 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/delete-user.d.ts +16 -0
- package/dist/auth/delete-user.d.ts.map +1 -0
- package/dist/auth/delete-user.js +21 -0
- package/dist/auth/delete-user.js.map +1 -0
- package/dist/auth/deps.d.ts +27 -0
- package/dist/auth/deps.d.ts.map +1 -0
- package/dist/auth/deps.js +3 -0
- package/dist/auth/deps.js.map +1 -0
- package/dist/auth/email-token.d.ts +8 -0
- package/dist/auth/email-token.d.ts.map +1 -0
- package/dist/auth/email-token.js +15 -0
- package/dist/auth/email-token.js.map +1 -0
- package/dist/auth/hooks.d.ts +4 -0
- package/dist/auth/hooks.d.ts.map +1 -0
- package/dist/auth/hooks.js +143 -0
- package/dist/auth/hooks.js.map +1 -0
- package/dist/auth/options.d.ts +17 -0
- package/dist/auth/options.d.ts.map +1 -0
- package/dist/auth/options.js +98 -0
- package/dist/auth/options.js.map +1 -0
- package/dist/auth/password.d.ts +10 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +22 -0
- package/dist/auth/password.js.map +1 -0
- package/dist/auth/plugins.d.ts +4 -0
- package/dist/auth/plugins.d.ts.map +1 -0
- package/dist/auth/plugins.js +45 -0
- package/dist/auth/plugins.js.map +1 -0
- package/dist/auth/principal.d.ts +38 -0
- package/dist/auth/principal.d.ts.map +1 -0
- package/dist/auth/principal.js +69 -0
- package/dist/auth/principal.js.map +1 -0
- package/dist/auth/roles.d.ts +50 -0
- package/dist/auth/roles.d.ts.map +1 -0
- package/dist/auth/roles.js +18 -0
- package/dist/auth/roles.js.map +1 -0
- package/dist/auth/schema-names.d.ts +130 -0
- package/dist/auth/schema-names.d.ts.map +1 -0
- package/dist/auth/schema-names.js +53 -0
- package/dist/auth/schema-names.js.map +1 -0
- package/dist/auth/secret.d.ts +12 -0
- package/dist/auth/secret.d.ts.map +1 -0
- package/dist/auth/secret.js +32 -0
- package/dist/auth/secret.js.map +1 -0
- package/dist/auth/security-notices.d.ts +34 -0
- package/dist/auth/security-notices.d.ts.map +1 -0
- package/dist/auth/security-notices.js +81 -0
- package/dist/auth/security-notices.js.map +1 -0
- package/dist/auth/signup.d.ts +23 -0
- package/dist/auth/signup.d.ts.map +1 -0
- package/dist/auth/signup.js +61 -0
- package/dist/auth/signup.js.map +1 -0
- package/dist/auth/sso.d.ts +21 -0
- package/dist/auth/sso.d.ts.map +1 -0
- package/dist/auth/sso.js +35 -0
- package/dist/auth/sso.js.map +1 -0
- package/dist/auth/wire.d.ts +16 -0
- package/dist/auth/wire.d.ts.map +1 -0
- package/dist/auth/wire.js +47 -0
- package/dist/auth/wire.js.map +1 -0
- package/dist/cli-args.d.ts +5 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +86 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +44 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -0
- package/dist/db/db.d.ts +22 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +64 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/migrate.d.ts +6 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +35 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema-auth.d.ts +2 -0
- package/dist/db/schema-auth.d.ts.map +1 -0
- package/dist/db/schema-auth.js +93 -0
- package/dist/db/schema-auth.js.map +1 -0
- package/dist/db/schema-mail.d.ts +2 -0
- package/dist/db/schema-mail.d.ts.map +1 -0
- package/dist/db/schema-mail.js +8 -0
- package/dist/db/schema-mail.js.map +1 -0
- package/dist/db/schema-notify.d.ts +2 -0
- package/dist/db/schema-notify.d.ts.map +1 -0
- package/dist/db/schema-notify.js +26 -0
- package/dist/db/schema-notify.js.map +1 -0
- package/dist/db/schema-sso.d.ts +2 -0
- package/dist/db/schema-sso.d.ts.map +1 -0
- package/dist/db/schema-sso.js +17 -0
- package/dist/db/schema-sso.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +208 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/http/http.d.ts +19 -0
- package/dist/http/http.d.ts.map +1 -0
- package/dist/http/http.js +66 -0
- package/dist/http/http.js.map +1 -0
- package/dist/http/limiter.d.ts +11 -0
- package/dist/http/limiter.d.ts.map +1 -0
- package/dist/http/limiter.js +31 -0
- package/dist/http/limiter.js.map +1 -0
- package/dist/http/router.d.ts +21 -0
- package/dist/http/router.d.ts.map +1 -0
- package/dist/http/router.js +28 -0
- package/dist/http/router.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +4 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +53 -0
- package/dist/jobs.js.map +1 -0
- package/dist/mail/blocks.d.ts +14 -0
- package/dist/mail/blocks.d.ts.map +1 -0
- package/dist/mail/blocks.js +35 -0
- package/dist/mail/blocks.js.map +1 -0
- package/dist/mail/content.d.ts +68 -0
- package/dist/mail/content.d.ts.map +1 -0
- package/dist/mail/content.js +6 -0
- package/dist/mail/content.js.map +1 -0
- package/dist/mail/emails-digest.d.ts +10 -0
- package/dist/mail/emails-digest.d.ts.map +1 -0
- package/dist/mail/emails-digest.js +55 -0
- package/dist/mail/emails-digest.js.map +1 -0
- package/dist/mail/emails-health.d.ts +6 -0
- package/dist/mail/emails-health.d.ts.map +1 -0
- package/dist/mail/emails-health.js +36 -0
- package/dist/mail/emails-health.js.map +1 -0
- package/dist/mail/emails-security.d.ts +14 -0
- package/dist/mail/emails-security.d.ts.map +1 -0
- package/dist/mail/emails-security.js +27 -0
- package/dist/mail/emails-security.js.map +1 -0
- package/dist/mail/emails-team.d.ts +11 -0
- package/dist/mail/emails-team.d.ts.map +1 -0
- package/dist/mail/emails-team.js +45 -0
- package/dist/mail/emails-team.js.map +1 -0
- package/dist/mail/emails.d.ts +9 -0
- package/dist/mail/emails.d.ts.map +1 -0
- package/dist/mail/emails.js +49 -0
- package/dist/mail/emails.js.map +1 -0
- package/dist/mail/layout.d.ts +6 -0
- package/dist/mail/layout.d.ts.map +1 -0
- package/dist/mail/layout.js +10 -0
- package/dist/mail/layout.js.map +1 -0
- package/dist/mail/mailer.d.ts +25 -0
- package/dist/mail/mailer.d.ts.map +1 -0
- package/dist/mail/mailer.js +49 -0
- package/dist/mail/mailer.js.map +1 -0
- package/dist/mail/preview.d.ts +6 -0
- package/dist/mail/preview.d.ts.map +1 -0
- package/dist/mail/preview.js +26 -0
- package/dist/mail/preview.js.map +1 -0
- package/dist/mail/resend.d.ts +11 -0
- package/dist/mail/resend.d.ts.map +1 -0
- package/dist/mail/resend.js +34 -0
- package/dist/mail/resend.js.map +1 -0
- package/dist/mail/samples.d.ts +3 -0
- package/dist/mail/samples.d.ts.map +1 -0
- package/dist/mail/samples.js +63 -0
- package/dist/mail/samples.js.map +1 -0
- package/dist/mail/stop-route.d.ts +4 -0
- package/dist/mail/stop-route.d.ts.map +1 -0
- package/dist/mail/stop-route.js +41 -0
- package/dist/mail/stop-route.js.map +1 -0
- package/dist/mail/svix.d.ts +8 -0
- package/dist/mail/svix.d.ts.map +1 -0
- package/dist/mail/svix.js +17 -0
- package/dist/mail/svix.js.map +1 -0
- package/dist/mail/templates.d.ts +12 -0
- package/dist/mail/templates.d.ts.map +1 -0
- package/dist/mail/templates.js +18 -0
- package/dist/mail/templates.js.map +1 -0
- package/dist/mail/text.d.ts +4 -0
- package/dist/mail/text.d.ts.map +1 -0
- package/dist/mail/text.js +17 -0
- package/dist/mail/text.js.map +1 -0
- package/dist/mail/theme.d.ts +26 -0
- package/dist/mail/theme.d.ts.map +1 -0
- package/dist/mail/theme.js +21 -0
- package/dist/mail/theme.js.map +1 -0
- package/dist/mail/unsubscribe.d.ts +14 -0
- package/dist/mail/unsubscribe.d.ts.map +1 -0
- package/dist/mail/unsubscribe.js +39 -0
- package/dist/mail/unsubscribe.js.map +1 -0
- package/dist/mail/webhook.d.ts +5 -0
- package/dist/mail/webhook.d.ts.map +1 -0
- package/dist/mail/webhook.js +41 -0
- package/dist/mail/webhook.js.map +1 -0
- package/dist/notify/digest-data.d.ts +31 -0
- package/dist/notify/digest-data.d.ts.map +1 -0
- package/dist/notify/digest-data.js +37 -0
- package/dist/notify/digest-data.js.map +1 -0
- package/dist/notify/digest.d.ts +17 -0
- package/dist/notify/digest.d.ts.map +1 -0
- package/dist/notify/digest.js +76 -0
- package/dist/notify/digest.js.map +1 -0
- package/dist/notify/health.d.ts +6 -0
- package/dist/notify/health.d.ts.map +1 -0
- package/dist/notify/health.js +43 -0
- package/dist/notify/health.js.map +1 -0
- package/dist/notify/team-describe.d.ts +22 -0
- package/dist/notify/team-describe.d.ts.map +1 -0
- package/dist/notify/team-describe.js +67 -0
- package/dist/notify/team-describe.js.map +1 -0
- package/dist/notify/team.d.ts +6 -0
- package/dist/notify/team.d.ts.map +1 -0
- package/dist/notify/team.js +56 -0
- package/dist/notify/team.js.map +1 -0
- package/dist/repo/alerts.d.ts +52 -0
- package/dist/repo/alerts.d.ts.map +1 -0
- package/dist/repo/alerts.js +38 -0
- package/dist/repo/alerts.js.map +1 -0
- package/dist/repo/audit.d.ts +11 -0
- package/dist/repo/audit.d.ts.map +1 -0
- package/dist/repo/audit.js +17 -0
- package/dist/repo/audit.js.map +1 -0
- package/dist/repo/auth-prune.d.ts +4 -0
- package/dist/repo/auth-prune.d.ts.map +1 -0
- package/dist/repo/auth-prune.js +8 -0
- package/dist/repo/auth-prune.js.map +1 -0
- package/dist/repo/mail-prefs.d.ts +35 -0
- package/dist/repo/mail-prefs.d.ts.map +1 -0
- package/dist/repo/mail-prefs.js +34 -0
- package/dist/repo/mail-prefs.js.map +1 -0
- package/dist/repo/metric-read.d.ts +12 -0
- package/dist/repo/metric-read.d.ts.map +1 -0
- package/dist/repo/metric-read.js +11 -0
- package/dist/repo/metric-read.js.map +1 -0
- package/dist/repo/metric-write.d.ts +9 -0
- package/dist/repo/metric-write.d.ts.map +1 -0
- package/dist/repo/metric-write.js +53 -0
- package/dist/repo/metric-write.js.map +1 -0
- package/dist/repo/orgs.d.ts +37 -0
- package/dist/repo/orgs.d.ts.map +1 -0
- package/dist/repo/orgs.js +33 -0
- package/dist/repo/orgs.js.map +1 -0
- package/dist/repo/projects.d.ts +37 -0
- package/dist/repo/projects.d.ts.map +1 -0
- package/dist/repo/projects.js +57 -0
- package/dist/repo/projects.js.map +1 -0
- package/dist/repo/suppressions.d.ts +8 -0
- package/dist/repo/suppressions.d.ts.map +1 -0
- package/dist/repo/suppressions.js +16 -0
- package/dist/repo/suppressions.js.map +1 -0
- package/dist/repo/trace-read.d.ts +28 -0
- package/dist/repo/trace-read.d.ts.map +1 -0
- package/dist/repo/trace-read.js +76 -0
- package/dist/repo/trace-read.js.map +1 -0
- package/dist/repo/trace-write.d.ts +9 -0
- package/dist/repo/trace-write.d.ts.map +1 -0
- package/dist/repo/trace-write.js +82 -0
- package/dist/repo/trace-write.js.map +1 -0
- package/dist/repo/users.d.ts +13 -0
- package/dist/repo/users.d.ts.map +1 -0
- package/dist/repo/users.js +7 -0
- package/dist/repo/users.js.map +1 -0
- package/dist/routes/alerts.d.ts +4 -0
- package/dist/routes/alerts.d.ts.map +1 -0
- package/dist/routes/alerts.js +116 -0
- package/dist/routes/alerts.js.map +1 -0
- package/dist/routes/data.d.ts +7 -0
- package/dist/routes/data.d.ts.map +1 -0
- package/dist/routes/data.js +74 -0
- package/dist/routes/data.js.map +1 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +25 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/ingest.d.ts +8 -0
- package/dist/routes/ingest.d.ts.map +1 -0
- package/dist/routes/ingest.js +38 -0
- package/dist/routes/ingest.js.map +1 -0
- package/dist/routes/mcp.d.ts +4 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +52 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/me.d.ts +4 -0
- package/dist/routes/me.d.ts.map +1 -0
- package/dist/routes/me.js +37 -0
- package/dist/routes/me.js.map +1 -0
- package/dist/routes/metrics.d.ts +13 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +29 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/models.d.ts +4 -0
- package/dist/routes/models.d.ts.map +1 -0
- package/dist/routes/models.js +71 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/notify.d.ts +4 -0
- package/dist/routes/notify.d.ts.map +1 -0
- package/dist/routes/notify.js +55 -0
- package/dist/routes/notify.js.map +1 -0
- package/dist/routes/orgs.d.ts +4 -0
- package/dist/routes/orgs.d.ts.map +1 -0
- package/dist/routes/orgs.js +73 -0
- package/dist/routes/orgs.js.map +1 -0
- package/dist/routes/projects.d.ts +4 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +55 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/shares.d.ts +11 -0
- package/dist/routes/shares.d.ts.map +1 -0
- package/dist/routes/shares.js +61 -0
- package/dist/routes/shares.js.map +1 -0
- package/dist/scale-exec.d.ts +11 -0
- package/dist/scale-exec.d.ts.map +1 -0
- package/dist/scale-exec.js +26 -0
- package/dist/scale-exec.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +126 -0
- package/dist/server.js.map +1 -0
- package/dist/settle.d.ts +26 -0
- package/dist/settle.d.ts.map +1 -0
- package/dist/settle.js +85 -0
- package/dist/settle.js.map +1 -0
- package/package.json +76 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emails-team.js","sourceRoot":"","sources":["../../src/mail/emails-team.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAE7D,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC;AAEjG,oFAAoF;AACpF,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,OAA2C,EAAE,GAAW,EAAE,IAAY;IAC7G,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC;IACjC,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK,OAAO,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,eAAe,GAAG,cAAc;QAChG,OAAO,EAAE,OAAO,CAAC,CAAC,CAAE,CAAC,IAAI;QACzB,OAAO,EAAE,cAAc,GAAG,GAAG;QAC7B,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACjI,MAAM,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE,GAAG,EAAE;QAC5C,MAAM,EAAE,qDAAqD,GAAG,GAAG;QACnE,WAAW,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE,GAAG,EAAE,IAAI,EAAE;KAC7D,CAAC;AACJ,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,IAAY,EAAE,GAAW,EAAE,IAAY;IAC9E,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,gBAAgB,GAAG,WAAW,IAAI,EAAE;QAC7C,OAAO,EAAE,eAAe,IAAI,OAAO,GAAG,GAAG;QACzC,OAAO,EAAE,eAAe,IAAI,GAAG;QAC/B,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,CAAC,IAAI,CAAC,gBAAgB,GAAG,2BAA2B,IAAI,qDAAqD,CAAC,CAAC;QACvH,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,EAAE,EAAE,GAAG,EAAE;QACrC,MAAM,EAAE,4CAA4C,GAAG,GAAG;QAC1D,WAAW,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE,GAAG,EAAE,IAAI,EAAE;KAC7D,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,EAAU,EAAE,GAAW;IAC7D,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,GAAG,GAAG,0BAA0B;QACzC,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,oBAAoB;QACjD,OAAO,EAAE,GAAG,GAAG,eAAe;QAC9B,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,uFAAuF,CAAC,CAAC;QAC3H,MAAM,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE;QACvC,MAAM,EAAE,6CAA6C,GAAG,GAAG;KAC5D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Notice } from "../alerts/notify.js";
|
|
2
|
+
import { type EmailContent } from "./content.js";
|
|
3
|
+
export declare const verifyEmail: (url: string) => EmailContent;
|
|
4
|
+
export declare const resetPassword: (url: string) => EmailContent;
|
|
5
|
+
export declare const changeEmail: (newEmail: string, url: string) => EmailContent;
|
|
6
|
+
export declare const deleteAccount: (url: string) => EmailContent;
|
|
7
|
+
export declare function invite(inviter: string, org: string, role: string, url: string): EmailContent;
|
|
8
|
+
export declare function alert(n: Notice): EmailContent;
|
|
9
|
+
//# sourceMappingURL=emails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emails.d.ts","sourceRoot":"","sources":["../../src/mail/emails.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,KAAK,YAAY,EAAqB,MAAM,cAAc,CAAC;AAUpE,eAAO,MAAM,WAAW,QAAS,MAAM,iBACsI,CAAC;AAE9K,eAAO,MAAM,aAAa,QAAS,MAAM,iBAIrC,CAAC;AAEL,eAAO,MAAM,WAAW,aAAc,MAAM,OAAO,MAAM,iBAIrD,CAAC;AAEL,eAAO,MAAM,aAAa,QAAS,MAAM,iBAIrC,CAAC;AAEL,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAW5F;AAMD,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,CAY7C"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { facts, note, text } from "./content.js";
|
|
2
|
+
// Every email the cloud sends, as content only. The words follow the README's voice: plain, short, concrete.
|
|
3
|
+
const IGNORE = "If you did not ask for this, you can ignore this email. Nothing changes.";
|
|
4
|
+
const ACCOUNT_FOOTER = "You got this email because someone used this address on WizeLens.";
|
|
5
|
+
function account(subject, heading, lines, action) {
|
|
6
|
+
return { kind: "account", subject, preview: lines[0], heading, tone: "neutral", blocks: [...lines.map(text), note(IGNORE)], action, footer: ACCOUNT_FOOTER };
|
|
7
|
+
}
|
|
8
|
+
export const verifyEmail = (url) => account("Confirm your email for WizeLens", "Confirm your email.", ["Confirm that this is your email address. The link works for 1 hour."], { label: "Confirm email", url });
|
|
9
|
+
export const resetPassword = (url) => account("Reset your WizeLens password", "Reset your password.", ["Someone asked to reset the password of this account. The link works for 30 minutes, once.", "A new password signs out every device."], {
|
|
10
|
+
label: "Set a new password",
|
|
11
|
+
url,
|
|
12
|
+
});
|
|
13
|
+
export const changeEmail = (newEmail, url) => account("Confirm your new WizeLens email", "Confirm the new email.", [`Someone asked to change the email of this account to ${newEmail}.`, "Confirm it here first. Then we send a link to the new address."], {
|
|
14
|
+
label: "Confirm the change",
|
|
15
|
+
url,
|
|
16
|
+
});
|
|
17
|
+
export const deleteAccount = (url) => account("Confirm deleting your WizeLens account", "Delete your account?", ["Someone asked to delete this account. This cannot be undone. The link works for 1 day."], {
|
|
18
|
+
label: "Delete my account",
|
|
19
|
+
url,
|
|
20
|
+
});
|
|
21
|
+
export function invite(inviter, org, role, url) {
|
|
22
|
+
return {
|
|
23
|
+
kind: "invite",
|
|
24
|
+
subject: `${inviter} invited you to ${org} on WizeLens`,
|
|
25
|
+
preview: `Join ${org} as ${role}.`,
|
|
26
|
+
heading: `Join ${org}.`,
|
|
27
|
+
tone: "neutral",
|
|
28
|
+
blocks: [text(`${inviter} invited you to join ${org} on WizeLens as ${role}.`), facts([["Team", org], ["Role", role], ["Works for", "7 days"]])],
|
|
29
|
+
action: { label: `Join ${org}`, url },
|
|
30
|
+
footer: `${inviter} sent this invite. If you do not know them, ignore it.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const VERB = { fired: "Alert", resolved: "Resolved", test: "Test" };
|
|
34
|
+
const TONE = { fired: "bad", resolved: "good", test: "neutral" };
|
|
35
|
+
const number = (v) => (v === null ? "no data" : String(Math.round(v * 100) / 100));
|
|
36
|
+
export function alert(n) {
|
|
37
|
+
const when = `${new Date(n.timeMs).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
38
|
+
return {
|
|
39
|
+
kind: "alert",
|
|
40
|
+
subject: `${VERB[n.state]}: ${n.rule} (${n.project})`,
|
|
41
|
+
preview: n.message,
|
|
42
|
+
heading: `${VERB[n.state]}: ${n.rule}.`,
|
|
43
|
+
tone: TONE[n.state],
|
|
44
|
+
blocks: [text(n.message), facts([["Project", n.project], ["Value", number(n.value)], ["Threshold", number(n.threshold)], ["When", when]])],
|
|
45
|
+
action: { label: "Open the alert", url: n.url },
|
|
46
|
+
footer: `You get this because an alert rule in ${n.project} emails you. Change it on the Alerts page.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=emails.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emails.js","sourceRoot":"","sources":["../../src/mail/emails.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EmailContent } from "./content.js";
|
|
2
|
+
/** The frame every email shares: a light page, one white card, the header, the body, and the footer under the card. */
|
|
3
|
+
export declare function EmailLayout({ content: c }: {
|
|
4
|
+
content: EmailContent;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/mail/layout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,uHAAuH;AACvH,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,+BAwCpE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Body, Container, Head, Heading, Hr, Html, Link, Preview, Section, Text } from "@react-email/components";
|
|
3
|
+
import { Action, BlockView, Header } from "./blocks.js";
|
|
4
|
+
import { theme } from "./theme.js";
|
|
5
|
+
const footerText = { margin: "0 0 4px", fontSize: "12px", lineHeight: "18px", color: theme.muted };
|
|
6
|
+
/** The frame every email shares: a light page, one white card, the header, the body, and the footer under the card. */
|
|
7
|
+
export function EmailLayout({ content: c }) {
|
|
8
|
+
return (_jsxs(Html, { lang: "en", children: [_jsxs(Head, { children: [_jsx("meta", { name: "color-scheme", content: "light" }), _jsx("meta", { name: "supported-color-schemes", content: "light" })] }), _jsx(Preview, { children: c.preview }), _jsxs(Body, { style: { margin: 0, padding: "32px 12px", background: theme.page, fontFamily: theme.sans }, children: [_jsxs(Container, { style: { maxWidth: `${theme.width}px`, background: theme.card, border: `1px solid ${theme.line}`, borderRadius: theme.radius }, children: [_jsx(Header, { tone: c.tone }), _jsxs(Section, { style: { padding: "8px 32px 8px" }, children: [_jsx(Heading, { as: "h1", style: { margin: "16px 0", fontSize: "26px", lineHeight: "32px", fontWeight: 800, letterSpacing: "-0.02em", color: theme.ink }, children: c.heading }), c.blocks.map((b, i) => (_jsx(BlockView, { block: b }, i))), c.action && _jsx(Action, { action: c.action })] })] }), _jsxs(Container, { style: { maxWidth: `${theme.width}px`, padding: "16px 32px 0" }, children: [_jsx(Text, { style: footerText, children: c.footer }), c.unsubscribe && (_jsx(Text, { style: footerText, children: _jsx(Link, { href: c.unsubscribe.url, style: { color: theme.muted, textDecoration: "underline" }, children: c.unsubscribe.label }) })), _jsx(Hr, { style: { margin: "12px 0", borderColor: theme.line } }), _jsx(Text, { style: footerText, children: _jsx(Link, { href: theme.homeUrl, style: { color: theme.muted }, children: theme.company }) })] })] })] }));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/mail/layout.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AACjH,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,UAAU,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;AAEnG,uHAAuH;AACvH,MAAM,UAAU,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC,EAA6B;IACnE,OAAO,CACL,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,aACb,MAAC,IAAI,eACH,eAAM,IAAI,EAAC,cAAc,EAAC,OAAO,EAAC,OAAO,GAAG,EAC5C,eAAM,IAAI,EAAC,yBAAyB,EAAC,OAAO,EAAC,OAAO,GAAG,IAClD,EACP,KAAC,OAAO,cAAE,CAAC,CAAC,OAAO,GAAW,EAC9B,MAAC,IAAI,IAAC,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,aAC9F,MAAC,SAAS,IAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,KAAK,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,EAAE,aACvI,KAAC,MAAM,IAAC,IAAI,EAAE,CAAC,CAAC,IAAI,GAAI,EACxB,MAAC,OAAO,IAAC,KAAK,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,aACzC,KAAC,OAAO,IAAC,EAAE,EAAC,IAAI,EAAC,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,YAC5I,CAAC,CAAC,OAAO,GACF,EACT,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CACtB,KAAC,SAAS,IAAS,KAAK,EAAE,CAAC,IAAX,CAAC,CAAc,CAChC,CAAC,EACD,CAAC,CAAC,MAAM,IAAI,KAAC,MAAM,IAAC,MAAM,EAAE,CAAC,CAAC,MAAM,GAAI,IACjC,IACA,EACZ,MAAC,SAAS,IAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,aACxE,KAAC,IAAI,IAAC,KAAK,EAAE,UAAU,YAAG,CAAC,CAAC,MAAM,GAAQ,EACzC,CAAC,CAAC,WAAW,IAAI,CAChB,KAAC,IAAI,IAAC,KAAK,EAAE,UAAU,YACrB,KAAC,IAAI,IAAC,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,YACtF,CAAC,CAAC,WAAW,CAAC,KAAK,GACf,GACF,CACR,EACD,KAAC,EAAE,IAAC,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,GAAI,EAC5D,KAAC,IAAI,IAAC,KAAK,EAAE,UAAU,YACrB,KAAC,IAAI,IAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,YACrD,KAAK,CAAC,OAAO,GACT,GACF,IACG,IACP,IACF,CACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CloudConfig } from "../config.js";
|
|
2
|
+
import type { Queryable } from "../db/db.js";
|
|
3
|
+
/** "account": mail the person asked for. "security": notices about their own account. The rest: mail someone or something else started. */
|
|
4
|
+
export type MailKind = "account" | "security" | "invite" | "alert" | "team" | "health" | "digest";
|
|
5
|
+
export interface Mail {
|
|
6
|
+
kind: MailKind;
|
|
7
|
+
to: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
text: string;
|
|
10
|
+
html: string;
|
|
11
|
+
/** Extra headers, for example List-Unsubscribe. */
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export type MailVia = "resend" | "smtp" | "test" | "log";
|
|
15
|
+
export interface Mailer {
|
|
16
|
+
/** True when mail really leaves (Resend, SMTP, or a test sender). False: mail is written to the log only. */
|
|
17
|
+
readonly live: boolean;
|
|
18
|
+
readonly via: MailVia;
|
|
19
|
+
/** Queue one email (or one still rendering). Never throws and never waits, so a slow mail server cannot slow or reveal anything. */
|
|
20
|
+
send(mail: Mail | Promise<Mail>): void;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/** The cloud's mail, with the suppression list checked before each send. */
|
|
24
|
+
export declare function createMailer(config: CloudConfig, db: Queryable): Promise<Mailer>;
|
|
25
|
+
//# sourceMappingURL=mailer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailer.d.ts","sourceRoot":"","sources":["../../src/mail/mailer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,2IAA2I;AAC3I,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAKlG,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAEzD,MAAM,WAAW,MAAM;IACrB,6GAA6G;IAC7G,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oIAAoI;IACpI,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiCD,4EAA4E;AAC5E,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBtF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { suppressionOf } from "../repo/suppressions.js";
|
|
2
|
+
import { resendSender } from "./resend.js";
|
|
3
|
+
/** Kinds a spam complaint does not stop: the person asked for them, or they protect the account. */
|
|
4
|
+
const ALWAYS = new Set(["account", "security"]);
|
|
5
|
+
async function smtpSender(config) {
|
|
6
|
+
const { createTransport } = await import("nodemailer");
|
|
7
|
+
const transport = createTransport(config.smtpUrl, { from: config.mailFrom });
|
|
8
|
+
return { via: "smtp", send: async (m) => void (await transport.sendMail({ from: config.mailFrom, ...m })), close: async () => transport.close() };
|
|
9
|
+
}
|
|
10
|
+
const logSender = async (m) => console.info(`[wizelens-cloud] mail (no mail service set, not sent) to ${m.to}: ${m.subject}\n${m.text}\n`);
|
|
11
|
+
/** The test sender, then Resend, then SMTP, then the log. */
|
|
12
|
+
async function transportOf(config) {
|
|
13
|
+
if (config.sendMail)
|
|
14
|
+
return { via: "test", send: config.sendMail };
|
|
15
|
+
if (config.resendApiKey)
|
|
16
|
+
return { via: "resend", send: resendSender({ apiKey: config.resendApiKey, from: config.mailFrom, fetch: config.fetch }) };
|
|
17
|
+
if (config.smtpUrl)
|
|
18
|
+
return smtpSender(config);
|
|
19
|
+
return { via: "log", send: logSender };
|
|
20
|
+
}
|
|
21
|
+
/** A bounce skips every email. A complaint skips mail the person did not ask for. */
|
|
22
|
+
async function skipReason(db, mail) {
|
|
23
|
+
const reason = await suppressionOf(db, mail.to);
|
|
24
|
+
if (reason === "bounce")
|
|
25
|
+
return "it bounced before";
|
|
26
|
+
if (reason === "complaint" && !ALWAYS.has(mail.kind))
|
|
27
|
+
return "it marked our mail as spam";
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** The cloud's mail, with the suppression list checked before each send. */
|
|
31
|
+
export async function createMailer(config, db) {
|
|
32
|
+
const t = await transportOf(config);
|
|
33
|
+
const deliver = async (pending) => {
|
|
34
|
+
const mail = await pending;
|
|
35
|
+
const skip = await skipReason(db, mail);
|
|
36
|
+
if (skip)
|
|
37
|
+
return console.info(`[wizelens-cloud] mail to ${mail.to} skipped: ${skip}.`);
|
|
38
|
+
await t.send(mail).catch((err) => {
|
|
39
|
+
throw new Error(`mail to ${mail.to} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
live: t.via !== "log",
|
|
44
|
+
via: t.via,
|
|
45
|
+
send: (mail) => void deliver(mail).catch((err) => console.error("[wizelens-cloud]", err instanceof Error ? err.message : err)),
|
|
46
|
+
close: async () => t.close?.(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=mailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailer.js","sourceRoot":"","sources":["../../src/mail/mailer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { SAMPLES } from "./samples.js";
|
|
3
|
+
export { SAMPLES };
|
|
4
|
+
/** Write each sample as .html and .txt, plus an index page that links them. */
|
|
5
|
+
export declare function writePreviews(dir: string): Promise<string[]>;
|
|
6
|
+
//# sourceMappingURL=preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/mail/preview.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnB,+EAA+E;AAC/E,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWlE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { SAMPLES } from "./samples.js";
|
|
6
|
+
import { renderMail } from "./templates.js";
|
|
7
|
+
export { SAMPLES };
|
|
8
|
+
/** Write each sample as .html and .txt, plus an index page that links them. */
|
|
9
|
+
export async function writePreviews(dir) {
|
|
10
|
+
await mkdir(dir, { recursive: true });
|
|
11
|
+
const names = Object.keys(SAMPLES);
|
|
12
|
+
for (const name of names) {
|
|
13
|
+
const mail = await renderMail("ann@acme.io", SAMPLES[name]);
|
|
14
|
+
await writeFile(join(dir, `${name}.html`), mail.html);
|
|
15
|
+
await writeFile(join(dir, `${name}.txt`), `Subject: ${mail.subject}\n\n${mail.text}`);
|
|
16
|
+
}
|
|
17
|
+
const links = names.map((n) => `<li><a href="${n}.html">${n}</a> · <a href="${n}.txt">text</a></li>`).join("");
|
|
18
|
+
await writeFile(join(dir, "index.html"), `<!doctype html><meta charset="utf-8"><title>WizeLens emails</title><ul>${links}</ul>`);
|
|
19
|
+
return names;
|
|
20
|
+
}
|
|
21
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
22
|
+
const dir = resolve(process.argv[2] ?? "email-preview");
|
|
23
|
+
const names = await writePreviews(dir);
|
|
24
|
+
console.log(`Wrote ${names.length} emails to ${dir}. Open index.html.`);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/mail/preview.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnB,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,IAAI,CAAE,CAAC,CAAC;QAC7D,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,MAAM,CAAC,EAAE,YAAY,IAAI,CAAC,OAAO,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,UAAU,CAAC,mBAAmB,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/G,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,0EAA0E,KAAK,OAAO,CAAC,CAAC;IACjI,OAAO,KAAK,CAAC;AACf,CAAC;AAED,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,MAAM,cAAc,GAAG,oBAAoB,CAAC,CAAC;AAC1E,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Mail } from "./mailer.js";
|
|
2
|
+
export interface ResendSettings {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
from: string;
|
|
5
|
+
fetch: typeof fetch;
|
|
6
|
+
/** Wait between tries. Tests pass a fast one. */
|
|
7
|
+
sleep?: (ms: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** Send one email through the Resend API. The same Idempotency-Key on every try, so a retry never sends it twice. */
|
|
10
|
+
export declare function resendSender(s: ResendSettings): (mail: Mail) => Promise<void>;
|
|
11
|
+
//# sourceMappingURL=resend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resend.d.ts","sourceRoot":"","sources":["../../src/mail/resend.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAKxC,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,iDAAiD;IACjD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAaD,qHAAqH;AACrH,wBAAgB,YAAY,CAAC,CAAC,EAAE,cAAc,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAkB7E"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const API = "https://api.resend.com/emails";
|
|
3
|
+
const TRIES = 4;
|
|
4
|
+
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
/** 429 and 5xx are worth another try, except a used-up daily or monthly quota. */
|
|
6
|
+
const retryable = (status, body) => (status === 429 && !/quota_exceeded/.test(body)) || status >= 500;
|
|
7
|
+
/** Seconds from Retry-After, else 1 s, 2 s, 4 s. At most 30 s. */
|
|
8
|
+
const waitMs = (res, attempt) => Math.min(30_000, Number(res?.headers.get("retry-after")) * 1000 || 1000 * 2 ** attempt);
|
|
9
|
+
const payload = (s, m) => JSON.stringify({ from: s.from, to: [m.to], subject: m.subject, html: m.html, text: m.text, tags: [{ name: "kind", value: m.kind }], ...(m.headers ? { headers: m.headers } : {}) });
|
|
10
|
+
/** Send one email through the Resend API. The same Idempotency-Key on every try, so a retry never sends it twice. */
|
|
11
|
+
export function resendSender(s) {
|
|
12
|
+
const sleep = s.sleep ?? pause;
|
|
13
|
+
return async (mail) => {
|
|
14
|
+
const headers = { Authorization: `Bearer ${s.apiKey}`, "Content-Type": "application/json", "Idempotency-Key": randomUUID() };
|
|
15
|
+
let problem = "";
|
|
16
|
+
for (let attempt = 0; attempt < TRIES; attempt++) {
|
|
17
|
+
const res = await s.fetch(API, { method: "POST", headers, body: payload(s, mail), signal: AbortSignal.timeout(15_000) }).catch((err) => {
|
|
18
|
+
problem = err instanceof Error ? err.message : String(err);
|
|
19
|
+
return null;
|
|
20
|
+
});
|
|
21
|
+
if (res?.ok)
|
|
22
|
+
return;
|
|
23
|
+
const body = res ? await res.text().catch(() => "") : "";
|
|
24
|
+
if (res)
|
|
25
|
+
problem = `Resend answered ${res.status}: ${body.slice(0, 300)}`;
|
|
26
|
+
if (res && !retryable(res.status, body))
|
|
27
|
+
break;
|
|
28
|
+
if (attempt < TRIES - 1)
|
|
29
|
+
await sleep(waitMs(res, attempt));
|
|
30
|
+
}
|
|
31
|
+
throw new Error(problem);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=resend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resend.js","sourceRoot":"","sources":["../../src/mail/resend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"samples.d.ts","sourceRoot":"","sources":["../../src/mail/samples.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA8CjD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAqBhD,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as emails from "./emails.js";
|
|
2
|
+
import { digest } from "./emails-digest.js";
|
|
3
|
+
import { projectBack, projectQuiet } from "./emails-health.js";
|
|
4
|
+
import * as security from "./emails-security.js";
|
|
5
|
+
import { orgDeleted, roleChanged, teamChanges } from "./emails-team.js";
|
|
6
|
+
// Sample data for every email, for the preview command and the template tests.
|
|
7
|
+
const APP = "https://app.wizelens.com/_wizelens";
|
|
8
|
+
const LINK = `${APP}/#/verify-email?token=sample-token`;
|
|
9
|
+
const STOP = `${APP}/api/mail/unsubscribe?token=sample`;
|
|
10
|
+
const AT = Date.UTC(2026, 9, 18, 14, 30);
|
|
11
|
+
const LINKS = { account: `${APP}/#/account`, forgot: `${APP}/#/forgot-password` };
|
|
12
|
+
const notice = (state) => ({
|
|
13
|
+
rule: "Checkout errors",
|
|
14
|
+
project: "shop",
|
|
15
|
+
state,
|
|
16
|
+
message: state === "fired" ? "Error rate is 7.4% (above 5)" : "Error rate is 1.2% (no longer above 5)",
|
|
17
|
+
value: state === "fired" ? 7.4 : 1.2,
|
|
18
|
+
threshold: 5,
|
|
19
|
+
timeMs: AT,
|
|
20
|
+
url: `${APP}/#/alerts?project=p1`,
|
|
21
|
+
});
|
|
22
|
+
const h = (value, previous) => ({ value, previous, change: ((value - previous) / previous) * 100, series: [] });
|
|
23
|
+
const WEEK = {
|
|
24
|
+
empty: false,
|
|
25
|
+
requests: h(48_210, 41_900),
|
|
26
|
+
errorRate: h(1.8, 1.1),
|
|
27
|
+
avgMs: h(132, 140),
|
|
28
|
+
sqlQueries: h(310_442, 306_100),
|
|
29
|
+
p95Ms: 412,
|
|
30
|
+
errors: [
|
|
31
|
+
{ type: "TimeoutError", message: "payments-api did not answer in 5000 ms", count: 214 },
|
|
32
|
+
{ type: "TypeError", message: "Cannot read properties of null (reading 'total')", count: 37 },
|
|
33
|
+
],
|
|
34
|
+
slowest: [
|
|
35
|
+
{ endpoint: "POST /checkout", p95Ms: 1840, count: 3120 },
|
|
36
|
+
{ endpoint: "GET /orders/:id", p95Ms: 620, count: 9811 },
|
|
37
|
+
],
|
|
38
|
+
insights: [{ title: "N+1 query: SELECT * FROM order_items WHERE order_id = ?", occurrences: 9811 }],
|
|
39
|
+
alertsFired: 1,
|
|
40
|
+
};
|
|
41
|
+
export const SAMPLES = {
|
|
42
|
+
"verify-email": emails.verifyEmail(LINK),
|
|
43
|
+
"reset-password": emails.resetPassword(LINK),
|
|
44
|
+
"change-email": emails.changeEmail("new@acme.io", LINK),
|
|
45
|
+
"delete-account": emails.deleteAccount(LINK),
|
|
46
|
+
invite: emails.invite("Olive", "Acme", "member", LINK),
|
|
47
|
+
"alert-fired": emails.alert(notice("fired")),
|
|
48
|
+
"alert-resolved": emails.alert(notice("resolved")),
|
|
49
|
+
"security-password": security.passwordChanged(LINKS, AT),
|
|
50
|
+
"security-email": security.emailChanged(LINKS, "olive@new.io", AT),
|
|
51
|
+
"security-2fa-off": security.twoFactorChanged(LINKS, false, AT),
|
|
52
|
+
"security-passkey": security.passkeyChanged(LINKS, true, "MacBook Touch ID", AT),
|
|
53
|
+
"security-backup-codes": security.backupCodesMade(LINKS, AT),
|
|
54
|
+
"security-new-sign-in": security.newSignIn(LINKS, "Chrome on Windows", "203.0.113.7", AT),
|
|
55
|
+
"team-changes": teamChanges("Acme", [{ text: "Vic (vic@acme.io) made an ingest key for shop.", timeMs: AT }, { text: "Vic (vic@acme.io) invited ann@acme.io as member.", timeMs: AT + 60_000 }], `${APP}/#/team`, STOP),
|
|
56
|
+
"team-role": roleChanged("Acme", "admin", `${APP}/#/team`, STOP),
|
|
57
|
+
"team-org-deleted": orgDeleted("Acme", "Olive (olive@acme.io)", `${APP}/`),
|
|
58
|
+
"health-quiet": projectQuiet("shop", "Acme", AT, AT + 3 * 3_600_000, `${APP}/#/`, STOP),
|
|
59
|
+
"health-back": projectBack("shop", "Acme", 5 * 3_600_000, `${APP}/#/`, STOP),
|
|
60
|
+
"digest-weekly": digest("shop", "Acme", "weekly", WEEK, { project: `${APP}/#/`, stop: STOP }),
|
|
61
|
+
"digest-daily": digest("shop", "Acme", "daily", { ...WEEK, alertsFired: 0, errorRate: h(0.9, 1.4), errors: [] }, { project: `${APP}/#/`, stop: STOP }),
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=samples.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"samples.js","sourceRoot":"","sources":["../../src/mail/samples.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Ctx } from "../app.js";
|
|
2
|
+
/** 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. */
|
|
3
|
+
export declare function stopRoute(ctx: Ctx): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=stop-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stop-route.d.ts","sourceRoot":"","sources":["../../src/mail/stop-route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AA0BrC,gJAAgJ;AAChJ,wBAAsB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAavD"}
|
|
@@ -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 @@
|
|
|
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"}
|