ghost 6.40.0 → 6.41.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 (184) hide show
  1. package/core/boot.js +8 -8
  2. package/core/built/admin/assets/{PolarAngleAxis-D7m1zwVk.js → PolarAngleAxis-Bh28wbPV.js} +1 -1
  3. package/core/built/admin/assets/{_baseAssignValue-BDsvYKNK.js → _baseAssignValue-hdDG700N.js} +1 -1
  4. package/core/built/admin/assets/{a-large-small-DxRNdz0F.js → a-large-small-BiK5_5LV.js} +1 -1
  5. package/core/built/admin/assets/{account-migration-SYnE1jBW.js → account-migration-DsSHanIB.js} +1 -1
  6. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  7. package/core/built/admin/assets/admin-x-settings/{code-editor-view-Bj9rgbLw.mjs → code-editor-view-BEIdFhw3.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{index-BrZ9YAzd.mjs → index-BEh3nFEy.mjs} +3 -3
  9. package/core/built/admin/assets/admin-x-settings/{index-DRndIh9T.mjs → index-BLF2n6YB.mjs} +2 -2
  10. package/core/built/admin/assets/admin-x-settings/{index-CIl25Teq.mjs → index-BmPIWs5F.mjs} +3 -3
  11. package/core/built/admin/assets/admin-x-settings/{index-CNyNY3Wh.mjs → index-CP9YnNzc.mjs} +3 -3
  12. package/core/built/admin/assets/admin-x-settings/{index-SGr5sE2h.mjs → index-Cd2Ns-D2.mjs} +2 -2
  13. package/core/built/admin/assets/admin-x-settings/{index-PGoLP2kh.mjs → index-CjqAcKpK.mjs} +5 -5
  14. package/core/built/admin/assets/admin-x-settings/{index-pCIv_7bB.mjs → index-CnMcD9aj.mjs} +6212 -6158
  15. package/core/built/admin/assets/admin-x-settings/{index-CPCcgad-.mjs → index-LzsrZC40.mjs} +3 -3
  16. package/core/built/admin/assets/admin-x-settings/{index-DPqY4h5h.mjs → index-ZYW0UW7I.mjs} +3 -3
  17. package/core/built/admin/assets/admin-x-settings/{modals-lo32WZLi.mjs → modals-C48Tq1rW.mjs} +9 -9
  18. package/core/built/admin/assets/{arrow-right-BixoEua4.js → arrow-right-Bk43nMGd.js} +1 -1
  19. package/core/built/admin/assets/{at-sign-BsHNViA_.js → at-sign-DWOf36uj.js} +1 -1
  20. package/core/built/admin/assets/{audience-0RCWjbIK.js → audience-CvP78Gug.js} +1 -1
  21. package/core/built/admin/assets/{automations-DTUfYvf6.js → automations-BBWZhrha.js} +1 -1
  22. package/core/built/admin/assets/{automations-DiGkcVt-.js → automations-CmulGWwu.js} +1 -1
  23. package/core/built/admin/assets/{avatar-flipboard-BgoH-wIq.js → avatar-flipboard-CVf-N6wp.js} +1 -1
  24. package/core/built/admin/assets/{bluesky-sharing-C2xwSoS-.js → bluesky-sharing-DVC3I9Um.js} +1 -1
  25. package/core/built/admin/assets/{chart-X5cLIH1X.js → chart-CUKSNmKY.js} +1 -1
  26. package/core/built/admin/assets/{checkbox-DMamS4oG.js → checkbox-B_yDSM8z.js} +1 -1
  27. package/core/built/admin/assets/{chevron-up-CVgBkTk5.js → chevron-up-DuaKtE_M.js} +1 -1
  28. package/core/built/admin/assets/{chunk.941.f47098cbbce89a049e03.js → chunk.331.2ec2e2702eda02de96cd.js} +3 -3
  29. package/core/built/admin/assets/{chunk.524.a1d668dcc84709a5d702.js → chunk.524.49cc01002e1a5dba8ef9.js} +4 -4
  30. package/core/built/admin/assets/{chunk.582.180bf2525cf552f5ac5f.js → chunk.582.4bbc54c5f7d3dfb93288.js} +6 -6
  31. package/core/built/admin/assets/{circle-alert-Dg3UkvRP.js → circle-alert-bcK_Yqap.js} +1 -1
  32. package/core/built/admin/assets/{code-editor-view-CAICoZJe.js → code-editor-view-Bl4Jzjhk.js} +1 -1
  33. package/core/built/admin/assets/{comments-BHru9ZQ8.js → comments-DMea0nul.js} +1 -1
  34. package/core/built/admin/assets/{content-helpers-BByMp-Y5.js → content-helpers-B2yRzdvJ.js} +1 -1
  35. package/core/built/admin/assets/{copy-D7jqplrv.js → copy-DVtQi_VJ.js} +1 -1
  36. package/core/built/admin/assets/{data-list-DUaY9nlT.js → data-list-BF4GeUmt.js} +1 -1
  37. package/core/built/admin/assets/{deleted-feed-item-jLcSKuul.js → deleted-feed-item-DO2u3qmi.js} +1 -1
  38. package/core/built/admin/assets/{dropzone-DSPf3N_o.js → dropzone-Cs4DrURS.js} +1 -1
  39. package/core/built/admin/assets/{edit-profile-BvU2-lW5.js → edit-profile-CnuBNQhO.js} +1 -1
  40. package/core/built/admin/assets/{editor-D3fcjt_F.js → editor-CTO9FIUW.js} +1 -1
  41. package/core/built/admin/assets/{empty-indicator-2Pjmce3n.js → empty-indicator-BvyaMcmw.js} +1 -1
  42. package/core/built/admin/assets/{en-Bu-6PYn5.js → en-CjPXRj8j.js} +1 -1
  43. package/core/built/admin/assets/{feed-CygLK1Ad.js → feed-D2wk4cCS.js} +1 -1
  44. package/core/built/admin/assets/{filter-query-core-a0PNU2EO.js → filter-query-core-VEnwTCyO.js} +1 -1
  45. package/core/built/admin/assets/{filters-jiyRsLMH.js → filters-DtIsuqv5.js} +1 -1
  46. package/core/built/admin/assets/{gh-chart-CEhBBu3F.js → gh-chart-JxzKqaoO.js} +1 -1
  47. package/core/built/admin/assets/{ghost-0ab96884815339963364804ab266c99f.js → ghost-30c3dedbe1cca943b92efa7efd2710e4.js} +36 -21
  48. package/core/built/admin/assets/{growth-Fz3Oatml.js → growth-CPNrBpHK.js} +1 -1
  49. package/core/built/admin/assets/{hash-CeFrjo3C.js → hash-CJzbTA5k.js} +1 -1
  50. package/core/built/admin/assets/{inbox-CCirPaG2.js → inbox-CVVAkCmd.js} +1 -1
  51. package/core/built/admin/assets/{index-Dd2BJRZH.js → index-AkQJUr1z.js} +1 -1
  52. package/core/built/admin/assets/{index-CYPN5csJ.js → index-BLWOUf31.js} +1 -1
  53. package/core/built/admin/assets/{index-D7Ui3Fih.js → index-BRBRkRqa.js} +1 -1
  54. package/core/built/admin/assets/{index-CFEzD2_M.js → index-BkbhBpVC.js} +3 -3
  55. package/core/built/admin/assets/{index-BYwL7gFN.js → index-BxFFWYdP.js} +1 -1
  56. package/core/built/admin/assets/{index-D3LfHeBl.js → index-C5_ZKpC1.js} +1 -1
  57. package/core/built/admin/assets/{index-Bjw2qAph.js → index-Cdbufdfy.js} +1 -1
  58. package/core/built/admin/assets/{index-BTf6YUBb.js → index-Cq5vsvxE.js} +1 -1
  59. package/core/built/admin/assets/{index-DPbi4ie_.js → index-CuGoX8Ub.js} +1 -1
  60. package/core/built/admin/assets/{index-CsaU9dfl.js → index-CuH5raTz.js} +1 -1
  61. package/core/built/admin/assets/{index-Bf83suMc.js → index-D3A8RTi0.js} +1 -1
  62. package/core/built/admin/assets/{index-Dskrbhv3.js → index-DIyK69JM.js} +1 -1
  63. package/core/built/admin/assets/{index-GExKMpy_.js → index-DLFVpwt3.js} +1 -1
  64. package/core/built/admin/assets/{index-BOMdiHRG.js → index-DQZp_NsL.js} +1 -1
  65. package/core/built/admin/assets/{index-2qcjFCQs.js → index-DR74ulj2.js} +1 -1
  66. package/core/built/admin/assets/{index-WKMARia9.js → index-DYE2untj.js} +1 -1
  67. package/core/built/admin/assets/{index-DNZUJQ8v.js → index-DssW7xhk.js} +1 -1
  68. package/core/built/admin/assets/{index-Cq4mS7QK.js → index-DvyNqFct.js} +1 -1
  69. package/core/built/admin/assets/{index-C7xLyJVb.js → index-DxB7NHpG.js} +1 -1
  70. package/core/built/admin/assets/{index-Bw4DM4Pr.js → index-IDaHqjnu.js} +1 -1
  71. package/core/built/admin/assets/{index-ClOB5u8L.js → index-TtBMzSeP.js} +1 -1
  72. package/core/built/admin/assets/{index-takfwHSn.js → index-UMWVIyz7.js} +1 -1
  73. package/core/built/admin/assets/{koenig-lexical-BJM9wAwQ.js → koenig-lexical-DVAmevhl.js} +1 -1
  74. package/core/built/admin/assets/{kpi-card-BP-2yicn.js → kpi-card-D3CzvsrI.js} +1 -1
  75. package/core/built/admin/assets/{kpi-card-DOplXdDT.js → kpi-card-DV-nN7xT.js} +1 -1
  76. package/core/built/admin/assets/{kpi-tabs-Rq5jJW2Z.js → kpi-tabs-B3NkaTwb.js} +1 -1
  77. package/core/built/admin/assets/{kpis-yHgME_iy.js → kpis-CVwF08QL.js} +1 -1
  78. package/core/built/admin/assets/{label-fijjy1Mb.js → label-BiPpMny2.js} +1 -1
  79. package/core/built/admin/assets/{links-s3IeAkbo.js → links-BY0Uo6O5.js} +1 -1
  80. package/core/built/admin/assets/{list-page-CUcB5ebB.js → list-page-COHNMpOi.js} +1 -1
  81. package/core/built/admin/assets/{loader-circle-C7mWzo52.js → loader-circle-B1uEfpbZ.js} +1 -1
  82. package/core/built/admin/assets/{mail-9TFkAaAh.js → mail-ccQhEr7k.js} +1 -1
  83. package/core/built/admin/assets/{main-layout-XtviVkg1.js → main-layout-BYKdA6Z1.js} +1 -1
  84. package/core/built/admin/assets/{members-XkHao5Yf.js → members-DkpEnvG5.js} +1 -1
  85. package/core/built/admin/assets/minus-C68D81nx.js +1 -0
  86. package/core/built/admin/assets/{modals-B6R7oqvb.js → modals-X6RSWn5Y.js} +3 -3
  87. package/core/built/admin/assets/{moderation-B2Tixfce.js → moderation-BFrrwfvx.js} +1 -1
  88. package/core/built/admin/assets/{newsletter-CbESXhxf.js → newsletter-n7kc6cxR.js} +1 -1
  89. package/core/built/admin/assets/{newsletters-DOIL_cOY.js → newsletters-CSCa3QdE.js} +1 -1
  90. package/core/built/admin/assets/{note-mUikaB-p.js → note-CMZjeK8e.js} +1 -1
  91. package/core/built/admin/assets/{onboarding-route-Dqv2nP7b.js → onboarding-route-BaX0a-pu.js} +1 -1
  92. package/core/built/admin/assets/{overview-yZ-7KJdx.js → overview-CS9dmE_6.js} +1 -1
  93. package/core/built/admin/assets/{pagemenu-TxGvHwRO.js → pagemenu-O_y8Jm4v.js} +1 -1
  94. package/core/built/admin/assets/{pencil-CKTgzafp.js → pencil-BcymGPnE.js} +1 -1
  95. package/core/built/admin/assets/{pin-BU_u-xRX.js → pin-Bl3nSr7I.js} +1 -1
  96. package/core/built/admin/assets/{post-analytics-CWA8invj.js → post-analytics-DW-TKu-t.js} +1 -1
  97. package/core/built/admin/assets/{post-analytics-context-BWDhMRq_.js → post-analytics-context-D7PgEMo8.js} +1 -1
  98. package/core/built/admin/assets/{post-analytics-header-Bm0mcxjK.js → post-analytics-header-B8C0vA5Y.js} +1 -1
  99. package/core/built/admin/assets/{post-share-modal-D8BtRD2u.js → post-share-modal-D0OlASBQ.js} +1 -1
  100. package/core/built/admin/assets/{posts-u-CAKddc.js → posts-BZaw4-dc.js} +1 -1
  101. package/core/built/admin/assets/{power-DR9gpiQR.js → power-DWm6pts5.js} +1 -1
  102. package/core/built/admin/assets/{referrers-DV_Ihg9y.js → referrers-BsZ85Z0a.js} +1 -1
  103. package/core/built/admin/assets/{repeat-uXmOJ6y8.js → repeat-DQay9e22.js} +1 -1
  104. package/core/built/admin/assets/{reply-9ZyqaxI-.js → reply-CoNqmExw.js} +1 -1
  105. package/core/built/admin/assets/{rocket-DMOr1OCt.js → rocket-Dne6P_8f.js} +1 -1
  106. package/core/built/admin/assets/{select-CPSy44ld.js → select-BfWJkrKc.js} +1 -1
  107. package/core/built/admin/assets/{settings-krk0432u.js → settings-BoXzKMhL.js} +1 -1
  108. package/core/built/admin/assets/{settings-66Zv8QvL.js → settings-rRY210wx.js} +25 -25
  109. package/core/built/admin/assets/{share-modal-BRmGllZq.js → share-modal-CtdLQbnQ.js} +1 -1
  110. package/core/built/admin/assets/{sort-button-5fyeViQL.js → sort-button-7pVgpa0d.js} +1 -1
  111. package/core/built/admin/assets/{source-icon-DeeHyZRn.js → source-icon-Dtua85oN.js} +1 -1
  112. package/core/built/admin/assets/{sprout-956Q4On2.js → sprout-CpwUJAPT.js} +1 -1
  113. package/core/built/admin/assets/{square-BaCvbKJY.js → square-CDrQjv8R.js} +1 -1
  114. package/core/built/admin/assets/{stats-BVKNZJO6.js → stats-Yo9Gco0_.js} +1 -1
  115. package/core/built/admin/assets/{stats-view-gtyaqudG.js → stats-view-DubukX9V.js} +1 -1
  116. package/core/built/admin/assets/{step-1-09wdjzdX.js → step-1-CH8jWKi7.js} +1 -1
  117. package/core/built/admin/assets/{step-2-8iaQ0efq.js → step-2-CWy7Qi0Q.js} +1 -1
  118. package/core/built/admin/assets/{step-3-BuHAZbdD.js → step-3-CNiDFH17.js} +1 -1
  119. package/core/built/admin/assets/{table-DTofVyjA.js → table-qQi9EXB1.js} +1 -1
  120. package/core/built/admin/assets/{tabs-CL5eaCGL.js → tabs-M6kFEqPE.js} +1 -1
  121. package/core/built/admin/assets/{tags-DJrbEw5-.js → tags-74U6lgwY.js} +1 -1
  122. package/core/built/admin/assets/{tags-U3lciBmd.js → tags-DuMshkTz.js} +1 -1
  123. package/core/built/admin/assets/{textarea-DdDuNixZ.js → textarea-CGBp9nOo.js} +1 -1
  124. package/core/built/admin/assets/{tiers-DXKbghdk.js → tiers-D2DYHaeO.js} +1 -1
  125. package/core/built/admin/assets/{toggle-group-DaZKTYDS.js → toggle-group-GYp_w6Po.js} +1 -1
  126. package/core/built/admin/assets/{topic-filter-BzqiVOAU.js → topic-filter-N3CzJ01P.js} +1 -1
  127. package/core/built/admin/assets/{trash-CKK_p9G5.js → trash-CUqAK3SQ.js} +1 -1
  128. package/core/built/admin/assets/{underline-Bq0P-5Zy.js → underline-CwuJG-Ql.js} +1 -1
  129. package/core/built/admin/assets/{undo-2-CcBtw-tW.js → undo-2-D6ZRqONE.js} +1 -1
  130. package/core/built/admin/assets/{upload-BcylHEMw.js → upload-BLiT66bJ.js} +1 -1
  131. package/core/built/admin/assets/{use-growth-stats-Ci0gymWp.js → use-growth-stats-u18So4Gq.js} +1 -1
  132. package/core/built/admin/assets/{use-simple-pagination-ByaFwxfZ.js → use-simple-pagination-Dp2sTt2S.js} +1 -1
  133. package/core/built/admin/assets/{user-round-check-D0I7NO1g.js → user-round-check-BDomHaso.js} +1 -1
  134. package/core/built/admin/assets/{user-round-x-DeGVH29o.js → user-round-x-Cc0RL-V9.js} +1 -1
  135. package/core/built/admin/assets/{virtual-list-window-S0xZxQAf.js → virtual-list-window-BnjCoNIr.js} +1 -1
  136. package/core/built/admin/assets/{wallet-cards-B9TQg4x3.js → wallet-cards-CVZ99Nm3.js} +1 -1
  137. package/core/built/admin/assets/{web-BRN2mUpo.js → web-B_MLxGxZ.js} +1 -1
  138. package/core/built/admin/index.html +5 -5
  139. package/core/server/{services/custom-redirects/file-store.js → adapters/redirects/FileStore.js} +6 -5
  140. package/core/server/{services/custom-redirects/file-store.ts → adapters/redirects/FileStore.ts} +6 -4
  141. package/core/server/adapters/redirects/RedirectsStoreBase.d.ts +9 -0
  142. package/core/server/adapters/redirects/RedirectsStoreBase.js +8 -0
  143. package/core/server/{services/custom-redirects/gcs-store.js → adapters/redirects/S3RedirectsStore.js} +50 -18
  144. package/core/server/adapters/redirects/S3RedirectsStore.ts +163 -0
  145. package/core/server/adapters/scheduling/scheduling-base.js +41 -0
  146. package/core/server/adapters/scheduling/types.js +2 -0
  147. package/core/server/adapters/scheduling/types.ts +36 -0
  148. package/core/server/api/endpoints/authentication.js +26 -5
  149. package/core/server/api/endpoints/schedules.js +0 -30
  150. package/core/server/api/endpoints/utils/serializers/output/authentication.js +10 -0
  151. package/core/server/data/migrations/versions/6.41/2026-05-13-12-00-00-rename-reset-all-passwords-permission.js +21 -0
  152. package/core/server/data/schema/fixtures/fixtures.json +3 -3
  153. package/core/server/models/api-key.js +18 -0
  154. package/core/server/models/user.js +18 -0
  155. package/core/server/services/adapter-manager/config.js +6 -0
  156. package/core/server/services/adapter-manager/index.js +1 -0
  157. package/core/server/services/auth/index.js +4 -0
  158. package/core/server/services/auth/reset-authentication.js +50 -0
  159. package/core/server/services/auth/reset-authentication.ts +75 -0
  160. package/core/server/services/automations/index.js +21 -15
  161. package/core/server/services/custom-redirects/index.js +2 -4
  162. package/core/server/services/gifts/gift-bookshelf-repository.js +7 -0
  163. package/core/server/services/gifts/gift-bookshelf-repository.ts +10 -0
  164. package/core/server/services/gifts/gift-reminder-scheduler.js +94 -0
  165. package/core/server/services/gifts/gift-reminder-scheduler.ts +109 -0
  166. package/core/server/services/gifts/gift-repository.ts +1 -0
  167. package/core/server/services/gifts/gift-service-wrapper.js +10 -7
  168. package/core/server/services/gifts/gift-service.js +1 -32
  169. package/core/server/services/gifts/gift-service.ts +3 -59
  170. package/core/server/services/internal-keys/index.js +1 -3
  171. package/core/server/services/internal-keys/index.ts +11 -3
  172. package/core/server/services/post-scheduling/index.js +16 -28
  173. package/core/server/services/post-scheduling/index.ts +14 -0
  174. package/core/server/services/post-scheduling/post-scheduling.js +115 -0
  175. package/core/server/services/post-scheduling/post-scheduling.ts +138 -0
  176. package/core/server/services/users.js +29 -16
  177. package/core/server/web/api/endpoints/admin/middleware.js +8 -4
  178. package/core/server/web/api/endpoints/admin/routes.js +1 -1
  179. package/core/shared/config/defaults.json +5 -0
  180. package/core/shared/labs.js +2 -1
  181. package/package.json +1 -1
  182. package/core/built/admin/assets/minus-DPrXh11r.js +0 -1
  183. package/core/server/services/custom-redirects/gcs-store.ts +0 -117
  184. package/core/server/services/post-scheduling/post-scheduler-service.js +0 -126
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shape of a job the scheduler adapter queues. Time is a unix timestamp;
3
+ * url carries a JWT-signed admin token; extra forwards through to the
4
+ * HTTP callback the adapter fires.
5
+ */
6
+ export interface SchedulerJob {
7
+ time: number;
8
+ url: string;
9
+ extra: {
10
+ httpMethod: string;
11
+ oldTime?: number | null;
12
+ };
13
+ }
14
+
15
+ /**
16
+ * Implemented by scheduler-using subsystems (post-scheduling, automations,
17
+ * gift reminders). The adapter calls `rescheduleAll` on every registered
18
+ * rescheduler when something requires the queue to be rebuilt — currently
19
+ * after an internal API key rotation.
20
+ */
21
+ export interface Rescheduler {
22
+ rescheduleAll(opts?: {previousKey?: {id: string; secret: string}}): Promise<void>;
23
+ }
24
+
25
+ /**
26
+ * The contract Ghost expects of any concrete scheduling adapter. Adapters
27
+ * that extend `SchedulingBase` inherit `register` and `rescheduleAll` for
28
+ * free; the three runtime methods (`schedule`, `unschedule`, `run`) are
29
+ * the ones each adapter implementation must provide.
30
+ */
31
+ export interface SchedulerAdapter {
32
+ run(): void;
33
+ schedule(job: SchedulerJob): void;
34
+ unschedule(job: SchedulerJob, opts?: {bootstrap?: boolean}): void;
35
+ register(rescheduler: Rescheduler): void;
36
+ }
@@ -12,7 +12,17 @@ const apiMail = require('./index').mail;
12
12
  const apiSettings = require('./index').settings;
13
13
  const UsersService = require('../../services/users');
14
14
  const userService = new UsersService({dbBackup, models, auth, apiMail, apiSettings});
15
- const {deleteAllSessions} = require('../../services/auth/session');
15
+ const adapterManager = require('../../services/adapter-manager');
16
+ const schedulerAdapter = adapterManager.getAdapter('scheduling');
17
+
18
+ async function destroyRequestSession(req) {
19
+ if (!req || !req.session) {
20
+ return;
21
+ }
22
+ await new Promise((resolve, reject) => {
23
+ req.session.destroy(err => (err ? reject(err) : resolve()));
24
+ });
25
+ }
16
26
 
17
27
  const messages = {
18
28
  notTheBlogOwner: 'You are not the site owner.'
@@ -224,15 +234,26 @@ const controller = {
224
234
  }
225
235
  },
226
236
 
227
- resetAllPasswords: {
228
- statusCode: 204,
237
+ reset: {
238
+ statusCode: 200,
229
239
  headers: {
230
240
  cacheInvalidate: false
231
241
  },
232
242
  permissions: true,
233
243
  async query(frame) {
234
- await userService.resetAllPasswords(frame.options);
235
- await deleteAllSessions();
244
+ const result = await auth.resetAuthentication({
245
+ schedulerAdapter,
246
+ userService,
247
+ options: frame.options
248
+ });
249
+
250
+ // Express-session would otherwise re-save the current request's
251
+ // session on the response, resurrecting the row deleteAllSessions
252
+ // just wiped. Destroying the request session prevents the re-save
253
+ // and emits a Set-Cookie that expires the cookie on the client.
254
+ await destroyRequestSession(frame.original.session && frame.original.session.req);
255
+
256
+ return result;
236
257
  }
237
258
  }
238
259
  };
@@ -1,4 +1,3 @@
1
- const models = require('../../models');
2
1
  const postScheduling = require('../../services/posts/post-scheduling');
3
2
 
4
3
  /** @type {import('@tryghost/api-framework').Controller} */
@@ -52,35 +51,6 @@ const controller = {
52
51
  response[resourceType] = [scheduledResource];
53
52
  return response;
54
53
  }
55
- },
56
-
57
- getScheduled: {
58
- // NOTE: this method is for internal use only by DefaultScheduler
59
- // it is not exposed anywhere!
60
- headers: {
61
- cacheInvalidate: false
62
- },
63
- permissions: false,
64
- validation: {
65
- options: {
66
- resource: {
67
- required: true,
68
- values: ['posts', 'pages']
69
- }
70
- }
71
- },
72
- async query(frame) {
73
- const resourceModel = 'Post';
74
- const resourceType = (frame.options.resource === 'post') ? 'post' : 'page';
75
- const cleanOptions = {};
76
- cleanOptions.filter = `status:scheduled+type:${resourceType}`;
77
- cleanOptions.columns = ['id', 'published_at', 'created_at', 'type'];
78
-
79
- const result = await models[resourceModel].findAll(cleanOptions);
80
- let response = {};
81
- response[resourceType] = result;
82
- return response;
83
- }
84
54
  }
