emdash 0.17.2 → 0.19.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 (415) hide show
  1. package/dist/api/route-utils.d.mts +2 -2
  2. package/dist/api/route-utils.mjs +14 -14
  3. package/dist/api/schemas/index.d.mts +2 -2
  4. package/dist/api/schemas/index.mjs +3 -3
  5. package/dist/{api-B7GATEYo.mjs → api-BZ6bhjYs.mjs} +88 -16
  6. package/dist/api-BZ6bhjYs.mjs.map +1 -0
  7. package/dist/{apply-BrVqULFe.mjs → apply-hQkKKBCf.mjs} +23 -23
  8. package/dist/apply-hQkKKBCf.mjs.map +1 -0
  9. package/dist/astro/index.d.mts +8 -8
  10. package/dist/astro/index.d.mts.map +1 -1
  11. package/dist/astro/index.mjs +113 -23
  12. package/dist/astro/index.mjs.map +1 -1
  13. package/dist/astro/middleware/auth.d.mts +7 -7
  14. package/dist/astro/middleware/auth.mjs +2 -2
  15. package/dist/astro/middleware/redirect.mjs +4 -4
  16. package/dist/astro/middleware/request-context.mjs +2 -2
  17. package/dist/astro/middleware.d.mts +26 -4
  18. package/dist/astro/middleware.d.mts.map +1 -1
  19. package/dist/astro/middleware.mjs +414 -215
  20. package/dist/astro/middleware.mjs.map +1 -1
  21. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
  22. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
  23. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  24. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  25. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +5 -5
  26. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
  27. package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
  28. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
  29. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +12 -12
  30. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +12 -12
  31. package/dist/astro/routes/api/admin/bylines/index.mjs +12 -12
  32. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +11 -11
  33. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  34. package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
  35. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  36. package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
  37. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
  38. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
  39. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  40. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  41. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +31 -31
  42. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +31 -31
  43. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +30 -30
  44. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +30 -30
  45. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +30 -30
  46. package/dist/astro/routes/api/admin/plugins/index.mjs +30 -30
  47. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  48. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +30 -30
  49. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +30 -30
  50. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +30 -30
  51. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +30 -30
  52. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +31 -31
  53. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +30 -30
  54. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +31 -31
  55. package/dist/astro/routes/api/admin/plugins/updates.mjs +30 -30
  56. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +30 -30
  57. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  58. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +30 -30
  59. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
  60. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  61. package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
  62. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
  63. package/dist/astro/routes/api/admin/users/index.mjs +5 -5
  64. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  65. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  66. package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
  67. package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
  68. package/dist/astro/routes/api/auth/invite/register-options.mjs +6 -6
  69. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  70. package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
  71. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  72. package/dist/astro/routes/api/auth/me.mjs +6 -6
  73. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +2 -2
  74. package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
  75. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  76. package/dist/astro/routes/api/auth/passkey/options.mjs +7 -7
  77. package/dist/astro/routes/api/auth/passkey/register/options.mjs +6 -6
  78. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
  79. package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
  80. package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
  81. package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
  82. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  83. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
  84. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +6 -5
  86. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  87. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  88. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  89. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +8 -8
  90. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +9 -8
  91. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  92. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  93. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  94. package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
  95. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +12 -10
  96. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  97. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +11 -11
  98. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  99. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +6 -5
  100. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  101. package/dist/astro/routes/api/content/_collection_/_id_.mjs +9 -8
  102. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  103. package/dist/astro/routes/api/content/_collection_/authors.d.mts +8 -0
  104. package/dist/astro/routes/api/content/_collection_/authors.d.mts.map +1 -0
  105. package/dist/astro/routes/api/content/_collection_/authors.mjs +19 -0
  106. package/dist/astro/routes/api/content/_collection_/authors.mjs.map +1 -0
  107. package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
  108. package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
  109. package/dist/astro/routes/api/dashboard.mjs +7 -7
  110. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  111. package/dist/astro/routes/api/import/probe.d.mts +2 -2
  112. package/dist/astro/routes/api/import/probe.mjs +6 -6
  113. package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
  114. package/dist/astro/routes/api/import/wordpress/execute.d.mts +7 -7
  115. package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -9
  116. package/dist/astro/routes/api/import/wordpress/media.mjs +6 -6
  117. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  118. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
  119. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +6 -6
  120. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +9 -9
  121. package/dist/astro/routes/api/manifest.mjs +3 -3
  122. package/dist/astro/routes/api/mcp.mjs +28 -28
  123. package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
  124. package/dist/astro/routes/api/media/_id_.mjs +6 -6
  125. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  126. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  127. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  128. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  129. package/dist/astro/routes/api/media/upload-url.mjs +6 -6
  130. package/dist/astro/routes/api/media.mjs +7 -7
  131. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
  132. package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
  133. package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
  134. package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
  135. package/dist/astro/routes/api/menus/_name_.mjs +7 -7
  136. package/dist/astro/routes/api/menus/index.mjs +7 -7
  137. package/dist/astro/routes/api/oauth/authorize.mjs +1 -1
  138. package/dist/astro/routes/api/oauth/device/authorize.mjs +4 -4
  139. package/dist/astro/routes/api/oauth/device/code.mjs +5 -5
  140. package/dist/astro/routes/api/oauth/device/token.mjs +5 -5
  141. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  142. package/dist/astro/routes/api/oauth/token/refresh.mjs +4 -4
  143. package/dist/astro/routes/api/oauth/token/revoke.mjs +4 -4
  144. package/dist/astro/routes/api/oauth/token.mjs +4 -4
  145. package/dist/astro/routes/api/openapi.json.mjs +17 -3
  146. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  147. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  148. package/dist/astro/routes/api/redirects/404s/index.mjs +9 -9
  149. package/dist/astro/routes/api/redirects/404s/summary.mjs +9 -9
  150. package/dist/astro/routes/api/redirects/_id_.mjs +10 -10
  151. package/dist/astro/routes/api/redirects/index.mjs +10 -10
  152. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  153. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  154. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +30 -30
  155. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +30 -30
  156. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +30 -30
  157. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +30 -30
  158. package/dist/astro/routes/api/schema/collections/index.mjs +30 -30
  159. package/dist/astro/routes/api/schema/index.mjs +6 -6
  160. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +30 -30
  161. package/dist/astro/routes/api/schema/orphans/index.mjs +30 -30
  162. package/dist/astro/routes/api/search/enable.mjs +9 -9
  163. package/dist/astro/routes/api/search/index.mjs +8 -8
  164. package/dist/astro/routes/api/search/rebuild.mjs +9 -9
  165. package/dist/astro/routes/api/search/stats.mjs +6 -6
  166. package/dist/astro/routes/api/search/suggest.mjs +8 -8
  167. package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
  168. package/dist/astro/routes/api/sections/index.mjs +8 -8
  169. package/dist/astro/routes/api/settings/email.mjs +5 -5
  170. package/dist/astro/routes/api/settings.mjs +12 -12
  171. package/dist/astro/routes/api/setup/admin-verify.mjs +6 -6
  172. package/dist/astro/routes/api/setup/admin.mjs +6 -6
  173. package/dist/astro/routes/api/setup/dev-bypass.mjs +18 -18
  174. package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
  175. package/dist/astro/routes/api/setup/index.mjs +21 -21
  176. package/dist/astro/routes/api/setup/status.mjs +3 -3
  177. package/dist/astro/routes/api/snapshot.mjs +5 -5
  178. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -11
  179. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -11
  180. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -11
  181. package/dist/astro/routes/api/taxonomies/index.mjs +11 -11
  182. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  183. package/dist/astro/routes/api/typegen.mjs +5 -5
  184. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  185. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
  186. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
  187. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
  188. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  189. package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
  190. package/dist/astro/routes/api/widget-components.mjs +2 -2
  191. package/dist/astro/routes/robots.txt.mjs +6 -6
  192. package/dist/astro/routes/sitemap-_collection_.xml.mjs +6 -6
  193. package/dist/astro/routes/sitemap.xml.mjs +6 -6
  194. package/dist/astro/types.d.mts +15 -8
  195. package/dist/astro/types.d.mts.map +1 -1
  196. package/dist/{authorize-CLTmOUyx.mjs → authorize-C_8t2KGa.mjs} +2 -2
  197. package/dist/{authorize-CLTmOUyx.mjs.map → authorize-C_8t2KGa.mjs.map} +1 -1
  198. package/dist/{byline-CAhk4FrG.mjs → byline-DUx48sJp.mjs} +6 -6
  199. package/dist/{byline-CAhk4FrG.mjs.map → byline-DUx48sJp.mjs.map} +1 -1
  200. package/dist/{byline-fields-Dr-xcb6S.mjs → byline-fields-51kg6Vuv.mjs} +3 -3
  201. package/dist/{byline-fields-Dr-xcb6S.mjs.map → byline-fields-51kg6Vuv.mjs.map} +1 -1
  202. package/dist/{byline-fields-DC3Wkk-U.mjs → byline-fields-C_OsR-KF.mjs} +2 -2
  203. package/dist/{byline-fields-DC3Wkk-U.mjs.map → byline-fields-C_OsR-KF.mjs.map} +1 -1
  204. package/dist/{byline-fields-CR5hGLMw.d.mts → byline-fields-DYXKDuNX.d.mts} +53 -29
  205. package/dist/byline-fields-DYXKDuNX.d.mts.map +1 -0
  206. package/dist/{byline-registry-CxK5g559.mjs → byline-registry-CWP7I71B.mjs} +3 -3
  207. package/dist/{byline-registry-CxK5g559.mjs.map → byline-registry-CWP7I71B.mjs.map} +1 -1
  208. package/dist/{bylines-CbrD7STW.mjs → bylines-Cx5n-WqP.mjs} +3 -3
  209. package/dist/{bylines-CbrD7STW.mjs.map → bylines-Cx5n-WqP.mjs.map} +1 -1
  210. package/dist/{bylines-DCczH3AV.mjs → bylines-wurS258E.mjs} +50 -6
  211. package/dist/{bylines-DCczH3AV.mjs.map → bylines-wurS258E.mjs.map} +1 -1
  212. package/dist/{cache-DIHHyPkt.mjs → cache-B_HzASVT.mjs} +3 -3
  213. package/dist/{cache-DIHHyPkt.mjs.map → cache-B_HzASVT.mjs.map} +1 -1
  214. package/dist/{chunks-DnnHlRG3.mjs → chunks-BerYVuve.mjs} +2 -2
  215. package/dist/{chunks-DnnHlRG3.mjs.map → chunks-BerYVuve.mjs.map} +1 -1
  216. package/dist/cli/index.mjs +40 -27
  217. package/dist/cli/index.mjs.map +1 -1
  218. package/dist/client/cf-access.d.mts +1 -1
  219. package/dist/client/index.d.mts +1 -1
  220. package/dist/{comment-DkAfGX9E.mjs → comment-sqQxNpN3.mjs} +2 -2
  221. package/dist/{comment-DkAfGX9E.mjs.map → comment-sqQxNpN3.mjs.map} +1 -1
  222. package/dist/{comments-DLFnXs7J.mjs → comments-CJ0RZsYR.mjs} +3 -3
  223. package/dist/{comments-DLFnXs7J.mjs.map → comments-CJ0RZsYR.mjs.map} +1 -1
  224. package/dist/{content-C7aJ7keg.mjs → content-BIlVx-RX.mjs} +132 -43
  225. package/dist/content-BIlVx-RX.mjs.map +1 -0
  226. package/dist/{context-Ca0HkaIh.mjs → context-GG52SPgh.mjs} +10 -10
  227. package/dist/{context-Ca0HkaIh.mjs.map → context-GG52SPgh.mjs.map} +1 -1
  228. package/dist/{cron-DZovZUnC.mjs → cron-BJ2ClIlj.mjs} +4 -3
  229. package/dist/cron-BJ2ClIlj.mjs.map +1 -0
  230. package/dist/{dashboard-BrfLIsX1.mjs → dashboard-2JgAMWxK.mjs} +4 -4
  231. package/dist/{dashboard-BrfLIsX1.mjs.map → dashboard-2JgAMWxK.mjs.map} +1 -1
  232. package/dist/db/index.d.mts +2 -2
  233. package/dist/db/index.mjs +1 -1
  234. package/dist/{device-flow-ptLrVINd.mjs → device-flow-s6_q3T7A.mjs} +2 -2
  235. package/dist/{device-flow-ptLrVINd.mjs.map → device-flow-s6_q3T7A.mjs.map} +1 -1
  236. package/dist/{error-Bk9s3Ism.mjs → error-RwM4dD35.mjs} +2 -2
  237. package/dist/{error-Bk9s3Ism.mjs.map → error-RwM4dD35.mjs.map} +1 -1
  238. package/dist/{fts-manager-XpDfbIKo.mjs → fts-manager-1RgHmopc.mjs} +2 -2
  239. package/dist/{fts-manager-XpDfbIKo.mjs.map → fts-manager-1RgHmopc.mjs.map} +1 -1
  240. package/dist/{index-D60_SzHG.d.mts → index-BpYeJO1E.d.mts} +2 -2
  241. package/dist/{index-D60_SzHG.d.mts.map → index-BpYeJO1E.d.mts.map} +1 -1
  242. package/dist/{index-C8ciqSMJ.d.mts → index-FfiTQJq2.d.mts} +202 -20
  243. package/dist/index-FfiTQJq2.d.mts.map +1 -0
  244. package/dist/index.d.mts +9 -9
  245. package/dist/index.mjs +43 -43
  246. package/dist/{load-CF5oETkh.mjs → load-B84ohfBk.mjs} +2 -2
  247. package/dist/{load-CF5oETkh.mjs.map → load-B84ohfBk.mjs.map} +1 -1
  248. package/dist/{loader-BxyvbrZP.mjs → loader-CpZKpFz0.mjs} +32 -30
  249. package/dist/loader-CpZKpFz0.mjs.map +1 -0
  250. package/dist/media/index.mjs +1 -1
  251. package/dist/media/local-runtime.d.mts +7 -7
  252. package/dist/media/local-runtime.mjs +6 -6
  253. package/dist/{media-Cyz5BhSN.mjs → media-JOf3pNkw.mjs} +2 -2
  254. package/dist/{media-Cyz5BhSN.mjs.map → media-JOf3pNkw.mjs.map} +1 -1
  255. package/dist/{menus-PFp8FDuO.mjs → menus-DX4_E01q.mjs} +3 -3
  256. package/dist/{menus-PFp8FDuO.mjs.map → menus-DX4_E01q.mjs.map} +1 -1
  257. package/dist/{menus-CIdZ_Q6U.mjs → menus-Dp9xporj.mjs} +112 -16
  258. package/dist/menus-Dp9xporj.mjs.map +1 -0
  259. package/dist/{normalize-DVV8nbrL.mjs → normalize-CK5o04zr.mjs} +2 -2
  260. package/dist/{normalize-DVV8nbrL.mjs.map → normalize-CK5o04zr.mjs.map} +1 -1
  261. package/dist/{oauth-authorization-DvBAL75d.mjs → oauth-authorization-1aPAYjiC.mjs} +2 -2
  262. package/dist/{oauth-authorization-DvBAL75d.mjs.map → oauth-authorization-1aPAYjiC.mjs.map} +1 -1
  263. package/dist/{options-BL4X94qY.mjs → options-BPCVnesz.mjs} +1 -1
  264. package/dist/{options-BL4X94qY.mjs.map → options-BPCVnesz.mjs.map} +1 -1
  265. package/dist/{options-tb7DJROi.d.mts → options-D4MnavW_.d.mts} +3 -3
  266. package/dist/{options-tb7DJROi.d.mts.map → options-D4MnavW_.d.mts.map} +1 -1
  267. package/dist/{parse-B-K21lvm.mjs → parse-CrGndy1A.mjs} +2 -2
  268. package/dist/{parse-B-K21lvm.mjs.map → parse-CrGndy1A.mjs.map} +1 -1
  269. package/dist/{patterns-CqG5Ya3i.mjs → patterns-p-RBdTbM.mjs} +1 -1
  270. package/dist/{patterns-CqG5Ya3i.mjs.map → patterns-p-RBdTbM.mjs.map} +1 -1
  271. package/dist/plugin-utils.d.mts +7 -7
  272. package/dist/plugins/adapt-sandbox-entry.d.mts +7 -7
  273. package/dist/{query-Cc649nDl.mjs → query-BFQ029Ts.mjs} +21 -15
  274. package/dist/query-BFQ029Ts.mjs.map +1 -0
  275. package/dist/{rate-limit-BI1OdpQH.mjs → rate-limit-ClFFUga6.mjs} +2 -2
  276. package/dist/{rate-limit-BI1OdpQH.mjs.map → rate-limit-ClFFUga6.mjs.map} +1 -1
  277. package/dist/{redirect-C-FeA4j9.mjs → redirect-CRWIt8Zj.mjs} +3 -3
  278. package/dist/{redirect-C-FeA4j9.mjs.map → redirect-CRWIt8Zj.mjs.map} +1 -1
  279. package/dist/{redirects-C0L9JUk4.mjs → redirects-DEygMrRO.mjs} +25 -3
  280. package/dist/redirects-DEygMrRO.mjs.map +1 -0
  281. package/dist/{redirects-C1UgU9E0.mjs → redirects-OIu6vQ2i.mjs} +5 -5
  282. package/dist/{redirects-C1UgU9E0.mjs.map → redirects-OIu6vQ2i.mjs.map} +1 -1
  283. package/dist/{registry-C-T_PWgp.mjs → registry-brYh-rAT.mjs} +6 -6
  284. package/dist/{registry-C-T_PWgp.mjs.map → registry-brYh-rAT.mjs.map} +1 -1
  285. package/dist/{request-cache-BYMs-BGX.mjs → request-cache-D32LpnmI.mjs} +1 -1
  286. package/dist/{request-cache-BYMs-BGX.mjs.map → request-cache-D32LpnmI.mjs.map} +1 -1
  287. package/dist/{runner-BiuUfx-V.mjs → runner--4wMWwKM.mjs} +224 -168
  288. package/dist/runner--4wMWwKM.mjs.map +1 -0
  289. package/dist/{runner-DM1yR5qd.d.mts → runner-BcRuXq_h.d.mts} +2 -2
  290. package/dist/{runner-DM1yR5qd.d.mts.map → runner-BcRuXq_h.d.mts.map} +1 -1
  291. package/dist/runtime.d.mts +7 -7
  292. package/dist/runtime.mjs +2 -2
  293. package/dist/{schema-BpCJh2lU.mjs → schema-CS7Eg5gh.mjs} +5 -5
  294. package/dist/{schema-BpCJh2lU.mjs.map → schema-CS7Eg5gh.mjs.map} +1 -1
  295. package/dist/{search-BrF7k0Ho.mjs → search-o-aQzHI1.mjs} +4 -4
  296. package/dist/{search-BrF7k0Ho.mjs.map → search-o-aQzHI1.mjs.map} +1 -1
  297. package/dist/{secrets-YYbTgB1w.mjs → secrets-C_ZtRos3.mjs} +2 -2
  298. package/dist/{secrets-YYbTgB1w.mjs.map → secrets-C_ZtRos3.mjs.map} +1 -1
  299. package/dist/{sections-8DEa-dWt.mjs → sections-DhsZ0ns9.mjs} +3 -3
  300. package/dist/{sections-8DEa-dWt.mjs.map → sections-DhsZ0ns9.mjs.map} +1 -1
  301. package/dist/seed/index.d.mts +2 -2
  302. package/dist/seed/index.mjs +16 -16
  303. package/dist/seo/index.d.mts +1 -1
  304. package/dist/{seo-CKr7pLfA.mjs → seo-B5e6y9Wk.mjs} +2 -2
  305. package/dist/{seo-CKr7pLfA.mjs.map → seo-B5e6y9Wk.mjs.map} +1 -1
  306. package/dist/{service-9P2cdyR_.mjs → service-DAxg8RPR.mjs} +2 -2
  307. package/dist/{service-9P2cdyR_.mjs.map → service-DAxg8RPR.mjs.map} +1 -1
  308. package/dist/{settings-Jro4YcUb.mjs → settings-B1p-gPUK.mjs} +5 -5
  309. package/dist/{settings-Jro4YcUb.mjs.map → settings-B1p-gPUK.mjs.map} +1 -1
  310. package/dist/{settings-DYVzINdn.mjs → settings-DIsbHTRE.mjs} +3 -3
  311. package/dist/{settings-DYVzINdn.mjs.map → settings-DIsbHTRE.mjs.map} +1 -1
  312. package/dist/{setup-complete-VoEZfasi.mjs → setup-complete-Yuv78yua.mjs} +2 -2
  313. package/dist/{setup-complete-VoEZfasi.mjs.map → setup-complete-Yuv78yua.mjs.map} +1 -1
  314. package/dist/{site-url-Cm8-sJy7.mjs → site-url-mEVmwIFi.mjs} +2 -2
  315. package/dist/{site-url-Cm8-sJy7.mjs.map → site-url-mEVmwIFi.mjs.map} +1 -1
  316. package/dist/{taxonomies-CGD6y79Q.mjs → taxonomies-BEW7S5AI.mjs} +10 -8
  317. package/dist/taxonomies-BEW7S5AI.mjs.map +1 -0
  318. package/dist/{taxonomies-C0bVme_m.mjs → taxonomies-UusDXv3C.mjs} +4 -4
  319. package/dist/{taxonomies-C0bVme_m.mjs.map → taxonomies-UusDXv3C.mjs.map} +1 -1
  320. package/dist/{taxonomy-Db5xwphL.mjs → taxonomy-CdllE4oq.mjs} +3 -3
  321. package/dist/{taxonomy-Db5xwphL.mjs.map → taxonomy-CdllE4oq.mjs.map} +1 -1
  322. package/dist/{transaction-NQj4VJ7Z.mjs → transaction-x2tJQ-A1.mjs} +1 -1
  323. package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-x2tJQ-A1.mjs.map} +1 -1
  324. package/dist/{transport-OnMNbsIA.d.mts → transport-BwQeeY2p.d.mts} +1 -1
  325. package/dist/{transport-OnMNbsIA.d.mts.map → transport-BwQeeY2p.d.mts.map} +1 -1
  326. package/dist/{types-CfyYQ7eY.mjs → types-BXSUSAjt.mjs} +16 -3
  327. package/dist/{types-CfyYQ7eY.mjs.map → types-BXSUSAjt.mjs.map} +1 -1
  328. package/dist/{types-D8bhH891.mjs → types-DZk_y-MU.mjs} +1 -1
  329. package/dist/{types-D8bhH891.mjs.map → types-DZk_y-MU.mjs.map} +1 -1
  330. package/dist/{types-DawhLFwy.d.mts → types-OT_Es5mp.d.mts} +26 -1
  331. package/dist/{types-DawhLFwy.d.mts.map → types-OT_Es5mp.d.mts.map} +1 -1
  332. package/dist/{types-i8_uzhMD.d.mts → types-WVmpZBJV.d.mts} +18 -3
  333. package/dist/types-WVmpZBJV.d.mts.map +1 -0
  334. package/dist/{user-tLdHUEXV.mjs → user-C0um7wrg.mjs} +18 -2
  335. package/dist/user-C0um7wrg.mjs.map +1 -0
  336. package/dist/{validate-Dy6nkNls.d.mts → validate-BPAHUSge.d.mts} +10 -2
  337. package/dist/validate-BPAHUSge.d.mts.map +1 -0
  338. package/dist/{validate-DWmnRg6E.mjs → validate-ZP9Dvg0P.mjs} +6 -3
  339. package/dist/validate-ZP9Dvg0P.mjs.map +1 -0
  340. package/dist/{validation-BQ_TP-On.mjs → validation-CE5i4q0c.mjs} +5 -5
  341. package/dist/{validation-BQ_TP-On.mjs.map → validation-CE5i4q0c.mjs.map} +1 -1
  342. package/dist/version-Dw0JXu45.mjs +7 -0
  343. package/dist/{version-CgcnMvqS.mjs.map → version-Dw0JXu45.mjs.map} +1 -1
  344. package/dist/{widgets-DzlINGI6.mjs → widgets-ClEnYQCH.mjs} +2 -2
  345. package/dist/{widgets-DzlINGI6.mjs.map → widgets-ClEnYQCH.mjs.map} +1 -1
  346. package/dist/{zod-generator-MMm56Prt.mjs → zod-generator-Djo_VHCt.mjs} +4 -3
  347. package/dist/zod-generator-Djo_VHCt.mjs.map +1 -0
  348. package/package.json +7 -7
  349. package/src/api/handlers/content.ts +107 -8
  350. package/src/api/handlers/index.ts +2 -0
  351. package/src/api/openapi/document.ts +25 -0
  352. package/src/api/schemas/content.ts +33 -0
  353. package/src/astro/integration/index.ts +98 -0
  354. package/src/astro/integration/routes.ts +6 -0
  355. package/src/astro/integration/virtual-modules.ts +39 -0
  356. package/src/astro/integration/vite-config.ts +12 -0
  357. package/src/astro/middleware/stream-end-metrics.ts +96 -0
  358. package/src/astro/middleware.ts +107 -31
  359. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  360. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +4 -2
  361. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +8 -4
  362. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +4 -2
  363. package/src/astro/routes/api/content/[collection]/[id].ts +4 -2
  364. package/src/astro/routes/api/content/[collection]/authors.ts +34 -0
  365. package/src/astro/types.ts +8 -1
  366. package/src/bylines/index.ts +57 -0
  367. package/src/cli/commands/export-seed.ts +28 -12
  368. package/src/components/EmDashImage.astro +23 -4
  369. package/src/components/Image.astro +20 -3
  370. package/src/database/migrations/043_content_references.ts +121 -0
  371. package/src/database/migrations/runner.ts +9 -2
  372. package/src/database/repositories/content.ts +225 -67
  373. package/src/database/repositories/index.ts +7 -0
  374. package/src/database/repositories/relation.ts +467 -0
  375. package/src/database/repositories/types.ts +31 -0
  376. package/src/database/repositories/user.ts +18 -0
  377. package/src/database/types.ts +34 -0
  378. package/src/emdash-runtime.ts +318 -168
  379. package/src/index.ts +8 -1
  380. package/src/loader.ts +67 -34
  381. package/src/media/responsive.ts +125 -0
  382. package/src/menus/index.ts +27 -9
  383. package/src/plugins/cron.ts +3 -2
  384. package/src/plugins/hooks.ts +35 -6
  385. package/src/plugins/index.ts +5 -0
  386. package/src/plugins/manager.ts +1 -0
  387. package/src/plugins/scheduler/node.ts +9 -2
  388. package/src/query.ts +32 -5
  389. package/src/scheduled-publish.ts +153 -0
  390. package/src/schema/zod-generator.ts +6 -2
  391. package/src/seed/apply.ts +16 -6
  392. package/src/seed/types.ts +9 -0
  393. package/src/seed/validate.ts +15 -0
  394. package/src/taxonomies/index.ts +13 -8
  395. package/src/utils/init-lock.ts +143 -0
  396. package/src/virtual-modules.d.ts +11 -0
  397. package/dist/api-B7GATEYo.mjs.map +0 -1
  398. package/dist/apply-BrVqULFe.mjs.map +0 -1
  399. package/dist/byline-fields-CR5hGLMw.d.mts.map +0 -1
  400. package/dist/content-C7aJ7keg.mjs.map +0 -1
  401. package/dist/cron-DZovZUnC.mjs.map +0 -1
  402. package/dist/index-C8ciqSMJ.d.mts.map +0 -1
  403. package/dist/loader-BxyvbrZP.mjs.map +0 -1
  404. package/dist/menus-CIdZ_Q6U.mjs.map +0 -1
  405. package/dist/query-Cc649nDl.mjs.map +0 -1
  406. package/dist/redirects-C0L9JUk4.mjs.map +0 -1
  407. package/dist/runner-BiuUfx-V.mjs.map +0 -1
  408. package/dist/taxonomies-CGD6y79Q.mjs.map +0 -1
  409. package/dist/types-i8_uzhMD.d.mts.map +0 -1
  410. package/dist/user-tLdHUEXV.mjs.map +0 -1
  411. package/dist/validate-DWmnRg6E.mjs.map +0 -1
  412. package/dist/validate-Dy6nkNls.d.mts.map +0 -1
  413. package/dist/version-CgcnMvqS.mjs +0 -7
  414. package/dist/zod-generator-MMm56Prt.mjs.map +0 -1
  415. package/src/plugins/scheduler/piggyback.ts +0 -71
