ghost 6.54.0 → 6.55.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 (327) hide show
  1. package/components/ghost-storage-base-3.0.0.tgz +0 -0
  2. package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
  3. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  4. package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
  5. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  6. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  7. package/core/boot.js +3 -6
  8. package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-sjHLx5dy.js} +1 -1
  9. package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-DoYXtnZ8.js} +4 -4
  10. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  11. package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
  12. package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
  13. package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-D8sFfdpC.js} +2 -2
  14. package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-CWueFW6q.js} +5 -5
  15. package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BGxPU7N5.js} +3 -3
  16. package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-DBK-u0Ch.js} +3 -3
  17. package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-Bcir5RQZ.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-BV-CIT3d.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-yC0nfvzl.js} +4 -4
  20. package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-DTRS9TDk.js} +2 -2
  21. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
  22. package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-Dwl9t0pb.js} +4 -4
  23. package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-B-zKsZ7r.js} +3 -3
  24. package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-DjJZD54t.js} +3 -3
  25. package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-CoU8vs6d.js} +6 -6
  26. package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-BAsaJekx.js} +6 -6
  27. package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CLH3OVTP.js} +28 -25
  28. package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-DA6pNJ-Q.js} +3 -3
  29. package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-CQD42Pw5.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-Cg0saow1.js} +7 -7
  31. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
  32. package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
  33. package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-DRy7qY-J.js} +2 -2
  34. package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-Bx3VcFIL.js} +1 -1
  35. package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
  36. package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
  37. package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-BD0U2ygg.js} +1 -1
  38. package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-fjQKRVyY.js} +1 -1
  39. package/core/built/admin/assets/checkbox-CWa28lgO.js +1 -0
  40. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
  41. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
  42. package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
  43. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +25 -0
  44. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +27 -0
  45. package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +1 -0
  46. package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
  47. package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-BFp58EMk.js} +1 -1
  48. package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
  49. package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
  50. package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
  51. package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
  52. package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
  53. package/core/built/admin/assets/dropzone-DlK-K2DQ.js +5 -0
  54. package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-BKT-REJw.js} +1 -1
  55. package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
  56. package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-ClUpJZOp.js} +1 -1
  57. package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-DF92dUMP.js} +1 -1
  58. package/core/built/admin/assets/field-Do7lieBA.js +1 -0
  59. package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-BV2rnpfA.js} +1 -1
  60. package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
  61. package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-DfbIWc_l.js} +1 -1
  62. package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
  63. package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-c4057474943effc568c5659ccc4909d7.js} +407 -406
  64. package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
  65. package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
  66. package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
  67. package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
  68. package/core/built/admin/assets/{Growth-CSaHoSVD.js → growth-CVw1My4S.js} +2 -2
  69. package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
  70. package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-CFBSu4Ed.js} +12 -12
  71. package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
  72. package/core/built/admin/assets/index-BGG7NckO.js +67 -0
  73. package/core/built/admin/assets/input-group-BSgG_lVw.js +1 -0
  74. package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
  75. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
  76. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
  77. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  78. package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-f8vHRYvz.js} +28 -28
  79. package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DSap9rAy.js} +1 -1
  80. package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-DU0rn9Cj.js} +1 -1
  81. package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-Dz33AV6h.js} +1 -1
  82. package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-BtTTrKvJ.js} +1 -1
  83. package/core/built/admin/assets/{label-BOR9XdKl.js → label-DS8Mi6G5.js} +1 -1
  84. package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
  85. package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
  86. package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
  87. package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
  88. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
  89. package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
  90. package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
  91. package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
  92. package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
  93. package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
  94. package/core/built/admin/assets/{Newsletters-C5cs6FGo.js → newsletters-B3QQXfGT.js} +1 -1
  95. package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
  96. package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
  97. package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
  98. package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
  99. package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
  100. package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
  101. package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
  102. package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-B3Wb9Jqv.js} +1 -1
  103. package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
  104. package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-We-ko5Cb.js} +1 -1
  105. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
  106. package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
  107. package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-DFfO93_0.js} +1 -1
  108. package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
  109. package/core/built/admin/assets/{preferences-D4EANGoc.js → preferences-DJPfHEGu.js} +2 -2
  110. package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
  111. package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
  112. package/core/built/admin/assets/select-C5_xEt5b.js +1 -0
  113. package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
  114. package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-WXNeWyU1.js} +1 -1
  115. package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
  116. package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-DsgW3wbg.js} +1 -1
  117. package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
  118. package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-Bb_FCl7e.js} +1 -1
  119. package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-BeaRxUn9.js} +1 -1
  120. package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-Bjj_kmJ1.js} +1 -1
  121. package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-BxPbW6K1.js} +1 -1
  122. package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-B1e3-1F8.js} +1 -1
  123. package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-C5WaeBJo.js} +1 -1
  124. package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-B46iVPwu.js} +2 -2
  125. package/core/built/admin/assets/switch-z5Y6Uzt6.js +1 -0
  126. package/core/built/admin/assets/{table-B8LgTQJZ.js → table-CB-4_HBJ.js} +1 -1
  127. package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
  128. package/core/built/admin/assets/{tags-B4l891z2.js → tags-Cg2VJGmG.js} +1 -1
  129. package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
  130. package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
  131. package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-b3IlxvhV.js} +1 -1
  132. package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-DAFT87tQ.js} +1 -1
  133. package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-S0FTd43s.js} +1 -1
  134. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
  135. package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-CIaiHOVb.js} +1 -1
  136. package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-Cq2vBPrA.js} +1 -1
  137. package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
  138. package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-CjYNxdwe.js} +1 -1
  139. package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-falHAtuK.js} +1 -1
  140. package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-D7-8ND1y.js} +1 -1
  141. package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
  142. package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-CvnC9zF2.js} +1 -1
  143. package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
  144. package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
  145. package/core/built/admin/index.html +14 -10
  146. package/core/frontend/services/proxy.js +10 -1
  147. package/core/frontend/services/routing/router-manager.js +14 -14
  148. package/core/frontend/services/routing/static-routes-router.js +8 -10
  149. package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
  150. package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
  151. package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
  152. package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
  153. package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
  154. package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
  155. package/core/server/adapters/storage/LocalStorageBase.js +107 -167
  156. package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
  157. package/core/server/adapters/storage/utils.js +36 -50
  158. package/core/server/adapters/storage/utils.ts +60 -0
  159. package/core/server/api/endpoints/automation-action-links.js +68 -0
  160. package/core/server/api/endpoints/automation-action-links.ts +46 -0
  161. package/core/server/api/endpoints/db.js +6 -0
  162. package/core/server/api/endpoints/index.js +4 -0
  163. package/core/server/api/endpoints/members.js +25 -28
  164. package/core/server/api/endpoints/pages.js +6 -1
  165. package/core/server/api/endpoints/posts.js +11 -2
  166. package/core/server/api/endpoints/settings.js +0 -2
  167. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
  168. package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
  169. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
  170. package/core/server/data/exporter/export-filename.js +2 -1
  171. package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
  172. package/core/server/data/importer/importers/json-to-html.js +24 -4
  173. package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
  174. package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
  175. package/core/server/data/schema/commands.js +5 -0
  176. package/core/server/data/schema/default-settings/default-settings.json +0 -4
  177. package/core/server/data/schema/schema.js +5 -1
  178. package/core/server/lib/image/image-size.js +3 -17
  179. package/core/server/lib/image/image-utils.js +15 -1
  180. package/core/server/lib/request-external.js +1 -1
  181. package/core/server/models/user.js +46 -70
  182. package/core/server/services/auth/session/session-service.js +28 -1
  183. package/core/server/services/automations/automations-api.js +21 -6
  184. package/core/server/services/automations/automations-api.ts +25 -6
  185. package/core/server/services/automations/automations-repository.ts +19 -0
  186. package/core/server/services/automations/database-automations-repository.js +116 -28
  187. package/core/server/services/automations/database-automations-repository.ts +118 -9
  188. package/core/server/services/automations/database-date.js +10 -0
  189. package/core/server/services/automations/database-date.ts +5 -0
  190. package/core/server/services/automations/poll.js +5 -2
  191. package/core/server/services/automations/poll.ts +7 -2
  192. package/core/server/services/automations/scheduler-poll-time.js +40 -0
  193. package/core/server/services/automations/scheduler-poll-time.ts +38 -0
  194. package/core/server/services/automations/service.js +15 -3
  195. package/core/server/services/automations/service.ts +16 -3
  196. package/core/server/services/comments/comments-service-emails.js +14 -8
  197. package/core/server/services/comments/comments-service.js +9 -4
  198. package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
  199. package/core/server/services/email-service/email-service-wrapper.js +6 -1
  200. package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
  201. package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
  202. package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
  203. package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
  204. package/core/server/services/link-redirection/link-redirect.js +2 -0
  205. package/core/server/services/link-redirection/link-redirect.ts +3 -1
  206. package/core/server/services/link-redirection/link-redirects-service.js +38 -2
  207. package/core/server/services/link-tracking/click-event.js +3 -0
  208. package/core/server/services/link-tracking/index.js +38 -6
  209. package/core/server/services/link-tracking/link-click-repository.js +30 -9
  210. package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
  211. package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
  212. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
  213. package/core/server/services/member-welcome-emails/service.js +11 -3
  214. package/core/server/services/members/{importer → import-export}/config.js +4 -9
  215. package/core/server/services/members/{importer → import-export}/config.ts +4 -9
  216. package/core/server/services/members/import-export/csv/formula.js +16 -0
  217. package/core/server/services/members/import-export/csv/formula.ts +14 -0
  218. package/core/server/services/members/import-export/csv/index.js +12 -0
  219. package/core/server/services/members/import-export/csv/index.ts +3 -0
  220. package/core/server/services/members/import-export/csv/parse.js +64 -0
  221. package/core/server/services/members/import-export/csv/parse.ts +69 -0
  222. package/core/server/services/members/import-export/csv/serialize.js +13 -0
  223. package/core/server/services/members/import-export/csv/serialize.ts +18 -0
  224. package/core/server/services/members/import-export/export/exporter.js +202 -0
  225. package/core/server/services/members/import-export/export/exporter.ts +325 -0
  226. package/core/server/services/members/import-export/import/completion-email.js +84 -0
  227. package/core/server/services/members/import-export/import/completion-email.ts +141 -0
  228. package/core/server/services/members/import-export/import/importer.js +297 -0
  229. package/core/server/services/members/import-export/import/importer.ts +468 -0
  230. package/core/server/services/members/import-export/import/reader.js +32 -0
  231. package/core/server/services/members/import-export/import/reader.ts +33 -0
  232. package/core/server/services/members/import-export/import/row.js +33 -0
  233. package/core/server/services/members/import-export/import/row.ts +52 -0
  234. package/core/server/services/members/import-export/import/spool.js +30 -0
  235. package/core/server/services/members/import-export/import/spool.ts +38 -0
  236. package/core/server/services/members/import-export/index.js +99 -0
  237. package/core/server/services/members/import-export/index.ts +137 -0
  238. package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
  239. package/core/server/services/members/service.js +25 -20
  240. package/core/server/services/members-custom-fields/values-service.js +19 -7
  241. package/core/server/services/members-custom-fields/values-service.ts +19 -7
  242. package/core/server/services/mentions/service.js +5 -1
  243. package/core/server/services/posts/posts-exporter.js +6 -4
  244. package/core/server/services/posts/posts-service.js +2 -2
  245. package/core/server/services/route-settings/activation-bridge.js +59 -49
  246. package/core/server/services/route-settings/activation-bridge.ts +91 -51
  247. package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
  248. package/core/server/services/route-settings/index.js +1 -16
  249. package/core/server/services/settings/settings-service.js +0 -18
  250. package/core/server/services/themes/storage.js +9 -0
  251. package/core/server/services/themes/theme-storage.js +1 -1
  252. package/core/server/services/url/url-service-facade.js +1 -1
  253. package/core/server/services/url/url-service-facade.ts +8 -1
  254. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  255. package/core/shared/config/defaults.json +2 -1
  256. package/core/shared/express.js +6 -1
  257. package/core/shared/labs.js +2 -1
  258. package/package.json +11 -11
  259. package/pnpm-lock.yaml +18 -18
  260. package/pnpm-workspace.yaml +8 -7
  261. package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
  262. package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
  263. package/core/built/admin/assets/Web-_fD935OX.js +0 -2
  264. package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
  265. package/core/built/admin/assets/audience-BKRdsFmy.js +0 -1
  266. package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
  267. package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
  268. package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
  269. package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
  270. package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
  271. package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
  272. package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
  273. package/core/built/admin/assets/domain-B7_n5nfx.js +0 -1
  274. package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
  275. package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
  276. package/core/built/admin/assets/field-BW-M2Zwp.js +0 -1
  277. package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
  278. package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +0 -1
  279. package/core/built/admin/assets/growth-Ck2s3JjR.js +0 -1
  280. package/core/built/admin/assets/index-BBf502Wg.js +0 -69
  281. package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
  282. package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
  283. package/core/built/admin/assets/label-picker-BdNAWuUX.js +0 -1
  284. package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
  285. package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
  286. package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +0 -1
  287. package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
  288. package/core/built/admin/assets/members-DsPxYAVm.js +0 -10
  289. package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
  290. package/core/built/admin/assets/moderation-Dlp7M5sa.js +0 -1
  291. package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +0 -1
  292. package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
  293. package/core/built/admin/assets/note-BrAZ7Z0o.js +0 -1
  294. package/core/built/admin/assets/notifications-BnbHLOPC.js +0 -1
  295. package/core/built/admin/assets/onboarding-route-eU146kuA.js +0 -1
  296. package/core/built/admin/assets/overview-BpbaK-xu.js +0 -1
  297. package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
  298. package/core/built/admin/assets/post-analytics-provider-BDqcfBor.js +0 -1
  299. package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
  300. package/core/built/admin/assets/select--2uVN9_-.js +0 -1
  301. package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
  302. package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
  303. package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
  304. package/core/built/admin/assets/tabs-D3ZOYkM7.js +0 -1
  305. package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
  306. package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
  307. package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
  308. package/core/server/services/mail/templates/newsletter.html +0 -876
  309. package/core/server/services/members/csv/index.js +0 -10
  310. package/core/server/services/members/csv/index.ts +0 -3
  311. package/core/server/services/members/csv/parse.js +0 -94
  312. package/core/server/services/members/csv/parse.ts +0 -111
  313. package/core/server/services/members/csv/types.js +0 -2
  314. package/core/server/services/members/csv/types.ts +0 -39
  315. package/core/server/services/members/csv/unparse.js +0 -69
  316. package/core/server/services/members/csv/unparse.ts +0 -79
  317. package/core/server/services/members/exporter/query.js +0 -233
  318. package/core/server/services/members/importer/index.js +0 -30
  319. package/core/server/services/members/importer/labels.js +0 -125
  320. package/core/server/services/members/importer/members-csv-importer.js +0 -520
  321. package/core/server/services/route-settings/default-settings-manager.js +0 -63
  322. package/core/server/services/route-settings/route-settings.js +0 -170
  323. package/core/server/services/route-settings/settings-loader.js +0 -60
  324. package/core/server/services/route-settings/settings-path-manager.js +0 -47
  325. package/core/server/services/route-settings/validate.js +0 -440
  326. /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
  327. /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
