ghost 5.115.0 → 5.115.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  2. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  3. package/components/{tryghost-announcement-bar-settings-5.115.0.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
  4. package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.115.1.tgz +0 -0
  6. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.115.0.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
  9. package/components/tryghost-domain-events-5.115.1.tgz +0 -0
  10. package/components/tryghost-donations-5.115.1.tgz +0 -0
  11. package/components/{tryghost-email-addresses-5.115.0.tgz → tryghost-email-addresses-5.115.1.tgz} +0 -0
  12. package/components/{tryghost-email-content-generator-5.115.0.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
  13. package/components/{tryghost-email-events-5.115.0.tgz → tryghost-email-events-5.115.1.tgz} +0 -0
  14. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  15. package/components/{tryghost-email-suppression-list-5.115.0.tgz → tryghost-email-suppression-list-5.115.1.tgz} +0 -0
  16. package/components/{tryghost-express-dynamic-redirects-5.115.0.tgz → tryghost-express-dynamic-redirects-5.115.1.tgz} +0 -0
  17. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  18. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  19. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  20. package/components/{tryghost-importer-handler-content-files-5.115.0.tgz → tryghost-importer-handler-content-files-5.115.1.tgz} +0 -0
  21. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  22. package/components/{tryghost-job-manager-5.115.0.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
  23. package/components/{tryghost-link-redirects-5.115.0.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
  24. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  25. package/components/{tryghost-magic-link-5.115.0.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
  26. package/components/{tryghost-mailgun-client-5.115.0.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
  27. package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
  28. package/components/{tryghost-member-events-5.115.0.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
  29. package/components/{tryghost-members-api-5.115.0.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
  30. package/components/{tryghost-members-csv-5.115.0.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
  31. package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
  32. package/components/{tryghost-members-payments-5.115.0.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
  33. package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
  34. package/components/{tryghost-minifier-5.115.0.tgz → tryghost-minifier-5.115.1.tgz} +0 -0
  35. package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
  36. package/components/{tryghost-mw-version-match-5.115.0.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
  37. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  38. package/components/tryghost-post-events-5.115.1.tgz +0 -0
  39. package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
  40. package/components/{tryghost-posts-service-5.115.0.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
  41. package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
  42. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  43. package/components/{tryghost-security-5.115.0.tgz → tryghost-security-5.115.1.tgz} +0 -0
  44. package/components/{tryghost-slack-notifications-5.115.0.tgz → tryghost-slack-notifications-5.115.1.tgz} +0 -0
  45. package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
  46. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  47. package/content/themes/casper/LICENSE +1 -1
  48. package/content/themes/casper/README.md +1 -1
  49. package/content/themes/source/LICENSE +1 -1
  50. package/content/themes/source/README.md +1 -1
  51. package/content/themes/source/assets/built/screen.css +1 -1
  52. package/content/themes/source/assets/built/screen.css.map +1 -1
  53. package/content/themes/source/assets/css/screen.css +11 -6
  54. package/content/themes/source/partials/feature-image.hbs +2 -2
  55. package/core/boot.js +3 -1
  56. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
  57. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  58. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
  59. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
  60. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
  61. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  62. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
  63. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
  64. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
  65. package/core/built/admin/assets/{chunk.524.31419fdf6fb3859ecc1e.js → chunk.524.2439684964c164c598ab.js} +6 -6
  66. package/core/built/admin/assets/{chunk.582.08c816d5e4ab766486a7.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
  67. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
  68. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
  69. package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +181 -159
  70. package/core/built/admin/assets/stats/stats.js +11824 -0
  71. package/core/built/admin/index.html +4 -4
  72. package/core/frontend/helpers/ghost_head.js +3 -1
  73. package/core/frontend/src/cards/css/cta.css +1 -1
  74. package/core/server/api/endpoints/slugs.js +6 -2
  75. package/core/server/data/importer/import-manager.js +2 -2
  76. package/core/server/data/importer/importers/importer-revue.js +128 -0
  77. package/core/server/data/importer/importers/json-to-html.js +107 -0
  78. package/core/server/data/migrations/utils/tables.js +2 -4
  79. package/core/server/lib/bootstrap-socket.js +87 -0
  80. package/core/server/lib/package-json/index.js +1 -0
  81. package/core/server/lib/package-json/package-json.js +160 -0
  82. package/core/server/lib/package-json/parse.js +57 -0
  83. package/core/server/models/base/plugins/actions.js +44 -31
  84. package/core/server/models/base/plugins/generate-slug.js +6 -0
  85. package/core/server/notify.js +1 -1
  86. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  87. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  88. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  89. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  90. package/core/server/services/api-version-compatibility/index.js +2 -2
  91. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  92. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  93. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  94. package/core/server/services/audience-feedback/Feedback.js +35 -0
  95. package/core/server/services/audience-feedback/index.js +4 -2
  96. package/core/server/services/auth/session/emails/signin.js +168 -0
  97. package/core/server/services/auth/session/index.js +2 -2
  98. package/core/server/services/auth/session/session-from-token.js +69 -0
  99. package/core/server/services/auth/session/session-service.js +364 -0
  100. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  101. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  102. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  103. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  104. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  105. package/core/server/services/explore-ping/index.js +31 -0
  106. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  107. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  108. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  109. package/core/server/services/invitations/accept.js +5 -2
  110. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  111. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  112. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  113. package/core/server/services/mail-events/MailEvent.js +20 -0
  114. package/core/server/services/mail-events/MailEvent.ts +10 -0
  115. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  116. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  117. package/core/server/services/mail-events/MailEventService.js +124 -0
  118. package/core/server/services/mail-events/MailEventService.ts +169 -0
  119. package/core/server/services/mail-events/index.js +1 -1
  120. package/core/server/services/mail-events/libraries.d.ts +2 -0
  121. package/core/server/services/members/CaptchaService.js +80 -0
  122. package/core/server/services/members/api.js +1 -1
  123. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  124. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  125. package/core/server/services/members/importer/email-template.js +182 -0
  126. package/core/server/services/members/importer/index.js +30 -0
  127. package/core/server/services/members/members-ssr.js +333 -0
  128. package/core/server/services/members/service.js +2 -2
  129. package/core/server/services/posts/stats/PostStats.js +13 -0
  130. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  131. package/core/server/services/route-settings/index.js +1 -1
  132. package/core/server/services/stripe/README.md +63 -0
  133. package/core/server/services/stripe/StripeAPI.js +931 -0
  134. package/core/server/services/stripe/StripeMigrations.js +613 -0
  135. package/core/server/services/stripe/StripeService.js +175 -0
  136. package/core/server/services/stripe/WebhookController.js +100 -0
  137. package/core/server/services/stripe/WebhookManager.js +175 -0
  138. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  139. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  140. package/core/server/services/stripe/events/index.js +4 -0
  141. package/core/server/services/stripe/service.js +1 -1
  142. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  143. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  144. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  145. package/core/server/services/themes/loader.js +1 -1
  146. package/core/server/services/themes/to-json.js +1 -1
  147. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  148. package/core/server/web/shared/middleware/cache-control.js +51 -0
  149. package/core/server/web/shared/middleware/index.js +1 -1
  150. package/core/server/web/well-known.js +1 -1
  151. package/core/shared/labs.js +3 -1
  152. package/core/shared/settings-cache/CacheManager.js +64 -6
  153. package/package.json +103 -134
  154. package/tsconfig.tsbuildinfo +1 -1
  155. package/yarn.lock +7 -93
  156. package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
  157. package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
  158. package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
  159. package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
  160. package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
  161. package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
  162. package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
  163. package/components/tryghost-constants-5.115.0.tgz +0 -0
  164. package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
  165. package/components/tryghost-domain-events-5.115.0.tgz +0 -0
  166. package/components/tryghost-donations-5.115.0.tgz +0 -0
  167. package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
  168. package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
  169. package/components/tryghost-email-service-5.115.0.tgz +0 -0
  170. package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
  171. package/components/tryghost-ghost-5.115.0.tgz +0 -0
  172. package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
  173. package/components/tryghost-i18n-5.115.0.tgz +0 -0
  174. package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
  175. package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
  176. package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
  177. package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
  178. package/components/tryghost-mail-events-5.115.0.tgz +0 -0
  179. package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
  180. package/components/tryghost-members-importer-5.115.0.tgz +0 -0
  181. package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
  182. package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
  183. package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
  184. package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
  185. package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
  186. package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
  187. package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
  188. package/components/tryghost-package-json-5.115.0.tgz +0 -0
  189. package/components/tryghost-post-events-5.115.0.tgz +0 -0
  190. package/components/tryghost-recommendations-5.115.0.tgz +0 -0
  191. package/components/tryghost-referrers-5.115.0.tgz +0 -0
  192. package/components/tryghost-session-service-5.115.0.tgz +0 -0
  193. package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
  194. package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
  195. package/components/tryghost-webmentions-5.115.0.tgz +0 -0
  196. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  197. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
@@ -0,0 +1,333 @@
1
+ const {parse: parseUrl} = require('url');
2
+ const createCookies = require('cookies');
3
+ const debug = require('@tryghost/debug')('members-ssr');
4
+
5
+ const {
6
+ BadRequestError,
7
+ IncorrectUsageError
8
+ } = require('@tryghost/errors');
9
+
10
+ /**
11
+ * @typedef {import('http').IncomingMessage} Request
12
+ * @typedef {import('http').ServerResponse} Response
13
+ * @typedef {import('cookies').ICookies} Cookies
14
+ * @typedef {import('cookies').Option} CookiesOptions
15
+ * @typedef {import('cookies').SetOption} SetCookieOptions
16
+ * @typedef {string} JWT
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} Member
21
+ * @prop {string} id
22
+ * @prop {string} transient_id
23
+ * @prop {string} email
24
+ */
25
+
26
+ const SIX_MONTHS_MS = 1000 * 60 * 60 * 24 * 184;
27
+
28
+ class MembersSSR {
29
+ /**
30
+ * @typedef {object} MembersSSROptions
31
+ *
32
+ * @prop {string|string[]} cookieKeys - A secret or array of secrets used to sign cookies
33
+ * @prop {() => object} getMembersApi - A function which returns an instance of members-api
34
+ * @prop {boolean} [cookieSecure = true] - Whether the cookie should have Secure flag
35
+ * @prop {string} [cookieName] - The name of the members-ssr cookie
36
+ * @prop {number} [cookieMaxAge] - The max age in ms of the members-ssr cookie
37
+ * @prop {string} [cookiePath] - The Path flag for the cookie
38
+ * @prop {boolean} [dangerousRemovalOfSignedCookie] - Flag for removing signed cookie
39
+ */
40
+
41
+ /**
42
+ * Create an instance of MembersSSR
43
+ *
44
+ * @param {MembersSSROptions} options - The options for the members ssr class
45
+ */
46
+ constructor(options) {
47
+ const {
48
+ cookieSecure = true,
49
+ cookieName = 'members-ssr',
50
+ cookieMaxAge = SIX_MONTHS_MS,
51
+ cookiePath = '/',
52
+ cookieKeys,
53
+ getMembersApi,
54
+ dangerousRemovalOfSignedCookie
55
+ } = options;
56
+
57
+ if (!getMembersApi) {
58
+ throw new IncorrectUsageError({message: 'Missing option getMembersApi'});
59
+ }
60
+
61
+ this._getMembersApi = getMembersApi;
62
+
63
+ if (!cookieKeys) {
64
+ throw new IncorrectUsageError({message: 'Missing option cookieKeys'});
65
+ }
66
+
67
+ this.sessionCookieName = cookieName;
68
+
69
+ /**
70
+ * @type SetCookieOptions
71
+ */
72
+ this.sessionCookieOptions = {
73
+ signed: true,
74
+ httpOnly: true,
75
+ sameSite: 'lax',
76
+ maxAge: cookieMaxAge,
77
+ path: cookiePath
78
+ };
79
+
80
+ if (dangerousRemovalOfSignedCookie === true) {
81
+ this.sessionCookieOptions.signed = false;
82
+ }
83
+
84
+ /**
85
+ * @type CookiesOptions
86
+ */
87
+ this.cookiesOptions = {
88
+ keys: Array.isArray(cookieKeys) ? cookieKeys : [cookieKeys],
89
+ secure: cookieSecure
90
+ };
91
+ }
92
+
93
+ /**
94
+ * @method _getCookies
95
+ *
96
+ * @param {Request} req
97
+ * @param {Response} res
98
+ *
99
+ * @returns {Cookies} An instance of the cookies object for current request/response
100
+ */
101
+ _getCookies(req, res) {
102
+ return createCookies(req, res, this.cookiesOptions);
103
+ }
104
+
105
+ /**
106
+ * @method _removeSessionCookie
107
+ *
108
+ * @param {Request} req
109
+ * @param {Response} res
110
+ */
111
+ _removeSessionCookie(req, res) {
112
+ const cookies = this._getCookies(req, res);
113
+ cookies.set(this.sessionCookieName, null, this.sessionCookieOptions);
114
+ // If members caching cookies are set, remove them
115
+ if (cookies.get('ghost-access') || cookies.get('ghost-access-hmac')) {
116
+ cookies.set('ghost-access', null, {...this.sessionCookieOptions, signed: false});
117
+ cookies.set('ghost-access-hmac', null, {...this.sessionCookieOptions, signed: false});
118
+ }
119
+ }
120
+
121
+ /**
122
+ * @method _setSessionCookie
123
+ *
124
+ * @param {Request} req
125
+ * @param {Response} res
126
+ * @param {string} value
127
+ */
128
+ _setSessionCookie(req, res, value) {
129
+ if (!value) {
130
+ return this._removeSessionCookie(req, res);
131
+ }
132
+ const cookies = this._getCookies(req, res);
133
+ cookies.set(this.sessionCookieName, value, this.sessionCookieOptions);
134
+ }
135
+
136
+ /**
137
+ * @method _getSessionCookies
138
+ *
139
+ * @param {Request} req
140
+ * @param {Response} res
141
+ *
142
+ * @returns {string} The cookie value
143
+ */
144
+ _getSessionCookies(req, res) {
145
+ const cookies = this._getCookies(req, res);
146
+ const value = cookies.get(this.sessionCookieName, {signed: true});
147
+ if (!value) {
148
+ throw new BadRequestError({
149
+ message: `Cookie ${this.sessionCookieName} not found`
150
+ });
151
+ }
152
+ return value;
153
+ }
154
+
155
+ /**
156
+ * @method _getMemberDataFromToken
157
+ *
158
+ * @param {JWT} token
159
+ *
160
+ * @returns {Promise<Member>} member
161
+ */
162
+ async _getMemberDataFromToken(token) {
163
+ const api = await this._getMembersApi();
164
+ return api.getMemberDataFromMagicLinkToken(token);
165
+ }
166
+
167
+ /**
168
+ * @method _getMemberIdentityData
169
+ *
170
+ * @param {string} email
171
+ *
172
+ * @returns {Promise<Member>} member
173
+ */
174
+ async _getMemberIdentityData(email) {
175
+ const api = await this._getMembersApi();
176
+ return api.getMemberIdentityData(email);
177
+ }
178
+
179
+ /**
180
+ * @method _getMemberIdentityData
181
+ *
182
+ * @param {string} transientId
183
+ *
184
+ * @returns {Promise<Member>} member
185
+ */
186
+ async _getMemberIdentityDataFromTransientId(transientId) {
187
+ const api = await this._getMembersApi();
188
+ return api.getMemberIdentityDataFromTransientId(transientId);
189
+ }
190
+
191
+ /**
192
+ * @method _getMemberIdentityToken
193
+ *
194
+ * @param {string} email
195
+ *
196
+ * @returns {Promise<JWT>} member
197
+ */
198
+ async _getMemberIdentityToken(transientId) {
199
+ const api = await this._getMembersApi();
200
+ return api.getMemberIdentityToken(transientId);
201
+ }
202
+
203
+ /**
204
+ * @method _setMemberGeolocationFromIp
205
+ * @param {string} email
206
+ * @param {string} ip
207
+ *
208
+ * @returns {Promise<Member>} member
209
+ */
210
+ async _setMemberGeolocationFromIp(email, ip) {
211
+ const api = await this._getMembersApi();
212
+ return api.setMemberGeolocationFromIp(email, ip);
213
+ }
214
+
215
+ /**
216
+ * @method exchangeTokenForSession
217
+ * @param {Request} req
218
+ * @param {Response} res
219
+ *
220
+ * @returns {Promise<Member>} The member the session was created for
221
+ */
222
+ async exchangeTokenForSession(req, res) {
223
+ if (!req.url) {
224
+ return Promise.reject(new BadRequestError({
225
+ message: 'Expected token param containing JWT'
226
+ }));
227
+ }
228
+
229
+ const {query} = parseUrl(req.url, true);
230
+ if (!query || !query.token) {
231
+ return Promise.reject(new BadRequestError({
232
+ message: 'Expected token param containing JWT'
233
+ }));
234
+ }
235
+
236
+ const token = Array.isArray(query.token) ? query.token[0] : query.token;
237
+ const member = await this._getMemberDataFromToken(token);
238
+
239
+ if (!member) {
240
+ // The member doesn't exist any longer (could be a sign in token for a member that was deleted)
241
+ return Promise.reject(new BadRequestError({
242
+ message: 'Invalid token'
243
+ }));
244
+ }
245
+
246
+ // perform and store geoip lookup for members when they log in
247
+ if (!member.geolocation) {
248
+ try {
249
+ await this._setMemberGeolocationFromIp(member.email, req.ip);
250
+ } catch (err) {
251
+ // no-op, we don't want to stop anything working due to
252
+ // geolocation lookup failing
253
+ debug(`Geolocation lookup failed: ${err.message}`);
254
+ }
255
+ }
256
+
257
+ this._setSessionCookie(req, res, member.transient_id);
258
+
259
+ return member;
260
+ }
261
+
262
+ async _cycleTransientId(memberId) {
263
+ const api = await this._getMembersApi();
264
+ return api.cycleTransientId(memberId);
265
+ }
266
+
267
+ /**
268
+ * @method deleteSession
269
+ * @param {Request} req
270
+ * @param {Response} res
271
+ *
272
+ * @returns {Promise<void>}
273
+ */
274
+ async deleteSession(req, res) {
275
+ if (req.body && typeof req.body === 'object' && req.body.all) {
276
+ // Update transient_id to invalidate all sessions
277
+ const member = await this.getMemberDataFromSession(req, res);
278
+ if (member) {
279
+ await this._cycleTransientId(member.id);
280
+ }
281
+ }
282
+ this._removeSessionCookie(req, res);
283
+ }
284
+
285
+ /**
286
+ * @method getMemberDataFromSession
287
+ *
288
+ * @param {Request} req
289
+ * @param {Response} res
290
+ *
291
+ * @returns {Promise<Member>}
292
+ */
293
+ async getMemberDataFromSession(req, res) {
294
+ const transientId = this._getSessionCookies(req, res);
295
+ const member = await this._getMemberIdentityDataFromTransientId(transientId);
296
+ return member;
297
+ }
298
+
299
+ /**
300
+ * @method getIdentityTokenForMemberFromSession
301
+ *
302
+ * @param {Request} req
303
+ * @param {Response} res
304
+ *
305
+ * @returns {Promise<JWT>} identity token
306
+ */
307
+ async getIdentityTokenForMemberFromSession(req, res) {
308
+ const transientId = this._getSessionCookies(req, res);
309
+ const token = await this._getMemberIdentityToken(transientId);
310
+ if (!token) {
311
+ await this.deleteSession(req, res);
312
+ throw new BadRequestError({
313
+ message: 'Invalid session, could not get identity token'
314
+ });
315
+ }
316
+ return token;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Factory function for creating instance of MembersSSR
322
+ *
323
+ * @param {MembersSSROptions} options
324
+ * @returns {MembersSSR}
325
+ */
326
+ module.exports = function create(options) {
327
+ if (!options) {
328
+ throw new IncorrectUsageError({
329
+ message: 'Must pass options'
330
+ });
331
+ }
332
+ return new MembersSSR(options);
333
+ };
@@ -1,10 +1,10 @@
1
1
  const _ = require('lodash');
2
2
  const errors = require('@tryghost/errors');
3
3
  const tpl = require('@tryghost/tpl');
4
- const MembersSSR = require('@tryghost/members-ssr');
4
+ const MembersSSR = require('./members-ssr');
5
5
  const db = require('../../data/db');
6
6
  const MembersConfigProvider = require('./MembersConfigProvider');
7
- const makeMembersCSVImporter = require('@tryghost/members-importer');
7
+ const makeMembersCSVImporter = require('./importer');
8
8
  const MembersStats = require('./stats/MembersStats');
9
9
  const memberJobs = require('./jobs');
10
10
  const logging = require('@tryghost/logging');
@@ -20,6 +20,19 @@ class PostStats {
20
20
  return result?.[0]?.published_at ? new Date(result?.[0]?.published_at) : null;
21
21
  }
22
22
 
23
+ /**
24
+ * Returns the first published post date
25
+ */
26
+ async getFirstPublishedPostDate() {
27
+ const result = await this.#db.knex.select('published_at')
28
+ .from('posts')
29
+ .whereIn('status', ['sent', 'published'])
30
+ .orderBy('published_at', 'asc')
31
+ .limit(1);
32
+
33
+ return result?.[0]?.published_at ? new Date(result?.[0]?.published_at) : null;
34
+ }
35
+
23
36
  /**
24
37
  * Fetches count of all published posts
25
38
  */
@@ -0,0 +1,47 @@
1
+ const path = require('path');
2
+ const tpl = require('@tryghost/tpl');
3
+ const format = require('date-fns/format');
4
+ const {IncorrectUsageError} = require('@tryghost/errors');
5
+
6
+ const messages = {
7
+ incorrectPathsParameter: 'Attempted to setup settings path manager without paths values.'
8
+ };
9
+
10
+ class SettingsPathManager {
11
+ /**
12
+ *
13
+ * @param {Object} options
14
+ * @param {String[]} options.paths - file location paths ordered in priority by where to locate them first
15
+ * @param {String} options.type setting file type, e.g: 'routes' or 'redirects'
16
+ * @param {String[]} [options.extensions] the supported file extensions with 'yaml' and 'json' defaults. Note 'yml' extension is ignored on purpose
17
+ */
18
+ constructor({type, paths, extensions = ['yaml', 'json']}) {
19
+ if (!paths || !paths.length) {
20
+ throw new IncorrectUsageError({
21
+ message: tpl(messages.incorrectPathsParameter)
22
+ });
23
+ }
24
+
25
+ this.type = type;
26
+ this.filename = type;
27
+
28
+ this.paths = paths;
29
+ this.defaultPath = paths[0];
30
+
31
+ this.extensions = extensions;
32
+ this.defaultExtension = extensions[0];
33
+ }
34
+
35
+ getDefaultFilePath() {
36
+ const settingsFolder = this.defaultPath;
37
+ return path.join(settingsFolder, `${this.filename}.${this.defaultExtension}`);
38
+ }
39
+
40
+ getBackupFilePath() {
41
+ const settingsFolder = this.defaultPath;
42
+ const dateStamp = format(new Date(), 'yyyy-MM-dd-HH-mm-ss');
43
+ return path.join(settingsFolder, `${this.filename}-${dateStamp}.${this.defaultExtension}`);
44
+ }
45
+ }
46
+
47
+ module.exports = SettingsPathManager;
@@ -1,6 +1,5 @@
1
1
  const config = require('../../../shared/config');
2
2
  const parseYaml = require('./yaml-parser');
3
- const SettingsPathManager = require('@tryghost/settings-path-manager');
4
3
 
5
4
  let settingsLoader;
6
5
  let routeSettings;
@@ -10,6 +9,7 @@ module.exports = {
10
9
  const RouteSettings = require('./RouteSettings');
11
10
  const SettingsLoader = require('./SettingsLoader');
12
11
  const DefaultSettingsManager = require('./DefaultSettingsManager');
12
+ const SettingsPathManager = require('./SettingsPathManager');
13
13
 
14
14
  const settingsPathManager = new SettingsPathManager({type: 'routes', paths: [config.getContentPath('settings')]});
15
15
  settingsLoader = new SettingsLoader({parseYaml, settingFilePath: settingsPathManager.getDefaultFilePath()});
@@ -0,0 +1,63 @@
1
+ # Stripe Service
2
+ This package contains code for Ghost's Stripe integration. It interacts with Stripe's API and handles webhooks.
3
+
4
+ The main export of this package is the `StripeService` class. It includes a wrapper around the Stripe API and webhook handling logic. It is instantiated in Ghost's `core/server/services/stripe` service.
5
+
6
+ ## Stripe API
7
+ The `StripeAPI` class is a wrapper around the Stripe API. It is used by the `StripeService` class to interact with Stripe's API.
8
+
9
+ ## Stripe Webhooks
10
+ Ghost listens for Stripe webhooks to know when a customer has subscribed to a plan, when a subscription has been cancelled, when a payment has failed, etc.
11
+
12
+ Things to keep in mind when working with Stripe webhooks:
13
+ - Webhooks can arrive out of order. `checkout.session.completed` webhooks may arrive before or after `customer.subscription.created` webhooks.
14
+ - Webhooks can be received and processed in parallel, so you should not rely on the order of the webhooks to determine the order of operations.
15
+ - Operations in Stripe almost always produce multiple events, increasing the likelihood of race conditions.
16
+
17
+ See Stripe's [Webhooks Guide](https://docs.stripe.com/webhooks) for more information.
18
+
19
+ ### Webhook Manager
20
+ This class is responsible for registering the webhook endpoints with Stripe, so Stripe knows where to send the webhooks.
21
+
22
+ ### Webhook Controller
23
+ This class is responsible for handling the webhook events. It accepts the webhook event payload and delegates it to the appropriate handler based on the event type.
24
+
25
+ ### Events
26
+ The Webhook Controller listens for the following events:
27
+ - `customer.subscription.deleted`
28
+ - `customer.subscription.updated`
29
+ - `customer.subscription.created`
30
+ - `invoice.payment_succeeded`
31
+ - `checkout.session.completed`
32
+
33
+
34
+ ## Stripe Flows
35
+
36
+ ### Checkout Session Flow: New Subscription
37
+ ```mermaid
38
+ sequenceDiagram
39
+ actor Member as Member
40
+ participant Portal
41
+ participant Ghost
42
+ participant Stripe
43
+
44
+ Member->>Portal: Signs up for a paid plan
45
+ Portal->>Ghost: Create checkout session
46
+ Ghost->>Stripe: Create checkout session
47
+ Stripe-->>Ghost: Return session ID
48
+ Ghost-->>Portal: Return session ID
49
+ Portal->>Stripe: Redirect to checkout page
50
+ Note over Portal: Member enters payment details in Stripe's secure portal
51
+ Stripe-->>Portal: Redirect to success URL
52
+
53
+ par Webhook Events
54
+ Stripe->>Ghost: customer.subscription.created
55
+ Ghost->>Ghost: Upsert member and subscription
56
+ Stripe->>Ghost: checkout.session.completed
57
+ Ghost->>Ghost: Upsert member and subscription
58
+ Stripe->>Ghost: customer.subscription.updated
59
+ Ghost->>Ghost: Upsert member and subscription
60
+ Stripe->>Ghost: invoice.payment_succeeded
61
+ Ghost->>Ghost: Record payment
62
+ end
63
+ ```