@@ -14,13 +14,17 @@ import { RevisionRepository } from "../../database/repositories/revision.js";
14
14
  import { SeoRepository } from "../../database/repositories/seo.js";
15
15
  import {
16
16
  EmDashValidationError,
17
+ ScheduledNotDueError,
17
18
  InvalidCursorError,
18
19
  type BylineSummary,
19
20
  type ContentBylineCredit,
21
+ type ContentDateField,
20
22
  type ContentItem,
21
23
  type ContentSeo,
22
24
  type ContentSeoInput,
25
+ type FindManyOptions,
23
26
  } from "../../database/repositories/types.js";
27
+ import { UserRepository } from "../../database/repositories/user.js";
24
28
  import { withTransaction } from "../../database/transaction.js";
25
29
  import type { Database } from "../../database/types.js";
26
30
  import { validateIdentifier } from "../../database/validate.js";
@@ -323,6 +327,24 @@ async function resolveSearchColumns(db: Kysely<Database>, collection: string): P
323
327
  return columns;
324
328
  }
325
329
 
330
+ /** Matches a date-only `YYYY-MM-DD` bound (no time component). */
331
+ const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
332
+
333
+ /**
334
+ * Normalize a date-range bound to an ISO datetime for lexicographic comparison
335
+ * against stored ISO 8601 timestamps. A bare `YYYY-MM-DD` is widened to the
336
+ * appropriate UTC day boundary so the range stays inclusive: a `start` bound
337
+ * becomes the start of the day and an `end` bound the end of the day.
338
+ * Otherwise a date-only upper bound would exclude every same-day row (since
339
+ * `2024-06-01T12:00:00Z` sorts after `2024-06-01`). Full datetimes pass
340
+ * through unchanged.
341
+ */
342
+ function normalizeDateBound(value: string | undefined, edge: "start" | "end"): string | undefined {
343
+ if (!value) return undefined;
344
+ if (!DATE_ONLY_RE.test(value)) return value;
345
+ return edge === "start" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
346
+ }
347
+
326
348
  /**
327
349
  * Create content list handler
328
350
  */