@@ -31,8 +31,8 @@ module.exports = class LinkClickRepository {
31
31
  this.#DomainEvents = deps.DomainEvents;
32
32
 
33
33
  // Memoize the findOne function
34
- this.memoizedFindOne = _.memoize(async (uuid) => {
35
- return await this.#Member.findOne({uuid});
34
+ this.memoizedFindOne = _.memoize(async (uuid, options) => {
35
+ return await this.#Member.findOne({uuid}, options);
36
36
  });
37
37
  }
38
38
 
@@ -54,15 +54,18 @@ module.exports = class LinkClickRepository {
54
54
 
55
55
  /**
56
56
  * @param {LinkClick} linkClick
57
- * @returns {Promise<void>}
57
+ * @param {object} [options]
58
+ * @param {object} [options.transacting]
59
+ * @returns {Promise<string | undefined>}
58
60
  */
59
- async save(linkClick) {
61
+ async save(linkClick, options = {}) {
60
62
  let member;
63
+ const memberLookupOptions = options.transacting ? {transacting: options.transacting} : undefined;
61
64
 
62
65
  if (config && config.get('linkClickTrackingCacheMemberUuid')) {
63
- member = await this.memoizedFindOne(linkClick.member_uuid);
66
+ member = await this.memoizedFindOne(linkClick.member_uuid, memberLookupOptions);
64
67
  } else {
65
- member = await this.#Member.findOne({uuid: linkClick.member_uuid});
68
+ member = await this.#Member.findOne({uuid: linkClick.member_uuid}, memberLookupOptions);
66
69
  }
67
70
 
68
71
  if (!member) {
@@ -76,11 +79,29 @@ module.exports = class LinkClickRepository {
76
79
  // Only store the pathname (no support for variable query strings)
77
80
  redirect_id: linkClick.link_id.toHexString(),
78
81
  member_id: member.id
79
- }, {});
82
+ }, options);
80
83
 
81
84
  linkClick.event_id = ObjectID.createFromHexString(model.id);
85
+ const timestamp = linkClick.timestamp ?? new Date();
86
+
87
+ const event = this.#MemberLinkClickEvent.create({
88
+ memberId: member.id,
89
+ memberLastSeenAt: member.get('last_seen_at'),
90
+ linkId: linkClick.link_id.toHexString()
91
+ }, timestamp);
92
+
93
+ if (options.transacting) {
94
+ options.transacting.executionPromise.then(() => {
95
+ try {
96
+ this.#DomainEvents.dispatch(event);
97
+ } catch (error) {
98
+ sentry.captureException(error);
99
+ }
100
+ }, () => undefined);
101
+ } else {
102
+ this.#DomainEvents.dispatch(event);
103
+ }
82
104
 
83
- // Dispatch event
84
- this.#DomainEvents.dispatch(this.#MemberLinkClickEvent.create({memberId: member.id, memberLastSeenAt: member.get('last_seen_at'), linkId: linkClick.link_id.toHexString()}, new Date()));
105
+ return member.id;
85
106
  }