85
55
  };
86
56
 
@@ -71,5 +71,15 @@ module.exports = {
71
71
  valid: !!data
72
72
  }]
73
73
  };
74
+ },
75
+
76
+ reset(data, apiConfig, frame) {
77
+ frame.response = {
78
+ security_action: [{
79
+ action: 'reset_authentication',
80
+ api_keys_rotated: data?.apiKeysRotated ?? 0,
81
+ users_locked: data?.usersLocked ?? 0
82
+ }]
83
+ };
74
84
  }
75
85
  };
@@ -0,0 +1,21 @@
1
+ const {createTransactionalMigration} = require('../../utils');
2
+
3
+ // The `resetAllPasswords` action on the `authentication` object backed the
4
+ // orphaned `/authentication/global_password_reset` endpoint. That endpoint is
5
+ // replaced by `/authentication/reset`, which rotates every credential
6
+ // (api keys, passwords, sessions) in one shot — so we rename the existing
7
+ // permission row to match its new contract rather than introduce a fresh row
8
+ // alongside a stale one.
9
+
10
+ module.exports = createTransactionalMigration(
11
+ async function up(knex) {
12
+ await knex('permissions')
13
+ .where({action_type: 'resetAllPasswords', object_type: 'authentication'})
14
+ .update({name: 'Reset authentication', action_type: 'reset'});
15
+ },
16
+ async function down(knex) {
17
+ await knex('permissions')
18
+ .where({action_type: 'reset', object_type: 'authentication'})
19
+ .update({name: 'Reset all passwords', action_type: 'resetAllPasswords'});
20
+ }
21
+ );
@@ -630,8 +630,8 @@
630
630
  "object_type": "offer"
