ghost 6.1.0 → 6.3.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.
Files changed (69) hide show
  1. package/components/tryghost-i18n-6.3.0.tgz +0 -0
  2. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
  3. package/core/built/admin/assets/admin-x-activitypub/{index-DmCoswaX.mjs → index-C8tyOPu-.mjs} +2 -2
  4. package/core/built/admin/assets/admin-x-activitypub/{index-lT95Q15h.mjs → index-QqbAPyqT.mjs} +77 -76
  5. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-Bu9qXr9c.mjs → CodeEditorView-CHa5Y-LX.mjs} +3 -3
  6. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  7. package/core/built/admin/assets/admin-x-settings/{index-o4Q9MNrB.mjs → index-8WxO2QXI.mjs} +3017 -2827
  8. package/core/built/admin/assets/admin-x-settings/{index-qEdfz2hd.mjs → index-CGFCkAXn.mjs} +10 -6
  9. package/core/built/admin/assets/admin-x-settings/{index-BEpRBH9g.mjs → index-Cg4zMcj4.mjs} +2 -2
  10. package/core/built/admin/assets/admin-x-settings/{index-BgCSf8S1.mjs → index-DD3HKlR3.mjs} +306 -315
  11. package/core/built/admin/assets/admin-x-settings/{modals-BtQORnS4.mjs → modals-DH5H9Tgk.mjs} +8801 -8807
  12. package/core/built/admin/assets/{chunk.397.e5d027e53a68dff31d76.js → chunk.397.a720333cfffc99c47e71.js} +5 -4
  13. package/core/built/admin/assets/{chunk.524.2aa0847042f20c9a2a00.js → chunk.524.aac61953956de04feb53.js} +6 -6
  14. package/core/built/admin/assets/{chunk.582.9182c19afab95991771e.js → chunk.582.0a1461429ddbaef85ea9.js} +7 -7
  15. package/core/built/admin/assets/{ghost-9c47d152972b304cab0fb982dc3fccc1.js → ghost-1bfab97cb7f550726e894fae6650a808.js} +24 -22
  16. package/core/built/admin/assets/ghost-8ade80412a20088a4f0a9a1159f0bdba.css +1 -0
  17. package/core/built/admin/assets/ghost-dark-b128f29fc44b34b6cfb0fc8492266c2a.css +1 -0
  18. package/core/built/admin/assets/posts/posts.js +30617 -30330
  19. package/core/built/admin/assets/stats/stats.js +21342 -21272
  20. package/core/built/admin/index.html +5 -5
  21. package/core/frontend/helpers/ghost_head.js +2 -1
  22. package/core/server/api/endpoints/stats.js +37 -1
  23. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +1 -0
  24. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +1 -0
  25. package/core/server/data/migrations/utils/schema.js +11 -6
  26. package/core/server/data/migrations/versions/6.2/2025-09-30-14-28-09-add-utm-fields.js +24 -0
  27. package/core/server/data/migrations/versions/6.3/2025-10-02-15-13-31-add-members-otc-secret-setting.js +9 -0
  28. package/core/server/data/schema/commands.js +21 -6
  29. package/core/server/data/schema/default-settings/default-settings.json +4 -0
  30. package/core/server/data/schema/schema.js +24 -0
  31. package/core/server/models/settings.js +1 -0
  32. package/core/server/services/donations/DonationBookshelfRepository.js +6 -1
  33. package/core/server/services/donations/DonationBookshelfRepository.ts +11 -1
  34. package/core/server/services/donations/DonationPaymentEvent.js +10 -0
  35. package/core/server/services/donations/DonationPaymentEvent.ts +10 -0
  36. package/core/server/services/email-service/EmailRenderer.js +1 -1
  37. package/core/server/services/lib/MailgunClient.js +4 -3
  38. package/core/server/services/lib/magic-link/MagicLink.js +9 -9
  39. package/core/server/services/mail/GhostMailer.js +4 -1
  40. package/core/server/services/member-attribution/AttributionBuilder.js +55 -10
  41. package/core/server/services/member-attribution/README.md +101 -0
  42. package/core/server/services/member-attribution/ReferrerTranslator.js +40 -3
  43. package/core/server/services/member-attribution/UrlHistory.js +5 -0
  44. package/core/server/services/members/MembersConfigProvider.js +0 -15
  45. package/core/server/services/members/SingleUseTokenProvider.js +8 -8
  46. package/core/server/services/members/api.js +1 -1
  47. package/core/server/services/members/members-api/controllers/RouterController.js +26 -0
  48. package/core/server/services/members/members-api/repositories/MemberRepository.js +6 -1
  49. package/core/server/services/members-events/EventStorage.js +10 -0
  50. package/core/server/services/stats/ReferrersStatsService.js +143 -0
  51. package/core/server/services/stats/StatsService.js +17 -0
  52. package/core/server/services/stripe/StripeAPI.js +7 -2
  53. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +6 -1
  54. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  55. package/core/server/web/members/app.js +2 -0
  56. package/core/server/web/shared/middleware/api/spam-prevention.js +76 -0
  57. package/core/server/web/shared/middleware/brute.js +23 -0
  58. package/core/shared/config/defaults.json +13 -1
  59. package/core/shared/config/env/config.testing-browser.json +12 -0
  60. package/core/shared/config/env/config.testing-mysql.json +12 -0
  61. package/core/shared/config/env/config.testing.json +12 -0
  62. package/core/shared/labs.js +1 -0
  63. package/package.json +8 -8
  64. package/tsconfig.tsbuildinfo +1 -1
  65. package/yarn.lock +288 -292
  66. package/components/tryghost-i18n-6.1.0.tgz +0 -0
  67. package/core/built/admin/assets/ghost-791574a9e2efe65c88412947d2e80170.css +0 -1
  68. package/core/built/admin/assets/ghost-dark-1a7d101d525c0fdcf406ac0abd98540f.css +0 -1
  69. /package/core/built/admin/assets/{chunk.397.e5d027e53a68dff31d76.js.LICENSE.txt → chunk.397.a720333cfffc99c47e71.js.LICENSE.txt} +0 -0
