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
@@ -6,7 +6,7 @@
6
6
  <title>Ghost</title>
7
7
 
8
8
 
9
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.1%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%229c120d864e%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22d668621a75%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%2285275dce41%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%2231e7d6e61e%22%2C%22adminXActivitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D" />
9
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.3%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2274f827c663%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%221e670e9cd7%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22c53b194bad%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%2265427ccad3%22%2C%22adminXActivitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D" />
10
10
 
11
11
  <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover" />
12
12
  <meta name="pinterest" content="nopin" />
@@ -28,7 +28,7 @@
28
28
  </style>
29
29
 
30
30
  <link integrity="" rel="stylesheet" href="assets/vendor-0ede59da8efb5e28fa929557f7ff7154.css">
31
- <link integrity="" rel="stylesheet" href="assets/ghost-791574a9e2efe65c88412947d2e80170.css" title="light">
31
+ <link integrity="" rel="stylesheet" href="assets/ghost-8ade80412a20088a4f0a9a1159f0bdba.css" title="light">
32
32
 
33
33
 
34
34
  </head>
@@ -48,8 +48,8 @@
48
48
  <div id="ember-basic-dropdown-wormhole"></div>
49
49
 
50
50
  <script src="assets/vendor-aed0068cf9b67d042dd23a6343545b7b.js"></script>
51
- <script src="assets/chunk.397.e5d027e53a68dff31d76.js"></script>
52
- <script src="assets/chunk.524.2aa0847042f20c9a2a00.js"></script>
53
- <script src="assets/ghost-9c47d152972b304cab0fb982dc3fccc1.js"></script>
51
+ <script src="assets/chunk.397.a720333cfffc99c47e71.js"></script>
52
+ <script src="assets/chunk.524.aac61953956de04feb53.js"></script>
53
+ <script src="assets/ghost-1bfab97cb7f550726e894fae6650a808.js"></script>
54
54
  </body>
55
55
  </html>
@@ -64,7 +64,8 @@ function getMembersHelper(data, frontendKey, excludeList) {
64
64
  key: frontendKey,
65
65
  api: urlUtils.urlFor('api', {type: 'content'}, true),
66
66
  locale: settingsCache.get('locale') || 'en',
67
- 'members-signin-otc': labs.isSet('membersSigninOTC') // html.dataset converts dash-attrs to camelCase
67
+ 'members-signin-otc': labs.isSet('membersSigninOTC'), // html.dataset converts dash-attrs to camelCase
68
+ 'members-signin-otc-alpha': labs.isSet('membersSigninOTCAlpha') // html.dataset converts dash-attrs to camelCase
68
69
  };