631
631
  },
632
632
  {
633
- "name": "Reset all passwords",
634
- "action_type": "resetAllPasswords",
633
+ "name": "Reset authentication",
634
+ "action_type": "reset",
635
635
  "object_type": "authentication"
636
636
  },
637
637
  {
@@ -957,7 +957,7 @@
957
957
  "snippet": "all",
958
958
  "custom_theme_setting": "all",
959
959
  "offer": "all",
960
- "authentication": "resetAllPasswords",
960
+ "authentication": "reset",
961
961
  "members_stripe_connect": "auth",
962
962
  "newsletter": "all",
963
963
  "explore": "read",
@@ -3,6 +3,7 @@ const security = require('@tryghost/security');
3
3
  const ghostBookshelf = require('./base');
4
4
  const {Role} = require('./role');
5
5
 
6
+ // secretlint-disable-next-line @secretlint/secretlint-rule-pattern
6
7
  const ApiKey = ghostBookshelf.Model.extend({
7
8
  tableName: 'api_keys',
8
9
 
@@ -61,6 +62,22 @@ const ApiKey = ghostBookshelf.Model.extend({
61
62
  refreshSecret(data, options) {
62
63
  const secret = security.secret.create(data.type);
63
64
  return this.edit(Object.assign({}, data, {secret}), options);
65
+ },
66
+
67
+ /**
68
+ * Refresh the secret on every API key row, returning the count rotated.
69
+ * Used by the danger-zone reset flow; callers are expected to wrap this
70
+ * in a transaction.
71
+ *
72
+ * @param {Object} options
73
+ * @returns {Promise<{count: number}>}
74
+ */
75
+ async refreshAllSecrets(options) {
76
+ const apiKeys = await this.findAll(options);
77
+ for (const apiKey of apiKeys.models) {
78
+ await this.refreshSecret(apiKey.toJSON(), Object.assign({}, options, {id: apiKey.id}));
79
+ }
80
+ return {count: apiKeys.length};
64
81
  }
65
82
  });
66
83
 
@@ -69,6 +86,7 @@ const ApiKeys = ghostBookshelf.Collection.extend({
69
86
  });
70
87
 
71
88
  module.exports = {
89
+ // secretlint-disable-next-line @secretlint/secretlint-rule-pattern
72
90
  ApiKey: ghostBookshelf.model('ApiKey', ApiKey),
73
91
  ApiKeys: ghostBookshelf.collection('ApiKeys', ApiKeys)
74
92
  };
@@ -174,6 +174,24 @@ User = ghostBookshelf.Model.extend({
174
174
  return this.get('status') === 'locked';
175
175
  },
176
176
 
177
+ /**
178
+ * Replace this user's password with an opaque random value, and mark
179
+ * them as locked unless they are already inactive (suspended). Suspended
180
+ * users must not be transitioned out of `inactive` — they retain the
181
+ * suspended-signin path — but their password is still rotated so a
182
+ * compromised credential cannot survive a future unsuspend.
183
+ */
184
+ lock: function lock(options) {
185
+ const update = {
186
+ // secretlint-disable-next-line @secretlint/secretlint-rule-pattern
187
+ password: security.identifier.uid(50)
188
+ };
189
+ if (this.get('status') !== 'inactive') {
190
+ update.status = 'locked';
191
+ }
192
+ return this.save(update, {...options, patch: true});
193
+ },
194
+
177
195
  isInactive: function isInactive() {
178
196
  return this.get('status') === 'inactive';
179
197
  },
@@ -25,5 +25,11 @@ module.exports = function getAdapterServiceConfig(config) {
25
25
  };
26
26
  }
27
27
 
28
+ // FileStore needs Ghost's resolved content path, which isn't
29
+ // representable as a static value in defaults.json.
30
+ if (adapterServiceConfig.redirects?.FileStore) {
31
+ adapterServiceConfig.redirects.FileStore.basePath ||= config.getContentPath('data');
32
+ }
33
+
28
34
  return adapterServiceConfig;
29
35
  };
@@ -16,6 +16,7 @@ adapterManager.registerAdapter('storage', require('ghost-storage-base'));
16
16
  adapterManager.registerAdapter('scheduling', require('../../adapters/scheduling/scheduling-base'));
17
17
  adapterManager.registerAdapter('sso', require('../../adapters/sso/SSOBase'));
18
18
  adapterManager.registerAdapter('cache', require('@tryghost/adapter-base-cache'));
19
+ adapterManager.registerAdapter('redirects', require('../../adapters/redirects/RedirectsStoreBase'));
19
20
 
20
21
  module.exports = {
21
22
  /**
@@ -17,5 +17,9 @@ module.exports = {
17
17
 
18
18
  get passwordreset() {
19
19
  return require('./passwordreset');
20
+ },
21
+
22
+ get resetAuthentication() {
23
+ return require('./reset-authentication').default;
20
24
  }
21
25
  };
@@ -0,0 +1,50 @@
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.default = resetAuthentication;
7
+ const internal_keys_1 = __importDefault(require("../internal-keys"));
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ const modelsDefault = require('../../models');
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
11
+ const { deleteAllSessions: deleteAllSessionsDefault } = require('./session');
12
+ /**
13
+ * Rotation, user lock and the audit row commit in a single transaction so
14
+ * app crashes mid-flight can't leave the system half-rotated or lose the
15
+ * audit trail. Session deletion runs immediately after the commit, before
16
+ * the rescheduleAll, so a failure inside the adapter can't leave stale
17
+ * session rows live for an attacker.
18
+ *
19
+ * The schedulerAdapter and userService come from boot's lifecycle, so they
20
+ * are explicit parameters. The auth-domain primitives (models, internalKeys,
21
+ * sessions) default to their module singletons; tests pass overrides.
22
+ */
23
+ async function resetAuthentication({ schedulerAdapter, userService, options, models = modelsDefault, internalKeys = internal_keys_1.default, deleteAllSessions = deleteAllSessionsDefault }) {
24
+ const previousSchedulerKey = await internalKeys.get('ghost-scheduler');
25
+ const actorId = options?.context?.user ?? null;
26
+ const { apiKeysRotated, usersLocked } = await models.Base.transaction(async (tx) => {
27
+ const txOptions = Object.assign({}, options, { transacting: tx });
28
+ const { count: rotated } = await models.ApiKey.refreshAllSecrets(txOptions);
29
+ const { count: locked } = await userService.lockAll(txOptions);
30
+ if (actorId) {
31
+ await models.Action.add({
32
+ event: 'edited',
33
+ resource_type: 'security_action',
34
+ resource_id: null,
35
+ actor_type: 'user',
36
+ actor_id: actorId,
37
+ context: {
38
+ action_name: 'reset_authentication',
39
+ api_keys_rotated: rotated,
40
+ users_locked: locked
41
+ }
42
+ }, { transacting: tx, autoRefresh: false });
43
+ }
44
+ return { apiKeysRotated: rotated, usersLocked: locked };
45
+ });
46
+ internalKeys.clear();
47
+ await deleteAllSessions();
48
+ await schedulerAdapter.rescheduleAll({ previousKey: previousSchedulerKey });
49
+ return { apiKeysRotated, usersLocked };
50
+ }
@@ -0,0 +1,75 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type {Knex} from 'knex';
3
+ import type {InternalApiKey, InternalKeys} from '../internal-keys';
4
+ import internalKeysDefault from '../internal-keys';
5
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
6
+ const modelsDefault = require('../../models');
7
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
8
+ const {deleteAllSessions: deleteAllSessionsDefault} = require('./session');
9
+
10
+ interface ResetAuthenticationArgs {
11
+ schedulerAdapter: {rescheduleAll(opts: {previousKey?: InternalApiKey}): Promise<unknown>};
12
+ userService: {lockAll(options: any): Promise<{count: number}>};
13
+ options: {context?: {user?: string}; [key: string]: unknown};
14
+ models?: any;
15
+ internalKeys?: InternalKeys;
16
+ deleteAllSessions?: () => Promise<void>;
17
+ }
18
+
19
+ interface ResetAuthenticationResult {
20
+ apiKeysRotated: number;
21
+ usersLocked: number;
22
+ }
23
+
24
+ /**
25
+ * Rotation, user lock and the audit row commit in a single transaction so
26
+ * app crashes mid-flight can't leave the system half-rotated or lose the
27
+ * audit trail. Session deletion runs immediately after the commit, before
28
+ * the rescheduleAll, so a failure inside the adapter can't leave stale
29
+ * session rows live for an attacker.
30
+ *
31
+ * The schedulerAdapter and userService come from boot's lifecycle, so they
32
+ * are explicit parameters. The auth-domain primitives (models, internalKeys,
33
+ * sessions) default to their module singletons; tests pass overrides.
34
+ */
35
+ export default async function resetAuthentication({
36
+ schedulerAdapter,
37
+ userService,
38
+ options,
39
+ models = modelsDefault,
40
+ internalKeys = internalKeysDefault,
41
+ deleteAllSessions = deleteAllSessionsDefault
42
+ }: ResetAuthenticationArgs): Promise<ResetAuthenticationResult> {
43
+ const previousSchedulerKey = await internalKeys.get('ghost-scheduler');
44
+ const actorId = options?.context?.user ?? null;
45
+
46
+ const {apiKeysRotated, usersLocked} = await models.Base.transaction(async (tx: Knex.Transaction) => {
47
+ const txOptions = Object.assign({}, options, {transacting: tx});
48
+
49
+ const {count: rotated} = await models.ApiKey.refreshAllSecrets(txOptions);
50
+ const {count: locked} = await userService.lockAll(txOptions);
51
+
52
+ if (actorId) {
53
+ await models.Action.add({
54
+ event: 'edited',
55
+ resource_type: 'security_action',
56
+ resource_id: null,
57
+ actor_type: 'user',
58
+ actor_id: actorId,
59
+ context: {
60
+ action_name: 'reset_authentication',
61
+ api_keys_rotated: rotated,
62
+ users_locked: locked
63
+ }
64
+ }, {transacting: tx, autoRefresh: false});
65
+ }
66
+
67
+ return {apiKeysRotated: rotated, usersLocked: locked};
68
+ });
69
+
70
+ internalKeys.clear();
71
+ await deleteAllSessions();
72
+ await schedulerAdapter.rescheduleAll({previousKey: previousSchedulerKey});
73
+
74
+ return {apiKeysRotated, usersLocked};
75
+ }
@@ -18,10 +18,12 @@ const memberWelcomeEmailService = require('../member-welcome-emails/service');
18
18
  * httpMethod: string;
19
19
  * };
20
20
  * }) => void} schedule