@@ -244,6 +244,23 @@ class StatsService {
244
244
  return this.referrers.getTopSourcesWithRange(options);
245
245
  }
246
246
 
247
+ /**
248
+ * Get UTM growth stats broken down by UTM field (fixture data)
249
+ * Can be filtered by post using post_id parameter
250
+ * @param {Object} options
251
+ * @param {string} [options.utm_type='utm_source'] - Which UTM field to group by ('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content')
252
+ * @param {string} [options.order='free_members desc'] - Sort order
253
+ * @param {number} [options.limit=50] - Maximum number of results (ignored when filtering by post)
254
+ * @param {string} [options.date_from] - Start date in YYYY-MM-DD format (not yet implemented for fixtures)
255
+ * @param {string} [options.date_to] - End date in YYYY-MM-DD format (not yet implemented for fixtures)
256
+ * @param {string} [options.timezone] - Timezone to use for date interpretation (not yet implemented for fixtures)
257
+ * @param {string} [options.post_id] - Optional filter by post ID
258
+ * @returns {Promise<{data: import('./ReferrersStatsService').UtmGrowthStat[], meta: {}}>}
259
+ */
260
+ async getUtmGrowthStats(options = {}) {
261
+ return this.referrers.getUtmGrowthStats(options);
262
+ }
263
+
247
264
  /**
248
265
  * @param {object} deps
249
266
  *
@@ -526,13 +526,18 @@ module.exports = class StripeAPI {
526
526
  items: [{
527
527
  plan: priceId
528
528
  }],
529
- metadata: {
529
+ metadata: {
530
530
  attribution_id: metadata?.attribution_id,
531
531
  attribution_url: metadata?.attribution_url,
532
532
  attribution_type: metadata?.attribution_type,
533
533
  referrer_source: metadata?.referrer_source,
534
534
  referrer_medium: metadata?.referrer_medium,
535
- referrer_url: metadata?.referrer_url
535
+ referrer_url: metadata?.referrer_url,
536
+ utm_source: metadata?.utm_source,
537
+ utm_medium: metadata?.utm_medium,
538
+ utm_campaign: metadata?.utm_campaign,
539
+ utm_term: metadata?.utm_term,
540
+ utm_content: metadata?.utm_content
536
541
  }
537
542
  };
538
543
 
@@ -75,7 +75,12 @@ module.exports = class CheckoutSessionEventService {
75
75
  attributionType: session.metadata?.attribution_type ?? null,
76
76
  referrerSource: session.metadata?.referrer_source ?? null,
77
77
  referrerMedium: session.metadata?.referrer_medium ?? null,
78
- referrerUrl: session.metadata?.referrer_url ?? null
78
+ referrerUrl: session.metadata?.referrer_url ?? null,
79
+ utmSource: session.metadata?.utm_source ?? null,
80
+ utmMedium: session.metadata?.utm_medium ?? null,
81
+ utmCampaign: session.metadata?.utm_campaign ?? null,
82
+ utmTerm: session.metadata?.utm_term ?? null,
83
+ utmContent: session.metadata?.utm_content ?? null
79
84
  });
80
85
 
81
86
  const donationRepository = this.deps.donationRepository;
@@ -165,6 +165,7 @@ module.exports = function apiRoutes() {
165
165
  router.get('/stats/posts/:id/top-referrers', mw.authAdminApi, http(api.stats.postReferrersAlpha));
166
166
  router.get('/stats/posts/:id/growth', mw.authAdminApi, http(api.stats.postGrowthStats));
167
167
  router.get('/stats/top-sources-growth', mw.authAdminApi, http(api.stats.topSourcesGrowth));
168
+ router.get('/stats/utm-growth', mw.authAdminApi, http(api.stats.utmGrowth));
168
169
  router.post('/stats/posts-visitor-counts', mw.authAdminApi, http(api.stats.postsVisitorCounts));
169
170
  router.post('/stats/posts-member-counts', mw.authAdminApi, http(api.stats.postsMemberCounts));
170
171
 
@@ -89,6 +89,8 @@ module.exports = function setupMembersApp() {
89
89
  '/api/verify-otc',
90
90
  bodyParser.json(),
91
91
  middleware.verifyIntegrityToken,
92
+ shared.middleware.brute.otcVerificationEnumeration,
93
+ shared.middleware.brute.otcVerification,
92
94
  // NOTE: this is wrapped in a function to ensure we always go via the getter
93
95
  function lazyVerifyOTCMw(req, res, next) {
94
96
  return membersService.api.middleware.verifyOTC(req, res, next);
@@ -21,6 +21,10 @@ const messages = {
21
21
  context: 'Too many login attempts.'
22
22
  },
23
23
  tooManyAttempts: 'Too many attempts.',
24
+ tooManyOTCVerificationAttempts: {
25
+ error: 'Too many attempts for this verification code.',
26
+ context: 'Too many verification code attempts.'
27
+ },
24
28
  webmentionsBlock: 'Too many mention attempts',
25
29
  emailPreviewBlock: 'Only 10 test emails can be sent per hour'
26
30
  };
@@ -35,6 +39,8 @@ let spamMemberLogin = spam.member_login || {};
35
39
  let spamContentApiKey = spam.content_api_key || {};
36
40
  let spamWebmentionsBlock = spam.webmentions_block || {};
37
41
  let spamEmailPreviewBlock = spam.email_preview_block || {};
42
+ let spamOtcVerificationEnumeration = spam.otc_verification_enumeration || {};
43
+ let spamOtcVerification = spam.otc_verification || {};
38
44
 
39
45
  let store;
40
46
  let memoryStore;
@@ -50,6 +56,8 @@ let sendVerificationCodeInstance;
50
56
  let userVerificationInstance;
51
57
  let contentApiKeyInstance;
52
58
  let emailPreviewBlockInstance;
59
+ let otcVerificationEnumerationInstance;
60
+ let otcVerificationInstance;
53
61
 
54
62
  const spamConfigKeys = ['freeRetries', 'minWait', 'maxWait', 'lifetime'];
55
63
 
@@ -248,6 +256,68 @@ const membersAuthEnumeration = () => {
248
256
  return membersAuthEnumerationInstance;
249
257
  };
250
258
 
259
+ const otcVerificationEnumeration = () => {
260
+ const ExpressBrute = require('express-brute');
261
+ const BruteKnex = require('brute-knex');
262
+ const db = require('../../../../data/db');
263
+
264
+ store = store || new BruteKnex({
265
+ tablename: 'brute',
266
+ createTable: false,
267
+ knex: db.knex
268
+ });
269
+
270
+ if (!otcVerificationEnumerationInstance) {
271
+ otcVerificationEnumerationInstance = new ExpressBrute(store,
272
+ extend({
273
+ attachResetToRequest: false,
274
+ failCallback(req, res, next, nextValidRequestDate) {
275
+ return next(new errors.TooManyRequestsError({
276
+ message: `Too many verification attempts across multiple codes, try again in ${moment(nextValidRequestDate).fromNow(true)}`,
277
+ context: tpl(messages.tooManyOTCVerificationAttempts.context),
278
+ help: tpl(messages.tooManyOTCVerificationAttempts.context),
279
+ code: 'OTC_TOTAL_ATTEMPTS_RATE_LIMITED'
280
+ }));
281
+ },
282
+ handleStoreError: handleStoreError
283
+ }, pick(spamOtcVerificationEnumeration, spamConfigKeys))
284
+ );
285
+ }
286
+
287
+ return otcVerificationEnumerationInstance;
288
+ };
289
+
290
+ const otcVerification = () => {
291
+ const ExpressBrute = require('express-brute');
292
+ const BruteKnex = require('brute-knex');
293
+ const db = require('../../../../data/db');
294
+
295
+ store = store || new BruteKnex({
296
+ tablename: 'brute',
297
+ createTable: false,
298
+ knex: db.knex
299
+ });
300
+
301
+ if (!otcVerificationInstance) {
302
+ otcVerificationInstance = new ExpressBrute(store,
303
+ extend({
304
+ attachResetToRequest: false,
305
+ failCallback(req, res, next, nextValidRequestDate) {
306
+ return next(new errors.TooManyRequestsError({
307
+ message: `Too many attempts for this verification code, try again in ${moment(nextValidRequestDate).fromNow(true)}`,
308
+ context: tpl(messages.tooManyOTCVerificationAttempts.context),
309
+ help: tpl(messages.tooManyOTCVerificationAttempts.context),
310
+ code: 'OTC_CODE_ATTEMPTS_RATE_LIMITED'
311
+ }));
312
+ },
313
+ handleStoreError: handleStoreError
314
+ }, pick(spamOtcVerification, spamConfigKeys))
315
+ );
316
+ }
317
+
318
+ return otcVerificationInstance;
319
+ };
320
+
251
321
  // Stops login attempts for a user+IP pair with an increasing time period starting from 10 minutes
252
322
  // and rising to a week in a fibonnaci sequence
253
323
  // The user+IP count is reset when on successful login
@@ -432,6 +502,8 @@ module.exports = {
432
502
  userVerification: userVerification,
433
503
  membersAuth: membersAuth,
434
504
  membersAuthEnumeration: membersAuthEnumeration,
505
+ otcVerification: otcVerification,
506
+ otcVerificationEnumeration: otcVerificationEnumeration,
435
507
  userReset: userReset,
436
508
  privateBlog: privateBlog,
437
509
  contentApiKey: contentApiKey,
@@ -450,6 +522,8 @@ module.exports = {
450
522
  sendVerificationCodeInstance = undefined;
451
523
  userVerificationInstance = undefined;
452
524
  contentApiKeyInstance = undefined;
525
+ otcVerificationEnumerationInstance = undefined;
526
+ otcVerificationInstance = undefined;
453
527
 
454
528
  spam = config.get('spam') || {};
455
529
  spamPrivateBlock = spam.private_block || {};
@@ -461,5 +535,7 @@ module.exports = {
461
535
  spamUserVerification = spam.user_verification || {};
462
536
  spamMemberLogin = spam.member_login || {};
463
537
  spamContentApiKey = spam.content_api_key || {};
538
+ spamOtcVerificationEnumeration = spam.otc_verification_enumeration || {};
539
+ spamOtcVerification = spam.otc_verification || {};
464
540
  }
465
541
  };
@@ -128,6 +128,29 @@ module.exports = {
128
128
  return spamPrevention.membersAuthEnumeration().prevent(req, res, next);
129
129
  },
130
130
 
131
+ /**
132
+ * Block too many OTC verification attempts from same IP (blocks user enumeration)
133
+ */
134
+ otcVerificationEnumeration(req, res, next) {
135
+ return spamPrevention.otcVerificationEnumeration().prevent(req, res, next);
136
+ },
137
+
138
+ /**
139
+ * Block too many attempts for the same otcRef
140
+ */
141
+ otcVerification(req, res, next) {
142
+ return spamPrevention.otcVerification().getMiddleware({
143
+ // ignoring IP here blocks rotating ip attacks, only one IP should receive an otcRef so it shouldn't cause false positives
144
+ ignoreIP: true,
145
+ key(_req, _res, _next) {
146
+ if (_req.body.otcRef) {
147
+ return _next(`${_req.body.otcRef}otc_verification`);
148
+ }
149
+ return _next();
150
+ }
151
+ })(req, res, next);
152
+ },
153
+
131
154
  /**
132
155
  * Blocks webmention spam
133
156
  */