69
70
  if (colorString) {
70
71
  attributes['accent-color'] = colorString;
@@ -514,7 +514,7 @@ const controller = {
514
514
  },
515
515
  options: [
516
516
  'order',
517
- 'limit',
517
+ 'limit',
518
518
  'date_from',
519
519
  'date_to',
520
520
  'timezone',
@@ -541,6 +541,42 @@ const controller = {
541
541
  async query(frame) {
542
542
  return await statsService.api.getTopSourcesWithRange(frame.options);
543
543
  }
544
+ },
545
+ utmGrowth: {
546
+ headers: {
547
+ cacheInvalidate: false
548
+ },
549
+ options: [
550
+ 'utm_type',
551
+ 'order',
552
+ 'limit',
553
+ 'date_from',
554
+ 'date_to',
555
+ 'timezone',
556
+ 'post_id'
557
+ ],
558
+ permissions: {
559
+ docName: 'posts',
560
+ method: 'browse'
561
+ },
562
+ cache: statsService.cache,
563
+ generateCacheKeyData(frame) {
564
+ return {
565
+ method: 'utmGrowth',
566
+ options: {
567
+ utm_type: frame.options.utm_type,
568
+ order: frame.options.order,
569
+ limit: frame.options.limit,
570
+ date_from: frame.options.date_from,
571
+ date_to: frame.options.date_to,
572
+ timezone: frame.options.timezone,
573
+ post_id: frame.options.post_id
574
+ }
575
+ };
576
+ },
577
+ async query(frame) {
578
+ return await statsService.api.getUtmGrowthStats(frame.options);
579
+ }
544
580
  }
545
581
 
546
582
  };
@@ -3,6 +3,7 @@ const keyGroupMapping = {
3
3
  members_public_key: 'core',
4
4
  members_private_key: 'core',
5
5
  members_email_auth_secret: 'core',
6
+ members_otc_secret: 'core',
6
7
  db_hash: 'core',
7
8
  next_update_check: 'core',
8
9
  notifications: 'core',
@@ -31,6 +31,7 @@ const keyTypeMapping = {
31
31
  members_public_key: 'string',
32
32
  members_private_key: 'string',
33
33
  members_email_auth_secret: 'string',
34
+ members_otc_secret: 'string',
34
35
  default_content_visibility: 'string',
35
36
  stripe_secret_key: 'string',
36
37
  stripe_publishable_key: 'string',
@@ -11,7 +11,7 @@ const {createNonTransactionalMigration, createTransactionalMigration} = require(
11
11
  *
12
12
  * @returns {Migration}
13
13
  */
14
- function createAddColumnMigration(table, column, columnDefinition) {
14
+ function createAddColumnMigration(table, column, columnDefinition, options = {}) {
15
15
  return createNonTransactionalMigration(
16
16
  // up
17
17
  commands.createColumnMigration({
@@ -20,7 +20,8 @@ function createAddColumnMigration(table, column, columnDefinition) {
20
20
  dbIsInCorrectState: hasColumn => hasColumn === true,
21
21
  operation: commands.addColumn,
22
22
  operationVerb: 'Adding',
23
- columnDefinition
23
+ columnDefinition,
24
+ options
24
25
  }),
25
26
  // down
26
27
  commands.createColumnMigration({
@@ -29,7 +30,8 @@ function createAddColumnMigration(table, column, columnDefinition) {
29
30
  dbIsInCorrectState: hasColumn => hasColumn === false,
30
31
  operation: commands.dropColumn,
31
32
  operationVerb: 'Removing',
32
- columnDefinition
33
+ columnDefinition,
34
+ options
33
35
  })
34
36
  );
35
37
  }
@@ -41,7 +43,7 @@ function createAddColumnMigration(table, column, columnDefinition) {
41
43
  *
42
44
  * @returns {Migration}
43
45
  */
44
- function createDropColumnMigration(table, column, columnDefinition) {
46
+ function createDropColumnMigration(table, column, columnDefinition, options = {}) {
45
47
  return createNonTransactionalMigration(
46
48
  // up
47
49
  commands.createColumnMigration({
@@ -49,7 +51,9 @@ function createDropColumnMigration(table, column, columnDefinition) {
49
51
  column,
50
52
  dbIsInCorrectState: hasColumn => hasColumn === false,
51
53
  operation: commands.dropColumn,
52
- operationVerb: 'Removing'
54
+ operationVerb: 'Removing',
55
+ columnDefinition,
56
+ options
53
57
  }),
54
58
  // down
55
59
  commands.createColumnMigration({
@@ -58,7 +62,8 @@ function createDropColumnMigration(table, column, columnDefinition) {
58
62
  dbIsInCorrectState: hasColumn => hasColumn === true,
59
63
  operation: commands.addColumn,
60
64
  operationVerb: 'Adding',
61
- columnDefinition
65
+ columnDefinition,
66
+ options
62
67
  })
63
68
  );
64
69
  }
@@ -0,0 +1,24 @@
1
+ const {combineNonTransactionalMigrations, createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ // members_created_events
5
+ createAddColumnMigration('members_created_events', 'utm_source', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
6
+ createAddColumnMigration('members_created_events', 'utm_medium', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
7
+ createAddColumnMigration('members_created_events', 'utm_campaign', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
8
+ createAddColumnMigration('members_created_events', 'utm_term', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
9
+ createAddColumnMigration('members_created_events', 'utm_content', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
10
+
11
+ // members_subscription_created_events
12
+ createAddColumnMigration('members_subscription_created_events', 'utm_source', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
13
+ createAddColumnMigration('members_subscription_created_events', 'utm_medium', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
14
+ createAddColumnMigration('members_subscription_created_events', 'utm_campaign', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
15
+ createAddColumnMigration('members_subscription_created_events', 'utm_term', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
16
+ createAddColumnMigration('members_subscription_created_events', 'utm_content', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
17
+
18
+ // donation_payment_events
19
+ createAddColumnMigration('donation_payment_events', 'utm_source', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
20
+ createAddColumnMigration('donation_payment_events', 'utm_medium', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
21
+ createAddColumnMigration('donation_payment_events', 'utm_campaign', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
22
+ createAddColumnMigration('donation_payment_events', 'utm_term', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
23
+ createAddColumnMigration('donation_payment_events', 'utm_content', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'})
24
+ );
@@ -0,0 +1,9 @@
1
+ const {addSetting} = require('../../utils');
2
+ const crypto = require('crypto');
3
+
4
+ module.exports = addSetting({
5
+ key: 'members_otc_secret',
6
+ value: crypto.randomBytes(64).toString('hex'),
7
+ type: 'string',
8
+ group: 'core'
9
+ });
@@ -96,8 +96,10 @@ function dropNullable(tableName, column, transaction = db.knex) {
96
96
  * @param {string} column
97
97
  * @param {import('knex').Knex.Transaction} [transaction]
98
98
  * @param {object} columnSpec
99
+ * @param {object} [options]
100
+ * @param {'inplace'|'copy'|'auto'} [options.algorithm] - MySQL only
99
101
  */
100
- async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
102
+ async function addColumn(tableName, column, transaction = db.knex, columnSpec, options = {}) {
101
103
  const addColumnBuilder = transaction.schema.table(tableName, function (table) {
102
104
  addTableColumn(tableName, table, column, columnSpec);
103
105
  });
@@ -114,7 +116,12 @@ async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
114
116
 
115
117
  if (DatabaseInfo.isMySQL(transaction)) {
116
118
  // Guard against an ending semicolon
117
- sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
119
+ sql = sql.replace(/;\s*$/, '');
120
+ if (options?.algorithm !== 'auto') {
121
+ // default to copy if not specified
122
+ const algorithm = options?.algorithm || 'copy';
123
+ sql += `, algorithm=${algorithm}`;
124
+ }
118
125
  }
119
126
 
120
127
  await transaction.raw(sql);
@@ -126,8 +133,10 @@ async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
126
133
  * @param {string} column
127
134
  * @param {import('knex').Knex} [transaction]
128
135
  * @param {object} [columnSpec]
136
+ * @param {object} [options]
137
+ * @param {'inplace'|'copy'|'auto'} [options.algorithm] - MySQL only
129
138
  */
130
- async function dropColumn(tableName, column, transaction = db.knex, columnSpec = {}) {
139
+ async function dropColumn(tableName, column, transaction = db.knex, columnSpec = {}, options = {}) {
131
140
  if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
132
141
  const [toTable, toColumn] = columnSpec.references.split('.');
133
142
  await dropForeign({fromTable: tableName, fromColumn: column, toTable, toColumn, constraintName: columnSpec.constraintName, transaction});
@@ -149,7 +158,12 @@ async function dropColumn(tableName, column, transaction = db.knex, columnSpec =
149
158
 
150
159
  if (DatabaseInfo.isMySQL(transaction)) {
151
160
  // Guard against an ending semicolon
152
- sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
161
+ sql = sql.replace(/;\s*$/, '');
162
+ if (options?.algorithm !== 'auto') {
163
+ // default to copy if not specified
164
+ const algorithm = options?.algorithm || 'copy';
165
+ sql += `, algorithm=${algorithm}`;
166
+ }
153
167
  }
154
168
 
155
169
  await transaction.raw(sql);
@@ -561,7 +575,8 @@ function createColumnMigration(...migrations) {
561
575
  dbIsInCorrectState,
562
576
  operation,
563
577
  operationVerb,
564
- columnDefinition
578
+ columnDefinition,
579
+ options
565
580
  } = migration;
566
581
 
567
582
  const hasColumn = await conn.schema.hasColumn(table, column);
@@ -571,7 +586,7 @@ function createColumnMigration(...migrations) {
571
586
  logging.warn(`${operationVerb} ${table}.${column} column - skipping as table is correct`);
572
587
  } else {
573
588
  logging.info(`${operationVerb} ${table}.${column} column`);
574
- await operation(table, column, conn, columnDefinition);
589
+ await operation(table, column, conn, columnDefinition, options);
575
590
  }
576
591
  }
577
592
 
@@ -60,6 +60,10 @@
60
60
  "defaultValue": null,
61
61
  "type": "string"
62
62
  },
63
+ "members_otc_secret": {
64
+ "defaultValue": null,
65
+ "type": "string"
66
+ },
63
67
  "site_uuid": {
64
68
  "defaultValue": null,
65
69
  "type": "string",
@@ -525,6 +525,7 @@ module.exports = {
525
525
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
526
526
  created_at: {type: 'dateTime', nullable: false},
527
527
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
528
+ // attribution values from ghost-history (member attribution tracking script)
528
529
  attribution_id: {type: 'string', maxlength: 24, nullable: true, index: true},
529
530
  attribution_type: {
530
531
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -532,9 +533,16 @@ module.exports = {
532
533
  }
533
534
  },
534
535
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
536
+ // referrer values from browser, processed by our referrerParser library
535
537
  referrer_source: {type: 'string', maxlength: 191, nullable: true},
536
538
  referrer_medium: {type: 'string', maxlength: 191, nullable: true},
537
539
  referrer_url: {type: 'string', maxlength: 2000, nullable: true},
540
+ // raw values from URL query parameters
541
+ utm_source: {type: 'string', maxlength: 191, nullable: true},
542
+ utm_medium: {type: 'string', maxlength: 191, nullable: true},
543
+ utm_campaign: {type: 'string', maxlength: 191, nullable: true},
544
+ utm_term: {type: 'string', maxlength: 191, nullable: true},
545
+ utm_content: {type: 'string', maxlength: 191, nullable: true},
538
546
  source: {
539
547
  type: 'string', maxlength: 50, nullable: false, validations: {
540
548
  isIn: [['member', 'import', 'system', 'api', 'admin']]
@@ -705,6 +713,7 @@ module.exports = {
705
713
  created_at: {type: 'dateTime', nullable: false},
706
714
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
707
715
  subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
716
+ // attribution values from ghost-history (member attribution tracking script)
708
717
  attribution_id: {type: 'string', maxlength: 24, nullable: true, index: true},
709
718
  attribution_type: {
710
719
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -712,9 +721,16 @@ module.exports = {
712
721
  }
713
722
  },
714
723
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
724
+ // referrer values from browser, processed by our referrerParser library
715
725
  referrer_source: {type: 'string', maxlength: 191, nullable: true},
716
726
  referrer_medium: {type: 'string', maxlength: 191, nullable: true},
717
727
  referrer_url: {type: 'string', maxlength: 2000, nullable: true},
728
+ // raw values from URL query parameters
729
+ utm_source: {type: 'string', maxlength: 191, nullable: true},
730
+ utm_medium: {type: 'string', maxlength: 191, nullable: true},
731
+ utm_campaign: {type: 'string', maxlength: 191, nullable: true},
732
+ utm_term: {type: 'string', maxlength: 191, nullable: true},
733
+ utm_content: {type: 'string', maxlength: 191, nullable: true},
718
734
  batch_id: {type: 'string', maxlength: 24, nullable: true}
719
735
  },
720
736
  offer_redemptions: {
@@ -746,6 +762,7 @@ module.exports = {
746
762
  member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id', setNullDelete: true},
747
763
  amount: {type: 'integer', nullable: false},
748
764
  currency: {type: 'string', maxlength: 50, nullable: false},
765
+ // attribution values from ghost-history (member attribution tracking script)
749
766
  attribution_id: {type: 'string', maxlength: 24, nullable: true},
750
767
  attribution_type: {
751
768
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -753,9 +770,16 @@ module.exports = {
753
770
  }
754
771
  },
755
772
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
773
+ // referrer values from browser, processed by our referrerParser library
756
774
  referrer_source: {type: 'string', maxlength: 191, nullable: true},
757
775
  referrer_medium: {type: 'string', maxlength: 191, nullable: true},
758
776
  referrer_url: {type: 'string', maxlength: 2000, nullable: true},
777
+ // raw values from URL query parameters
778
+ utm_source: {type: 'string', maxlength: 191, nullable: true},
779
+ utm_medium: {type: 'string', maxlength: 191, nullable: true},
780
+ utm_campaign: {type: 'string', maxlength: 191, nullable: true},
781
+ utm_term: {type: 'string', maxlength: 191, nullable: true},
782
+ utm_content: {type: 'string', maxlength: 191, nullable: true},
759
783
  created_at: {type: 'dateTime', nullable: false},
760
784
  donation_message: {type: 'string', maxlength: 255, nullable: true} // https://docs.stripe.com/payments/checkout/custom-fields
761
785
  },
@@ -57,6 +57,7 @@ function parseDefaultSettings() {
57
57
  members_public_key: () => getMembersKey('public'),
58
58
  members_private_key: () => getMembersKey('private'),
59
59
  members_email_auth_secret: () => crypto.randomBytes(64).toString('hex'),
60
+ members_otc_secret: () => crypto.randomBytes(64).toString('hex'),
60
61
  ghost_public_key: () => getGhostKey('public'),
61
62
  ghost_private_key: () => getGhostKey('private'),
62
63
  site_uuid: () => getOrGenerateSiteUuid()
@@ -19,7 +19,12 @@ class DonationBookshelfRepository {
19
19
  attribution_type: event.attributionType,
20
20
  referrer_source: event.referrerSource,
21
21
  referrer_medium: event.referrerMedium,
22
- referrer_url: event.referrerUrl
22
+ referrer_url: event.referrerUrl,
23
+ utm_source: event.utmSource,
24
+ utm_medium: event.utmMedium,
25
+ utm_campaign: event.utmCampaign,
26
+ utm_term: event.utmTerm,
27
+ utm_content: event.utmContent
23
28
  });
24
29
  }
25
30
  }
@@ -23,6 +23,11 @@ type DonationEventModelInstance = BookshelfModelInstance & {
23
23
  referrer_source: string | null;
24
24
  referrer_medium: string | null;
25
25
  referrer_url: string | null;
26
+ utm_source: string | null;
27
+ utm_medium: string | null;
28
+ utm_campaign: string | null;
29
+ utm_term: string | null;
30
+ utm_content: string | null;
26
31
  }
27
32
  type DonationPaymentEventBookshelfModel = BookshelfModel<DonationEventModelInstance>;
28
33
 
@@ -47,7 +52,12 @@ export class DonationBookshelfRepository implements DonationRepository {
47
52
  attribution_type: event.attributionType,
48
53
  referrer_source: event.referrerSource,
49
54
  referrer_medium: event.referrerMedium,
50
- referrer_url: event.referrerUrl
55
+ referrer_url: event.referrerUrl,
56
+ utm_source: event.utmSource,
57
+ utm_medium: event.utmMedium,
58
+ utm_campaign: event.utmCampaign,
59
+ utm_term: event.utmTerm,
60
+ utm_content: event.utmContent
51
61
  });
52
62
  }
53
63
  }
@@ -15,6 +15,11 @@ class DonationPaymentEvent {
15
15
  referrerSource;
16
16
  referrerMedium;
17
17
  referrerUrl;
18
+ utmSource;
19
+ utmMedium;
20
+ utmCampaign;
21
+ utmTerm;
22
+ utmContent;
18
23
  constructor(data, timestamp) {
19
24
  this.timestamp = timestamp;
20
25
  this.name = data.name;
@@ -29,6 +34,11 @@ class DonationPaymentEvent {
29
34
  this.referrerSource = data.referrerSource;
30
35
  this.referrerMedium = data.referrerMedium;
31
36
  this.referrerUrl = data.referrerUrl;
37
+ this.utmSource = data.utmSource;
38
+ this.utmMedium = data.utmMedium;
39
+ this.utmCampaign = data.utmCampaign;
40
+ this.utmTerm = data.utmTerm;
41
+ this.utmContent = data.utmContent;
32
42
  }
33
43
  static create(data, timestamp) {
34
44
  return new DonationPaymentEvent(data, timestamp ?? new Date());
@@ -13,6 +13,11 @@ export class DonationPaymentEvent {
13
13
  referrerSource: string | null;
14
14
  referrerMedium: string | null;
15
15
  referrerUrl: string | null;
16
+ utmSource: string | null;
17
+ utmMedium: string | null;
18
+ utmCampaign: string | null;
19
+ utmTerm: string | null;
20
+ utmContent: string | null;
16
21
 
17
22
  constructor(data: Omit<DonationPaymentEvent, 'timestamp'>, timestamp: Date) {
18
23
  this.timestamp = timestamp;
@@ -30,6 +35,11 @@ export class DonationPaymentEvent {
30
35
  this.referrerSource = data.referrerSource;
31
36
  this.referrerMedium = data.referrerMedium;
32
37
  this.referrerUrl = data.referrerUrl;
38
+ this.utmSource = data.utmSource;
39
+ this.utmMedium = data.utmMedium;
40
+ this.utmCampaign = data.utmCampaign;
41
+ this.utmTerm = data.utmTerm;
42
+ this.utmContent = data.utmContent;
33
43
  }
34
44
 
35
45
  static create(data: Omit<DonationPaymentEvent, 'timestamp'>, timestamp?: Date) {
@@ -1241,7 +1241,7 @@ class EmailRenderer {
1241
1241
  }, true) : null
1242
1242
  },
1243
1243
  preheader: this.#getEmailPreheader(post, segment, html),
1244
- preheaderSpacing: '&zwnj;&nbsp;'.repeat(75),
1244
+ preheaderSpacing: `${'&#8199;&#847; '.repeat(150)}${'&shy; '.repeat(200)} &nbsp;`,
1245
1245
  html,
1246
1246
 
1247
1247
  post: {
@@ -67,7 +67,8 @@ module.exports = class MailgunClient {
67
67
  subject: messageContent.subject,
68
68
  html: messageContent.html,
69
69
  text: messageContent.plaintext,
70
- 'recipient-variables': JSON.stringify(recipientData)
70
+ 'recipient-variables': JSON.stringify(recipientData),
71
+ 'h:Sender': message.from
71
72
  };
72
73
 
73
74
  // Do we have a custom List-Unsubscribe header set?
@@ -348,9 +349,9 @@ module.exports = class MailgunClient {
348
349
  /**
349
350
  * Returns the configured target delivery window in seconds
350
351
  * Ghost will attempt to deliver emails evenly distributed over this window
351
- *
352
+ *
352
353
  * Defaults to 0 (no delay) if not set
353
- *
354
+ *
354
355
  * @returns {number}
355
356
  */
356
357
  getTargetDeliveryWindow() {
@@ -23,7 +23,7 @@ const messages = {
23
23
  * @typedef {Object} TokenProvider<T, D>
24
24
  * @prop {(data: D) => Promise<T>} create
25
25
  * @prop {(token: T, options?: TokenValidateOptions) => Promise<D>} validate
26
- * @prop {(token: T) => Promise<string | null>} [getIdByToken]
26
+ * @prop {(token: T) => Promise<string | null>} [getRefByToken]
27
27
  * @prop {(otcRef: string, tokenValue: T) => string} [deriveOTC]
28
28
  */
29
29
 
@@ -108,7 +108,7 @@ class MagicLink {
108
108
  let otcRef = null;
109
109
  if (this.labsService?.isSet('membersSigninOTC') && otc) {
110
110
  try {
111
- otcRef = await this.getIdFromToken(token);
111
+ otcRef = await this.getRefFromToken(token);
112
112
  } catch (err) {
113
113
  this.sentry?.captureException?.(err);
114
114
  otcRef = null;
@@ -135,17 +135,17 @@ class MagicLink {
135
135
  }
136
136
 
137
137
  /**
138
- * getIdFromToken
138
+ * getRefFromToken
139
139
  *
140
- * @param {Token} token - The token to get the id from
141
- * @returns {Promise<string|null>} id - The id of the token
140
+ * @param {Token} token - The token to get the ref from
141
+ * @returns {Promise<string|null>} ref - The ref of the token
142
142
  */
143
- async getIdFromToken(token) {
144
- if (typeof this.tokenProvider.getIdByToken !== 'function') {
143
+ async getRefFromToken(token) {
144
+ if (typeof this.tokenProvider.getRefByToken !== 'function') {
145
145
  return null;
146
146
  }
147
147
 
148
- const id = await this.tokenProvider.getIdByToken(token);
148
+ const id = await this.tokenProvider.getRefByToken(token);
149
149
  return id;
150
150
  }
151
151
 
@@ -156,7 +156,7 @@ class MagicLink {
156
156
  * @returns {Promise<string|null>} otc - The otc of the token
157
157
  */
158
158
  async getOTCFromToken(token) {
159
- const tokenId = await this.getIdFromToken(token);
159
+ const tokenId = await this.getRefFromToken(token);
160
160
 
161
161
  if (!tokenId || typeof this.tokenProvider.deriveOTC !== 'function') {
162
162
  return null;
@@ -68,7 +68,10 @@ function createMessage(message) {
68
68
  ...message,
69
69
  ...addresses,
70
70
  generateTextFromHTML,
71
- encoding
71
+ encoding,
72
+ headers: {
73
+ Sender: addresses.from
74
+ }
72
75
  };
73
76
  }
74
77