@@ -337,18 +359,28 @@ export async function handleContentList(
337
359
  order?: "asc" | "desc";
338
360
  locale?: string;
339
361
  q?: string;
362
+ authorId?: string;
363
+ dateField?: ContentDateField;
364
+ dateFrom?: string;
365
+ dateTo?: string;
340
366
  },
341
367
  ): Promise<ApiResult<ContentListResponse>> {
342
368
  try {
343
369
  const repo = new ContentRepository(db);
344
- const where: {
345
- status?: string;
346
- locale?: string;
347
- q?: string;
348
- searchColumns?: string[];
349
- } = {};
370
+ const where: FindManyOptions["where"] = {};
350
371
  if (params.status) where.status = params.status;
351
372
  if (params.locale) where.locale = params.locale;
373
+ if (params.authorId) where.authorId = params.authorId;
374
+
375
+ // A date range requires a target column; ignore stray from/to without
376
+ // a field so a half-specified filter doesn't silently drop all rows.
377
+ if (params.dateField && (params.dateFrom || params.dateTo)) {
378
+ where.dateFilter = {
379
+ field: params.dateField,
380
+ from: normalizeDateBound(params.dateFrom, "start"),
381
+ to: normalizeDateBound(params.dateTo, "end"),
382
+ };
383
+ }
352
384
 
353
385
  const q = params.q?.trim();
354
386
  if (q) {
@@ -412,6 +444,62 @@ export async function handleContentList(
412
444
  }
413
445
  }
414
446
 
447
+ /** A content author option for the admin author filter. */
448
+ export interface ContentAuthor {
449
+ id: string;
450
+ name: string | null;
451
+ email: string;
452
+ avatarUrl: string | null;
453
+ }
454
+
455
+ /**
456
+ * List the distinct authors of a collection's live content.
457
+ *
458
+ * Backs the admin content-list author filter. Unlike `/admin/users` (ADMIN
459
+ * only), this is gated on `content:read`, so any editor can filter by author.
460
+ * Returns only users who have authored at least one non-trashed entry, sorted
461
+ * by display name then email for a stable dropdown order.
462
+ */
463
+ export async function handleContentAuthors(
464
+ db: Kysely<Database>,
465
+ collection: string,
466
+ ): Promise<ApiResult<{ items: ContentAuthor[] }>> {
467
+ try {
468
+ const repo = new ContentRepository(db);
469
+ const authorIds = await repo.findDistinctAuthorIds(collection);
470
+ if (authorIds.length === 0) {
471
+ return { success: true, data: { items: [] } };
472
+ }
473
+
474
+ const userRepo = new UserRepository(db);
475
+ const users = await userRepo.findByIds(authorIds);
476
+
477
+ const items: ContentAuthor[] = users
478
+ .map((u) => ({ id: u.id, name: u.name, email: u.email, avatarUrl: u.avatarUrl }))
479
+ .toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email));
480
+
481
+ return { success: true, data: { items } };
482
+ } catch (error) {
483
+ if (isMissingTableError(error)) {
484
+ return {
485
+ success: false,
486
+ error: {
487
+ code: "COLLECTION_NOT_FOUND",
488
+ message: `Collection '${collection}' not found`,
489
+ },
490
+ };
491
+ }
492
+ console.error("Content authors error:", error);
493
+ return {
494
+ success: false,
495
+ error: {
496
+ code: "CONTENT_AUTHORS_ERROR",
497
+ message: "Failed to list content authors",
498
+ },
499
+ };
500
+ }
501
+ }
502
+
415
503
  /**
416
504
  * Get single content item
417
505
  */
