@wizeworks/lens-cloud 0.3.0-alpha.1 → 0.3.0-alpha.3
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/README.md +58 -5
- package/dist/alerts/evaluate.d.ts.map +1 -1
- package/dist/alerts/evaluate.js +20 -6
- package/dist/alerts/evaluate.js.map +1 -1
- package/dist/alerts/notify.d.ts +13 -2
- package/dist/alerts/notify.d.ts.map +1 -1
- package/dist/alerts/notify.js +22 -16
- package/dist/alerts/notify.js.map +1 -1
- package/dist/app.d.ts +16 -2
- package/dist/app.d.ts.map +1 -1
- 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 +0 -5
- package/dist/auth/crypto.d.ts.map +1 -1
- package/dist/auth/crypto.js +3 -20
- package/dist/auth/crypto.js.map +1 -1
- 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/join-scope.d.ts +7 -0
- package/dist/auth/join-scope.d.ts.map +1 -0
- package/dist/auth/join-scope.js +17 -0
- package/dist/auth/join-scope.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 +8 -3
- package/dist/auth/principal.d.ts.map +1 -1
- package/dist/auth/principal.js +13 -8
- package/dist/auth/principal.js.map +1 -1
- package/dist/auth/roles.d.ts +43 -3
- package/dist/auth/roles.d.ts.map +1 -1
- package/dist/auth/roles.js +15 -6
- package/dist/auth/roles.js.map +1 -1
- 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/changes.d.ts +15 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +45 -0
- package/dist/changes.js.map +1 -0
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +66 -14
- package/dist/cli-args.js.map +1 -1
- package/dist/cli.js +22 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +30 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -1
- package/dist/copy-cli.d.ts +3 -0
- package/dist/copy-cli.d.ts.map +1 -0
- package/dist/copy-cli.js +24 -0
- package/dist/copy-cli.js.map +1 -0
- package/dist/db/db.d.ts +7 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +28 -10
- package/dist/db/db.js.map +1 -1
- package/dist/db/migrate.d.ts +2 -2
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +15 -5
- package/dist/db/migrate.js.map +1 -1
- 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-search.d.ts +2 -0
- package/dist/db/schema-search.d.ts.map +1 -0
- package/dist/db/schema-search.js +16 -0
- package/dist/db/schema-search.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.map +1 -1
- package/dist/db/schema.js +10 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/trigram.d.ts +4 -0
- package/dist/db/trigram.d.ts.map +1 -0
- package/dist/db/trigram.js +34 -0
- package/dist/db/trigram.js.map +1 -0
- package/dist/http/http.d.ts +2 -0
- package/dist/http/http.d.ts.map +1 -1
- package/dist/http/http.js +7 -3
- package/dist/http/http.js.map +1 -1
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +23 -11
- package/dist/jobs.js.map +1 -1
- 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 +50 -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 +13 -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 +32 -0
- package/dist/mail/theme.d.ts.map +1 -0
- package/dist/mail/theme.js +27 -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 +32 -0
- package/dist/notify/digest-data.d.ts.map +1 -0
- package/dist/notify/digest-data.js +31 -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 +42 -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 +6 -2
- package/dist/repo/alerts.d.ts.map +1 -1
- package/dist/repo/alerts.js.map +1 -1
- 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/orgs.d.ts +15 -25
- package/dist/repo/orgs.d.ts.map +1 -1
- package/dist/repo/orgs.js +18 -60
- package/dist/repo/orgs.js.map +1 -1
- package/dist/repo/services.d.ts +6 -0
- package/dist/repo/services.d.ts.map +1 -0
- package/dist/repo/services.js +16 -0
- package/dist/repo/services.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-join.d.ts +9 -0
- package/dist/repo/trace-join.d.ts.map +1 -0
- package/dist/repo/trace-join.js +35 -0
- package/dist/repo/trace-join.js.map +1 -0
- package/dist/repo/trace-read.d.ts +6 -13
- package/dist/repo/trace-read.d.ts.map +1 -1
- package/dist/repo/trace-read.js +8 -3
- package/dist/repo/trace-read.js.map +1 -1
- package/dist/repo/trace-write.d.ts +5 -1
- package/dist/repo/trace-write.d.ts.map +1 -1
- package/dist/repo/trace-write.js +11 -2
- package/dist/repo/trace-write.js.map +1 -1
- package/dist/repo/users.d.ts +5 -16
- package/dist/repo/users.d.ts.map +1 -1
- package/dist/repo/users.js +2 -60
- package/dist/repo/users.js.map +1 -1
- package/dist/routes/alerts.d.ts.map +1 -1
- package/dist/routes/alerts.js +23 -6
- package/dist/routes/alerts.js.map +1 -1
- package/dist/routes/changes.d.ts +4 -0
- package/dist/routes/changes.d.ts.map +1 -0
- package/dist/routes/changes.js +19 -0
- package/dist/routes/changes.js.map +1 -0
- package/dist/routes/data.d.ts.map +1 -1
- package/dist/routes/data.js +24 -9
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +8 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/ingest.d.ts.map +1 -1
- package/dist/routes/ingest.js +7 -2
- package/dist/routes/ingest.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +42 -25
- package/dist/routes/mcp.js.map +1 -1
- 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 +2 -2
- package/dist/routes/metrics.d.ts.map +1 -1
- package/dist/routes/metrics.js +2 -3
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/models.d.ts +2 -0
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +10 -5
- package/dist/routes/models.js.map +1 -1
- 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.map +1 -1
- package/dist/routes/orgs.js +34 -53
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +5 -3
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/search.d.ts +4 -0
- package/dist/routes/search.d.ts.map +1 -0
- package/dist/routes/search.js +33 -0
- package/dist/routes/search.js.map +1 -0
- package/dist/routes/shares.d.ts.map +1 -1
- package/dist/routes/shares.js +8 -5
- package/dist/routes/shares.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +59 -16
- package/dist/server.js.map +1 -1
- package/dist/settle.d.ts +7 -6
- package/dist/settle.d.ts.map +1 -1
- package/dist/settle.js +30 -13
- package/dist/settle.js.map +1 -1
- package/dist/telemetry/cached.d.ts +11 -0
- package/dist/telemetry/cached.d.ts.map +1 -0
- package/dist/telemetry/cached.js +30 -0
- package/dist/telemetry/cached.js.map +1 -0
- package/dist/telemetry/ch/admin.d.ts +10 -0
- package/dist/telemetry/ch/admin.d.ts.map +1 -0
- package/dist/telemetry/ch/admin.js +65 -0
- package/dist/telemetry/ch/admin.js.map +1 -0
- package/dist/telemetry/ch/http.d.ts +19 -0
- package/dist/telemetry/ch/http.d.ts.map +1 -0
- package/dist/telemetry/ch/http.js +91 -0
- package/dist/telemetry/ch/http.js.map +1 -0
- package/dist/telemetry/ch/metrics.d.ts +10 -0
- package/dist/telemetry/ch/metrics.d.ts.map +1 -0
- package/dist/telemetry/ch/metrics.js +61 -0
- package/dist/telemetry/ch/metrics.js.map +1 -0
- package/dist/telemetry/ch/queries.d.ts +19 -0
- package/dist/telemetry/ch/queries.d.ts.map +1 -0
- package/dist/telemetry/ch/queries.js +30 -0
- package/dist/telemetry/ch/queries.js.map +1 -0
- package/dist/telemetry/ch/queue.d.ts +34 -0
- package/dist/telemetry/ch/queue.d.ts.map +1 -0
- package/dist/telemetry/ch/queue.js +79 -0
- package/dist/telemetry/ch/queue.js.map +1 -0
- package/dist/telemetry/ch/read.d.ts +12 -0
- package/dist/telemetry/ch/read.d.ts.map +1 -0
- package/dist/telemetry/ch/read.js +72 -0
- package/dist/telemetry/ch/read.js.map +1 -0
- package/dist/telemetry/ch/schema-facts.d.ts +6 -0
- package/dist/telemetry/ch/schema-facts.d.ts.map +1 -0
- package/dist/telemetry/ch/schema-facts.js +49 -0
- package/dist/telemetry/ch/schema-facts.js.map +1 -0
- package/dist/telemetry/ch/schema-raw.d.ts +8 -0
- package/dist/telemetry/ch/schema-raw.d.ts.map +1 -0
- package/dist/telemetry/ch/schema-raw.js +35 -0
- package/dist/telemetry/ch/schema-raw.js.map +1 -0
- package/dist/telemetry/ch/schema-rollups.d.ts +2 -0
- package/dist/telemetry/ch/schema-rollups.d.ts.map +1 -0
- package/dist/telemetry/ch/schema-rollups.js +61 -0
- package/dist/telemetry/ch/schema-rollups.js.map +1 -0
- package/dist/telemetry/ch/schema.d.ts +12 -0
- package/dist/telemetry/ch/schema.d.ts.map +1 -0
- package/dist/telemetry/ch/schema.js +25 -0
- package/dist/telemetry/ch/schema.js.map +1 -0
- package/dist/telemetry/ch/settle-rows.d.ts +13 -0
- package/dist/telemetry/ch/settle-rows.d.ts.map +1 -0
- package/dist/telemetry/ch/settle-rows.js +67 -0
- package/dist/telemetry/ch/settle-rows.js.map +1 -0
- package/dist/telemetry/ch/settle.d.ts +7 -0
- package/dist/telemetry/ch/settle.d.ts.map +1 -0
- package/dist/telemetry/ch/settle.js +96 -0
- package/dist/telemetry/ch/settle.js.map +1 -0
- package/dist/telemetry/ch/store.d.ts +16 -0
- package/dist/telemetry/ch/store.d.ts.map +1 -0
- package/dist/telemetry/ch/store.js +65 -0
- package/dist/telemetry/ch/store.js.map +1 -0
- package/dist/telemetry/ch/util.d.ts +20 -0
- package/dist/telemetry/ch/util.d.ts.map +1 -0
- package/dist/telemetry/ch/util.js +45 -0
- package/dist/telemetry/ch/util.js.map +1 -0
- package/dist/telemetry/ch/write.d.ts +11 -0
- package/dist/telemetry/ch/write.d.ts.map +1 -0
- package/dist/telemetry/ch/write.js +59 -0
- package/dist/telemetry/ch/write.js.map +1 -0
- package/dist/telemetry/copy.d.ts +9 -0
- package/dist/telemetry/copy.d.ts.map +1 -0
- package/dist/telemetry/copy.js +60 -0
- package/dist/telemetry/copy.js.map +1 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +21 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/pg-store.d.ts +5 -0
- package/dist/telemetry/pg-store.d.ts.map +1 -0
- package/dist/telemetry/pg-store.js +78 -0
- package/dist/telemetry/pg-store.js.map +1 -0
- package/dist/telemetry/store.d.ts +74 -0
- package/dist/telemetry/store.d.ts.map +1 -0
- package/dist/telemetry/store.js +2 -0
- package/dist/telemetry/store.js.map +1 -0
- package/package.json +20 -6
- package/dist/routes/auth.d.ts +0 -4
- package/dist/routes/auth.d.ts.map +0 -1
- package/dist/routes/auth.js +0 -106
- package/dist/routes/auth.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
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 defaults = { from: config.mailFrom, ...(config.mailReplyTo ? { replyTo: config.mailReplyTo } : {}) };
|
|
8
|
+
const transport = createTransport(config.smtpUrl, defaults);
|
|
9
|
+
return { via: "smtp", send: async (m) => void (await transport.sendMail({ ...defaults, ...m })), close: async () => transport.close() };
|
|
10
|
+
}
|
|
11
|
+
const logSender = async (m) => console.info(`[wizelens-cloud] mail (no mail service set, not sent) to ${m.to}: ${m.subject}\n${m.text}\n`);
|
|
12
|
+
/** The test sender, then Resend, then SMTP, then the log. */
|
|
13
|
+
async function transportOf(config) {
|
|
14
|
+
if (config.sendMail)
|
|
15
|
+
return { via: "test", send: config.sendMail };
|
|
16
|
+
if (config.resendApiKey)
|
|
17
|
+
return { via: "resend", send: resendSender({ apiKey: config.resendApiKey, from: config.mailFrom, replyTo: config.mailReplyTo, fetch: config.fetch }) };
|
|
18
|
+
if (config.smtpUrl)
|
|
19
|
+
return smtpSender(config);
|
|
20
|
+
return { via: "log", send: logSender };
|
|
21
|
+
}
|
|
22
|
+
/** A bounce skips every email. A complaint skips mail the person did not ask for. */
|
|
23
|
+
async function skipReason(db, mail) {
|
|
24
|
+
const reason = await suppressionOf(db, mail.to);
|
|
25
|
+
if (reason === "bounce")
|
|
26
|
+
return "it bounced before";
|
|
27
|
+
if (reason === "complaint" && !ALWAYS.has(mail.kind))
|
|
28
|
+
return "it marked our mail as spam";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** The cloud's mail, with the suppression list checked before each send. */
|
|
32
|
+
export async function createMailer(config, db) {
|
|
33
|
+
const t = await transportOf(config);
|
|
34
|
+
const deliver = async (pending) => {
|
|
35
|
+
const mail = await pending;
|
|
36
|
+
const skip = await skipReason(db, mail);
|
|
37
|
+
if (skip)
|
|
38
|
+
return console.info(`[wizelens-cloud] mail to ${mail.to} skipped: ${skip}.`);
|
|
39
|
+
await t.send(mail).catch((err) => {
|
|
40
|
+
throw new Error(`mail to ${mail.to} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
live: t.via !== "log",
|
|
45
|
+
via: t.via,
|
|
46
|
+
send: (mail) => void deliver(mail).catch((err) => console.error("[wizelens-cloud]", err instanceof Error ? err.message : err)),
|
|
47
|
+
close: async () => t.close?.(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# 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,13 @@
|
|
|
1
|
+
import type { Mail } from "./mailer.js";
|
|
2
|
+
export interface ResendSettings {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
from: string;
|
|
5
|
+
/** Where replies go. Left out when unset. */
|
|
6
|
+
replyTo?: string;
|
|
7
|
+
fetch: typeof fetch;
|
|
8
|
+
/** Wait between tries. Tests pass a fast one. */
|
|
9
|
+
sleep?: (ms: number) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** Send one email through the Resend API. The same Idempotency-Key on every try, so a retry never sends it twice. */
|
|
12
|
+
export declare function resendSender(s: ResendSettings): (mail: Mail) => Promise<void>;
|
|
13
|
+
//# 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,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,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, ...(s.replyTo ? { reply_to: s.replyTo } : {}), 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,32 @@
|
|
|
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
|
+
/** The mark with its center dot in the tone color. The site build makes these (site/scripts/og/build.mjs). */
|
|
22
|
+
readonly markUrl: {
|
|
23
|
+
readonly neutral: "https://wizelens.com/brand/mail-neutral.png";
|
|
24
|
+
readonly good: "https://wizelens.com/brand/mail-good.png";
|
|
25
|
+
readonly warn: "https://wizelens.com/brand/mail-warn.png";
|
|
26
|
+
readonly bad: "https://wizelens.com/brand/mail-bad.png";
|
|
27
|
+
};
|
|
28
|
+
readonly homeUrl: "https://wizelens.com";
|
|
29
|
+
readonly company: "WizeLens by WizeWorks LLC, Visalia, California";
|
|
30
|
+
};
|
|
31
|
+
export type Tone = keyof typeof theme.tone;
|
|
32
|
+
//# 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;IACV,8GAA8G;aAC9G,OAAO;iBACL,OAAO,EAAE,6CAA6C;iBACtD,IAAI,EAAE,0CAA0C;iBAChD,IAAI,EAAE,0CAA0C;iBAChD,GAAG,EAAE,yCAAyC;;aAEhD,OAAO,EAAE,sBAAsB;aAC/B,OAAO,EAAE,gDAAgD;CACjD,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,MAAM,OAAO,KAAK,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/** The mark with its center dot in the tone color. The site build makes these (site/scripts/og/build.mjs). */
|
|
18
|
+
markUrl: {
|
|
19
|
+
neutral: "https://wizelens.com/brand/mail-neutral.png",
|
|
20
|
+
good: "https://wizelens.com/brand/mail-good.png",
|
|
21
|
+
warn: "https://wizelens.com/brand/mail-warn.png",
|
|
22
|
+
bad: "https://wizelens.com/brand/mail-bad.png",
|
|
23
|
+
},
|
|
24
|
+
homeUrl: "https://wizelens.com",
|
|
25
|
+
company: "WizeLens by WizeWorks LLC, Visalia, California",
|
|
26
|
+
};
|
|
27
|
+
//# 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,8GAA8G;IAC9G,OAAO,EAAE;QACP,OAAO,EAAE,6CAA6C;QACtD,IAAI,EAAE,0CAA0C;QAChD,IAAI,EAAE,0CAA0C;QAChD,GAAG,EAAE,yCAAyC;KAC/C;IACD,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":"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"}
|
|
@@ -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"}
|