ghost 5.115.0 → 5.116.0
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/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
- package/components/tryghost-constants-5.116.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.116.0.tgz +0 -0
- package/components/{tryghost-donations-5.115.0.tgz → tryghost-donations-5.116.0.tgz} +0 -0
- package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
- package/components/tryghost-email-service-5.116.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
- package/components/tryghost-i18n-5.116.0.tgz +0 -0
- package/components/tryghost-job-manager-5.116.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
- package/components/tryghost-magic-link-5.116.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.116.0.tgz +0 -0
- package/components/tryghost-member-events-5.116.0.tgz +0 -0
- package/components/tryghost-members-api-5.116.0.tgz +0 -0
- package/components/tryghost-members-csv-5.116.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
- package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
- package/components/{tryghost-post-events-5.115.0.tgz → tryghost-post-events-5.116.0.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
- package/components/tryghost-posts-service-5.116.0.tgz +0 -0
- package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
- package/components/tryghost-security-5.116.0.tgz +0 -0
- package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.116.0.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +11 -6
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/core/boot.js +3 -43
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +30494 -29403
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
- package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-a9601514.mjs} +5 -4
- package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-c1789d04.mjs} +67 -65
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-84580c3a.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f744cab7.mjs} +3147 -3123
- package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-d9ca60c5.mjs} +1198 -1192
- package/core/built/admin/assets/chunk.524.8371443ef8f60db429d0.js +35 -0
- package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.js +37 -0
- package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
- package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +272 -251
- package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
- package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
- package/core/built/admin/assets/posts/posts.js +5732 -5667
- package/core/built/admin/assets/stats/stats.js +75373 -0
- package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
- package/core/built/admin/index.html +6 -6
- package/core/cli/generate-data.js +1 -1
- package/core/frontend/helpers/ghost_head.js +8 -1
- package/core/frontend/public/ghost-stats.js +55 -2
- package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
- package/core/frontend/services/assets-minification/CardAssets.js +1 -1
- package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
- package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
- package/core/frontend/services/assets-minification/Minifier.js +191 -0
- package/core/frontend/services/routing/controllers/previews.js +2 -1
- package/core/frontend/src/cards/css/cta.css +1 -1
- package/core/server/adapters/cache/Redis.js +1 -1
- package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
- package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
- package/core/server/api/endpoints/posts.js +9 -3
- package/core/server/api/endpoints/previews.js +35 -1
- package/core/server/api/endpoints/slugs.js +6 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
- package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
- package/core/server/data/db/connection.js +2 -0
- package/core/server/data/db/index.js +1 -0
- package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
- package/core/server/data/importer/import-manager.js +3 -3
- package/core/server/data/importer/importers/importer-revue.js +128 -0
- package/core/server/data/importer/importers/json-to-html.js +107 -0
- package/core/server/data/migrations/utils/tables.js +2 -4
- package/core/server/data/seeders/DataGenerator.js +288 -0
- package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
- package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
- package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
- package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
- package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
- package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
- package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
- package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
- package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
- package/core/server/data/seeders/importers/MembersImporter.js +111 -0
- package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
- package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
- package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
- package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
- package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
- package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
- package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
- package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
- package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
- package/core/server/data/seeders/importers/OffersImporter.js +70 -0
- package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
- package/core/server/data/seeders/importers/PostsImporter.js +102 -0
- package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
- package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
- package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
- package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
- package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
- package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
- package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
- package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
- package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
- package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
- package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
- package/core/server/data/seeders/importers/TableImporter.js +187 -0
- package/core/server/data/seeders/importers/TagsImporter.js +41 -0
- package/core/server/data/seeders/importers/UsersImporter.js +31 -0
- package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
- package/core/server/data/seeders/importers/index.js +41 -0
- package/core/server/data/seeders/utils/JsonImporter.js +39 -0
- package/core/server/data/seeders/utils/blog-info.js +3 -0
- package/core/server/data/seeders/utils/database-date.js +7 -0
- package/core/server/data/seeders/utils/event-generator.js +48 -0
- package/core/server/data/seeders/utils/random.js +13 -0
- package/core/server/data/seeders/utils/topological-sort.js +33 -0
- package/core/server/lib/bootstrap-socket.js +87 -0
- package/core/server/lib/package-json/index.js +1 -0
- package/core/server/lib/package-json/package-json.js +160 -0
- package/core/server/lib/package-json/parse.js +57 -0
- package/core/server/models/base/plugins/actions.js +44 -31
- package/core/server/models/base/plugins/generate-slug.js +6 -0
- package/core/server/notify.js +1 -1
- package/core/server/services/activitypub/ActivityPubService.ts +1 -1
- package/core/server/services/adapter-manager/AdapterManager.js +161 -0
- package/core/server/services/adapter-manager/index.js +1 -1
- package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
- package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
- package/core/server/services/announcement-bar-service/index.js +1 -1
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
- package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
- package/core/server/services/api-version-compatibility/index.js +2 -2
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
- package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
- package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
- package/core/server/services/audience-feedback/Feedback.js +35 -0
- package/core/server/services/audience-feedback/index.js +4 -2
- package/core/server/services/auth/session/emails/signin.js +168 -0
- package/core/server/services/auth/session/index.js +2 -2
- package/core/server/services/auth/session/session-from-token.js +69 -0
- package/core/server/services/auth/session/session-service.js +374 -0
- package/core/server/services/custom-redirects/index.js +1 -1
- package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
- package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
- package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
- package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
- package/core/server/services/email-suppression-list/service.js +1 -1
- package/core/server/services/explore-ping/ExplorePingService.js +106 -0
- package/core/server/services/explore-ping/index.js +31 -0
- package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
- package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
- package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
- package/core/server/services/invitations/accept.js +5 -2
- package/core/server/services/lib/DynamicRedirectManager.js +156 -0
- package/core/server/services/lib/EmailContentGenerator.js +54 -0
- package/core/server/services/lib/InMemoryRepository.js +62 -0
- package/core/server/services/lib/InMemoryRepository.ts +80 -0
- package/core/server/services/lib/MailgunClient.js +364 -0
- package/core/server/services/link-redirection/LinkRedirect.js +26 -0
- package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
- package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
- package/core/server/services/link-redirection/README.md +151 -0
- package/core/server/services/link-redirection/RedirectEvent.js +24 -0
- package/core/server/services/link-redirection/index.js +1 -1
- package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
- package/core/server/services/mail/index.js +1 -1
- package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
- package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
- package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
- package/core/server/services/mail-events/MailEvent.js +20 -0
- package/core/server/services/mail-events/MailEvent.ts +10 -0
- package/core/server/services/mail-events/MailEventRepository.js +2 -0
- package/core/server/services/mail-events/MailEventRepository.ts +5 -0
- package/core/server/services/mail-events/MailEventService.js +124 -0
- package/core/server/services/mail-events/MailEventService.ts +169 -0
- package/core/server/services/mail-events/index.js +1 -1
- package/core/server/services/mail-events/libraries.d.ts +2 -0
- package/core/server/services/members/CaptchaService.js +80 -0
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
- package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
- package/core/server/services/members/importer/email-template.js +182 -0
- package/core/server/services/members/importer/index.js +30 -0
- package/core/server/services/members/members-ssr.js +333 -0
- package/core/server/services/members/service.js +2 -2
- package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
- package/core/server/services/offers/service.js +1 -1
- package/core/server/services/posts/stats/PostStats.js +13 -0
- package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
- package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
- package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
- package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
- package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
- package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
- package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
- package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
- package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
- package/core/server/services/recommendations/service/ClickEvent.js +33 -0
- package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
- package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
- package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
- package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
- package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
- package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
- package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
- package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
- package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
- package/core/server/services/recommendations/service/Recommendation.js +140 -0
- package/core/server/services/recommendations/service/Recommendation.ts +201 -0
- package/core/server/services/recommendations/service/RecommendationController.js +208 -0
- package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
- package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
- package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
- package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
- package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
- package/core/server/services/recommendations/service/RecommendationService.js +228 -0
- package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
- package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
- package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
- package/core/server/services/recommendations/service/UnsafeData.js +183 -0
- package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
- package/core/server/services/recommendations/service/WellknownService.js +36 -0
- package/core/server/services/recommendations/service/WellknownService.ts +47 -0
- package/core/server/services/recommendations/service/index.js +31 -0
- package/core/server/services/recommendations/service/index.ts +15 -0
- package/core/server/services/recommendations/service/libraries.d.ts +5 -0
- package/core/server/services/route-settings/SettingsPathManager.js +47 -0
- package/core/server/services/route-settings/index.js +1 -1
- package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
- package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
- package/core/server/services/slack-notifications/service.js +4 -6
- package/core/server/services/stripe/README.md +63 -0
- package/core/server/services/stripe/StripeAPI.js +931 -0
- package/core/server/services/stripe/StripeMigrations.js +613 -0
- package/core/server/services/stripe/StripeService.js +175 -0
- package/core/server/services/stripe/WebhookController.js +100 -0
- package/core/server/services/stripe/WebhookManager.js +175 -0
- package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
- package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
- package/core/server/services/stripe/events/index.js +4 -0
- package/core/server/services/stripe/service.js +1 -1
- package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
- package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
- package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
- package/core/server/services/themes/loader.js +1 -1
- package/core/server/services/themes/to-json.js +1 -1
- package/core/server/web/api/endpoints/admin/app.js +1 -21
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/api/middleware/version-match.js +41 -0
- package/core/server/web/shared/middleware/cache-control.js +51 -0
- package/core/server/web/shared/middleware/index.js +1 -1
- package/core/server/web/well-known.js +1 -1
- package/core/shared/labs.js +5 -3
- package/core/shared/settings-cache/CacheManager.js +64 -6
- package/package.json +98 -146
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +1478 -1634
- package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
- package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.115.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
- package/components/tryghost-constants-5.115.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
- package/components/tryghost-data-generator-5.115.0.tgz +0 -0
- package/components/tryghost-domain-events-5.115.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.115.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.115.0.tgz +0 -0
- package/components/tryghost-email-events-5.115.0.tgz +0 -0
- package/components/tryghost-email-service-5.115.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.115.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.115.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
- package/components/tryghost-ghost-5.115.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
- package/components/tryghost-i18n-5.115.0.tgz +0 -0
- package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.115.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
- package/components/tryghost-job-manager-5.115.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.115.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
- package/components/tryghost-magic-link-5.115.0.tgz +0 -0
- package/components/tryghost-mail-events-5.115.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.115.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
- package/components/tryghost-member-events-5.115.0.tgz +0 -0
- package/components/tryghost-members-api-5.115.0.tgz +0 -0
- package/components/tryghost-members-csv-5.115.0.tgz +0 -0
- package/components/tryghost-members-importer-5.115.0.tgz +0 -0
- package/components/tryghost-members-payments-5.115.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
- package/components/tryghost-minifier-5.115.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.115.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
- package/components/tryghost-package-json-5.115.0.tgz +0 -0
- package/components/tryghost-posts-service-5.115.0.tgz +0 -0
- package/components/tryghost-recommendations-5.115.0.tgz +0 -0
- package/components/tryghost-referrers-5.115.0.tgz +0 -0
- package/components/tryghost-security-5.115.0.tgz +0 -0
- package/components/tryghost-session-service-5.115.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.115.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
- package/components/tryghost-webmentions-5.115.0.tgz +0 -0
- package/core/built/admin/assets/chunk.524.31419fdf6fb3859ecc1e.js +0 -35
- package/core/built/admin/assets/chunk.582.08c816d5e4ab766486a7.js +0 -37
- package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
- package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
- /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Link Redirects
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Develop
|
|
8
|
+
|
|
9
|
+
This is a monorepo package.
|
|
10
|
+
|
|
11
|
+
Follow the instructions for the top-level repo.
|
|
12
|
+
1. `git clone` this repo & `cd` into it as usual
|
|
13
|
+
2. Run `yarn` to install top-level dependencies.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Test
|
|
18
|
+
|
|
19
|
+
- `yarn lint` run just eslint
|
|
20
|
+
- `yarn test` run lint and tests
|
|
21
|
+
|
|
22
|
+
## Overview of how Ghost handles LinkRedirects
|
|
23
|
+
### Summary
|
|
24
|
+
When a publisher sends an email newsletter with email click analytics enabled, Ghost will replace all the links in the email's content with a link of the form `https://{site_url}/r/{redirect hash}?m={member UUID}`. When a member clicks on a link in their email, Ghost receives the request, redirects the user to the original link's URL, then updates some analytics data in the database.
|
|
25
|
+
|
|
26
|
+
### The details
|
|
27
|
+
The following deep-dive covers the link redirect flow from when the member clicks on a link in an email.
|
|
28
|
+
|
|
29
|
+
First, we lookup the redirect by the `/r/{hash}` value in the URL:
|
|
30
|
+
```
|
|
31
|
+
select `redirects`.* from `redirects` where `redirects`.`from` = ? limit ? undefined
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If the redirect exists, the `LinkRedirectsService` emits a `RedirectEvent`, and then responds to the HTTP request with a 302.
|
|
35
|
+
|
|
36
|
+
The `LinkClickTrackingService` subscribes to the `RedirectEvent` and kicks off the analytics inserts/updates. First we grab the `uuid` from the `?m={uuid}` parameter and lookup the member by `uuid`:
|
|
37
|
+
```
|
|
38
|
+
select `members`.* from `members` where `members`.`uuid` = ? limit ? undefined
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then we insert a row into the `members_click_events` table to record the click:
|
|
42
|
+
```
|
|
43
|
+
insert into `members_click_events` (`created_at`, `id`, `member_id`, `redirect_id`) values (?, ?, ?, ?) undefined
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then we query for the row we just inserted:
|
|
47
|
+
```
|
|
48
|
+
select `members_click_events`.* from `members_click_events` where `members_click_events`.`id` = ? limit ? undefined
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
At this point, we emit a `MemberLinkClickEvent` with the member ID and `lastSeenAt` timestamp.
|
|
52
|
+
|
|
53
|
+
The `LastSeenAtUpdater` subscribes to the `MemberLinkClickEvent`. First, it checks if the `lastSeenAt` value has already been updated in the current day.
|
|
54
|
+
|
|
55
|
+
If it has, we stop here.
|
|
56
|
+
|
|
57
|
+
If it hasn't, we continue to update the member. First, we select the member by ID:
|
|
58
|
+
```
|
|
59
|
+
select `members`.* from `members` where `members`.`id` = ? limit ? undefined
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then we start a transaction and get a lock on the member for updating:
|
|
63
|
+
```
|
|
64
|
+
BEGIN; trx34
|
|
65
|
+
select `members`.* from `members` where `members`.`id` = ? limit ? for update trx34
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Since we're editing the member, we will eventually need to emit a `member.edited` webhook with the standard includes (labels and newsletters) so we also query them here:
|
|
69
|
+
```
|
|
70
|
+
select `labels`.*, `members_labels`.`member_id` as `_pivot_member_id`, `members_labels`.`label_id` as `_pivot_label_id`, `members_labels`.`sort_order` as `_pivot_sort_order` from `labels` inner join `members_labels` on `members_labels`.`label_id` = `labels`.`id` where `members_labels`.`member_id` in (?) order by `sort_order` ASC for update trx34
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then we query the member's newsletters:
|
|
74
|
+
```
|
|
75
|
+
select `newsletters`.*, `members_newsletters`.`member_id` as `_pivot_member_id`, `members_newsletters`.`newsletter_id` as `_pivot_newsletter_id` from `newsletters` inner join `members_newsletters` on `members_newsletters`.`newsletter_id` = `newsletters`.`id` where `members_newsletters`.`member_id` in (?) order by `newsletters`.`sort_order` ASC for update trx34
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then we update the member:
|
|
79
|
+
```
|
|
80
|
+
update `members` set `uuid` = ?, `transient_id` = ?, `email` = ?, `status` = ?, `name` = ?, `expertise` = ?, `note` = ?, `geolocation` = ?, `enable_comment_notifications` = ?, `email_count` = ?, `email_opened_count` = ?, `email_open_rate` = ?, `email_disabled` = ?, `last_seen_at` = ?, `last_commented_at` = ?, `created_at` = ?, `created_by` = ?, `updated_at` = ?, `updated_by` = ? where `id` = ? trx34
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then we select the member by ID again to get the freshly updated values from the DB:
|
|
84
|
+
```
|
|
85
|
+
select `members`.* from `members` where `members`.`id` = ? limit ? trx34
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then we commit the transaction:
|
|
89
|
+
```
|
|
90
|
+
COMMIT; trx34
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Finally, we query for any member.edited webhooks and fire the `member.edited` event:
|
|
94
|
+
```
|
|
95
|
+
select `webhooks`.* from `webhooks` where `event` = ? trx34
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### Sequence Diagram
|
|
100
|
+
```mermaid
|
|
101
|
+
sequenceDiagram
|
|
102
|
+
actor Member
|
|
103
|
+
participant Ghost
|
|
104
|
+
participant Ghost Async
|
|
105
|
+
participant DB
|
|
106
|
+
rect rgba(0,100,0, 0.1)
|
|
107
|
+
Member ->>Ghost: Clicks link in email
|
|
108
|
+
Ghost ->> DB: Query: lookup redirect
|
|
109
|
+
DB ->> Ghost: Redirect record
|
|
110
|
+
Note right of DB: Serve the redirect
|
|
111
|
+
Ghost -->> Ghost Async: Emit RedirectEvent
|
|
112
|
+
Ghost ->> Member: 302 Redirect
|
|
113
|
+
end
|
|
114
|
+
rect rgba(100,0,0,0.1)
|
|
115
|
+
Ghost Async ->> DB: Lookup Member by `uuid` from URL param
|
|
116
|
+
DB ->> Ghost Async: `member` record
|
|
117
|
+
Ghost Async ->> DB: Insert `member_click_event`
|
|
118
|
+
Note right of DB: Insert click event
|
|
119
|
+
DB ->> Ghost Async: 👌
|
|
120
|
+
Ghost Async ->> DB: Select `member_click_event`
|
|
121
|
+
DB ->> Ghost Async: `member_click_event` record
|
|
122
|
+
end
|
|
123
|
+
rect rgba(0,0,100, 0.1)
|
|
124
|
+
Ghost Async ->> DB: Select `member` by id
|
|
125
|
+
DB ->> Ghost Async: `member` record
|
|
126
|
+
Ghost Async ->> DB: Begin transaction
|
|
127
|
+
activate DB
|
|
128
|
+
DB ->> Ghost Async: 👌
|
|
129
|
+
Ghost Async ->> DB: Select `member` for update
|
|
130
|
+
DB ->> Ghost Async: `member` record
|
|
131
|
+
Ghost Async ->> DB: Select member labels for update
|
|
132
|
+
Note right of DB: Update member `lastSeenAt`
|
|
133
|
+
DB ->> Ghost Async: Member's labels
|
|
134
|
+
Ghost Async ->> DB: Select member newsletters for update
|
|
135
|
+
DB ->> Ghost Async: Member's newsletters
|
|
136
|
+
Ghost Async ->> DB: Update member's `lastSeenAt` timestamp
|
|
137
|
+
DB ->> Ghost Async: 👌
|
|
138
|
+
Ghost Async ->> DB: Select `member` by ID
|
|
139
|
+
DB ->> Ghost Async: `member` record
|
|
140
|
+
Ghost Async ->> DB: Commit transaction
|
|
141
|
+
DB ->> Ghost Async: 👌
|
|
142
|
+
deactivate DB
|
|
143
|
+
end
|
|
144
|
+
rect rgba(100,100,0,0.1)
|
|
145
|
+
Ghost Async ->> DB: Select `webhooks`
|
|
146
|
+
Note right of DB: Send `member.edited` webhook
|
|
147
|
+
DB ->> Ghost Async: `webhook` records
|
|
148
|
+
create participant Webhook Recipient
|
|
149
|
+
Ghost Async ->> Webhook Recipient: `member.edited` webhook
|
|
150
|
+
end
|
|
151
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} RedirectEventData
|
|
3
|
+
* @prop {URL} url
|
|
4
|
+
* @prop {import('./LinkRedirect')} link
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = class RedirectEvent {
|
|
8
|
+
/**
|
|
9
|
+
* @param {RedirectEventData} data
|
|
10
|
+
* @param {Date} timestamp
|
|
11
|
+
*/
|
|
12
|
+
constructor(data, timestamp) {
|
|
13
|
+
this.data = data;
|
|
14
|
+
this.timestamp = timestamp;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {RedirectEventData} data
|
|
19
|
+
* @param {Date} [timestamp]
|
|
20
|
+
*/
|
|
21
|
+
static create(data, timestamp) {
|
|
22
|
+
return new RedirectEvent(data, timestamp ?? new Date);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -14,7 +14,7 @@ class LinkRedirectsServiceWrapper {
|
|
|
14
14
|
// Wire up all the dependencies
|
|
15
15
|
const models = require('../../models');
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const LinkRedirectsService = require('./LinkRedirectsService');
|
|
18
18
|
|
|
19
19
|
this.linkRedirectRepository = new LinkRedirectRepository({
|
|
20
20
|
LinkRedirect: models.Redirect,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
3
|
const settingsCache = require('../../../shared/settings-cache');
|
|
4
|
-
const EmailContentGenerator = require('
|
|
4
|
+
const EmailContentGenerator = require('../lib/EmailContentGenerator');
|
|
5
5
|
|
|
6
6
|
const emailContentGenerator = new EmailContentGenerator({
|
|
7
7
|
getSiteUrl: () => urlUtils.urlFor('home', true),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {import('
|
|
3
|
-
* @typedef {import('
|
|
2
|
+
* @typedef {import('./MailEventRepository')} MailEventRepository
|
|
3
|
+
* @typedef {import('./MailEvent').MailEvent} MailEvent
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryMailEventRepository = void 0;
|
|
4
|
+
const InMemoryRepository_1 = require("../lib/InMemoryRepository");
|
|
5
|
+
class InMemoryMailEventRepository extends InMemoryRepository_1.InMemoryRepository {
|
|
6
|
+
toPrimitive() {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.InMemoryMailEventRepository = InMemoryMailEventRepository;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MailEvent = void 0;
|
|
4
|
+
class MailEvent {
|
|
5
|
+
id;
|
|
6
|
+
type;
|
|
7
|
+
messageId;
|
|
8
|
+
recipient;
|
|
9
|
+
timestampMs;
|
|
10
|
+
deleted;
|
|
11
|
+
constructor(id, type, messageId, recipient, timestampMs, deleted = false) {
|
|
12
|
+
this.id = id;
|
|
13
|
+
this.type = type;
|
|
14
|
+
this.messageId = messageId;
|
|
15
|
+
this.recipient = recipient;
|
|
16
|
+
this.timestampMs = timestampMs;
|
|
17
|
+
this.deleted = deleted;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.MailEvent = MailEvent;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MailEventService = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const errors_1 = __importDefault(require("@tryghost/errors"));
|
|
9
|
+
const tpl_1 = __importDefault(require("@tryghost/tpl"));
|
|
10
|
+
const MailEvent_1 = require("./MailEvent");
|
|
11
|
+
/**
|
|
12
|
+
* @see https://documentation.mailgun.com/en/latest/user_manual.html#events-1
|
|
13
|
+
*/
|
|
14
|
+
var EventType;
|
|
15
|
+
(function (EventType) {
|
|
16
|
+
EventType["CLICKED"] = "clicked";
|
|
17
|
+
EventType["COMPLAINED"] = "complained";
|
|
18
|
+
EventType["DELIVERED"] = "delivered";
|
|
19
|
+
EventType["FAILED"] = "failed";
|
|
20
|
+
EventType["OPENED"] = "opened";
|
|
21
|
+
EventType["UNSUBSCRIBED"] = "unsubscribed";
|
|
22
|
+
})(EventType || (EventType = {}));
|
|
23
|
+
const VALIDATION_MESSAGES = {
|
|
24
|
+
signingKeyNotConfigured: 'payload signing key is not configured',
|
|
25
|
+
payloadSignatureMissing: 'Payload is missing "signature"',
|
|
26
|
+
payloadSignatureInvalid: '"signature" is invalid',
|
|
27
|
+
payloadEventsMissing: 'Payload is missing "mail_events"',
|
|
28
|
+
payloadEventsInvalid: '"mail_events" is not an array',
|
|
29
|
+
payloadEventKeyMissing: 'Event [{idx}] is missing "{key}"'
|
|
30
|
+
};
|
|
31
|
+
class MailEventService {
|
|
32
|
+
mailEventRepository;
|
|
33
|
+
config;
|
|
34
|
+
labs;
|
|
35
|
+
static LABS_KEY = 'mailEvents';
|
|
36
|
+
static CONFIG_KEY_PAYLOAD_SIGNING_KEY = 'hostSettings:mailEventsPayloadSigningKey';
|
|
37
|
+
constructor(mailEventRepository, config, labs) {
|
|
38
|
+
this.mailEventRepository = mailEventRepository;
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.labs = labs;
|
|
41
|
+
}
|
|
42
|
+
async processPayload(payload) {
|
|
43
|
+
if (this.labs.isSet(MailEventService.LABS_KEY) === false) {
|
|
44
|
+
throw new errors_1.default.NotFoundError();
|
|
45
|
+
}
|
|
46
|
+
const payloadSigningKey = this.config.get(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY);
|
|
47
|
+
// Verify that the service is configured correctly - We expect a string
|
|
48
|
+
// for the payload signing key but as a safeguard we check the type here
|
|
49
|
+
// to prevent any unexpected behaviour
|
|
50
|
+
if (typeof payloadSigningKey !== 'string') {
|
|
51
|
+
throw new errors_1.default.InternalServerError({
|
|
52
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.signingKeyNotConfigured)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Verify the payload
|
|
56
|
+
this.verifyPayload(payload, payloadSigningKey);
|
|
57
|
+
// Store known events
|
|
58
|
+
const eventTypes = new Set(Object.values(EventType));
|
|
59
|
+
for (const payloadEvent of payload.mail_events) {
|
|
60
|
+
if (eventTypes.has(payloadEvent.event) === false) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await this.mailEventRepository.save(new MailEvent_1.MailEvent(payloadEvent.id, payloadEvent.event, payloadEvent.message.headers['message-id'], payloadEvent.recipient, payloadEvent.timestamp * 1000));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new errors_1.default.InternalServerError({
|
|
68
|
+
message: 'Event could not be stored',
|
|
69
|
+
err: err
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
validatePayload(payload) {
|
|
75
|
+
if (payload.signature === undefined) {
|
|
76
|
+
throw new errors_1.default.ValidationError({
|
|
77
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadSignatureMissing)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (typeof payload.signature !== 'string') {
|
|
81
|
+
throw new errors_1.default.ValidationError({
|
|
82
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadSignatureInvalid)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (payload.mail_events === undefined) {
|
|
86
|
+
throw new errors_1.default.ValidationError({
|
|
87
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventsMissing)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(payload.mail_events) === false) {
|
|
91
|
+
throw new errors_1.default.ValidationError({
|
|
92
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventsInvalid)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const expectedKeys = ['id', 'timestamp', 'event', 'message', 'recipient'];
|
|
96
|
+
payload.mail_events.forEach((payloadEvent, idx) => {
|
|
97
|
+
expectedKeys.forEach((key) => {
|
|
98
|
+
if (payloadEvent[key] === undefined) {
|
|
99
|
+
throw new errors_1.default.ValidationError({
|
|
100
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventKeyMissing, { idx, key })
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (key === 'message' && payloadEvent.message.headers?.['message-id'] === undefined) {
|
|
104
|
+
throw new errors_1.default.ValidationError({
|
|
105
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadEventKeyMissing, { idx, key: 'message.headers.message-id' })
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
verifyPayload(payload, payloadSigningKey) {
|
|
112
|
+
const data = JSON.stringify(payload.mail_events);
|
|
113
|
+
const signature = crypto_1.default
|
|
114
|
+
.createHmac('sha256', payloadSigningKey)
|
|
115
|
+
.update(data)
|
|
116
|
+
.digest('hex');
|
|
117
|
+
if (signature !== payload.signature) {
|
|
118
|
+
throw new errors_1.default.UnauthorizedError({
|
|
119
|
+
message: (0, tpl_1.default)(VALIDATION_MESSAGES.payloadSignatureInvalid)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.MailEventService = MailEventService;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import errors from '@tryghost/errors';
|
|
3
|
+
import tpl from '@tryghost/tpl';
|
|
4
|
+
|
|
5
|
+
import {MailEvent} from './MailEvent';
|
|
6
|
+
import {MailEventRepository} from './MailEventRepository';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @see https://documentation.mailgun.com/en/latest/user_manual.html#events-1
|
|
10
|
+
*/
|
|
11
|
+
enum EventType { // eslint-disable-line no-shadow
|
|
12
|
+
CLICKED = 'clicked',
|
|
13
|
+
COMPLAINED = 'complained',
|
|
14
|
+
DELIVERED = 'delivered',
|
|
15
|
+
FAILED = 'failed',
|
|
16
|
+
OPENED = 'opened',
|
|
17
|
+
UNSUBSCRIBED = 'unsubscribed'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PayloadEvent {
|
|
21
|
+
id: string;
|
|
22
|
+
timestamp: number; // Unix timestamp in seconds
|
|
23
|
+
event: string;
|
|
24
|
+
message: {
|
|
25
|
+
headers: {
|
|
26
|
+
'message-id': string;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
recipient: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Payload {
|
|
33
|
+
signature: string;
|
|
34
|
+
mail_events: PayloadEvent[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Labs {
|
|
38
|
+
isSet(key: string): boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Config {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
get(key: string): any;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const VALIDATION_MESSAGES = {
|
|
47
|
+
signingKeyNotConfigured: 'payload signing key is not configured',
|
|
48
|
+
payloadSignatureMissing: 'Payload is missing "signature"',
|
|
49
|
+
payloadSignatureInvalid: '"signature" is invalid',
|
|
50
|
+
payloadEventsMissing: 'Payload is missing "mail_events"',
|
|
51
|
+
payloadEventsInvalid: '"mail_events" is not an array',
|
|
52
|
+
payloadEventKeyMissing: 'Event [{idx}] is missing "{key}"'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export class MailEventService {
|
|
56
|
+
static readonly LABS_KEY = 'mailEvents';
|
|
57
|
+
static readonly CONFIG_KEY_PAYLOAD_SIGNING_KEY = 'hostSettings:mailEventsPayloadSigningKey';
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private mailEventRepository: MailEventRepository,
|
|
61
|
+
private config: Config,
|
|
62
|
+
private labs: Labs
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
async processPayload(payload: Payload) {
|
|
66
|
+
if (this.labs.isSet(MailEventService.LABS_KEY) === false) {
|
|
67
|
+
throw new errors.NotFoundError();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const payloadSigningKey = this.config.get(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY);
|
|
71
|
+
|
|
72
|
+
// Verify that the service is configured correctly - We expect a string
|
|
73
|
+
// for the payload signing key but as a safeguard we check the type here
|
|
74
|
+
// to prevent any unexpected behaviour
|
|
75
|
+
if (typeof payloadSigningKey !== 'string') {
|
|
76
|
+
throw new errors.InternalServerError({
|
|
77
|
+
message: tpl(VALIDATION_MESSAGES.signingKeyNotConfigured)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Verify the payload
|
|
82
|
+
this.verifyPayload(payload, payloadSigningKey);
|
|
83
|
+
|
|
84
|
+
// Store known events
|
|
85
|
+
const eventTypes = new Set<string>(Object.values(EventType) as string[]);
|
|
86
|
+
|
|
87
|
+
for (const payloadEvent of payload.mail_events) {
|
|
88
|
+
if (eventTypes.has(payloadEvent.event) === false) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await this.mailEventRepository.save(
|
|
94
|
+
new MailEvent(
|
|
95
|
+
payloadEvent.id,
|
|
96
|
+
payloadEvent.event,
|
|
97
|
+
payloadEvent.message.headers['message-id'],
|
|
98
|
+
payloadEvent.recipient,
|
|
99
|
+
payloadEvent.timestamp * 1000
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new errors.InternalServerError({
|
|
104
|
+
message: 'Event could not be stored',
|
|
105
|
+
err: err
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
validatePayload(payload: Payload) {
|
|
112
|
+
if (payload.signature === undefined) {
|
|
113
|
+
throw new errors.ValidationError({
|
|
114
|
+
message: tpl(VALIDATION_MESSAGES.payloadSignatureMissing)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof payload.signature !== 'string') {
|
|
119
|
+
throw new errors.ValidationError({
|
|
120
|
+
message: tpl(VALIDATION_MESSAGES.payloadSignatureInvalid)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (payload.mail_events === undefined) {
|
|
125
|
+
throw new errors.ValidationError({
|
|
126
|
+
message: tpl(VALIDATION_MESSAGES.payloadEventsMissing)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (Array.isArray(payload.mail_events) === false) {
|
|
131
|
+
throw new errors.ValidationError({
|
|
132
|
+
message: tpl(VALIDATION_MESSAGES.payloadEventsInvalid)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const expectedKeys: (keyof PayloadEvent)[] = ['id', 'timestamp', 'event', 'message', 'recipient'];
|
|
137
|
+
|
|
138
|
+
payload.mail_events.forEach((payloadEvent, idx) => {
|
|
139
|
+
expectedKeys.forEach((key) => {
|
|
140
|
+
if (payloadEvent[key] === undefined) {
|
|
141
|
+
throw new errors.ValidationError({
|
|
142
|
+
message: tpl(VALIDATION_MESSAGES.payloadEventKeyMissing, {idx, key})
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (key === 'message' && payloadEvent.message.headers?.['message-id'] === undefined) {
|
|
147
|
+
throw new errors.ValidationError({
|
|
148
|
+
message: tpl(VALIDATION_MESSAGES.payloadEventKeyMissing, {idx, key: 'message.headers.message-id'})
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private verifyPayload(payload: Payload, payloadSigningKey: string) {
|
|
156
|
+
const data = JSON.stringify(payload.mail_events);
|
|
157
|
+
|
|
158
|
+
const signature = crypto
|
|
159
|
+
.createHmac('sha256', payloadSigningKey)
|
|
160
|
+
.update(data)
|
|
161
|
+
.digest('hex');
|
|
162
|
+
|
|
163
|
+
if (signature !== payload.signature) {
|
|
164
|
+
throw new errors.UnauthorizedError({
|
|
165
|
+
message: tpl(VALIDATION_MESSAGES.payloadSignatureInvalid)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const hcaptcha = require('hcaptcha');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
const {InternalServerError, BadRequestError, utils: errorUtils} = require('@tryghost/errors');
|
|
4
|
+
|
|
5
|
+
class CaptchaService {
|
|
6
|
+
#enabled;
|
|
7
|
+
#scoreThreshold;
|
|
8
|
+
#secretKey;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {boolean} [options.enabled] Whether hCaptcha is enabled
|
|
13
|
+
* @param {number} [options.scoreThreshold] Score threshold for bot detection
|
|
14
|
+
* @param {string} [options.secretKey] hCaptcha secret key
|
|
15
|
+
*/
|
|
16
|
+
constructor({
|
|
17
|
+
enabled,
|
|
18
|
+
scoreThreshold,
|
|
19
|
+
secretKey
|
|
20
|
+
}) {
|
|
21
|
+
this.#enabled = enabled;
|
|
22
|
+
this.#secretKey = secretKey;
|
|
23
|
+
this.#scoreThreshold = scoreThreshold;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getMiddleware() {
|
|
27
|
+
const scoreThreshold = this.#scoreThreshold;
|
|
28
|
+
const secretKey = this.#secretKey;
|
|
29
|
+
|
|
30
|
+
if (!this.#enabled) {
|
|
31
|
+
return function captchaNoOpMiddleware(req, res, next) {
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return async function captchaMiddleware(req, res, next) {
|
|
37
|
+
let captchaResponse;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (!req.body || !req.body.token) {
|
|
41
|
+
throw new BadRequestError({
|
|
42
|
+
message: 'hCaptcha token missing'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
captchaResponse = await hcaptcha.verify(secretKey, req.body.token, req.ip);
|
|
47
|
+
|
|
48
|
+
if ('score' in captchaResponse && captchaResponse.score < scoreThreshold) {
|
|
49
|
+
// Using hCaptcha enterprise, so score is present
|
|
50
|
+
next();
|
|
51
|
+
} else if (!('score' in captchaResponse) && captchaResponse.success) {
|
|
52
|
+
// Using regular hCaptcha, so challenge-based
|
|
53
|
+
next();
|
|
54
|
+
} else {
|
|
55
|
+
logging.error(`Blocking request due to high score (${captchaResponse.score})`);
|
|
56
|
+
|
|
57
|
+
// Intentionally left sparse to avoid leaking information
|
|
58
|
+
throw new InternalServerError();
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (errorUtils.isGhostError(err)) {
|
|
62
|
+
return next(err);
|
|
63
|
+
} else {
|
|
64
|
+
const message = 'Failed to verify hCaptcha token';
|
|
65
|
+
|
|
66
|
+
logging.error(new InternalServerError({
|
|
67
|
+
message,
|
|
68
|
+
err
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
return next(new InternalServerError({
|
|
72
|
+
message
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = CaptchaService;
|
|
@@ -18,7 +18,7 @@ const tiersService = require('../tiers');
|
|
|
18
18
|
const newslettersService = require('../newsletters');
|
|
19
19
|
const memberAttributionService = require('../member-attribution');
|
|
20
20
|
const emailSuppressionList = require('../email-suppression-list');
|
|
21
|
-
const CaptchaService = require('
|
|
21
|
+
const CaptchaService = require('./CaptchaService');
|
|
22
22
|
const {t} = require('../i18n');
|
|
23
23
|
const sentry = require('../../../shared/sentry');
|
|
24
24
|
const sharedConfig = require('../../../shared/config');
|