@@ -1268,13 +1356,13 @@ export async function handleContentPublish(
1268
1356
  db: Kysely<Database>,
1269
1357
  collection: string,
1270
1358
  id: string,
1271
- options: { publishedAt?: string } = {},
1359
+ options: { publishedAt?: string; requireScheduledDue?: boolean } = {},
1272
1360
  ): Promise<ApiResult<ContentResponse>> {
1273
1361
  try {
1274
1362
  const item = await withTransaction(db, async (trx) => {
1275
1363
  const repo = new ContentRepository(trx);
1276
1364
  const resolvedId = (await resolveId(repo, collection, id)) ?? id;
1277
- return repo.publish(collection, resolvedId, options.publishedAt);
1365
+ return repo.publish(collection, resolvedId, options.publishedAt, options.requireScheduledDue);
1278
1366
  });
1279
1367
 
1280
1368
  const hasSeo = await collectionHasSeo(db, collection);
@@ -1285,6 +1373,17 @@ export async function handleContentPublish(
1285
1373
  data: { item },
1286
1374
  };
1287
1375
  } catch (error) {
1376
+ // The scheduled sweep gates publish on the row still being due; a row
1377
+ // unscheduled in the meantime is a silent skip, not a failure.
1378
+ if (error instanceof ScheduledNotDueError) {
1379
+ return {
1380
+ success: false,
1381
+ error: {
1382
+ code: "NOT_DUE",
1383
+ message: error.message,
1384
+ },
1385
+ };
1386
+ }
1288
1387
  if (error instanceof EmDashValidationError) {
1289
1388
  return {
1290
1389
  success: false,
@@ -7,6 +7,8 @@
7
7
  // Content handlers
8
8
  export {
9
9
  handleContentList,
10
+ handleContentAuthors,
11
+ type ContentAuthor,
10
12
  handleContentGet,
11
13
  handleContentGetIncludingTrashed,
12
14
  handleContentCreate,
@@ -24,6 +24,7 @@ import {
24
24
  import { apiErrorSchema, deleteResponseSchema, successEnvelope } from "../schemas/common.js";
25
25
  import {
26
26
  contentCompareResponseSchema,
27
+ contentAuthorsResponseSchema,
27
28
  contentCreateBody,
28
29
  contentItemSchema,
29
30
  contentListQuery,
@@ -597,6 +598,30 @@ const contentPaths = {
597
598
  },
598
599
  },
599
600
 
601
+ "/_emdash/api/content/{collection}/authors": {
602
+ get: {
603
+ operationId: "listContentAuthors",
604
+ summary: "List distinct authors of a collection's content",
605
+ tags: ["Content"],
606
+ requestParams: {
607
+ path: z.object({
608
+ collection: z.string().meta({ description: "Collection slug" }),
609
+ }),
610
+ },
611
+ responses: {
612
+ "200": {
613
+ description: "Content authors",
614
+ content: {
615
+ [JSON_CONTENT]: {
616
+ schema: successEnvelope(contentAuthorsResponseSchema),
617
+ },
618
+ },
619
+ },
620
+ ...authErrors,
621
+ ...standardErrors(500),
622
+ },
623
+ },
624
+ },
600
625
  "/_emdash/api/content/{collection}/trash": {
601
626
  get: {
602
627
  operationId: "listTrashedContent",
@@ -18,6 +18,14 @@ export const contentSeoInput = z
18
18
  })
19
19
  .meta({ id: "ContentSeoInput" });
20
20
 
21
+ /** ISO 8601 date or datetime bound for the content-list date range filter. */
22
+ const contentDateBound = z
23
+ .union([
24
+ z.iso.datetime({ offset: true, message: "must be an ISO 8601 datetime" }),
25
+ z.iso.date({ message: "must be an ISO 8601 date" }),
26
+ ])
27
+ .optional();
28
+
21
29
  export const contentListQuery = cursorPaginationQuery
22
30
  .extend({
23
31
  status: z.string().optional(),
@@ -26,6 +34,14 @@ export const contentListQuery = cursorPaginationQuery
26
34
  locale: localeCode.optional(),
27
35
  /** Case-insensitive substring search across the collection's title/name/slug. */
28
36
  q: z.string().trim().min(1).max(200).optional(),
37
+ /** Filter to entries authored by this user (the `author_id` column). */
38
+ authorId: z.string().min(1).max(64).optional(),
39
+ /** Which timestamp column the `dateFrom`/`dateTo` range applies to. */
40
+ dateField: z.enum(["createdAt", "updatedAt", "publishedAt"]).optional(),
41
+ /** Inclusive lower bound for the date range. Requires `dateField`. */
42
+ dateFrom: contentDateBound,
43
+ /** Inclusive upper bound for the date range. Requires `dateField`. */
44
+ dateTo: contentDateBound,
29
45
  })
30
46
  .meta({ id: "ContentListQuery" });
31
47
 
@@ -168,6 +184,23 @@ export const contentListResponseSchema = z
168
184
  })
169
185
  .meta({ id: "ContentListResponse" });
170
186
 
187
+ /** A distinct content author for the admin author filter */
188
+ export const contentAuthorSchema = z
189
+ .object({
190
+ id: z.string(),
191
+ name: z.string().nullable(),
192
+ email: z.string(),
193
+ avatarUrl: z.string().nullable(),
194
+ })
195
+ .meta({ id: "ContentAuthor" });
196
+
197
+ /** Response for the content authors endpoint */
198
+ export const contentAuthorsResponseSchema = z
199
+ .object({
200
+ items: z.array(contentAuthorSchema),
201
+ })
202
+ .meta({ id: "ContentAuthorsResponse" });
203
+
171
204
  /** Trashed content item */
172
205
  export const trashedContentItemSchema = z
173
206
  .object({
@@ -15,6 +15,7 @@ import { createRequire } from "node:module";
15
15
  import type { AstroIntegration, AstroIntegrationLogger } from "astro";
16
16
 
17
17
  import { validateAllowedOrigins, validateOriginShape } from "../../auth/allowed-origins.js";
18
+ import { INTERNAL_MEDIA_PREFIX } from "../../media/normalize.js";
18
19
  import type { ResolvedPlugin } from "../../plugins/types.js";
19
20
  import { local } from "../storage/adapters.js";
20
21
  import { notoSans } from "./font-provider.js";
@@ -59,6 +60,92 @@ const DEFAULT_STORAGE = local({
59
60
  baseUrl: "/_emdash/api/media/file",
60
61
  });
61
62
 
63
+ interface ImageRemotePattern {
64
+ protocol?: "http" | "https";
65
+ hostname?: string;
66
+ pathname?: string;
67
+ }
68
+
69
+ /**
70
+ * Build `image.remotePatterns` entries so Astro will optimize EmDash media.
71
+ *
72
+ * Astro's image services only transform **absolute** URLs whose host is
73
+ * authorized; everything else is passed through unoptimized. We authorize the
74
+ * media sources automatically:
75
+ *
76
+ * 1. The storage adapter's public URL host (R2 custom domain, S3/CDN), so
77
+ * media served directly from a public bucket is optimized.
78
+ * 2. The site's own origin, scoped to the media proxy route
79
+ * (`/_emdash/api/media/file/**`), so same-origin proxied media (local
80
+ * storage, or R2 without a public URL) is optimized too. The pathname
81
+ * scope keeps Astro's image endpoint from acting as an open proxy for the
82
+ * whole origin. Only registered when `siteUrl` is known at build time;
83
+ * `getPublicOrigin` resolves the matching origin at render time.
84
+ * 3. In `astro dev` the dev-server origin (`localhost:<port>`) isn't known at
85
+ * build time, so we register a host-agnostic pattern scoped to the media
86
+ * route. This is dev-only — it never ships in a production build — so the
87
+ * missing host check can't be abused on a deployed site.
88
+ *
89
+ * Returns an empty array when no source is statically known (e.g. a production
90
+ * build using local storage with no `siteUrl`), in which case media renders as
91
+ * a plain `<img>`.
92
+ *
93
+ * @internal Exported for unit testing.
94
+ */
95
+ export function buildImageRemotePatterns(
96
+ storage: { config?: unknown } | undefined,
97
+ siteUrl: string | undefined,
98
+ command: "dev" | "build" | "preview" | "sync",
99
+ ): ImageRemotePattern[] {
100
+ const patterns: ImageRemotePattern[] = [];
101
+
102
+ const config = storage?.config;
103
+ const publicUrl =
104
+ config && typeof config === "object"
105
+ ? (config as { publicUrl?: unknown }).publicUrl
106
+ : undefined;
107
+ if (typeof publicUrl === "string" && publicUrl) {
108
+ try {
109
+ const url = new URL(publicUrl);
110
+ // Only authorize http(s) hosts — a `file:`/`ftp:` URL is not a media
111
+ // origin Astro can fetch.
112
+ if (url.protocol === "http:" || url.protocol === "https:") {
113
+ const pattern: ImageRemotePattern = {
114
+ protocol: url.protocol === "http:" ? "http" : "https",
115
+ hostname: url.hostname,
116
+ };
117
+ // When the public URL has a path prefix (CDN sub-path), scope the
118
+ // pattern to it so we don't authorize the entire host. Media keys
119
+ // are appended as `${publicUrl}/${key}`, so the prefix is exact.
120
+ const prefix = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
121
+ if (prefix && prefix !== "/") {
122
+ pattern.pathname = `${prefix}/**`;
123
+ }
124
+ patterns.push(pattern);
125
+ }
126
+ } catch {
127
+ // ignore an unparseable public URL
128
+ }
129
+ }
130
+
131
+ if (siteUrl) {
132
+ try {
133
+ patterns.push({
134
+ hostname: new URL(siteUrl).hostname,
135
+ pathname: `${INTERNAL_MEDIA_PREFIX}**`,
136
+ });
137
+ } catch {
138
+ // ignore an unparseable site URL
139
+ }
140
+ }
141
+
142
+ if (command === "dev") {
143
+ patterns.push({ pathname: `${INTERNAL_MEDIA_PREFIX}**` });
144
+ }
145
+
146
+ return patterns;
147
+ }
148
+
62
149
  // Terminal formatting
63
150
  const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
64
151
  const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
@@ -298,8 +385,19 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
298
385
  },
299
386
  ];
300
387
 
388
+ // Authorize media sources for Astro image optimization so the
389
+ // Image components can generate a responsive srcset for R2/S3 and
390
+ // same-origin proxied media. `updateConfig` merges arrays, so any
391
+ // user-configured remotePatterns are preserved.
392
+ const imageRemotePatterns = buildImageRemotePatterns(
393
+ resolvedConfig.storage,
394
+ resolvedConfig.siteUrl,
395
+ command,
396
+ );
397
+
301
398
  updateConfig({
302
399
  security: securityConfig,
400
+ ...(imageRemotePatterns.length ? { image: { remotePatterns: imageRemotePatterns } } : {}),
303
401
  // fonts is a valid AstroConfig key but may not be in the
304
402
  // type definition for the minimum supported Astro version
305
403
  ...({ fonts: emdashFonts } as Record<string, unknown>),
@@ -88,6 +88,12 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
88
88
  entrypoint: resolveRoute("api/content/[collection]/[id]/preview-url.ts"),
89
89
  });
90
90
 
91
+ // Content authors (for the admin author filter)
92
+ injectRoute({
93
+ pattern: "/_emdash/api/content/[collection]/authors",
94
+ entrypoint: resolveRoute("api/content/[collection]/authors.ts"),
95
+ });
96
+
91
97
  // Trash/restore routes
92
98
  injectRoute({
93
99
  pattern: "/_emdash/api/content/[collection]/trash",
@@ -63,6 +63,9 @@ export const RESOLVED_VIRTUAL_SEED_ID = "\0" + VIRTUAL_SEED_ID;
63
63
  export const VIRTUAL_WAIT_UNTIL_ID = "virtual:emdash/wait-until";
64
64
  export const RESOLVED_VIRTUAL_WAIT_UNTIL_ID = "\0" + VIRTUAL_WAIT_UNTIL_ID;
65
65
 
66
+ export const VIRTUAL_SCHEDULER_ID = "virtual:emdash/scheduler";
67
+ export const RESOLVED_VIRTUAL_SCHEDULER_ID = "\0" + VIRTUAL_SCHEDULER_ID;
68
+
66
69
  /**
67
70
  * Generates the config virtual module.
68
71
  */
@@ -413,6 +416,42 @@ export function generateWaitUntilModule(adapterName: string | undefined): string
413
416
  return `export const waitUntil = undefined;`;
414
417
  }
415
418
 
419
+ /**
420
+ * Generates the scheduler virtual module.
421
+ *
422
+ * Decides — at build time, from the Astro adapter — whether the runtime gets a
423
+ * long-lived timer heartbeat. A *production* Cloudflare build has no persistent
424
+ * timers, so the Worker's `scheduled()` handler (a Cron Trigger) drives
425
+ * `runScheduledTasks()` instead and this exports `null`. Every other case — any
426
+ * other adapter (Node, Bun), and crucially local `astro dev` even under the
427
+ * Cloudflare adapter (no Cron Trigger fires in dev) — gets a `NodeCronScheduler`
428
+ * factory so plugin cron, scheduled publishing, and cleanup still run.
429
+ *
430
+ * Keeping the adapter check here — rather than in core's runtime — means the
431
+ * runtime has no Cloudflare-specific code path; it just calls `createScheduler`
432
+ * if one was injected. Mirrors the wait-until module's approach.
433
+ */
434
+ export function generateSchedulerModule(
435
+ adapterName: string | undefined,
436
+ command: "build" | "serve" | undefined,
437
+ ): string {
438
+ // Only suppress the timer for an actual Cloudflare *build* — that artifact
439
+ // runs in workerd where a Cron Trigger drives scheduled work. In `serve`
440
+ // (local dev) nothing fires the Cron Trigger, so fall through to the timer.
441
+ if (adapterName === "@astrojs/cloudflare" && command !== "serve") {
442
+ return `// Serverless build: an external Cron Trigger drives scheduled work.
443
+ export const createScheduler = null;
444
+ `;
445
+ }
446
+ return `// Long-lived runtime (or local dev): drive scheduled work from an in-process timer.
447
+ import { NodeCronScheduler } from "emdash";
448
+
449
+ export function createScheduler(executor) {
450
+ return new NodeCronScheduler(executor);
451
+ }
452
+ `;
453
+ }
454
+
416
455
  /**
417
456
  * Generates the seed virtual module.
418
457
  * Reads the user's seed file at build time (in Node context) and embeds it,
@@ -42,8 +42,11 @@ import {
42
42
  RESOLVED_VIRTUAL_SEED_ID,
43
43
  VIRTUAL_WAIT_UNTIL_ID,
44
44
  RESOLVED_VIRTUAL_WAIT_UNTIL_ID,
45
+ VIRTUAL_SCHEDULER_ID,
46
+ RESOLVED_VIRTUAL_SCHEDULER_ID,
45
47
  generateSeedModule,
46
48
  generateWaitUntilModule,
49
+ generateSchedulerModule,
47
50
  generateConfigModule,
48
51
  generateDialectModule,
49
52
  generateStorageModule,
@@ -203,6 +206,9 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
203
206
  if (id === VIRTUAL_WAIT_UNTIL_ID) {
204
207
  return RESOLVED_VIRTUAL_WAIT_UNTIL_ID;
205
208
  }
209
+ if (id === VIRTUAL_SCHEDULER_ID) {
210
+ return RESOLVED_VIRTUAL_SCHEDULER_ID;
211
+ }
206
212
  },
207
213
  load(id: string) {
208
214
  if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
@@ -271,6 +277,12 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
271
277
  if (id === RESOLVED_VIRTUAL_WAIT_UNTIL_ID) {
272
278
  return generateWaitUntilModule(astroConfig.adapter?.name);
273
279
  }
280
+ // Generate scheduler module — a NodeCronScheduler factory on
281
+ // long-lived runtimes, or null under the Cloudflare adapter where
282
+ // a Cron Trigger drives scheduled work instead.
283
+ if (id === RESOLVED_VIRTUAL_SCHEDULER_ID) {
284
+ return generateSchedulerModule(astroConfig.adapter?.name, viteCommand);
285
+ }
274
286
  },
275
287
  };
276
288
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Stream-end metrics
3
+ *
4
+ * Server-Timing db.* counters are snapshotted when middleware's next()
5
+ * returns — but at that point only the response *headers* are final.
6
+ * Astro streams the body afterwards, and components rendered during
7
+ * streaming issue further DB queries that the headers can never report.
8
+ *
9
+ * This module wraps the response body in an identity TransformStream and
10
+ * snapshots the request metrics in flush(), i.e. when the body actually
11
+ * finishes streaming. The metrics object lives on the request context
12
+ * (AsyncLocalStorage) and is mutated in-place by the Kysely log hook, so
13
+ * a reference captured before wrapping observes every post-header query.
14
+ * The snapshot is emitted as prefixed NDJSON on stdout (same transport as
15
+ * [emdash-query-log] — console.log works in both Node and workerd).
16
+ *
17
+ * Gated on isInstrumentationEnabled() (EMDASH_QUERY_LOG=1): zero overhead
18
+ * in normal production traffic.
19
+ */
20
+
21
+ import { isInstrumentationEnabled } from "../../database/instrumentation.js";
22
+ import { getRequestContext } from "../../request-context.js";
23
+
24
+ export const STREAM_END_PREFIX = "[emdash-stream-end]";
25
+
26
+ /**
27
+ * Astro attaches AstroCookies to outgoing responses via a well-known global
28
+ * symbol. Constructing a new Response drops non-header metadata, so the
29
+ * symbol must be forwarded explicitly or `cookies.set()` calls are silently
30
+ * dropped. Same pattern as finalizeResponse in ../middleware.ts.
31
+ */
32
+ const ASTRO_COOKIES_SYMBOL = Symbol.for("astro.cookies");
33
+
34
+ /** Shape of the NDJSON snapshot emitted when the body finishes streaming. */
35
+ export interface StreamEndSnapshot {
36
+ route?: string;
37
+ method?: string;
38
+ phase?: string;
39
+ /** Total elapsed ms from middleware entry to end of body streaming. */
40
+ totalMs: number;
41
+ dbCount: number;
42
+ dbTotalMs: number;
43
+ dbFirstOffset: number | null;
44
+ dbLastOffset: number | null;
45
+ cacheHits: number;
46
+ cacheMisses: number;
47
+ }
48
+
49
+ /**
50
+ * Wrap a response body so the FINAL request metrics are emitted when the
51
+ * body finishes streaming. Returns the response unchanged when
52
+ * instrumentation is disabled, the body is null, or no request metrics
53
+ * are attached (e.g. outside the middleware's ALS context).
54
+ */
55
+ export function wrapBodyForStreamMetrics(response: Response): Response {
56
+ if (!isInstrumentationEnabled()) return response;
57
+ if (!response.body) return response;
58
+
59
+ // Capture the context's metrics object BEFORE wrapping: flush() runs
60
+ // after the middleware's ALS frame may have exited, but the object
61
+ // reference stays live and is mutated in-place by the Kysely log hook.
62
+ const ctx = getRequestContext();
63
+ const metrics = ctx?.metrics;
64
+ if (!metrics) return response;
65
+ const recorder = ctx?.queryRecorder;
66
+
67
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
68
+ flush() {
69
+ const snapshot: StreamEndSnapshot = {
70
+ route: recorder?.route,
71
+ method: recorder?.method,
72
+ phase: recorder?.phase,
73
+ totalMs: performance.now() - metrics.start,
74
+ dbCount: metrics.dbCount,
75
+ dbTotalMs: metrics.dbTotalMs,
76
+ dbFirstOffset: metrics.dbFirstOffset,
77
+ dbLastOffset: metrics.dbLastOffset,
78
+ cacheHits: metrics.cacheHits,
79
+ cacheMisses: metrics.cacheMisses,
80
+ };
81
+ console.log(`${STREAM_END_PREFIX} ${JSON.stringify(snapshot)}`);
82
+ },
83
+ });
84
+
85
+ const wrapped = new Response(response.body.pipeThrough(transform), response);
86
+ const astroCookies = Reflect.get(response, ASTRO_COOKIES_SYMBOL);
87
+ if (astroCookies !== undefined) {
88
+ Reflect.set(wrapped, ASTRO_COOKIES_SYMBOL, astroCookies);
89
+ }
90
+ // The identity transform preserves byte counts today, but a stale
91
+ // Content-Length on a re-constructed streaming Response risks
92
+ // truncation if anything upstream changes; the header is recomputed
93
+ // (or chunked encoding used) by the server layer anyway.
94
+ wrapped.headers.delete("Content-Length");
95
+ return wrapped;
96
+ }