@@ -130,6 +130,18 @@
130
130
  "lifetime": 3600,
131
131
  "freeRetries": 10
132
132
  },
133
+ "otc_verification_enumeration": {
134
+ "minWait": 600000,
135
+ "maxWait": 43200000,
136
+ "lifetime": 43200,
137
+ "freeRetries": 8
138
+ },
139
+ "otc_verification": {
140
+ "minWait": 3600000,
141
+ "maxWait": 3600000,
142
+ "lifetime": 3600,
143
+ "freeRetries": 4
144
+ },
133
145
  "blocked_email_domains": []
134
146
  },
135
147
  "caching": {
@@ -212,7 +224,7 @@
212
224
  },
213
225
  "portal": {
214
226
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
215
- "version": "2.53"
227
+ "version": "2.55"
216
228
  },
217
229
  "sodoSearch": {
218
230
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -70,6 +70,18 @@
70
70
  "maxWait": 360000,
71
71
  "lifetime": 3600,
72
72
  "freeRetries": 10
73
+ },
74
+ "otc_verification_enumeration": {
75
+ "minWait": 600000,
76
+ "maxWait": 43200000,
77
+ "lifetime": 43200,
78
+ "freeRetries": 8
79
+ },
80
+ "otc_verification": {
81
+ "minWait": 300000,
82
+ "maxWait": 3600000,
83
+ "lifetime": 3600,
84
+ "freeRetries": 4
73
85
  }