21
+ * @prop {(rescheduler: {rescheduleAll: () => unknown}) => void} register
21
22
  */
22
23
 
23
24
  class AutomationsService {
24
25
  #initialized = false;
26
+ #enqueuePollNow;
25
27
 
26
28
  /**
27
29
  * @param {object} options
@@ -36,13 +38,9 @@ class AutomationsService {
36
38
  return;
37
39
  }
38
40
 
39
- const enqueuePollNow = () => {
40
- domainEvents.dispatch(StartAutomationsPollEvent.create());
41
- };
41
+ this.#enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());
42
42
 
43
- /**
44
- * @param {Readonly<Date>} date
45
- */
43
+ /** @param {Readonly<Date>} date */
46
44
  const enqueuePollAt = async (date) => {
47
45
  try {
48
46
  const key = await internalKeys.get('ghost-scheduler');
@@ -55,18 +53,26 @@ class AutomationsService {
55
53
  }
56
54
  };
57
55
 
58
- domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => {
59
- await poll({
60
- memberWelcomeEmailService,
61
- enqueueAnotherPollNow: enqueuePollNow,
62
- enqueueAnotherPollAt: enqueuePollAt
63
- });
64
- }));
56
+ domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
57
+ memberWelcomeEmailService,
58
+ enqueueAnotherPollNow: this.#enqueuePollNow,
59
+ enqueueAnotherPollAt: enqueuePollAt
60
+ })));
65
61
 