86
107
  };
@@ -10,7 +10,7 @@ const moment = require('moment');
10
10
 
11
11
  /**
12
12
  * @typedef {object} ILinkClickRepository
13
- * @prop {(event: LinkClick) => Promise<void>} save
13
+ * @prop {(event: LinkClick, options?: {transacting?: object}) => Promise<string | undefined>} save
14
14
  * @prop {({filter: string}) => Promise<LinkClick[]>} getAll
15
15
  */
16
16
 
@@ -61,6 +61,10 @@ class LinkClickTrackingService {
61
61
  #LinkRedirect;
62
62
  /** @type {Object} */
63
63
  #urlUtils;
64
+ /** @type {object} */
65
+ #automationsApi;
66
+ /** @type {(callback: (transacting: object) => Promise<void>) => Promise<void>} */
67
+ #runInTransaction;
64
68
 
65
69
  /**
66
70
  * @param {object} deps
@@ -69,6 +73,8 @@ class LinkClickTrackingService {
69
73
  * @param {IPostLinkRepository} deps.postLinkRepository
70
74
  * @param {DomainEvents} deps.DomainEvents
71
75
  * @param {urlUtils} deps.urlUtils
76
+ * @param {object} deps.automationsApi
77
+ * @param {(callback: (transacting: object) => Promise<void>) => Promise<void>} deps.runInTransaction
72
78
  */
73
79
  constructor(deps) {
74
80
  this.#linkClickRepository = deps.linkClickRepository;
@@ -76,6 +82,8 @@ class LinkClickTrackingService {
76
82
  this.#postLinkRepository = deps.postLinkRepository;
77
83
  this.#DomainEvents = deps.DomainEvents;
78
84
  this.#urlUtils = deps.urlUtils;
85
+ this.#automationsApi = deps.automationsApi;
86
+ this.#runInTransaction = deps.runInTransaction;
79
87
  }
80
88
 
81
89
  async init() {
@@ -233,6 +241,20 @@ class LinkClickTrackingService {
233
241
  return url;
234
242
  }
235
243
 
244
+ /**
245
+ * Add first-party tracking to a URL in an automation email
246
+ * @param {Readonly<URL>} url
247
+ * @param {string} automationActionRevisionId
248
+ * @param {string} memberUuid
249
+ * @return {Promise<URL>}
250
+ */
251
+ async addAutomationTrackingToUrl(url, automationActionRevisionId, memberUuid) {
252
+ const redirect = await this.#linkRedirectService.getOrAddAutomationRedirect(automationActionRevisionId, url);
253
+ const trackedUrl = new URL(redirect.from.href);
254
+ trackedUrl.searchParams.set('m', memberUuid);
255
+ return trackedUrl;
256
+ }
257
+
236
258
  subscribe() {
237
259
  this.#DomainEvents.subscribe(RedirectEvent, async (event) => {
238
260
  const uuid = event.data.url.searchParams.get('m');
@@ -242,9 +264,28 @@ class LinkClickTrackingService {
242
264
 
243
265
  const click = new LinkClick({
244
266
  member_uuid: uuid,
245
- link_id: event.data.link.link_id
267
+ link_id: event.data.link.link_id,
268
+ timestamp: event.timestamp
269
+ });
270
+
271
+ const automationActionRevisionId = event.data.link.automationActionRevisionId;
272
+ if (!automationActionRevisionId) {
273
+ await this.#linkClickRepository.save(click);
274
+ return;
275
+ }
276
+
277
+ await this.#runInTransaction(async (transacting) => {
278
+ const memberId = await this.#linkClickRepository.save(click, {transacting});
279
+ if (!memberId) {
280
+ return;
281
+ }
282
+
283
+ await this.#automationsApi.trackEmailClicked({
284
+ automationActionRevisionId,
285
+ memberId,
286
+ clickedAt: event.timestamp
287
+ }, {transacting});
246
288
  });
247
- await this.#linkClickRepository.save(click);
248
289
  });