74
86
  },
75
87
  "privacy": {
@@ -74,6 +74,18 @@
74
74
  "maxWait": 360000,
75
75
  "lifetime": 3600,
76
76
  "freeRetries": 10
77
+ },
78
+ "otc_verification_enumeration": {
79
+ "minWait": 600000,
80
+ "maxWait": 43200000,
81
+ "lifetime": 43200,
82
+ "freeRetries": 8
83
+ },
84
+ "otc_verification": {
85
+ "minWait": 300000,
86
+ "maxWait": 3600000,
87
+ "lifetime": 3600,
88
+ "freeRetries": 4
77
89
  }
78
90
  },
79
91
  "privacy": {
@@ -70,6 +70,18 @@
70
70
  "maxWait": 360000,
71
71
  "lifetime": 3600,
72
72
  "freeRetries": 10
73
+ },
74
+ "otc_verification_enumeration": {
75
+ "minWait": 600000,
76
+ "maxWait": 43200000,
77
+ "lifetime": 43200,
78
+ "freeRetries": 8
79
+ },
80
+ "otc_verification": {
81
+ "minWait": 300000,
82
+ "maxWait": 3600000,
83
+ "lifetime": 3600,
84
+ "freeRetries": 4
73
85
  }
74
86
  },
75
87
  "privacy": {
@@ -48,6 +48,7 @@ const PRIVATE_FEATURES = [
48
48
  'contentVisibilityAlpha',
49
49
  'emailCustomization',
50
50
  'membersSigninOTC',
51
+ 'membersSigninOTCAlpha',
51
52
  'tagsX',
52
53
  'utmTracking',
53
54
  'emailUniqueid'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "6.1.0",
3
+ "version": "6.3.0",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -86,7 +86,7 @@
86
86
  "@tryghost/helpers": "1.1.97",
87
87
  "@tryghost/html-to-plaintext": "1.0.4",
88
88
  "@tryghost/http-cache-utils": "0.1.20",
89
- "@tryghost/i18n": "file:components/tryghost-i18n-6.1.0.tgz",
89
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.3.0.tgz",
90
90
  "@tryghost/image-transform": "1.4.6",
91
91
  "@tryghost/job-manager": "1.0.3",
92
92
  "@tryghost/kg-card-factory": "5.1.2",
@@ -196,7 +196,7 @@
196
196
  "moment": "2.24.0",
197
197
  "moment-timezone": "0.5.45",
198
198
  "multer": "2.0.2",
199
- "mysql2": "3.15.1",
199
+ "mysql2": "3.15.2",
200
200
  "nconf": "0.13.0",
201
201
  "node-fetch": "2.7.0",
202
202
  "node-jose": "2.2.0",
@@ -208,7 +208,7 @@
208
208
  "probe-image-size": "7.2.3",
209
209
  "rss": "1.2.2",
210
210
  "sanitize-html": "2.17.0",
211
- "semver": "7.7.2",
211
+ "semver": "7.7.3",
212
212
  "simple-dom": "1.4.0",
213
213
  "stoppable": "1.1.0",
214
214
  "stripe": "8.222.0",
@@ -232,9 +232,9 @@
232
232
  "@types/bookshelf": "1.2.9",
233
233
  "@types/common-tags": "1.8.4",
234
234
  "@types/jsonwebtoken": "9.0.10",
235
- "@types/node": "22.18.6",
235
+ "@types/node": "22.18.8",
236
236
  "@types/node-jose": "1.1.13",
237
- "@types/nodemailer": "6.4.19",
237
+ "@types/nodemailer": "6.4.20",
238
238
  "@types/sinon": "17.0.4",
239
239
  "@types/supertest": "6.0.3",
240
240
  "c8": "10.1.3",
@@ -250,7 +250,7 @@
250
250
  "inquirer": "8.2.7",
251
251
  "jwk-to-pem": "2.0.7",
252
252
  "jwks-rsa": "3.2.0",
253
- "mocha": "11.7.2",
253
+ "mocha": "11.7.3",
254
254
  "mocha-slow-test-reporter": "0.1.2",
255
255
  "mock-knex": "TryGhost/mock-knex#68948e11b0ea4fe63456098dfdc169bea7f62009",
256
256
  "nock": "13.5.6",
@@ -273,7 +273,7 @@
273
273
  "jackspeak": "2.3.6",
274
274
  "moment": "2.24.0",
275
275
  "moment-timezone": "0.5.45",
276
- "@tryghost/i18n": "file:components/tryghost-i18n-6.1.0.tgz"
276
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.3.0.tgz"
277
277
  },
278
278
  "nx": {
279
279
  "targets": {