66
- enqueuePollNow();
62
+ schedulerAdapter.register(this);
67
63
 
64
+ this.#enqueuePollNow();
68
65
  this.#initialized = true;
69
66
  }
67
+
68
+ /**
69
+ * Re-arm the poll chain. A queued poll signed under the previous scheduler
70
+ * key fails JWT verification when fired; this dispatches a fresh in-process
71
+ * poll that re-schedules the next callback under the current key.
72
+ */
73
+ rescheduleAll() {
74
+ this.#enqueuePollNow?.();
75
+ }
70
76
  }
71
77
 
72
- module.exports = AutomationsService;
78
+ module.exports = new AutomationsService();
@@ -1,8 +1,8 @@
1
1
  const config = require('../../../shared/config');
2
2
  const urlUtils = require('../../../shared/url-utils');
3
+ const adapterManager = require('../adapter-manager');
3
4
 
4
5
  const DynamicRedirectManager = require('../lib/dynamic-redirect-manager');
5
- const {FileStore} = require('./file-store');
6
6
  const {RedirectsService} = require('./redirects-service');
7
7
  const validation = require('./validation');
8
8
 
@@ -18,9 +18,7 @@ module.exports = {
18
18
  init() {
19
19
  redirectManager = makeRedirectManager();
20
20
 
21
- const store = new FileStore({
22
- basePath: config.getContentPath('data')
23
- });
21
+ const store = adapterManager.getAdapter('redirects');
24
22
 
25
23
  redirectsService = new RedirectsService({
26
24
  store,
@@ -83,6 +83,13 @@ class GiftBookshelfRepository {
83
83
  });
84
84
  return collection.models.map(model => this.toGift(model));
85
85
  }
86
+ async findUnsentReminders() {
87
+ const now = new Date().toISOString();
88
+ const collection = await this.model.findAll({
89
+ filter: `status:redeemed+consumes_at:>'${now}'+consumes_soon_reminder_sent_at:null`
90
+ });
91
+ return collection.models.map(model => this.toGift(model));
92
+ }
86
93
  async create(gift, options = {}) {
87
94
  await this.model.add(this.toRow(gift), options);
88
95
  }
@@ -148,6 +148,16 @@ export class GiftBookshelfRepository implements GiftRepository {
148
148
  return collection.models.map(model => this.toGift(model));
149
149
  }
150
150
 
151
+ async findUnsentReminders(): Promise<Gift[]> {
152
+ const now = new Date().toISOString();
153
+
154
+ const collection = await this.model.findAll({
155
+ filter: `status:redeemed+consumes_at:>'${now}'+consumes_soon_reminder_sent_at:null`
156
+ });
157
+
158
+ return collection.models.map(model => this.toGift(model));
159
+ }
160
+
151
161
  async create(gift: Gift, options: RepositoryTransactionOptions = {}) {
152
162
  await this.model.add(this.toRow(gift), options);
153
163
  }
@@ -0,0 +1,94 @@
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.GiftReminderScheduler = void 0;
7
+ const logging_1 = __importDefault(require("@tryghost/logging"));
8
+ const constants_1 = require("./constants");
9
+ // Same-domain (scheduling) primitives, used unconditionally.
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
11
+ const urlUtils = require('../../../shared/url-utils');
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ const { getSignedAdminToken } = require('../../adapters/scheduling/utils');
14
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
15
+ const GIFT_REMINDER_LEAD_MS = constants_1.GIFT_REMINDER_LEAD_DAYS * MS_PER_DAY;
16
+ class GiftReminderScheduler {
17
+ #apiUrl;
18
+ #adapter;
19
+ #internalKeys;
20
+ #findUnsentReminders;
21
+ constructor({ apiUrl, adapter, internalKeys, findUnsentReminders }) {
22
+ this.#apiUrl = apiUrl;
23
+ this.#adapter = adapter;
24
+ this.#internalKeys = internalKeys;
25
+ this.#findUnsentReminders = findUnsentReminders;
26
+ this.#adapter.register(this);
27
+ }
28
+ /**
29
+ * Queue a single reminder callback for a freshly-redeemed gift. The
30
+ * callback fires at consumesAt - GIFT_REMINDER_LEAD_DAYS. Already-due
31
+ * reminders are skipped — the daily cron picks them up.
32
+ */
33
+ async scheduleFor(gift) {
34
+ if (!gift.consumesAt) {
35
+ return;
36
+ }
37
+ const time = gift.consumesAt.getTime() - GIFT_REMINDER_LEAD_MS;
38
+ if (time <= Date.now()) {
39
+ return;
40
+ }
41
+ try {
42
+ const key = await this.#internalKeys.get('ghost-scheduler');
43
+ this.#adapter.schedule(this.#buildJob(time, key));
44
+ }
45
+ catch (err) {
46
+ logging_1.default.error({
47
+ event: { name: 'gift_reminder_scheduler.schedule.failed' },
48
+ err,
49
+ giftToken: gift.token
50
+ }, 'Failed to schedule gift reminder');
51
+ }
52
+ }
53
+ /**
54
+ * Re-issue every queued reminder under the current scheduler key. Pass
55
+ * the pre-rotation secret as `previousKey` so each adapter-queued URL
56
+ * can be reconstructed for unschedule before resigning with the new
57
+ * key. Reminders whose fire time has already passed are skipped — the
58
+ * daily cron picks them up.
59
+ */
60
+ async rescheduleAll({ previousKey } = {}) {
61
+ const currentKey = await this.#internalKeys.get('ghost-scheduler');
62
+ const unscheduleKey = previousKey ?? currentKey;
63
+ const pending = await this.#findUnsentReminders();
64
+ // Same-key rebuild (no previousKey, boot path) → URL signature is
65
+ // identical to the about-to-be-scheduled job. The default adapter
66
+ // implements unschedule via tombstones keyed by URL+time, so a same-URL
67
+ // unschedule poisons the scheduled job. Bootstrap mode skips the
68
+ // tombstone write. Rotation (previousKey provided) → URLs differ, so
69
+ // the tombstone correctly targets the old queued entry.
70
+ const bootstrap = !previousKey;
71
+ for (const gift of pending) {
72
+ if (!gift.consumesAt) {
73
+ continue;
74
+ }
75
+ const time = gift.consumesAt.getTime() - GIFT_REMINDER_LEAD_MS;
76
+ if (time <= Date.now()) {
77
+ continue;
78
+ }
79
+ this.#adapter.unschedule(this.#buildJob(time, unscheduleKey), { bootstrap });
80
+ this.#adapter.schedule(this.#buildJob(time, currentKey));
81
+ }
82
+ }
83
+ #buildJob(time, key) {
84
+ const signedAdminToken = getSignedAdminToken({
85
+ publishedAt: new Date(time).toISOString(),
86
+ apiUrl: this.#apiUrl,
87
+ key
88
+ });
89
+ const url = new URL(urlUtils.urlJoin(this.#apiUrl, 'gifts', 'flush_reminders'));
90
+ url.searchParams.set('token', signedAdminToken);
91
+ return { time, url: url.toString(), extra: { httpMethod: 'PUT' } };
92
+ }
93
+ }
94
+ exports.GiftReminderScheduler = GiftReminderScheduler;