249
290
  }
250
291
  }
@@ -11,7 +11,9 @@ const blockedReferrerDomains = [
11
11
  'www.chicagomag.com',
12
12
  'bailii.org',
13
13
  'www.bailii.org',
14
- '*.doubleclick.net'
14
+ '*.doubleclick.net',
15
+ // Substack's client app throws on any non-empty ?ref= value and swaps the post for an error screen
16
+ '*.substack.com'
15
17
  ];
16
18
 
17
19
  const isBlockedReferrerDomain = (domain) => {
@@ -2,11 +2,13 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const lexicalLib = require('../../lib/lexical');
4
4
  const labs = require('../../../shared/labs');
5
+ const urlUtils = require('../../../shared/url-utils').default;
5
6
  const {finalize} = require('../email-rendering/finalize');
6
7
  const errors = require('@tryghost/errors');
7
8
  const {MESSAGES} = require('./constants');
8
9
  const {wrapReplacementStrings} = require('@tryghost/kg-default-nodes').utils.replacementStrings;
9
10
  const linkReplacer = require('../lib/link-replacer');
11
+ const linkTracking = require('../link-tracking');
10
12
  const emailDesign = require('../email-rendering/email-design');
11
13
  const {registerHelpers} = require('../email-service/helpers/register-helpers');
12
14
 
@@ -115,9 +117,11 @@ class MemberWelcomeEmailRenderer {
115
117
  * @param {Object} options.member - Member data (name, email)
116
118
  * @param {Object} options.siteSettings - Site settings (title, url, accentColor)
117
119
  * @param {string} [options.unsubscribeUrl] - When set, the footer shows an "Unsubscribe from these emails" link instead of "Manage your preferences"
120
+ * @param {boolean} [options.trackClicks]
121
+ * @param {string | null} [options.automationActionRevisionId]
118
122
  * @returns {Promise<{html: string, text: string, subject: string}>}
119
123
  */
120
- async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl}) {
124
+ async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl, trackClicks = false, automationActionRevisionId = null}) {
121
125
  designSettings = designSettings || {};
122
126
 
123
127
  const design = emailDesign.getEmailDesign({
@@ -138,7 +142,8 @@ class MemberWelcomeEmailRenderer {
138
142
 
139
143
  let content;
140
144
  try {
141
- content = await lexicalLib.render(lexical, {target: 'email', design});
145
+ const absoluteLexical = urlUtils.transformReadyToAbsolute(lexical);
146
+ content = await lexicalLib.render(absoluteLexical, {target: 'email', design});
142
147
  } catch (err) {
143
148
  throw new errors.IncorrectUsageError({
144
149
  message: MESSAGES.INVALID_LEXICAL_STRUCTURE,
@@ -158,8 +163,18 @@ class MemberWelcomeEmailRenderer {
158
163
  const contentWithReplacements = this.#applyReplacements({definitions, text: content, escapeHtml: true});
159
164
  const subjectWithReplacements = this.#applyReplacements({definitions, text: subject, escapeHtml: false});
160
165
 
161
- // Resolve relative links (e.g. #/portal/signup) to absolute URLs using the site URL
162
- const contentWithAbsoluteLinks = await linkReplacer.replace(contentWithReplacements, (url) => {
166
+ // Resolve relative links, including Ghost Portal routes such as #/portal/signup,
167
+ // while preserving document-local anchors such as #section.
168
+ const contentWithAbsoluteLinks = await linkReplacer.replace(contentWithReplacements, async (url, originalPath) => {
169
+ const isDocumentAnchor = originalPath.startsWith('#') && !originalPath.startsWith('#/');
170
+ if (isDocumentAnchor) {
171
+ return originalPath;
172
+ }
173
+ const isTrackable = ['http:', 'https:'].includes(url.protocol);
174
+ if (trackClicks && automationActionRevisionId && member.uuid && isTrackable) {
175
+ await linkTracking.init();
176
+ return await linkTracking.service.addAutomationTrackingToUrl(url, automationActionRevisionId, member.uuid);
177
+ }
163
178
  return url;
164
179
  }, {base: siteSettings.url});
165
180
 
@@ -368,10 +368,12 @@ class MemberWelcomeEmailService {
368
368
  * @param {null | object} options.email.designSettings
369
369
  * @param {'welcome' | 'automation'} options.emailType
370
370
  * @param {boolean} [options.trackOpens]
371
+ * @param {boolean} [options.trackClicks]
372
+ * @param {string | null} [options.automationActionRevisionId]
371
373
  * @param {null | {url: string, oneClickUrl: string}} [options.unsubscribe] - When set, the footer links to an unsubscribe URL and the email carries one-click List-Unsubscribe headers
372
374
  * @returns {Promise<unknown>}
373
375
  */
374
- async #sendEmail({member, memberStatus, email, emailType, trackOpens, unsubscribe = null}) {
376
+ async #sendEmail({member, memberStatus, email, emailType, trackOpens, trackClicks = false, automationActionRevisionId = null, unsubscribe = null}) {
375
377
  if (!member.email) {
376
378
  throw new errors.IncorrectUsageError({
377
379
  message: MESSAGES.MISSING_RECIPIENT_EMAIL
@@ -396,7 +398,9 @@ class MemberWelcomeEmailService {
396
398
  uuid: member.uuid
397
399
  },
398
400
  siteSettings: this.#getSiteSettings(),
399
- unsubscribeUrl: unsubscribe?.url
401
+ unsubscribeUrl: unsubscribe?.url,
402
+ trackClicks,
403
+ automationActionRevisionId
400
404
  });
401
405
 
402
406
  const senderOptions = await this.#getEffectiveSenderOptions(
@@ -474,9 +478,11 @@ class MemberWelcomeEmailService {
474
478
  * @param {string} options.member.uuid
475
479
  * @param {'free' | 'paid'} options.memberStatus
476
480
  * @param {boolean} options.trackOpens
481
+ * @param {boolean} options.trackClicks
482
+ * @param {string} options.automationActionRevisionId
477
483
  * @returns {Promise<unknown>}
478
484
  */
479
- async sendAutomationEmail({email, member, memberStatus, trackOpens}) {
485
+ async sendAutomationEmail({email, member, memberStatus, trackOpens, trackClicks, automationActionRevisionId}) {
480
486
  const designSettings = email.designSettingId ?
481
487
  await EmailDesignSetting.findOne({id: email.designSettingId}) :
482
488
  null;
@@ -502,6 +508,8 @@ class MemberWelcomeEmailService {
502
508
  designSettings: designSettingsJson
503
509
  },
504
510
  trackOpens,
511
+ trackClicks,
512
+ automationActionRevisionId,
505
513
  unsubscribe
506
514
  });
507
515
  }
@@ -2,15 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveInlineThreshold = resolveInlineThreshold;
4
4
  const zod_1 = require("zod");
5
- /**
6
- * Resolving Ghost's config into the values the importer runs on.
7
- *
8
- * Config arrives untyped: defaults.json supplies the shipped value, and any
9
- * higher layer — an operator's config file, an environment variable, argv — can
10
- * replace it with anything at all. Turning that into something usable is a
11
- * config concern, so it happens here, at the boundary. The importer is handed an
12
- * answer and never learns where it came from.
13
- */
5
+ // Config arrives untyped: defaults.json ships a value, and any higher layer -- an
6
+ // operator's config file, an environment variable, argv -- can replace it with anything.
7
+ // Validating it into a usable number is a config concern, so it happens here at the
8
+ // boundary; the importer is handed a resolved value.
14
9
  // The row count at or below which an import is performed while the request is
15
10
  // still open. Zero is allowed and means every import is deferred. A numeric
16
11
  // string is accepted because a value supplied through an environment variable
@@ -1,14 +1,9 @@
1
1
  import {z} from 'zod';
2
2
 
3
- /**
4
- * Resolving Ghost's config into the values the importer runs on.
5
- *
6
- * Config arrives untyped: defaults.json supplies the shipped value, and any
7
- * higher layer — an operator's config file, an environment variable, argv — can
8
- * replace it with anything at all. Turning that into something usable is a
9
- * config concern, so it happens here, at the boundary. The importer is handed an
10
- * answer and never learns where it came from.
11
- */
3
+ // Config arrives untyped: defaults.json ships a value, and any higher layer -- an
4
+ // operator's config file, an environment variable, argv -- can replace it with anything.
5
+ // Validating it into a usable number is a config concern, so it happens here at the
6
+ // boundary; the importer is handed a resolved value.
12
7
 
13
8
  // The row count at or below which an import is performed while the request is
14
9
  // still open. Zero is allowed and means every import is deferred. A numeric
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripFormulaGuard = stripFormulaGuard;
4
+ // The export writes with papaparse's escapeFormulae on (see serialize), which prefixes a
5
+ // cell starting with one of these with an apostrophe so a spreadsheet won't read it as a
6
+ // formula: `=SUM(A1)` is written as `'=SUM(A1)`.
7
+ const FORMULA_TRIGGERS = ['=', '+', '-', '@', '\t', '\r'];
8
+ // Strip that guard back off, so a re-imported value doesn't gain an apostrophe on every
9
+ // round trip. Only a lone apostrophe before a trigger is the guard; one before anything
10
+ // else is a character the value actually starts with, and is left alone.
11
+ function stripFormulaGuard(cell) {
12
+ if (cell.charAt(0) === '\'' && FORMULA_TRIGGERS.includes(cell.charAt(1))) {
13
+ return cell.slice(1);
14
+ }
15
+ return cell;
16
+ }
@@ -0,0 +1,14 @@
1
+ // The export writes with papaparse's escapeFormulae on (see serialize), which prefixes a
2
+ // cell starting with one of these with an apostrophe so a spreadsheet won't read it as a
3
+ // formula: `=SUM(A1)` is written as `'=SUM(A1)`.
4
+ const FORMULA_TRIGGERS = ['=', '+', '-', '@', '\t', '\r'];
5
+
6
+ // Strip that guard back off, so a re-imported value doesn't gain an apostrophe on every
7
+ // round trip. Only a lone apostrophe before a trigger is the guard; one before anything
8
+ // else is a character the value actually starts with, and is left alone.
9
+ export function stripFormulaGuard(cell: string): string {
10
+ if (cell.charAt(0) === '\'' && FORMULA_TRIGGERS.includes(cell.charAt(1))) {
11
+ return cell.slice(1);
12
+ }
13
+ return cell;
14
+ }
@@ -0,0 +1,12 @@
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.stripFormulaGuard = exports.serialize = exports.parse = void 0;
7
+ var parse_1 = require("./parse");
8
+ Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
9
+ var serialize_1 = require("./serialize");
10
+ Object.defineProperty(exports, "serialize", { enumerable: true, get: function () { return __importDefault(serialize_1).default; } });
11
+ var formula_1 = require("./formula");
12
+ Object.defineProperty(exports, "stripFormulaGuard", { enumerable: true, get: function () { return formula_1.stripFormulaGuard; } });
@@ -0,0 +1,3 @@
1
+ export {default as parse, type Row} from './parse';
2
+ export {default as serialize} from './serialize';
3
+ export {stripFormulaGuard} from './formula';
@@ -0,0 +1,64 @@
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 = parse;
7
+ const node_stream_1 = require("node:stream");
8
+ const papaparse_1 = __importDefault(require("papaparse"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ // A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
11
+ // as a key: writing it would shadow a method or reach the prototype.
12
+ function isSafeColumnName(name) {
13
+ return !(name in Object.prototype);
14
+ }
15
+ // headerMapping renames the CSV's headers to the columns they emit under; unmapped
16
+ // columns carry through under their own name.
17
+ function parse(path, headerMapping) {
18
+ return new Promise(function (resolve, reject) {
19
+ const csvFileStream = fs_extra_1.default.createReadStream(path);
20
+ const csvParserStream = papaparse_1.default.parse(papaparse_1.default.NODE_STREAM_INPUT, {
21
+ header: true
22
+ });
23
+ const rows = [];
24
+ const parsedCSVStream = (0, node_stream_1.pipeline)(csvFileStream, csvParserStream, (err) => {
25
+ if (err) {
26
+ return reject(err);
27
+ }
28
+ resolve(rows);
29
+ });
30
+ parsedCSVStream.on('data', (parsedRow) => {
31
+ // a throw here escapes as an uncaught exception and leaves this
32
+ // promise forever unsettled, so it has to become a rejection
33
+ try {
34
+ const row = {};
35
+ for (const [header, value] of Object.entries(parsedRow)) {
36
+ // papaparse gathers the overflow from a row carrying more
37
+ // fields than there are headers under __parsed_extra, as an
38
+ // array rather than a cell any mapping can name
39
+ if (typeof value !== 'string') {
40
+ continue;
41
+ }
42
+ // hasOwn, not `in`: a prototype-named header would otherwise match an
43
+ // inherited method on the mapping and take a function as its mapped name.
44
+ if (headerMapping && Object.hasOwn(headerMapping, header)) {
45
+ row[headerMapping[header]] = value;
46
+ }
47
+ else if (isSafeColumnName(header)) {
48
+ // Carry any unmapped column through untouched, so the import is not
49
+ // constrained to a known vocabulary: a custom_fields.* column
50
+ // survives parsing even though nothing consumes it yet.
51
+ row[header] = value;
52
+ }
53
+ }
54
+ if (!Object.keys(row).length) {
55
+ return;
56
+ }
57
+ rows.push(row);
58
+ }
59
+ catch (err) {
60
+ reject(err);
61
+ }
62
+ });
63
+ });
64
+ }
@@ -0,0 +1,69 @@
1
+ import {pipeline} from 'node:stream';
2
+ import papaparse from 'papaparse';
3
+ import fs from 'fs-extra';
4
+
5
+ // A parsed CSV row: raw string cells, keyed by (renamed) column. Parsing is mechanical;
6
+ // giving the columns meaning and coercing their values is the domain's job (the import
7
+ // schema).
8
+ export type Row = Record<string, string>;
9
+
10
+ // A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
11
+ // as a key: writing it would shadow a method or reach the prototype.
12
+ function isSafeColumnName(name: string): boolean {
13
+ return !(name in Object.prototype);
14
+ }
15
+
16
+ // headerMapping renames the CSV's headers to the columns they emit under; unmapped
17
+ // columns carry through under their own name.
18
+ export default function parse(path: string, headerMapping?: Record<string, string>): Promise<Row[]> {
19
+ return new Promise(function (resolve, reject) {
20
+ const csvFileStream = fs.createReadStream(path);
21
+ const csvParserStream = papaparse.parse(papaparse.NODE_STREAM_INPUT, {
22
+ header: true
23
+ });
24
+
25
+ const rows: Row[] = [];
26
+ const parsedCSVStream = pipeline(csvFileStream, csvParserStream, (err) => {
27
+ if (err) {
28
+ return reject(err);
29
+ }
30
+ resolve(rows);
31
+ });
32
+
33
+ parsedCSVStream.on('data', (parsedRow: Record<string, unknown>) => {
34
+ // a throw here escapes as an uncaught exception and leaves this
35
+ // promise forever unsettled, so it has to become a rejection
36
+ try {
37
+ const row: Row = {};
38
+
39
+ for (const [header, value] of Object.entries(parsedRow)) {
40
+ // papaparse gathers the overflow from a row carrying more
41
+ // fields than there are headers under __parsed_extra, as an
42
+ // array rather than a cell any mapping can name
43
+ if (typeof value !== 'string') {
44
+ continue;
45
+ }
46
+
47
+ // hasOwn, not `in`: a prototype-named header would otherwise match an
48
+ // inherited method on the mapping and take a function as its mapped name.
49
+ if (headerMapping && Object.hasOwn(headerMapping, header)) {
50
+ row[headerMapping[header]] = value;
51
+ } else if (isSafeColumnName(header)) {
52
+ // Carry any unmapped column through untouched, so the import is not
53
+ // constrained to a known vocabulary: a custom_fields.* column
54
+ // survives parsing even though nothing consumes it yet.
55
+ row[header] = value;
56
+ }
57
+ }
58
+
59
+ if (!Object.keys(row).length) {
60
+ return;
61
+ }
62
+
63
+ rows.push(row);
64
+ } catch (err) {
65
+ reject(err);
66
+ }
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,13 @@
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 = serialize;
7
+ const papaparse_1 = __importDefault(require("papaparse"));
8
+ // The papaparse boundary: given rows already shaped into cells, it writes CSV and
9
+ // nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
10
+ // is opened by a person, so a leading =, +, -, @ or tab must be escaped.
11
+ function serialize(rows, { columns, header = true } = {}) {
12
+ return papaparse_1.default.unparse(rows, { columns, header, escapeFormulae: true });
13
+ }
@@ -0,0 +1,18 @@
1
+ import papaparse from 'papaparse';
2
+
3
+ interface SerializeOptions {
4
+ // Omitted, papaparse derives the columns (and their order) from the first row's
5
+ // keys -- so a typed shaper's row can define them, with no separate list to drift.
6
+ columns?: string[];
7
+ header?: boolean;
8
+ }
9
+
10
+ // The papaparse boundary: given rows already shaped into cells, it writes CSV and
11
+ // nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
12
+ // is opened by a person, so a leading =, +, -, @ or tab must be escaped.
13
+ export default function serialize(
14
+ rows: Array<Record<string, unknown>>,
15
+ {columns, header = true}: SerializeOptions = {}
16
+ ): string {
17
+ return papaparse.unparse(rows, {columns, header, escapeFormulae: true});
18
+ }