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
@@ -22,9 +22,12 @@ import { getAuthMode } from "./auth/mode.js";
22
22
  import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
23
23
  import { isSqlite } from "./database/dialect-helpers.js";
24
24
  import { kyselyLogOption } from "./database/instrumentation.js";
25
- import { runMigrations } from "./database/migrations/runner.js";
25
+ import { MIGRATION_RACE_WAIT_MS, runMigrations } from "./database/migrations/runner.js";
26
26
  import { RevisionRepository } from "./database/repositories/revision.js";
27
- import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
27
+ import type {
28
+ ContentItem as ContentItemInternal,
29
+ ContentDateField,
30
+ } from "./database/repositories/types.js";
28
31
  import { validateIdentifier } from "./database/validate.js";
29
32
  import { normalizeMediaValue } from "./media/normalize.js";
30
33
  import type { MediaProvider, MediaProviderCapabilities } from "./media/types.js";
@@ -41,6 +44,7 @@ import type {
41
44
  } from "./plugins/types.js";
42
45
  import type { FieldType } from "./schema/types.js";
43
46
  import { hashString } from "./utils/hash.js";
47
+ import { createInitLock, type InitLock, initWithLock } from "./utils/init-lock.js";
44
48
  import { COMMIT, VERSION } from "./version.js";
45
49
 
46
50
  const LEADING_SLASH_PATTERN = /^\//;
@@ -114,6 +118,7 @@ import { validateEncryptionKeyAtStartup } from "./config/secrets.js";
114
118
  import { OptionsRepository } from "./database/repositories/options.js";
