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
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ export type {
46
46
  // API handlers
47
47
  export {
48
48
  handleContentList,
49
+ handleContentAuthors,
49
50
  handleContentGet,
50
51
  handleContentGetIncludingTrashed,
51
52
  handleContentCreate,
@@ -201,6 +202,8 @@ export {
201
202
  PluginManager,
202
203
  createPluginManager,
203
204
  PluginRouteError,
205
+ // Scheduler (Node timer heartbeat — used by virtual:emdash/scheduler)
206
+ NodeCronScheduler,
204
207
  // Sandbox
205
208
  NoopSandboxRunner,
206
209
  SandboxNotAvailableError,
@@ -253,6 +256,10 @@ export type {
253
256
  CollectionCommentSettings,
254
257
  StoredComment,
255
258
 
259
+ // Scheduler types
260
+ CronScheduler,
261
+ SystemCleanupFn,
262
+
256
263
  // Sandbox runtime types
257
264
  SandboxRunner,
258
265
  SandboxedPluginInstance,
@@ -406,7 +413,7 @@ export type {
406
413
  } from "./menus/types.js";
407
414
 
408
415
  // Bylines
409
- export { getByline, getBylineBySlug } from "./bylines/index.js";
416
+ export { getByline, getBylineBySlug, getEntriesByByline } from "./bylines/index.js";
410
417
  export type { BylineSummary, ContentBylineCredit } from "./database/repositories/types.js";
411
418
 
412
419
  // Taxonomies
package/src/loader.ts CHANGED
@@ -540,12 +540,16 @@ export interface CollectionFilter {
540
540
  */
541
541
  cursor?: string;
542
542
  /**
543
- * Filter by field values, taxonomy terms, or ranges.
543
+ * Filter by field values, taxonomy terms, byline credits, or ranges.
544
544
  *
545
545
  * Taxonomy names are detected automatically and filtered via JOIN.
546
+ * The reserved `byline` key filters by byline credit (any credit, not
547
+ * just the primary one) via the `_emdash_content_bylines` junction
548
+ * table; its value is one or more byline translation groups.
546
549
  * Other keys are treated as column filters on the content table.
547
550
  *
548
551
  * @example { category: 'news' } - taxonomy term
552
+ * @example { byline: '01HXYZ...' } - entries credited to a byline (any position)
549
553
  * @example { series: 'main' } - exact match on a content field
550
554
  * @example { published_at: { gte: '2024-01-01', lt: '2025-01-01' } } - date range
551
555
  */
@@ -673,13 +677,16 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
673
677
 
674
678
  // Build cursor condition if cursor is provided
675
679
  const cursorCondition = cursor ? buildCursorCondition(cursor, orderBy) : null;
676
- const cursorConditionPrefixed = cursor
677
- ? buildCursorCondition(cursor, orderBy, tableName)
678
- : null;
679
680
 
680
- // Separate taxonomy filters from field filters
681
+ // Separate taxonomy / byline filters from field filters
681
682
  let result: { rows: Record<string, unknown>[] };
682
683
  let taxonomyFilter: { name: string; slugs: string[] } | null = null;
684
+ // A byline filter matches entries credited to any of the given
685
+ // byline translation groups via the `_emdash_content_bylines`
686
+ // junction table. `null` means no byline filter; an empty
687
+ // `groups` array means the filter was requested but matches
688
+ // nothing (short-circuited to an empty result below).
689
+ let bylineFilter: { groups: string[] } | null = null;
683
690
  const fieldFilters: Record<string, WhereValue> = {};
684
691
 
685
692
  if (where && Object.keys(where).length > 0) {
@@ -687,7 +694,16 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
687
694
 
688
695
  for (const [key, value] of Object.entries(where)) {
689
696
  if (value == null) continue;
690
- if (taxNames.has(key)) {
697
+ if (key === "byline") {
698
+ if (isWhereRange(value)) {
699
+ console.warn(
700
+ `[emdash] where filter: range operators are not supported on "byline", ignored`,
701
+ );
702
+ continue;
703
+ }
704
+ const groups = Array.isArray(value) ? value : [value];
705
+ bylineFilter = { groups };
706
+ } else if (taxNames.has(key)) {
691
707
  if (isWhereRange(value)) {
692
708
  console.warn(
693
709
  `[emdash] where filter: range operators are not supported on taxonomy "${key}", ignored`,
@@ -708,35 +724,27 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
708
724
  }
709
725
  }
710
726
 
711
- if (taxonomyFilter) {
712
- const orderByClause = buildOrderByClause(orderBy, tableName);
713
- const statusCondition = buildStatusCondition(db, status, tableName);
714
- const localeCondition = locale
715
- ? sql`AND ${sql.ref(tableName)}.locale = ${locale}`
716
- : sql``;
717
- const cursorCond = cursorConditionPrefixed ? sql`AND ${cursorConditionPrefixed}` : sql``;
718
- const fieldConds = buildFieldConditions(fieldFilters, tableName);
719
- const fieldCondsSQL =
720
- fieldConds.length > 0 ? sql`${sql.join(fieldConds, sql` AND `)}` : null;
727
+ // A byline or taxonomy filter with no values matches nothing —
728
+ // short-circuit before building SQL (an empty `IN ()` is invalid
729
+ // SQL on both dialects).
730
+ if (
731
+ (bylineFilter && bylineFilter.groups.length === 0) ||
732
+ (taxonomyFilter && taxonomyFilter.slugs.length === 0)
733
+ ) {
734
+ return { entries: [], cacheHint: { tags: [type] } };
735
+ }
721
736
 
722
- result = await sql<Record<string, unknown>>`
723
- SELECT DISTINCT ${sql.ref(tableName)}.* FROM ${sql.ref(tableName)}
724
- INNER JOIN content_taxonomies ct
725
- ON ct.collection = ${type}
726
- AND ct.entry_id = ${sql.ref(tableName)}.id
727
- INNER JOIN taxonomies t
728
- ON t.id = ct.taxonomy_id
729
- WHERE ${sql.ref(tableName)}.deleted_at IS NULL
730
- AND ${statusCondition}
731
- ${localeCondition}
732
- ${cursorCond}
733
- AND t.name = ${taxonomyFilter.name}
734
- AND t.slug IN (${sql.join(taxonomyFilter.slugs.map((s) => sql`${s}`))})
735
- ${fieldCondsSQL ? sql`AND ${fieldCondsSQL}` : sql``}
736
- ${orderByClause}
737
- ${fetchLimit ? sql`LIMIT ${fetchLimit}` : sql``}
738
- `.execute(db);
739
- } else {
737
+ {
738
+ // Taxonomy and byline filters are applied as correlated
739
+ // `EXISTS` semi-joins rather than `INNER JOIN ... DISTINCT`.
740
+ // A join fan-out would force `SELECT DISTINCT table.*`, and
741
+ // Postgres cannot apply DISTINCT to a row containing a `json`
742
+ // column (no equality operator), so the join approach throws
743
+ // there. EXISTS matches "credited/tagged at least once"
744
+ // without duplicating rows, needs no DISTINCT, and works on
745
+ // both SQLite and Postgres. The base query stays a single-
746
+ // table `SELECT *`, so all field/status/locale/cursor/order
747
+ // conditions reference unprefixed columns as before.
740
748
  const orderByClause = buildOrderByClause(orderBy);
741
749
  const statusCondition = buildStatusCondition(db, status);
742
750
  const localeFilter = locale ? sql`AND locale = ${locale}` : sql``;
@@ -745,12 +753,37 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
745
753
  const fieldCondsSQL =
746
754
  fieldConds.length > 0 ? sql`${sql.join(fieldConds, sql` AND `)}` : null;
747
755
 
756
+ const taxonomyCond = taxonomyFilter
757
+ ? sql`AND EXISTS (
758
+ SELECT 1 FROM content_taxonomies ct
759
+ INNER JOIN taxonomies t ON t.id = ct.taxonomy_id
760
+ WHERE ct.collection = ${type}
761
+ AND ct.entry_id = ${sql.ref(tableName)}.id
762
+ AND t.name = ${taxonomyFilter.name}
763
+ AND t.slug IN (${sql.join(taxonomyFilter.slugs.map((s) => sql`${s}`))})
764
+ )`
765
+ : sql``;
766
+
767
+ // `_emdash_content_bylines.byline_id` stores the byline's
768
+ // translation_group (migration 040), so a credit spans every
769
+ // locale variant of the byline and we match the group directly.
770
+ const bylineCond = bylineFilter
771
+ ? sql`AND EXISTS (
772
+ SELECT 1 FROM _emdash_content_bylines cb
773
+ WHERE cb.collection_slug = ${type}
774
+ AND cb.content_id = ${sql.ref(tableName)}.id
775
+ AND cb.byline_id IN (${sql.join(bylineFilter.groups.map((g) => sql`${g}`))})
776
+ )`
777
+ : sql``;
778
+
748
779
  result = await sql<Record<string, unknown>>`
749
780
  SELECT * FROM ${sql.ref(tableName)}
750
781
  WHERE deleted_at IS NULL
751
782
  AND ${statusCondition}
752
783
  ${localeFilter}
753
784
  ${cursorCond}
785
+ ${taxonomyCond}
786
+ ${bylineCond}
754
787
  ${fieldCondsSQL ? sql`AND ${fieldCondsSQL}` : sql``}
755
788
  ${orderByClause}
756
789
  ${fetchLimit ? sql`LIMIT ${fetchLimit}` : sql``}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Responsive image helpers shared by the public Image components.
3
+ *
4
+ * These build a `srcset` for locally-stored / R2-stored media by delegating to
5
+ * Astro's configured image service (`astro:assets`). On Cloudflare that is the
6
+ * Images binding; on Node it is sharp; if neither is available it is a no-op
7
+ * passthrough. The calling `.astro` component passes Astro's `getImage` in so
8
+ * this module stays free of the `astro:assets` virtual import (which only
9
+ * resolves inside an Astro project, not in this precompiled package).
10
+ */
11
+
12
+ /** Standard responsive breakpoints. Matches CDN-provider srcset generation. */
13
+ export const RESPONSIVE_BREAKPOINTS = [640, 750, 828, 960, 1080, 1280, 1600, 1920];
14
+
15
+ /** Matches absolute http(s) URLs — the only shape Astro's image services optimize. */
16
+ const ABSOLUTE_HTTP_URL = /^https?:\/\//i;
17
+
18
+ /**
19
+ * Pick the srcset widths to generate for an image rendered at `maxWidth`.
20
+ * Includes breakpoints up to 2x (retina) plus the rendered width itself, so the
21
+ * browser always has an exact-fit candidate.
22
+ */
23
+ export function responsiveWidths(maxWidth: number): number[] {
24
+ const cap = maxWidth * 2;
25
+ const widths = new Set(RESPONSIVE_BREAKPOINTS.filter((w) => w <= cap));
26
+ widths.add(maxWidth);
27
+ return [...widths].toSorted((a, b) => a - b);
28
+ }
29
+
30
+ /** Build the `sizes` attribute for an image with a known display width. */
31
+ export function responsiveSizes(width: number | undefined): string {
32
+ return width ? `(min-width: ${width}px) ${width}px, 100vw` : "100vw";
33
+ }
34
+
35
+ /**
36
+ * Make a same-origin media URL absolute so Astro's image service can optimize it.
37
+ *
38
+ * Astro only optimizes absolute http(s) URLs; a same-origin proxy path like
39
+ * `/_emdash/api/media/file/x.jpg` is otherwise treated as an unoptimizable
40
+ * public asset. Resolving it against the site's public origin (and authorizing
41
+ * that origin via `image.remotePatterns`) lets the service transform it.
42
+ *
43
+ * Only **same-origin** root-relative paths are resolved. Protocol-relative
44
+ * URLs (`//evil.com/x`) and backslash tricks (`/\evil.com`) also start with `/`
45
+ * but resolve to a different origin -- a classic SSRF vector once a
46
+ * remotePattern authorizes the media path -- so anything that escapes the
47
+ * origin is returned unchanged (and then skipped by `buildResponsiveImage`,
48
+ * which only accepts absolute http(s) URLs). Already-absolute URLs (CDN/public
49
+ * bucket) and non-path values (`data:`, `blob:`) are returned unchanged too.
50
+ */
51
+ export function toAbsoluteMediaUrl(src: string, origin: string | undefined): string {
52
+ if (!src || !origin || !src.startsWith("/")) return src;
53
+ try {
54
+ const resolved = new URL(src, origin);
55
+ if (resolved.origin !== new URL(origin).origin) return src;
56
+ return resolved.href;
57
+ } catch {
58
+ return src;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Minimal structural subset of Astro's `getImage`. Astro's `ImageTransform`
64
+ * carries a `[key: string]: any` index signature, so the real `getImage` is
65
+ * assignable to this narrower type.
66
+ */
67
+ export type GetImage = (options: {
68
+ src: string;
69
+ width?: number;
70
+ height?: number;
71
+ widths?: number[];
72
+ sizes?: string;
73
+ }) => Promise<{ src: string; srcSet?: { attribute?: string } | undefined }>;
74
+
75
+ export interface ResponsiveImage {
76
+ src: string;
77
+ srcset?: string;
78
+ sizes?: string;
79
+ }
80
+
81
+ /**
82
+ * Generate a responsive `src`/`srcset`/`sizes` for a media URL via Astro's
83
+ * configured image service.
84
+ *
85
+ * Astro's image services (sharp, Cloudflare `/cdn-cgi/image`, and the default
86
+ * Cloudflare `cloudflare-binding` service) only optimize **absolute** URLs whose
87
+ * host is authorized via `image.domains` / `image.remotePatterns`. Anything else
88
+ * is passed through unchanged, which would yield a useless srcset (the same URL
89
+ * at every width descriptor). We therefore only attempt optimization for
90
+ * absolute http(s) URLs and verify the service actually rewrote the URL.
91
+ *
92
+ * Returns `null` so callers fall back to a plain `<img>` when:
93
+ * - dimensions are unknown (avoids an inferSize fetch on every render),
94
+ * - the URL is relative (a same-origin proxy/public asset Astro won't optimize),
95
+ * - the host isn't authorized (the service passed the URL through unchanged),
96
+ * - no image service is configured / `getImage` throws.
97
+ */
98
+ export async function buildResponsiveImage(
99
+ getImage: GetImage,
100
+ opts: { src: string; width?: number; height?: number },
101
+ ): Promise<ResponsiveImage | null> {
102
+ const { src, width, height } = opts;
103
+ if (!src || !width || !height) return null;
104
+ if (!ABSOLUTE_HTTP_URL.test(src)) return null;
105
+ try {
106
+ const sizes = responsiveSizes(width);
107
+ const result = await getImage({
108
+ src,
109
+ width,
110
+ height,
111
+ widths: responsiveWidths(width),
112
+ sizes,
113
+ });
114
+ // Passthrough: the service returned the source unchanged (unauthorized
115
+ // host or no optimization available). Don't emit a no-op srcset.
116
+ if (!result.src || result.src === src) return null;
117
+ return {
118
+ src: result.src,
119
+ srcset: result.srcSet?.attribute || undefined,
120
+ sizes,
121
+ };
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
@@ -136,15 +136,10 @@ async function buildMenuTree(
136
136
  }
137
137
  }
138
138
 
139
- const urlPatterns = new Map<string, string | null>();
140
- if (collectionSlugs.size > 0) {
141
- const rows = await db
142
- .selectFrom("_emdash_collections")
143
- .select(["slug", "url_pattern"])
144
- .where("slug", "in", [...collectionSlugs])
145
- .execute();
146
- for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
147
- }
139
+ const urlPatterns =
140
+ collectionSlugs.size > 0
141
+ ? await getCollectionUrlPatterns(db, collectionSlugs)
142
+ : new Map<string, string | null>();
148
143
 
149
144
  const resolvedItems = await Promise.all(
150
145
  items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
@@ -173,6 +168,29 @@ async function buildMenuTree(
173
168
  return rootItems;
174
169
  }
175
170
 
171
+ /**
172
+ * Look up the `url_pattern` for a set of collection slugs, request-cached so
173
+ * a page rendering several menus (header, footer, ...) only pays for the
174
+ * lookup once per distinct slug set. Callers must treat the returned map as
175
+ * read-only — it is shared across cache hits within the request.
176
+ */
177
+ function getCollectionUrlPatterns(
178
+ db: Kysely<Database>,
179
+ collectionSlugs: Set<string>,
180
+ ): Promise<Map<string, string | null>> {
181
+ const key = `menu-collection-patterns:${[...collectionSlugs].toSorted().join(",")}`;
182
+ return requestCached(key, async () => {
183
+ const rows = await db
184
+ .selectFrom("_emdash_collections")
185
+ .select(["slug", "url_pattern"])
186
+ .where("slug", "in", [...collectionSlugs])
187
+ .execute();
188
+ const urlPatterns = new Map<string, string | null>();
189
+ for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
190
+ return urlPatterns;
191
+ });
192
+ }
193
+
176
194
  /**
177
195
  * Resolve a single menu item's URL. `reference_id` is a translation_group
178
196
  * (migration 036 remapped all existing references); we join it against
@@ -34,8 +34,9 @@ export type RescheduleFn = () => void;
34
34
  /**
35
35
  * Executes overdue cron tasks.
36
36
  *
37
- * Called by platform-specific schedulers (NodeCronScheduler, EmDashScheduler DO,
38
- * PiggybackScheduler). Stateless all state lives in the database.
37
+ * Called by the platform driver: the NodeCronScheduler timer on Node, or the
38
+ * Worker's `scheduled()` handler (via runScheduledTasks) on Cloudflare.
39
+ * Stateless — all state lives in the database.
39
40
  */
40
41
  export class CronExecutor {
41
42
  constructor(
@@ -1297,6 +1297,13 @@ export interface ExclusiveHookResolutionOptions {
1297
1297
  isActive: (pluginId: string) => boolean;
1298
1298
  /** Read an option value from persistent storage. */
1299
1299
  getOption: (key: string) => Promise<string | null>;
1300
+ /**
1301
+ * Batch-read option values for many keys in a single round trip.
1302
+ * When provided, resolution reads all current selections through this
1303
+ * instead of one getOption() call per hook. Keys absent from the
1304
+ * returned map are treated as unset.
1305
+ */
1306
+ getOptions?: (keys: string[]) => Promise<ReadonlyMap<string, string>>;
1300
1307
  /** Write an option value to persistent storage. */
1301
1308
  setOption: (key: string, value: string) => Promise<void>;
1302
1309
  /** Delete an option from persistent storage. */
@@ -1322,8 +1329,26 @@ const EXCLUSIVE_HOOK_KEY_PREFIX = "emdash:exclusive_hook:";
1322
1329
  * 5. If multiple providers and no hint → leave unselected (admin must choose).
1323
1330
  */
1324
1331
  export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions): Promise<void> {
1325
- const { pipeline, isActive, getOption, setOption, deleteOption, preferredHints } = opts;
1332
+ const { pipeline, isActive, getOption, getOptions, setOption, deleteOption, preferredHints } =
1333
+ opts;
1326
1334
  const exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();
1335
+ if (exclusiveHookNames.length === 0) return;
1336
+
1337
+ // Batch-read current selections in one round trip when the caller
1338
+ // provides a batch reader (1 query instead of N sequential gets).
1339
+ let batchedSelections: ReadonlyMap<string, string> | undefined;
1340
+ if (getOptions) {
1341
+ try {
1342
+ batchedSelections = await getOptions(
1343
+ exclusiveHookNames.map((hookName) => `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`),
1344
+ );
1345
+ } catch {
1346
+ // Options table may not be ready. Matches the per-key tolerance
1347
+ // below: every hook's read would fail, so resolution is skipped
1348
+ // entirely without touching any selection.
1349
+ return;
1350
+ }
1351
+ }
1327
1352
 
1328
1353
  for (const hookName of exclusiveHookNames) {
1329
1354
  const providers = pipeline.getExclusiveHookProviders(hookName);
@@ -1333,11 +1358,15 @@ export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions
1333
1358
 
1334
1359
  const key = `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`;
1335
1360
  let currentSelection: string | null = null;
1336
- try {
1337
- currentSelection = await getOption(key);
1338
- } catch {
1339
- // Options table may not be ready
1340
- continue;
1361
+ if (batchedSelections) {
1362
+ currentSelection = batchedSelections.get(key) ?? null;
1363
+ } else {
1364
+ try {
1365
+ currentSelection = await getOption(key);
1366
+ } catch {
1367
+ // Options table may not be ready
1368
+ continue;
1369
+ }
1341
1370
  }
1342
1371
 
1343
1372
  // If selection exists and the plugin is still active → keep it
@@ -63,6 +63,11 @@ export type { RouteResult, InvokeRouteOptions } from "./routes.js";
63
63
  export { PluginManager, createPluginManager } from "./manager.js";
64
64
  export type { PluginManagerOptions, PluginState } from "./manager.js";
65
65
 
66
+ // Scheduler (Node timer-based heartbeat; consumed by the generated
67
+ // virtual:emdash/scheduler module on non-serverless adapters)
68
+ export { NodeCronScheduler } from "./scheduler/node.js";
69
+ export type { CronScheduler, SystemCleanupFn } from "./scheduler/types.js";
70
+
66
71
  // Sandbox
67
72
  export {
68
73
  NoopSandboxRunner,
@@ -544,6 +544,7 @@ export class PluginManager {
544
544
  pipeline: this.hookPipeline!,
545
545
  isActive: (pluginId) => this.isActive(pluginId),
546
546
  getOption: (key) => optionsRepo.get<string>(key),
547
+ getOptions: (keys) => optionsRepo.getMany<string>(keys),
547
548
  setOption: (key, value) => optionsRepo.set(key, value),
548
549
  deleteOption: async (key) => {
549
550
  await optionsRepo.delete(key);
@@ -15,8 +15,15 @@ import type { CronScheduler, SystemCleanupFn } from "./types.js";
15
15
  /** Minimum polling interval (ms) — prevents tight loops if next_run_at is in the past */
16
16
  const MIN_INTERVAL_MS = 1000;
17
17
 
18
- /** Maximum polling interval (ms) — wake up periodically to check for stale locks */
19
- const MAX_INTERVAL_MS = 5 * 60 * 1000;
18
+ /**
19
+ * Maximum polling interval (ms). Each wake runs the maintenance pass — stale
20
+ * lock recovery *and* the scheduled-publishing sweep + system cleanup. The cap
21
+ * is the worst-case latency for scheduled content when no plugin cron task is
22
+ * due sooner (`getNextDueTime()` only knows about cron tasks, not content
23
+ * `scheduled_at`). Held at 60s so Node publish latency matches the Cloudflare
24
+ * Cron Trigger cadence (`* * * * *`) rather than lagging up to five minutes.
25
+ */
26
+ const MAX_INTERVAL_MS = 60 * 1000;
20
27
 
21
28
  export class NodeCronScheduler implements CronScheduler {
22
29
  private timer: ReturnType<typeof setTimeout> | null = null;
package/src/query.ts CHANGED
@@ -101,13 +101,19 @@ export interface CollectionFilter {
101
101
  */
102
102
  cursor?: string;
103
103
  /**
104
- * Filter by field values, taxonomy terms, or ranges.
104
+ * Filter by field values, taxonomy terms, byline credits, or ranges.
105
105
  *
106
106
  * Taxonomy names are detected automatically and filtered via JOIN.
107
+ * The reserved `byline` key filters by byline credit (any credit, not
108
+ * just the primary one) via the `_emdash_content_bylines` junction
109
+ * table; its value is one or more byline translation groups. This
110
+ * matches co-authored entries, which `primary_byline_id` alone misses.
107
111
  * Other keys are treated as column filters on the content table.
108
112
  *
109
113
  * @example { category: 'news' } - Filter by taxonomy term
110
114
  * @example { category: ['news', 'featured'] } - Filter by multiple terms (OR)
115
+ * @example { byline: '01HXYZ...' } - Entries credited to a byline (any position)
116
+ * @example { byline: ['01HXYZ...', '01HABC...'] } - Credited to any of these bylines (OR)
111
117
  * @example { series: 'main' } - Exact match on a content field
112
118
  * @example { published_at: { gte: '2024-01-01', lt: '2025-01-01' } } - Date range
113
119
  */
@@ -533,7 +539,9 @@ async function getEmDashCollectionUncached<T extends string, D = InferCollection
533
539
  // round-trip cost on remote databases (D1 replicas, etc.).
534
540
  await Promise.all([
535
541
  hydrateEntryBylines(type, entriesWithEdit),
536
- hydrateEntryTerms(type, entriesWithEdit),
542
+ // Hydrate terms in the same locale the content rows were resolved to,
543
+ // otherwise localized entries get default-locale taxonomy terms (#1441).
544
+ hydrateEntryTerms(type, entriesWithEdit, resolvedLocale),
537
545
  ]);
538
546
 
539
547
  return { entries: entriesWithEdit, nextCursor, cacheHint: cacheHint ?? {} };
@@ -611,7 +619,17 @@ export async function getEmDashEntry<T extends string, D = InferCollectionData<T
611
619
  wrapped: ContentEntry<D>,
612
620
  opts: { isPreview: boolean; fallbackLocale?: string; cacheHint: CacheHint },
613
621
  ): Promise<EntryResult<D>> {
614
- await Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);
622
+ // Hydrate terms in the entry's resolved locale (fallback-aware) so a
623
+ // localized entry never picks up default-locale taxonomy terms (#1441).
624
+ // When i18n is disabled we leave the locale unset to preserve the
625
+ // legacy "do not filter by locale" behaviour.
626
+ const termLocale = isI18nEnabled()
627
+ ? dataStr(entryData(wrapped), "locale") || undefined
628
+ : undefined;
629
+ await Promise.all([
630
+ hydrateEntryBylines(type, [wrapped]),
631
+ hydrateEntryTerms(type, [wrapped], termLocale),
632
+ ]);
615
633
  return {
616
634
  entry: wrapped,
617
635
  isPreview: opts.isPreview,
@@ -788,9 +806,18 @@ async function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]):
788
806
  * results and call getEntryTerms() per entry. With hydration, the list page
789
807
  * stays at a single round-trip for term data.
790
808
  *
809
+ * `locale` must be the locale the entries were resolved to. It is forwarded to
810
+ * `getAllTermsForEntries` so terms are returned in the entry's locale rather
811
+ * than falling back to the request-context / default locale (#1441). Pass
812
+ * `undefined` to keep the legacy "do not filter by locale" behaviour.
813
+ *
791
814
  * Fails silently if the taxonomy tables don't exist yet (pre-migration).
792
815
  */
793
- async function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {
816
+ async function hydrateEntryTerms<D>(
817
+ type: string,
818
+ entries: ContentEntry<D>[],
819
+ locale?: string,
820
+ ): Promise<void> {
794
821
  if (entries.length === 0) return;
795
822
 
796
823
  try {
@@ -799,7 +826,7 @@ async function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): P
799
826
  const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
800
827
  if (ids.length === 0) return;
801
828
 
802
- const termsMap = await getAllTermsForEntries(type, ids);
829
+ const termsMap = await getAllTermsForEntries(type, ids, { locale });
803
830
 
804
831
  for (const entry of entries) {
805
832
  const data = entryData(entry);