115
119
  import {
116
120
  handleContentList,
121
+ handleContentAuthors,
117
122
  handleContentGet,
118
123
  handleContentGetIncludingTrashed,
119
124
  handleContentCreate,
@@ -157,13 +162,12 @@ import {
157
162
  import { normalizeManifestRoute } from "./plugins/manifest-schema.js";
158
163
  import { extractRequestMeta, sanitizeHeadersForSandbox } from "./plugins/request-meta.js";
159
164
  import { PluginRouteRegistry, type RouteMeta } from "./plugins/routes.js";
160
- import { NodeCronScheduler } from "./plugins/scheduler/node.js";
161
- import { PiggybackScheduler } from "./plugins/scheduler/piggyback.js";
162
165
  import type { CronScheduler } from "./plugins/scheduler/types.js";
163
166
  import { PluginStateRepository } from "./plugins/state.js";
164
167
  import { normalizeRegistryConfig } from "./registry/config.js";
165
168
  import { requestCached } from "./request-cache.js";
166
169
  import { getRequestContext } from "./request-context.js";
170
+ import { publishDueContent, type PublishedRef } from "./scheduled-publish.js";
167
171
  import { FTSManager } from "./search/fts-manager.js";
168
172
  import { invalidateSiteSettingsCache } from "./settings/index.js";
169
173
 
@@ -236,6 +240,13 @@ export interface MediaProviderContext {
236
240
  storage: Storage | null;
237
241
  }
238
242
 
243
+ /**
244
+ * Builds the timer-based scheduler that drives cron ticks and maintenance.
245
+ * Injected via `virtual:emdash/scheduler` so the platform — not core — decides
246
+ * whether a long-lived heartbeat exists.
247
+ */
248
+ export type CreateSchedulerFn = (executor: CronExecutor) => CronScheduler;
249
+
239
250
  /**
240
251
  * Dependencies injected from virtual modules (middleware reads these)
241
252
  */
@@ -249,6 +260,16 @@ export interface RuntimeDependencies {
249
260
  sandboxEnabled: boolean;
250
261
  /** sandbox: false escape hatch - load sandboxed plugins in-process */
251
262
  sandboxBypassed?: boolean;
263
+ /**
264
+ * Factory for the timer-based cron/maintenance heartbeat. Supplied by the
265
+ * generated `virtual:emdash/scheduler` module: a `NodeCronScheduler` factory
266
+ * on long-lived runtimes (Node/Bun), or `null` on serverless adapters where
267
+ * an external driver (e.g. the Cloudflare Worker's `scheduled()` Cron
268
+ * Trigger) calls `runScheduledTasks()` instead. When absent or null, the
269
+ * runtime starts no scheduler. Keeping the platform decision in the
270
+ * integration means core has no adapter-specific runtime checks.
271
+ */
272
+ createScheduler?: CreateSchedulerFn | null;
252
273
  /** Media provider entries from virtual module */
253
274
  mediaProviderEntries?: MediaProviderEntry[];
254
275
  sandboxedPluginEntries: SandboxedPluginEntry[];
@@ -310,9 +331,38 @@ function contentItemToRecord(item: ContentItemInternal): Record<string, unknown>
310
331
  return { ...item };
311
332
  }
312
333
 
313
- // Module-level caches (persist across requests within worker)
314
- const dbCache = new Map<string, Kysely<Database>>();
315
- let dbInitPromise: Promise<Kysely<Database>> | null = null;
334
+ /**
335
+ * Db init lock reclaim deadline. Derived from the migration race wait so
336
+ * they can't drift apart: a healthy init can legitimately block for the
337
+ * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold
338
+ * connect and migrator work, before it should be presumed dead. The outer
339
+ * runtime init lock (middleware.ts) must use a strictly larger deadline —
340
+ * it wraps create() → getDatabase() → this lock, and equal deadlines would
341
+ * let the outer reclaim while the inner is legitimately still working.
342
+ */
343
+ export const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;
344
+
345
+ /**
346
+ * Db cache + its init lock live on globalThis behind a Symbol: the bundler
347
+ * can duplicate this module across SSR chunks (same reasoning as
348
+ * request-cache.ts), and a duplicated cache/lock would mean concurrent
349
+ * independent db inits — and duplicate migrators — per isolate.
350
+ */
351
+ const DB_HOLDER_KEY = Symbol.for("emdash:db-cache");
352
+ interface DbHolder {
353
+ cache: Map<string, Kysely<Database>>;
354
+ lock: InitLock;
355
+ }
356
+ const globalSymbolStore = globalThis as Record<symbol, unknown>;
357
+ function getDbHolder(): DbHolder {
358
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below
359
+ let holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;
360
+ if (!holder) {
361
+ holder = { cache: new Map<string, Kysely<Database>>(), lock: createInitLock() };
362
+ globalSymbolStore[DB_HOLDER_KEY] = holder;
363
+ }
364
+ return holder;
365
+ }
316
366
  const storageCache = new Map<string, Storage>();
317
367
  const sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();
318
368
  /**
@@ -449,14 +499,65 @@ export class EmDashRuntime {
449
499
  }
450
500
 
451
501
  /**
452
- * Tick the cron system from request context (piggyback mode).
453
- * Call this from middleware on each request to ensure cron tasks
454
- * execute even when no dedicated scheduler is available.
502
+ * Publish any content whose scheduled time has passed.
503
+ * Returns the items promoted so callers can invalidate their cache tags.
455
504
  */
456
- tickCron(): void {
457
- if (this.cronScheduler instanceof PiggybackScheduler) {
458
- this.cronScheduler.onRequest();
505
+ async publishScheduled(): Promise<PublishedRef[]> {
506
+ return publishDueContent(this.db, {
507
+ publish: (collection, id, options) => this.handleContentPublish(collection, id, options),
508
+ });
509
+ }
510
+
511
+ /**
512
+ * Run the full scheduled-maintenance batch: cron tasks, scheduled
513
+ * publishing, and system cleanup. For request-less drivers — the
514
+ * Cloudflare `scheduled()` handler invokes this from a Cron Trigger.
515
+ * (On Node the timer-based scheduler drives the same work itself.)
516
+ *
517
+ * Each step is independent and non-fatal. Returns the content promoted
518
+ * by the publishing sweep so the caller can purge edge-cache tags.
519
+ *
520
+ * `onPublished` (optional) is awaited after each collection's batch so a
521
+ * request-less driver can invalidate edge-cache tags incrementally rather
522
+ * than only after the whole sweep — bounding stale-cache exposure if the
523
+ * runtime is killed mid-sweep.
524
+ */
525
+ async runScheduledTasks(
526
+ options: {
527
+ onPublished?: (refs: PublishedRef[]) => Promise<void>;
528
+ } = {},
529
+ ): Promise<{ published: PublishedRef[] }> {
530
+ if (this.cronExecutor) {
531
+ try {
532
+ await this.cronExecutor.tick();
533
+ } catch (error) {
534
+ console.error("[cron] Tick failed:", error);
535
+ }
536
+ try {
537
+ await this.cronExecutor.recoverStaleLocks();
538
+ } catch (error) {
539
+ console.error("[cron] Stale lock recovery failed:", error);
540
+ }
459
541
  }
542
+
543
+ let published: PublishedRef[] = [];
544
+ try {
545
+ // Route through the runtime wrapper so content:afterPublish hooks fire.
546
+ published = await publishDueContent(this.db, {
547
+ publish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),
548
+ onPublished: options.onPublished,
549
+ });
550
+ } catch (error) {
551
+ console.error("[scheduled-publish] Sweep failed:", error);
552
+ }
553
+
554
+ try {
555
+ await runSystemCleanup(this.db, this.storage ?? undefined);
556
+ } catch (error) {
557
+ console.error("[cleanup] System cleanup failed:", error);
558
+ }
559
+
560
+ return { published };
460
561
  }
461
562
 
462
563
  /**
@@ -887,19 +988,45 @@ export class EmDashRuntime {
887
988
  // Initialize storage (sync)
888
989
  const storage = EmDashRuntime.getStorage(deps);
889
990
 
890
- // Fetch plugin states from database
991
+ // Fetch plugin states and site info concurrently — independent reads
992
+ // against different tables (_plugin_state vs options), so they share
993
+ // one round-trip window instead of paying two sequential ones. Each
994
+ // phase() wrapper still records that phase's own duration, and each
995
+ // body keeps its own non-fatal catch.
891
996
  let pluginStates: Map<string, string> = new Map();
892
- await phase("rt.plugins", "Plugin states", async () => {
893
- try {
894
- const states = await db
895
- .selectFrom("_plugin_state")
896
- .select(["plugin_id", "status"])
897
- .execute();
898
- pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
899
- } catch {
900
- // Plugin state table may not exist yet
901
- }
902
- });
997
+ let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
998
+ await Promise.all([
999
+ // Fetch plugin states from database
1000
+ phase("rt.plugins", "Plugin states", async () => {
1001
+ try {
1002
+ const states = await db
1003
+ .selectFrom("_plugin_state")
1004
+ .select(["plugin_id", "status"])
1005
+ .execute();
1006
+ pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
1007
+ } catch {
1008
+ // Plugin state table may not exist yet
1009
+ }
1010
+ }),
1011
+ // Load site info for plugin context extensions (1 batch query instead of 3)
1012
+ phase("rt.site", "Site info options", async () => {
1013
+ try {
1014
+ const optionsRepo = new OptionsRepository(db);
1015
+ const siteOpts = await optionsRepo.getMany<string>([
1016
+ "emdash:site_title",
1017
+ "emdash:site_url",
1018
+ "emdash:locale",
1019
+ ]);
1020
+ siteInfo = {
1021
+ siteName: siteOpts.get("emdash:site_title") ?? undefined,
1022
+ siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
1023
+ locale: siteOpts.get("emdash:locale") ?? undefined,
1024
+ };
1025
+ } catch {
1026
+ // Options table may not exist yet (pre-setup)
1027
+ }
1028
+ }),
1029
+ ]);
903
1030
 
904
1031
  // Build set of enabled plugins
905
1032
  const enabledPlugins = new Set<string>();
@@ -910,26 +1037,6 @@ export class EmDashRuntime {
910
1037
  }
911
1038
  }
912
1039
 
913
- // Load site info for plugin context extensions (1 batch query instead of 3)
914
- let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
915
- await phase("rt.site", "Site info options", async () => {
916
- try {
917
- const optionsRepo = new OptionsRepository(db);
918
- const siteOpts = await optionsRepo.getMany<string>([
919
- "emdash:site_title",
920
- "emdash:site_url",
921
- "emdash:locale",
922
- ]);
923
- siteInfo = {
924
- siteName: siteOpts.get("emdash:site_title") ?? undefined,
925
- siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
926
- locale: siteOpts.get("emdash:locale") ?? undefined,
927
- };
928
- } catch {
929
- // Options table may not exist yet (pre-setup)
930
- }
931
- });
932
-
933
1040
  // Build the full list of pipeline-eligible plugins: all configured
934
1041
  // plugins (regardless of current enabled status) plus built-in plugins.
935
1042
  // rebuildHookPipeline() filters this to only enabled plugins.
@@ -1050,32 +1157,43 @@ export class EmDashRuntime {
1050
1157
  EmDashRuntime.loadSandboxedPlugins(deps, db, storage),
1051
1158
  );
1052
1159
 
1053
- // Cold-start: load marketplace-installed plugins from site R2 via
1054
- // the sandbox runner. In bypass mode this was already handled above.
1160
+ // Cold-start: load marketplace- and registry-installed plugins from
1161
+ // site R2 via the sandbox runner. The two tiers only depend on the
1162
+ // sandbox phase above, not on each other, so when both are enabled
1163
+ // they run concurrently instead of paying two sequential loads.
1164
+ // In bypass mode marketplace plugins were already handled above.
1165
+ const installedTierPhases: Promise<void>[] = [];
1055
1166
  if (deps.config.marketplace && storage && !deps.sandboxBypassed) {
1056
- await phase("rt.market", "Marketplace plugins", () =>
1057
- EmDashRuntime.loadInstalledSandboxedPlugins(
1058
- "marketplace",
1059
- db,
1060
- storage,
1061
- deps,
1062
- sandboxedPlugins,
1167
+ installedTierPhases.push(
1168
+ phase("rt.market", "Marketplace plugins", () =>
1169
+ EmDashRuntime.loadInstalledSandboxedPlugins(
1170
+ "marketplace",
1171
+ db,
1172
+ storage,
1173
+ deps,
1174
+ sandboxedPlugins,
1175
+ ),
1063
1176
  ),
1064
1177
  );
1065
1178
  }
1066
1179
 
1067
1180
  // Cold-start: load registry-installed plugins from site R2
1068
1181
  if (deps.config.experimental?.registry && storage) {
1069
- await phase("rt.registry", "Registry plugins", () =>
1070
- EmDashRuntime.loadInstalledSandboxedPlugins(
1071
- "registry",
1072
- db,
1073
- storage,
1074
- deps,
1075
- sandboxedPlugins,
1182
+ installedTierPhases.push(
1183
+ phase("rt.registry", "Registry plugins", () =>
1184
+ EmDashRuntime.loadInstalledSandboxedPlugins(
1185
+ "registry",
1186
+ db,
1187
+ storage,
1188
+ deps,
1189
+ sandboxedPlugins,
1190
+ ),
1076
1191
  ),
1077
1192
  );
1078
1193
  }
1194
+ if (installedTierPhases.length > 0) {
1195
+ await Promise.all(installedTierPhases);
1196
+ }
1079
1197
 
1080
1198
  // Initialize media providers
1081
1199
  const mediaProviders = new Map<string, MediaProvider>();
@@ -1126,6 +1244,11 @@ export class EmDashRuntime {
1126
1244
 
1127
1245
  let cronExecutor: CronExecutor | null = null;
1128
1246
  let cronScheduler: CronScheduler | null = null;
1247
+ // Populated with the constructed runtime just before this method returns,
1248
+ // so the timer scheduler's cleanup can route scheduled publishing through
1249
+ // the runtime wrapper (firing content:afterPublish hooks). The first tick
1250
+ // is ≥1s out, well after the synchronous assignment below.
1251
+ const runtimeRef: { current: EmDashRuntime | null } = { current: null };
1129
1252
 
1130
1253
  await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
1131
1254
  try {
@@ -1151,46 +1274,57 @@ export class EmDashRuntime {
1151
1274
  }
1152
1275
  });
1153
1276
 
1154
- // Detect platform and create appropriate scheduler.
1155
- // On Cloudflare Workers, setTimeout is available but unreliable for
1156
- // long durations use PiggybackScheduler as default.
1157
- // In Node/Bun, use NodeCronScheduler with real timers.
1158
- const isWorkersRuntime =
1159
- typeof globalThis.navigator !== "undefined" &&
1160
- globalThis.navigator.userAgent === "Cloudflare-Workers";
1161
-
1162
- if (isWorkersRuntime) {
1163
- cronScheduler = new PiggybackScheduler(cronExecutor);
1164
- } else {
1165
- cronScheduler = new NodeCronScheduler(cronExecutor);
1166
- }
1167
-
1168
- // Register system cleanup to run alongside each scheduler tick.
1169
- // Pass storage so cleanupPendingUploads can delete orphaned files.
1170
- cronScheduler.setSystemCleanup(async () => {
1171
- try {
1172
- await runSystemCleanup(db, storage ?? undefined);
1173
- } catch (error) {
1174
- // Non-fatal -- individual cleanup failures are already logged
1175
- // by runSystemCleanup. This catches unexpected errors.
1176
- console.error("[cleanup] System cleanup failed:", error);
1177
- }
1178
- });
1277
+ // The platform decides whether a long-lived timer heartbeat exists.
1278
+ // `createScheduler` is injected by the generated virtual:emdash/scheduler
1279
+ // module: a NodeCronScheduler factory on Node/Bun, or null on serverless
1280
+ // adapters (e.g. Cloudflare) where the Worker's `scheduled()` handler
1281
+ // drives runScheduledTasks() instead. No adapter check lives here.
1282
+ if (deps.createScheduler) {
1283
+ const scheduler = deps.createScheduler(cronExecutor);
1284
+ cronScheduler = scheduler;
1285
+
1286
+ // Run scheduled publishing and system cleanup alongside each tick.
1287
+ // Pass storage so cleanupPendingUploads can delete orphaned files.
1288
+ scheduler.setSystemCleanup(async () => {
1289
+ try {
1290
+ // Route through the runtime so content:afterPublish hooks fire.
1291
+ // Falls back to the raw handler if (improbably) the tick beats
1292
+ // the post-construction ref assignment.
1293
+ const runtime = runtimeRef.current;
1294
+ await publishDueContent(db, {
1295
+ publish: runtime
1296
+ ? (collection, id, options) =>
1297
+ runtime.handleContentPublish(collection, id, options)
1298
+ : undefined,
1299
+ });
1300
+ } catch (error) {
1301
+ console.error("[scheduled-publish] Sweep failed:", error);
1302
+ }
1303
+ try {
1304
+ await runSystemCleanup(db, storage ?? undefined);
1305
+ } catch (error) {
1306
+ // Non-fatal -- individual cleanup failures are already logged
1307
+ // by runSystemCleanup. This catches unexpected errors.
1308
+ console.error("[cleanup] System cleanup failed:", error);
1309
+ }
1310
+ });
1179
1311
 
1180
- // Add cron reschedule callback (merges with existing factory options)
1181
- pipeline.setContextFactory({
1182
- cronReschedule: () => cronScheduler?.reschedule(),
1183
- });
1312
+ // Add cron reschedule callback (merges with existing factory options)
1313
+ pipeline.setContextFactory({
1314
+ cronReschedule: () => cronScheduler?.reschedule(),
1315
+ });
1184
1316
 
1185
- // Start the scheduler
1186
- await cronScheduler.start();
1317
+ // start() is void on the timer scheduler but the interface
1318
+ // allows a promise (alarm-backed schedulers); we don't block on it.
1319
+ void scheduler.start();
1320
+ }
1187
1321
  } catch (error) {
1188
1322
  console.warn("[cron] Failed to initialize cron system:", error);
1189
1323
  // Non-fatal — CMS works without cron
1190
1324
  }
1191
1325
  });
1192
1326
 
1193
- return new EmDashRuntime({
1327
+ const runtime = new EmDashRuntime({
1194
1328
  db,
1195
1329
  storage,
1196
1330
  // Include bypassed sandboxed plugins in configuredPlugins so route
@@ -1213,6 +1347,10 @@ export class EmDashRuntime {
1213
1347
  runtimeDeps: deps,
1214
1348
  pipelineRef,
1215
1349
  });
1350
+ // Hand the constructed instance to the scheduler-cleanup closure so the
1351
+ // timer-driven sweep can fire publish hooks (see runtimeRef above).
1352
+ runtimeRef.current = runtime;
1353
+ return runtime;
1216
1354
  }
1217
1355
 
1218
1356
  /**
@@ -1270,83 +1408,86 @@ export class EmDashRuntime {
1270
1408
 
1271
1409
  const cacheKey = dbConfig.entrypoint;
1272
1410
 
1273
- // Return cached instance if available
1274
- const cached = dbCache.get(cacheKey);
1275
- if (cached) {
1276
- return cached;
1277
- }
1278
-
1279
- // Use initialization lock to prevent race conditions.
1280
- // Sharing this promise across requests is safe because the Kysely instance
1281
- // doesn't hold a request-scoped resource — the DO dialect uses a getStub()
1282
- // factory that creates a fresh stub per query execution.
1283
- if (dbInitPromise) {
1284
- return dbInitPromise;
1285
- }
1286
-
1287
- dbInitPromise = (async () => {
1288
- const dialect = deps.createDialect(dbConfig.config);
1289
- const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
1290
-
1291
- await runMigrations(db);
1292
-
1293
- // Note: legacy installs may carry a stray `emdash:manifest_cache`
1294
- // row in the options table from versions that persisted a JSON
1295
- // manifest. The runtime no longer reads or writes it. We do not
1296
- // proactively delete it: the row is a few hundred bytes of dead
1297
- // weight and is never on the read path, whereas a one-shot
1298
- // cleanup-flag check costs an extra `options.get()` on every
1299
- // isolate cold boot forever. Cheaper to leave it.
1300
-
1301
- // Auto-seed schema if no collections exist and setup hasn't run.
1302
- // This covers first-load on sites that skip the setup wizard.
1303
- // Dev-bypass and the wizard apply seeds explicitly.
1304
- try {
1305
- const [collectionCount, setupOption] = await Promise.all([
1306
- db
1307
- .selectFrom("_emdash_collections")
1308
- .select((eb) => eb.fn.countAll<number>().as("count"))
1309
- .executeTakeFirstOrThrow(),
1310
- db
1311
- .selectFrom("options")
1312
- .select("value")
1313
- .where("name", "=", "emdash:setup_complete")
1314
- .executeTakeFirst(),
1315
- ]);
1316
-
1317
- const setupDone = (() => {
1318
- try {
1319
- return setupOption && JSON.parse(setupOption.value) === true;
1320
- } catch {
1321
- return false;
1322
- }
1323
- })();
1324
-
1325
- if (collectionCount.count === 0 && !setupDone) {
1326
- const { applySeed } = await import("./seed/apply.js");
1327
- const { loadSeed } = await import("./seed/load.js");
1328
- const { validateSeed } = await import("./seed/validate.js");
1329
-
1330
- const seed = await loadSeed();
1331
- const validation = validateSeed(seed);
1332
- if (validation.valid) {
1333
- await applySeed(db, seed, { onConflict: "skip" });
1334
- console.log("Auto-seeded default collections");
1411
+ // Waiters poll the cache rather than sharing the initializing request's
1412
+ // promise: if the request that owns the init is cancelled mid-await
1413
+ // (e.g. client disconnect during cold migrations), a shared promise
1414
+ // never settles — and the owner's `finally` that would clear it never
1415
+ // runs — deadlocking every later request in the isolate. Prevention:
1416
+ // the in-flight init is anchored via after()/waitUntil so a cancelled
1417
+ // owner's init still completes and populates the cache. Net: a stale
1418
+ // lock is reclaimed after a deadline.
1419
+ const holder = getDbHolder();
1420
+ return initWithLock(
1421
+ holder.lock,
1422
+ () => holder.cache.get(cacheKey),
1423
+ async (isCurrentClaim) => {
1424
+ const dialect = deps.createDialect(dbConfig.config);
1425
+ const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
1426
+
1427
+ await runMigrations(db);
1428
+
1429
+ // Note: legacy installs may carry a stray `emdash:manifest_cache`
1430
+ // row in the options table from versions that persisted a JSON
1431
+ // manifest. The runtime no longer reads or writes it. We do not
1432
+ // proactively delete it: the row is a few hundred bytes of dead
1433
+ // weight and is never on the read path, whereas a one-shot
1434
+ // cleanup-flag check costs an extra `options.get()` on every
1435
+ // isolate cold boot forever. Cheaper to leave it.
1436
+
1437
+ // Auto-seed schema if no collections exist and setup hasn't run.
1438
+ // This covers first-load on sites that skip the setup wizard.
1439
+ // Dev-bypass and the wizard apply seeds explicitly.
1440
+ try {
1441
+ const [collectionCount, setupOption] = await Promise.all([
1442
+ db
1443
+ .selectFrom("_emdash_collections")
1444
+ .select((eb) => eb.fn.countAll<number>().as("count"))
1445
+ .executeTakeFirstOrThrow(),
1446
+ db
1447
+ .selectFrom("options")
1448
+ .select("value")
1449
+ .where("name", "=", "emdash:setup_complete")
1450
+ .executeTakeFirst(),
1451
+ ]);
1452
+
1453
+ const setupDone = (() => {
1454
+ try {
1455
+ return setupOption && JSON.parse(setupOption.value) === true;
1456
+ } catch {
1457
+ return false;
1458
+ }
1459
+ })();
1460
+
1461
+ if (collectionCount.count === 0 && !setupDone) {
1462
+ const { applySeed } = await import("./seed/apply.js");
1463
+ const { loadSeed } = await import("./seed/load.js");
1464
+ const { validateSeed } = await import("./seed/validate.js");
1465
+
1466
+ const seed = await loadSeed();
1467
+ const validation = validateSeed(seed);
1468
+ if (validation.valid) {
1469
+ await applySeed(db, seed, { onConflict: "skip" });
1470
+ console.log("Auto-seeded default collections");
1471
+ }
1335
1472
  }
1473
+ } catch {
1474
+ // Tables may not exist yet. Non-fatal.
1336
1475
  }
1337
- } catch {
1338
- // Tables may not exist yet. Non-fatal.
1339
- }
1340
1476
 
1341
- dbCache.set(cacheKey, db);
1342
- return db;
1343
- })();
1344
-
1345
- try {
1346
- return await dbInitPromise;
1347
- } finally {
1348
- dbInitPromise = null;
1349
- }
1477
+ // Publish only while still the current owner: a reclaimed slow
1478
+ // init must not flip the cached Kysely identity back after the
1479
+ // reclaimer has published its own. The unpublished instance is
1480
+ // still returned and fully valid for the request that built it.
1481
+ if (isCurrentClaim()) {
1482
+ holder.cache.set(cacheKey, db);
1483
+ }
1484
+ return db;
1485
+ },
1486
+ {
1487
+ deadlineMs: DB_INIT_DEADLINE_MS,
1488
+ anchor: (promise) => after(() => promise),
1489
+ },
1490
+ );
1350
1491
  }
1351
1492
 
1352
1493
  /**
@@ -1778,6 +1919,7 @@ export class EmDashRuntime {
1778
1919
  pipeline,
1779
1920
  isActive: () => true,
1780
1921
  getOption: (key) => optionsRepo.get<string>(key),
1922
+ getOptions: (keys) => optionsRepo.getMany<string>(keys),
1781
1923
  setOption: (key, value) => optionsRepo.set(key, value),
1782
1924
  deleteOption: async (key) => {
1783
1925
  await optionsRepo.delete(key);
@@ -2123,11 +2265,19 @@ export class EmDashRuntime {
2123
2265
  order?: "asc" | "desc";
2124
2266
  locale?: string;
2125
2267
  q?: string;
2268
+ authorId?: string;
2269
+ dateField?: ContentDateField;
2270
+ dateFrom?: string;
2271
+ dateTo?: string;
2126
2272
  },
2127
2273
  ) {
2128
2274
  return handleContentList(this.db, collection, params);
2129
2275
  }
2130
2276
 
2277
+ async handleContentAuthors(collection: string) {
2278
+ return handleContentAuthors(this.db, collection);
2279
+ }
2280
+
2131
2281
  async handleContentGet(collection: string, id: string, locale?: string) {
2132
2282
  const result = await handleContentGet(this.db, collection, id, locale);
2133
2283
  return this.hydrateDraftData(result);
@@ -2502,7 +2652,7 @@ export class EmDashRuntime {
2502
2652
  async handleContentPublish(
2503
2653
  collection: string,
2504
2654
  id: string,
2505
- options: { publishedAt?: string } = {},
2655
+ options: { publishedAt?: string; requireScheduledDue?: boolean } = {},
2506
2656
  ) {
2507
2657
  const result = await handleContentPublish(this.db, collection, id, options);
2508
2658