emdash 0.19.0 → 0.20.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 (403) hide show
  1. package/dist/{adapters-C5AWLJSD.d.mts → adapters-BzIHV3sw.d.mts} +1 -1
  2. package/dist/{adapters-C5AWLJSD.d.mts.map → adapters-BzIHV3sw.d.mts.map} +1 -1
  3. package/dist/{allowed-origins-CyYLEJkp.mjs → allowed-origins-B1u7Qnvg.mjs} +2 -2
  4. package/dist/{allowed-origins-CyYLEJkp.mjs.map → allowed-origins-B1u7Qnvg.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +5 -5
  7. package/dist/api/schemas/index.d.mts +1 -1
  8. package/dist/api/schemas/index.mjs +2 -2
  9. package/dist/{api-BZ6bhjYs.mjs → api-DStv36ik.mjs} +36 -5
  10. package/dist/api-DStv36ik.mjs.map +1 -0
  11. package/dist/{api-tokens-VrXNiNvV.mjs → api-tokens-DPfhPu5V.mjs} +2 -2
  12. package/dist/{api-tokens-VrXNiNvV.mjs.map → api-tokens-DPfhPu5V.mjs.map} +1 -1
  13. package/dist/{apply-hQkKKBCf.mjs → apply-Dr7snAMT.mjs} +7 -7
  14. package/dist/{apply-hQkKKBCf.mjs.map → apply-Dr7snAMT.mjs.map} +1 -1
  15. package/dist/astro/index.d.mts +10 -10
  16. package/dist/astro/index.mjs +3 -3
  17. package/dist/astro/middleware/auth.d.mts +9 -9
  18. package/dist/astro/middleware/auth.mjs +4 -4
  19. package/dist/astro/middleware/redirect.mjs +1 -1
  20. package/dist/astro/middleware/request-context.mjs +1 -1
  21. package/dist/astro/middleware/setup.mjs +1 -1
  22. package/dist/astro/middleware.d.mts +1 -1
  23. package/dist/astro/middleware.d.mts.map +1 -1
  24. package/dist/astro/middleware.mjs +63 -112
  25. package/dist/astro/middleware.mjs.map +1 -1
  26. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +2 -2
  27. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +2 -2
  28. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  29. package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
  30. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
  31. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
  32. package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
  33. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
  34. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +6 -6
  35. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +6 -6
  36. package/dist/astro/routes/api/admin/bylines/index.mjs +6 -6
  37. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +6 -6
  38. package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
  39. package/dist/astro/routes/api/admin/comments/bulk.mjs +4 -4
  40. package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
  41. package/dist/astro/routes/api/admin/comments/index.mjs +4 -4
  42. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +1 -1
  43. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +1 -1
  44. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +1 -1
  45. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +1 -1
  46. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +14 -14
  47. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +14 -14
  48. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +14 -14
  49. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +14 -14
  50. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +14 -14
  51. package/dist/astro/routes/api/admin/plugins/index.mjs +14 -14
  52. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +1 -1
  53. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +14 -14
  54. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +14 -14
  55. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +14 -14
  56. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +14 -14
  57. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +15 -15
  58. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +14 -14
  59. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +15 -15
  60. package/dist/astro/routes/api/admin/plugins/updates.mjs +14 -14
  61. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +14 -14
  62. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +1 -1
  63. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +14 -14
  64. package/dist/astro/routes/api/admin/users/_id_/index.mjs +2 -2
  65. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
  66. package/dist/astro/routes/api/admin/users/index.mjs +2 -2
  67. package/dist/astro/routes/api/auth/dev-bypass.mjs +2 -2
  68. package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
  69. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  70. package/dist/astro/routes/api/auth/invite/register-options.mjs +5 -5
  71. package/dist/astro/routes/api/auth/logout.mjs +1 -1
  72. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  73. package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
  74. package/dist/astro/routes/api/auth/me.mjs +2 -2
  75. package/dist/astro/routes/api/auth/mode.mjs +1 -1
  76. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  77. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  78. package/dist/astro/routes/api/auth/passkey/_id_.mjs +2 -2
  79. package/dist/astro/routes/api/auth/passkey/options.mjs +6 -6
  80. package/dist/astro/routes/api/auth/passkey/register/options.mjs +5 -5
  81. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
  82. package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
  83. package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
  84. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  85. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
  86. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +1 -1
  87. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +1 -1
  88. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +1 -1
  89. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +1 -1
  90. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  91. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +3 -3
  92. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +1 -1
  93. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +1 -1
  94. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +3 -3
  95. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +5 -5
  96. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +1 -1
  97. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +1 -1
  98. package/dist/astro/routes/api/content/_collection_/_id_.mjs +3 -3
  99. package/dist/astro/routes/api/content/_collection_/authors.mjs +1 -1
  100. package/dist/astro/routes/api/content/_collection_/index.mjs +3 -3
  101. package/dist/astro/routes/api/content/_collection_/trash.mjs +3 -3
  102. package/dist/astro/routes/api/dashboard.mjs +1 -1
  103. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  104. package/dist/astro/routes/api/import/probe.mjs +3 -3
  105. package/dist/astro/routes/api/import/wordpress/analyze.mjs +1 -1
  106. package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
  107. package/dist/astro/routes/api/import/wordpress/execute.mjs +3 -3
  108. package/dist/astro/routes/api/import/wordpress/media.mjs +3 -3
  109. package/dist/astro/routes/api/import/wordpress/prepare.mjs +3 -3
  110. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +3 -3
  111. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  112. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
  113. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  114. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
  115. package/dist/astro/routes/api/manifest.mjs +2 -2
  116. package/dist/astro/routes/api/mcp.mjs +18 -18
  117. package/dist/astro/routes/api/media/_id_/confirm.mjs +3 -3
  118. package/dist/astro/routes/api/media/_id_.mjs +3 -3
  119. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +1 -1
  120. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +1 -1
  121. package/dist/astro/routes/api/media/providers/index.mjs +1 -1
  122. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  123. package/dist/astro/routes/api/media.mjs +4 -4
  124. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +3 -3
  125. package/dist/astro/routes/api/menus/_name_/items.mjs +3 -3
  126. package/dist/astro/routes/api/menus/_name_/reorder.mjs +3 -3
  127. package/dist/astro/routes/api/menus/_name_/translations.mjs +3 -3
  128. package/dist/astro/routes/api/menus/_name_.mjs +3 -3
  129. package/dist/astro/routes/api/menus/index.mjs +3 -3
  130. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  131. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  132. package/dist/astro/routes/api/oauth/device/code.mjs +5 -5
  133. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  134. package/dist/astro/routes/api/oauth/register.mjs +1 -1
  135. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  136. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  137. package/dist/astro/routes/api/oauth/token.mjs +4 -4
  138. package/dist/astro/routes/api/openapi.json.mjs +1 -1
  139. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -2
  140. package/dist/astro/routes/api/redirects/404s/index.mjs +4 -4
  141. package/dist/astro/routes/api/redirects/404s/summary.mjs +4 -4
  142. package/dist/astro/routes/api/redirects/_id_.mjs +4 -4
  143. package/dist/astro/routes/api/redirects/index.mjs +4 -4
  144. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +1 -1
  145. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +1 -1
  146. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +14 -14
  147. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +14 -14
  148. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +14 -14
  149. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +14 -14
  150. package/dist/astro/routes/api/schema/collections/index.mjs +14 -14
  151. package/dist/astro/routes/api/schema/index.mjs +5 -10
  152. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  153. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +14 -14
  154. package/dist/astro/routes/api/schema/orphans/index.mjs +14 -14
  155. package/dist/astro/routes/api/search/enable.mjs +5 -5
  156. package/dist/astro/routes/api/search/index.mjs +4 -4
  157. package/dist/astro/routes/api/search/rebuild.mjs +5 -5
  158. package/dist/astro/routes/api/search/stats.mjs +3 -3
  159. package/dist/astro/routes/api/search/suggest.mjs +4 -4
  160. package/dist/astro/routes/api/sections/_slug_.mjs +5 -5
  161. package/dist/astro/routes/api/sections/index.mjs +5 -5
  162. package/dist/astro/routes/api/settings/email.mjs +1 -1
  163. package/dist/astro/routes/api/settings.mjs +6 -6
  164. package/dist/astro/routes/api/setup/admin-verify.mjs +7 -7
  165. package/dist/astro/routes/api/setup/admin.mjs +6 -6
  166. package/dist/astro/routes/api/setup/dev-bypass.mjs +10 -10
  167. package/dist/astro/routes/api/setup/index.mjs +9 -9
  168. package/dist/astro/routes/api/setup/status.mjs +2 -2
  169. package/dist/astro/routes/api/snapshot.mjs +3 -3
  170. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +6 -6
  171. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +6 -6
  172. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +6 -6
  173. package/dist/astro/routes/api/taxonomies/index.mjs +6 -6
  174. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  175. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  176. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  177. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  178. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +3 -3
  179. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -5
  180. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  181. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -5
  182. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  183. package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -3
  184. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  185. package/dist/astro/routes/api/widget-areas/index.mjs +6 -5
  186. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  187. package/dist/astro/routes/api/widget-components.mjs +1 -1
  188. package/dist/astro/routes/robots.txt.mjs +3 -3
  189. package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
  190. package/dist/astro/routes/sitemap-_collection_.xml.mjs +12 -5
  191. package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
  192. package/dist/astro/routes/sitemap.xml.mjs +4 -4
  193. package/dist/astro/types.d.mts +12 -12
  194. package/dist/auth/providers/github.d.mts +1 -1
  195. package/dist/auth/providers/google.d.mts +1 -1
  196. package/dist/{authorize-C_8t2KGa.mjs → authorize-DsMSVSaY.mjs} +1 -1
  197. package/dist/{authorize-C_8t2KGa.mjs.map → authorize-DsMSVSaY.mjs.map} +1 -1
  198. package/dist/{byline-fields-C_OsR-KF.mjs → byline-fields--WxSNS79.mjs} +1 -1
  199. package/dist/{byline-fields-C_OsR-KF.mjs.map → byline-fields--WxSNS79.mjs.map} +1 -1
  200. package/dist/{byline-fields-51kg6Vuv.mjs → byline-fields-8TMtkBnH.mjs} +2 -2
  201. package/dist/{byline-fields-51kg6Vuv.mjs.map → byline-fields-8TMtkBnH.mjs.map} +1 -1
  202. package/dist/{byline-fields-DYXKDuNX.d.mts → byline-fields-DbibsvTl.d.mts} +5 -1
  203. package/dist/byline-fields-DbibsvTl.d.mts.map +1 -0
  204. package/dist/{bylines-Cx5n-WqP.mjs → bylines-BdxWCnPL.mjs} +1 -1
  205. package/dist/{bylines-Cx5n-WqP.mjs.map → bylines-BdxWCnPL.mjs.map} +1 -1
  206. package/dist/{bylines-wurS258E.mjs → bylines-s8c2DXbH.mjs} +3 -3
  207. package/dist/{bylines-wurS258E.mjs.map → bylines-s8c2DXbH.mjs.map} +1 -1
  208. package/dist/{challenge-store-DGwuCc4R.mjs → challenge-store-DXX3rfdI.mjs} +1 -1
  209. package/dist/{challenge-store-DGwuCc4R.mjs.map → challenge-store-DXX3rfdI.mjs.map} +1 -1
  210. package/dist/cli/index.mjs +11 -10
  211. package/dist/cli/index.mjs.map +1 -1
  212. package/dist/client/cf-access.d.mts +1 -1
  213. package/dist/client/index.d.mts +1 -1
  214. package/dist/client/index.mjs +1 -1
  215. package/dist/{comments-CJ0RZsYR.mjs → comments-Vkivawyl.mjs} +1 -1
  216. package/dist/{comments-CJ0RZsYR.mjs.map → comments-Vkivawyl.mjs.map} +1 -1
  217. package/dist/{components-CTfpu3PZ.mjs → components-CK0cuUoH.mjs} +1 -1
  218. package/dist/{components-CTfpu3PZ.mjs.map → components-CK0cuUoH.mjs.map} +1 -1
  219. package/dist/{context-GG52SPgh.mjs → context-Y7BRkWes.mjs} +2 -2
  220. package/dist/{context-GG52SPgh.mjs.map → context-Y7BRkWes.mjs.map} +1 -1
  221. package/dist/database/instrumentation.d.mts +10 -1
  222. package/dist/database/instrumentation.d.mts.map +1 -1
  223. package/dist/database/instrumentation.mjs +13 -1
  224. package/dist/database/instrumentation.mjs.map +1 -1
  225. package/dist/db/index.d.mts +3 -3
  226. package/dist/db/libsql.d.mts +1 -1
  227. package/dist/db/postgres.d.mts +1 -1
  228. package/dist/db/sqlite.d.mts +1 -1
  229. package/dist/{default-xLFNSsZ9.mjs → default-IlBaTFxM.mjs} +1 -1
  230. package/dist/{default-xLFNSsZ9.mjs.map → default-IlBaTFxM.mjs.map} +1 -1
  231. package/dist/{device-flow-s6_q3T7A.mjs → device-flow-R23SIbQ2.mjs} +4 -4
  232. package/dist/{device-flow-s6_q3T7A.mjs.map → device-flow-R23SIbQ2.mjs.map} +1 -1
  233. package/dist/{escape-bIyGoW5W.mjs → escape-Ds07EEyu.mjs} +1 -1
  234. package/dist/{escape-bIyGoW5W.mjs.map → escape-Ds07EEyu.mjs.map} +1 -1
  235. package/dist/{index-FfiTQJq2.d.mts → index-B1keaX5Y.d.mts} +43 -12
  236. package/dist/{index-FfiTQJq2.d.mts.map → index-B1keaX5Y.d.mts.map} +1 -1
  237. package/dist/{index-BpYeJO1E.d.mts → index-DR56od45.d.mts} +3 -3
  238. package/dist/{index-BpYeJO1E.d.mts.map → index-DR56od45.d.mts.map} +1 -1
  239. package/dist/index.d.mts +16 -16
  240. package/dist/index.mjs +22 -22
  241. package/dist/{load-B84ohfBk.mjs → load-BBetCvLC.mjs} +1 -1
  242. package/dist/{load-B84ohfBk.mjs.map → load-BBetCvLC.mjs.map} +1 -1
  243. package/dist/{loader-CpZKpFz0.mjs → loader-ZN1ll-d-.mjs} +11 -14
  244. package/dist/loader-ZN1ll-d-.mjs.map +1 -0
  245. package/dist/{manifest-schema-Cj-YrzrF.mjs → manifest-schema-BtwbL_vj.mjs} +55 -2
  246. package/dist/manifest-schema-BtwbL_vj.mjs.map +1 -0
  247. package/dist/media/index.d.mts +1 -1
  248. package/dist/media/local-runtime.d.mts +11 -11
  249. package/dist/media/local-runtime.mjs +2 -2
  250. package/dist/{media-allowlist-CMcoYIjQ.mjs → media-allowlist-Dknq-OFY.mjs} +1 -1
  251. package/dist/{media-allowlist-CMcoYIjQ.mjs.map → media-allowlist-Dknq-OFY.mjs.map} +1 -1
  252. package/dist/media-url-VClf8glU.mjs +26 -0
  253. package/dist/media-url-VClf8glU.mjs.map +1 -0
  254. package/dist/{menus-Dp9xporj.mjs → menus-DrQLusqj.mjs} +6 -33
  255. package/dist/menus-DrQLusqj.mjs.map +1 -0
  256. package/dist/{mode-BjlXswIw.mjs → mode-CO2vQHfq.mjs} +1 -1
  257. package/dist/{mode-BjlXswIw.mjs.map → mode-CO2vQHfq.mjs.map} +1 -1
  258. package/dist/{oauth-authorization-1aPAYjiC.mjs → oauth-authorization-Bw4NdF_S.mjs} +4 -4
  259. package/dist/{oauth-authorization-1aPAYjiC.mjs.map → oauth-authorization-Bw4NdF_S.mjs.map} +1 -1
  260. package/dist/{oauth-clients-8mPDStMv.mjs → oauth-clients-BGGFp57s.mjs} +1 -1
  261. package/dist/{oauth-clients-8mPDStMv.mjs.map → oauth-clients-BGGFp57s.mjs.map} +1 -1
  262. package/dist/{oauth-state-store-BJ7YtrfD.mjs → oauth-state-store-97x0xtN2.mjs} +1 -1
  263. package/dist/{oauth-state-store-BJ7YtrfD.mjs.map → oauth-state-store-97x0xtN2.mjs.map} +1 -1
  264. package/dist/{oauth-user-lookup-BdDSDvjF.mjs → oauth-user-lookup-B_vnZHKO.mjs} +1 -1
  265. package/dist/{oauth-user-lookup-BdDSDvjF.mjs.map → oauth-user-lookup-B_vnZHKO.mjs.map} +1 -1
  266. package/dist/{options-D4MnavW_.d.mts → options-DyYIYpPd.d.mts} +3 -3
  267. package/dist/{options-D4MnavW_.d.mts.map → options-DyYIYpPd.d.mts.map} +1 -1
  268. package/dist/page/index.d.mts +2 -2
  269. package/dist/{passkey-config-BDVM86Tj.mjs → passkey-config-C3QgnQnU.mjs} +1 -1
  270. package/dist/{passkey-config-BDVM86Tj.mjs.map → passkey-config-C3QgnQnU.mjs.map} +1 -1
  271. package/dist/{placeholder-B9lUUEmj.d.mts → placeholder-CVBv5z8k.d.mts} +1 -1
  272. package/dist/{placeholder-B9lUUEmj.d.mts.map → placeholder-CVBv5z8k.d.mts.map} +1 -1
  273. package/dist/plugin-types.d.mts +1 -1
  274. package/dist/plugin-utils.d.mts +9 -9
  275. package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
  276. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  277. package/dist/{public-url-egRHCy1m.mjs → public-url-BFVC2OTJ.mjs} +1 -1
  278. package/dist/{public-url-egRHCy1m.mjs.map → public-url-BFVC2OTJ.mjs.map} +1 -1
  279. package/dist/{query-BFQ029Ts.mjs → query-CbUcI4Xk.mjs} +18 -8
  280. package/dist/query-CbUcI4Xk.mjs.map +1 -0
  281. package/dist/{rate-limit-ClFFUga6.mjs → rate-limit-C7hjdkS5.mjs} +1 -1
  282. package/dist/{rate-limit-ClFFUga6.mjs.map → rate-limit-C7hjdkS5.mjs.map} +1 -1
  283. package/dist/{redirect-Cw3JTlmj.mjs → redirect-B_q19j4v.mjs} +1 -1
  284. package/dist/{redirect-Cw3JTlmj.mjs.map → redirect-B_q19j4v.mjs.map} +1 -1
  285. package/dist/{redirects-DEygMrRO.mjs → redirects-CCbCqCCd.mjs} +4 -2
  286. package/dist/redirects-CCbCqCCd.mjs.map +1 -0
  287. package/dist/{redirects-OIu6vQ2i.mjs → redirects-DxVoR7PI.mjs} +1 -1
  288. package/dist/{redirects-OIu6vQ2i.mjs.map → redirects-DxVoR7PI.mjs.map} +1 -1
  289. package/dist/request-context.d.mts +7 -0
  290. package/dist/request-context.d.mts.map +1 -1
  291. package/dist/request-context.mjs +2 -1
  292. package/dist/request-context.mjs.map +1 -1
  293. package/dist/{runner-BcRuXq_h.d.mts → runner-DTdhuI9i.d.mts} +2 -2
  294. package/dist/{runner-BcRuXq_h.d.mts.map → runner-DTdhuI9i.d.mts.map} +1 -1
  295. package/dist/runtime.d.mts +10 -10
  296. package/dist/runtime.mjs +1 -1
  297. package/dist/{schema-CS7Eg5gh.mjs → schema-C1E70ug_.mjs} +2 -2
  298. package/dist/{schema-CS7Eg5gh.mjs.map → schema-C1E70ug_.mjs.map} +1 -1
  299. package/dist/{search-o-aQzHI1.mjs → search-B3SGZw91.mjs} +2 -2
  300. package/dist/{search-o-aQzHI1.mjs.map → search-B3SGZw91.mjs.map} +1 -1
  301. package/dist/{secrets-C_ZtRos3.mjs → secrets-ChPTmy9x.mjs} +1 -1
  302. package/dist/{secrets-C_ZtRos3.mjs.map → secrets-ChPTmy9x.mjs.map} +1 -1
  303. package/dist/{sections-DhsZ0ns9.mjs → sections-D_lVzwRZ.mjs} +2 -2
  304. package/dist/{sections-DhsZ0ns9.mjs.map → sections-D_lVzwRZ.mjs.map} +1 -1
  305. package/dist/seed/index.d.mts +2 -2
  306. package/dist/seed/index.mjs +6 -6
  307. package/dist/seo/index.d.mts +1 -1
  308. package/dist/seo/index.d.mts.map +1 -1
  309. package/dist/seo/index.mjs +3 -12
  310. package/dist/seo/index.mjs.map +1 -1
  311. package/dist/{seo-DfjLvu8i.mjs → seo-D_LPkOtu.mjs} +4 -3
  312. package/dist/seo-D_LPkOtu.mjs.map +1 -0
  313. package/dist/{service-DAxg8RPR.mjs → service-ChDcsTBs.mjs} +2 -2
  314. package/dist/{service-DAxg8RPR.mjs.map → service-ChDcsTBs.mjs.map} +1 -1
  315. package/dist/{settings-DIsbHTRE.mjs → settings-Cv47v9u8.mjs} +2 -2
  316. package/dist/{settings-DIsbHTRE.mjs.map → settings-Cv47v9u8.mjs.map} +1 -1
  317. package/dist/settings-DfxiWY_s.mjs +411 -0
  318. package/dist/settings-DfxiWY_s.mjs.map +1 -0
  319. package/dist/{setup-complete-Yuv78yua.mjs → setup-complete-yvPE4OsP.mjs} +1 -1
  320. package/dist/{setup-complete-Yuv78yua.mjs.map → setup-complete-yvPE4OsP.mjs.map} +1 -1
  321. package/dist/{setup-nonce-Bm0uKqmf.mjs → setup-nonce-C9aFzb94.mjs} +1 -1
  322. package/dist/{setup-nonce-Bm0uKqmf.mjs.map → setup-nonce-C9aFzb94.mjs.map} +1 -1
  323. package/dist/{site-url-mEVmwIFi.mjs → site-url-CnHlmAs9.mjs} +1 -1
  324. package/dist/{site-url-mEVmwIFi.mjs.map → site-url-CnHlmAs9.mjs.map} +1 -1
  325. package/dist/storage/local.d.mts +1 -1
  326. package/dist/storage/s3.d.mts +1 -1
  327. package/dist/{taxonomies-UusDXv3C.mjs → taxonomies-BILwiyGk.mjs} +2 -2
  328. package/dist/{taxonomies-UusDXv3C.mjs.map → taxonomies-BILwiyGk.mjs.map} +1 -1
  329. package/dist/{taxonomies-BEW7S5AI.mjs → taxonomies-BdAmbOwx.mjs} +46 -9
  330. package/dist/taxonomies-BdAmbOwx.mjs.map +1 -0
  331. package/dist/{transport-BwQeeY2p.d.mts → transport-B7PPP2CC.d.mts} +1 -1
  332. package/dist/{transport-BwQeeY2p.d.mts.map → transport-B7PPP2CC.d.mts.map} +1 -1
  333. package/dist/{transport--Ck3RBin.mjs → transport-CmpLD7W3.mjs} +1 -1
  334. package/dist/{transport--Ck3RBin.mjs.map → transport-CmpLD7W3.mjs.map} +1 -1
  335. package/dist/{types-DWnN7weG.d.mts → types-BFgrqwSk.d.mts} +1 -1
  336. package/dist/{types-DWnN7weG.d.mts.map → types-BFgrqwSk.d.mts.map} +1 -1
  337. package/dist/{types-Qa7-HJJC.d.mts → types-BH8-30hc.d.mts} +1 -1
  338. package/dist/{types-Qa7-HJJC.d.mts.map → types-BH8-30hc.d.mts.map} +1 -1
  339. package/dist/{types-OT_Es5mp.d.mts → types-BPzXTV9x.d.mts} +1 -1
  340. package/dist/{types-OT_Es5mp.d.mts.map → types-BPzXTV9x.d.mts.map} +1 -1
  341. package/dist/{types-DbCWhHet.d.mts → types-BUUVn1zr.d.mts} +2 -2
  342. package/dist/types-BUUVn1zr.d.mts.map +1 -0
  343. package/dist/{types-DMwSpvcw.d.mts → types-CPAPl93j.d.mts} +9 -3
  344. package/dist/{types-DMwSpvcw.d.mts.map → types-CPAPl93j.d.mts.map} +1 -1
  345. package/dist/types-CZI4E3qG.mjs +3 -0
  346. package/dist/{types-kwqCOUxj.d.mts → types-D4kUqbHh.d.mts} +1 -1
  347. package/dist/{types-kwqCOUxj.d.mts.map → types-D4kUqbHh.d.mts.map} +1 -1
  348. package/dist/{types-WVmpZBJV.d.mts → types-DTniiNto.d.mts} +2 -2
  349. package/dist/{types-WVmpZBJV.d.mts.map → types-DTniiNto.d.mts.map} +1 -1
  350. package/dist/types-DZk_y-MU.mjs.map +1 -1
  351. package/dist/{types-DX6v9KzJ.d.mts → types-S15DXXNi.d.mts} +1 -1
  352. package/dist/{types-DX6v9KzJ.d.mts.map → types-S15DXXNi.d.mts.map} +1 -1
  353. package/dist/{validate-ZP9Dvg0P.mjs → validate-Bz4vqcX1.mjs} +1 -1
  354. package/dist/{validate-ZP9Dvg0P.mjs.map → validate-Bz4vqcX1.mjs.map} +1 -1
  355. package/dist/{validate-BPAHUSge.d.mts → validate-CNwkPWzz.d.mts} +5 -5
  356. package/dist/{validate-BPAHUSge.d.mts.map → validate-CNwkPWzz.d.mts.map} +1 -1
  357. package/dist/{validation-CE5i4q0c.mjs → validation-DgGTJm3u.mjs} +1 -1
  358. package/dist/{validation-CE5i4q0c.mjs.map → validation-DgGTJm3u.mjs.map} +1 -1
  359. package/dist/version-D-5txk2m.mjs +7 -0
  360. package/dist/{version-Dw0JXu45.mjs.map → version-D-5txk2m.mjs.map} +1 -1
  361. package/dist/{widgets-ClEnYQCH.mjs → widgets-DZfmAbE4.mjs} +47 -44
  362. package/dist/widgets-DZfmAbE4.mjs.map +1 -0
  363. package/package.json +10 -10
  364. package/src/api/handlers/marketplace.ts +2 -5
  365. package/src/api/handlers/registry.ts +70 -0
  366. package/src/api/handlers/seo.ts +9 -1
  367. package/src/api/schemas/schema.ts +13 -1
  368. package/src/astro/middleware.ts +20 -6
  369. package/src/astro/routes/api/schema/index.ts +7 -15
  370. package/src/astro/routes/sitemap-[collection].xml.ts +13 -2
  371. package/src/cli/commands/bundle-utils.ts +2 -0
  372. package/src/cli/commands/secrets.ts +2 -2
  373. package/src/database/instrumentation.ts +13 -0
  374. package/src/emdash-runtime.ts +31 -25
  375. package/src/loader.ts +24 -15
  376. package/src/plugins/manifest-schema.ts +75 -0
  377. package/src/plugins/marketplace.ts +2 -5
  378. package/src/plugins/types.ts +12 -0
  379. package/src/query.ts +13 -2
  380. package/src/request-context.ts +8 -0
  381. package/src/schema/types.ts +11 -1
  382. package/src/seo/index.ts +2 -28
  383. package/src/seo/media-url.ts +32 -0
  384. package/src/settings/index.ts +32 -40
  385. package/src/taxonomies/index.ts +78 -12
  386. package/src/utils/isolate-cache.ts +189 -0
  387. package/src/widgets/index.ts +57 -54
  388. package/dist/api-BZ6bhjYs.mjs.map +0 -1
  389. package/dist/byline-fields-DYXKDuNX.d.mts.map +0 -1
  390. package/dist/loader-CpZKpFz0.mjs.map +0 -1
  391. package/dist/manifest-schema-Cj-YrzrF.mjs.map +0 -1
  392. package/dist/menus-Dp9xporj.mjs.map +0 -1
  393. package/dist/query-BFQ029Ts.mjs.map +0 -1
  394. package/dist/redirects-DEygMrRO.mjs.map +0 -1
  395. package/dist/seo-DfjLvu8i.mjs.map +0 -1
  396. package/dist/settings-B1p-gPUK.mjs +0 -235
  397. package/dist/settings-B1p-gPUK.mjs.map +0 -1
  398. package/dist/taxonomies-BEW7S5AI.mjs.map +0 -1
  399. package/dist/types-Cj2S6FuC.mjs +0 -3
  400. package/dist/types-DbCWhHet.d.mts.map +0 -1
  401. package/dist/version-Dw0JXu45.mjs +0 -7
  402. package/dist/widgets-ClEnYQCH.mjs.map +0 -1
  403. /package/dist/{api-tokens-B6VgoE6M.mjs → api-tokens-Oq39ba-Z.mjs} +0 -0
package/src/loader.ts CHANGED
@@ -680,7 +680,12 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
680
680
 
681
681
  // Separate taxonomy / byline filters from field filters
682
682
  let result: { rows: Record<string, unknown>[] };
683
- let taxonomyFilter: { name: string; slugs: string[] } | null = null;
683
+ // Taxonomy filters AND together: each entry constrains the base
684
+ // row to match at least one of its slugs *within that taxonomy*.
685
+ // Term slugs are unique only within a taxonomy, so every filter
686
+ // keeps its own `name` and emits its own `EXISTS` clause rather
687
+ // than pooling slugs into one `IN`.
688
+ const taxonomyFilters: { name: string; slugs: string[] }[] = [];
684
689
  // A byline filter matches entries credited to any of the given
685
690
  // byline translation groups via the `_emdash_content_bylines`
686
691
  // junction table. `null` means no byline filter; an empty
@@ -710,14 +715,8 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
710
715
  );
711
716
  continue;
712
717
  }
713
- if (taxonomyFilter) {
714
- console.warn(
715
- `[emdash] where filter: only one taxonomy is supported per query, "${key}" ignored`,
716
- );
717
- continue;
718
- }
719
718
  const slugs = Array.isArray(value) ? value : [value];
720
- taxonomyFilter = { name: key, slugs };
719
+ taxonomyFilters.push({ name: key, slugs });
721
720
  } else {
722
721
  fieldFilters[key] = value;
723
722
  }
@@ -729,7 +728,7 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
729
728
  // SQL on both dialects).
730
729
  if (
731
730
  (bylineFilter && bylineFilter.groups.length === 0) ||
732
- (taxonomyFilter && taxonomyFilter.slugs.length === 0)
731
+ taxonomyFilters.some((f) => f.slugs.length === 0)
733
732
  ) {
734
733
  return { entries: [], cacheHint: { tags: [type] } };
735
734
  }
@@ -753,16 +752,26 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
753
752
  const fieldCondsSQL =
754
753
  fieldConds.length > 0 ? sql`${sql.join(fieldConds, sql` AND `)}` : null;
755
754
 
756
- const taxonomyCond = taxonomyFilter
757
- ? sql`AND EXISTS (
755
+ // One `EXISTS` per taxonomy, AND'd together: an entry must be
756
+ // tagged with a matching term in *every* requested taxonomy.
757
+ // Each clause pins its own `t.name`, so slugs never pool
758
+ // across taxonomies (they're only unique within one).
759
+ const taxonomyCond =
760
+ taxonomyFilters.length > 0
761
+ ? sql`${sql.join(
762
+ taxonomyFilters.map(
763
+ (f) => sql`AND EXISTS (
758
764
  SELECT 1 FROM content_taxonomies ct
759
765
  INNER JOIN taxonomies t ON t.id = ct.taxonomy_id
760
766
  WHERE ct.collection = ${type}
761
767
  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``;
768
+ AND t.name = ${f.name}
769
+ AND t.slug IN (${sql.join(f.slugs.map((s) => sql`${s}`))})
770
+ )`,
771
+ ),
772
+ sql` `,
773
+ )}`
774
+ : sql``;
766
775
 
767
776
  // `_emdash_content_bylines.byline_id` stores the byline's
768
777
  // translation_group (migration 040), so a credit spans every
@@ -8,8 +8,14 @@
8
8
  * - Marketplace ingest extends this with publishing-specific fields
9
9
  */
10
10
 
11
+ import {
12
+ capabilitiesToDeclaredAccess,
13
+ declaredAccessToCapabilities,
14
+ } from "@emdash-cms/plugin-types";
11
15
  import { z } from "zod";
12
16
 
17
+ import type { PluginManifest } from "./types.js";
18
+
13
19
  // ── Enum values (must stay in sync with types.ts) ───────────────
14
20
 
15
21
  /**
@@ -219,16 +225,63 @@ const pluginAdminConfigSchema = z.object({
219
225
  .optional(),
220
226
  });
221
227
 
228
+ // ── declaredAccess ──────────────────────────────────────────────
229
+
230
+ /**
231
+ * An operation's constraint object. Open vocabulary: keys the runtime
232
+ * recognises are enforced, others are advisory. The bundler emits `{}` for a
233
+ * granted operation; presence (not value) signals the grant.
234
+ */
235
+ const accessConstraints = z.record(z.string(), z.unknown());
236
+
237
+ /**
238
+ * Structured trust contract embedded in the bundle manifest. Mirrors
239
+ * `DeclaredAccess` in `@emdash-cms/plugin-types`. Categories are host
240
+ * subsystems; operations are modes of participation.
241
+ */
242
+ const declaredAccessSchema = z.object({
243
+ content: z
244
+ .object({ read: accessConstraints.optional(), write: accessConstraints.optional() })
245
+ .optional(),
246
+ media: z
247
+ .object({ read: accessConstraints.optional(), write: accessConstraints.optional() })
248
+ .optional(),
249
+ network: z
250
+ .object({
251
+ // allowedHosts: absent = unrestricted; present = host-restricted. Reject
252
+ // an empty array (which the decoder would otherwise have to treat as
253
+ // deny-all) to match the record lexicon's `minLength: 1` and keep the
254
+ // "absent vs empty" distinction from ever reaching enforcement ambiguous.
255
+ request: z.object({ allowedHosts: z.array(z.string()).min(1).optional() }).optional(),
256
+ })
257
+ .optional(),
258
+ email: z
259
+ .object({
260
+ send: accessConstraints.optional(),
261
+ events: accessConstraints.optional(),
262
+ transport: accessConstraints.optional(),
263
+ })
264
+ .optional(),
265
+ page: z.object({ fragments: accessConstraints.optional() }).optional(),
266
+ users: z.object({ read: accessConstraints.optional() }).optional(),
267
+ });
268
+
222
269
  // ── Main schema ─────────────────────────────────────────────────
223
270
 
224
271
  /**
225
272
  * Zod schema matching the PluginManifest interface from types.ts.
226
273
  *
227
274
  * Every JSON.parse of a manifest.json should validate through this.
275
+ *
276
+ * `declaredAccess` is the trust contract; `capabilities`/`allowedHosts` are the
277
+ * runtime's enforcement currency. Apply `reconcileManifestAccess` after parsing
278
+ * to make them consistent (declaredAccess authoritative when present). Kept a
279
+ * plain object (no `.transform`) because callers `.pick()`/`.extend()` it.
228
280
  */
229
281
  export const pluginManifestSchema = z.object({
230
282
  id: z.string().min(1),
231
283
  version: z.string().min(1),
284
+ declaredAccess: declaredAccessSchema.optional(),
232
285
  capabilities: z.array(z.enum(PLUGIN_CAPABILITIES)),
233
286
  allowedHosts: z.array(z.string()),
234
287
  storage: z.record(z.string(), storageCollectionSchema),
@@ -254,6 +307,28 @@ export const pluginManifestSchema = z.object({
254
307
 
255
308
  export type ValidatedPluginManifest = z.infer<typeof pluginManifestSchema>;
256
309
 
310
+ /**
311
+ * Reconcile a parsed manifest's trust contract with its enforcement currency.
312
+ * `declaredAccess` is authoritative: when present, `capabilities`/`allowedHosts`
313
+ * are re-derived from it so what the runtime enforces always matches what was
314
+ * recorded and consented to. A pre-migration bundle without `declaredAccess`
315
+ * has it derived from the legacy capability list instead. The result always
316
+ * carries both, mutually consistent. Apply this at every bundle-parse site.
317
+ */
318
+ export function reconcileManifestAccess(manifest: ValidatedPluginManifest): PluginManifest {
319
+ const reconciled: ValidatedPluginManifest = manifest.declaredAccess
320
+ ? { ...manifest, ...declaredAccessToCapabilities(manifest.declaredAccess) }
321
+ : {
322
+ ...manifest,
323
+ declaredAccess: capabilitiesToDeclaredAccess(manifest.capabilities, manifest.allowedHosts),
324
+ };
325
+ // Block Kit admin elements are typed as `unknown` by the Zod schema (their
326
+ // Element shape is validated at render time), so the validated manifest
327
+ // needs a structural cast up to the runtime PluginManifest.
328
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- admin elements are unknown[] in Zod; Element type checked at render time
329
+ return reconciled as unknown as PluginManifest;
330
+ }
331
+
257
332
  /**
258
333
  * Normalize a manifest hook entry — plain strings become `{ name }` objects.
259
334
  */
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { createGzipDecoder, unpackTar } from "modern-tar";
10
10
 
11
- import { pluginManifestSchema } from "./manifest-schema.js";
11
+ import { pluginManifestSchema, reconcileManifestAccess } from "./manifest-schema.js";
12
12
  import type { PluginManifest } from "./types.js";
13
13
 
14
14
  // ── Module-level regex patterns ───────────────────────────────────
@@ -495,10 +495,7 @@ export async function extractBundle(tarballBytes: Uint8Array): Promise<PluginBun
495
495
  "INVALID_BUNDLE",
496
496
  );
497
497
  }
498
- // Elements are validated as unknown[] by Zod; cast to PluginManifest
499
- // for the Element[] type (Block Kit validation happens at render time).
500
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Zod types elements as unknown[]; Element type validated at render time
501
- manifest = result.data as unknown as PluginManifest;
498
+ manifest = reconcileManifestAccess(result.data);
502
499
  } catch (err) {
503
500
  if (err instanceof MarketplaceError) throw err;
504
501
  throw new MarketplaceError(
@@ -19,10 +19,13 @@ import type { Element } from "@emdash-cms/blocks";
19
19
  // (e.g. `import { PluginCapability } from "../plugins/types.js"`).
20
20
  import {
21
21
  CAPABILITY_RENAMES,
22
+ capabilitiesToDeclaredAccess,
23
+ declaredAccessToCapabilities,
22
24
  isDeprecatedCapability,
23
25
  normalizeCapabilities,
24
26
  normalizeCapability,
25
27
  type CurrentPluginCapability,
28
+ type DeclaredAccess,
26
29
  type DeprecatedPluginCapability,
27
30
  type ManifestHookEntry,
28
31
  type ManifestRouteEntry,
@@ -40,10 +43,13 @@ import type { FieldType } from "../schema/types.js";
40
43
 
41
44
  export {
42
45
  CAPABILITY_RENAMES,
46
+ capabilitiesToDeclaredAccess,
47
+ declaredAccessToCapabilities,
43
48
  isDeprecatedCapability,
44
49
  normalizeCapabilities,
45
50
  normalizeCapability,
46
51
  type CurrentPluginCapability,
52
+ type DeclaredAccess,
47
53
  type DeprecatedPluginCapability,
48
54
  type ManifestHookEntry,
49
55
  type ManifestRouteEntry,
@@ -1336,6 +1342,12 @@ export interface PluginAdminExports {
1336
1342
  export interface PluginManifest {
1337
1343
  id: string;
1338
1344
  version: string;
1345
+ /**
1346
+ * The trust contract (see `@emdash-cms/plugin-types`). Authoritative;
1347
+ * `capabilities`/`allowedHosts` are derived from it at the parse boundary
1348
+ * via `reconcileManifestAccess`. Optional during the wire-format migration.
1349
+ */
1350
+ declaredAccess?: DeclaredAccess;
1339
1351
  capabilities: PluginCapability[];
1340
1352
  allowedHosts: string[];
1341
1353
  storage: PluginStorageConfig;
package/src/query.ts CHANGED
@@ -247,6 +247,17 @@ function dataStr(data: Record<string, unknown>, key: string, fallback = ""): str
247
247
  return typeof val === "string" ? val : fallback;
248
248
  }
249
249
 
250
+ /** Safely read a date-like field from a Record */
251
+ function dataDate(data: Record<string, unknown>, key: string): Date | undefined {
252
+ const val = data[key];
253
+ if (val instanceof Date) {
254
+ return Number.isNaN(val.getTime()) ? undefined : val;
255
+ }
256
+ if (typeof val !== "string" && typeof val !== "number") return undefined;
257
+ const date = new Date(val);
258
+ return Number.isNaN(date.getTime()) ? undefined : date;
259
+ }
260
+
250
261
  /** Type guard for Record<string, unknown> */
251
262
  function isRecord(value: unknown): value is Record<string, unknown> {
252
263
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -602,10 +613,10 @@ export async function getEmDashEntry<T extends string, D = InferCollectionData<T
602
613
  function isVisible(entry: ContentEntry<D>): boolean {
603
614
  const data = entryData(entry);
604
615
  const status = dataStr(data, "status");
605
- const scheduledAt = dataStr(data, "scheduledAt") || undefined;
616
+ const scheduledAt = dataDate(data, "scheduledAt");
606
617
  const isPublished = status === "published";
607
618
  const isScheduledAndReady =
608
- status === "scheduled" && scheduledAt && new Date(scheduledAt) <= new Date();
619
+ status === "scheduled" && scheduledAt !== undefined && scheduledAt.getTime() <= Date.now();
609
620
  return isPublished || !!isScheduledAndReady;
610
621
  }
611
622
 
@@ -39,6 +39,13 @@ export interface RequestMetrics {
39
39
  dbLastOffset: number | null;
40
40
  cacheHits: number;
41
41
  cacheMisses: number;
42
+ /**
43
+ * Physical database round trips. Differs from `dbCount` (logical queries)
44
+ * when a backend batches: the DO SQL driver coalesces same-turn SELECTs into
45
+ * one RPC, so `rpcCount` can be far lower than `dbCount`. Bumped by the
46
+ * adapter, not the Kysely log hook.
47
+ */
48
+ rpcCount: number;
42
49
  }
43
50
 
44
51
  export function createRequestMetrics(start: number): RequestMetrics {
@@ -50,6 +57,7 @@ export function createRequestMetrics(start: number): RequestMetrics {
50
57
  dbLastOffset: null,
51
58
  cacheHits: 0,
52
59
  cacheMisses: 0,
60
+ rpcCount: 0,
53
61
  };
54
62
  }
55
63
 
@@ -102,7 +102,16 @@ export type CollectionSource =
102
102
  /** Sub-field definition for repeater fields */
103
103
  export interface RepeaterSubField {
104
104
  slug: string;
105
- type: "string" | "text" | "url" | "number" | "integer" | "boolean" | "datetime" | "select";
105
+ type:
106
+ | "string"
107
+ | "text"
108
+ | "url"
109
+ | "number"
110
+ | "integer"
111
+ | "boolean"
112
+ | "datetime"
113
+ | "select"
114
+ | "image";
106
115
  label: string;
107
116
  required?: boolean;
108
117
  options?: string[]; // For select sub-fields
@@ -118,6 +127,7 @@ export const REPEATER_SUB_FIELD_TYPES = [
118
127
  "boolean",
119
128
  "datetime",
120
129
  "select",
130
+ "image",
121
131
  ] as const;
122
132
 
123
133
  export interface FieldValidation {
package/src/seo/index.ts CHANGED
@@ -30,6 +30,7 @@
30
30
  */
31
31
 
32
32
  import type { ContentSeo } from "../database/repositories/types.js";
33
+ import { buildSeoImageUrl } from "./media-url.js";
33
34
 
34
35
  const TRAILING_SLASH_RE = /\/$/;
35
36
  const ABSOLUTE_URL_RE = /^https?:\/\//i;
@@ -117,7 +118,7 @@ export function getSeoMeta<T>(content: SeoContentInput<T>, options: SeoMetaOptio
117
118
  null;
118
119
 
119
120
  // OG image: SEO image > default
120
- const ogImage = seo.image ? buildMediaUrl(seo.image, siteUrl) : (defaultOgImage ?? null);
121
+ const ogImage = seo.image ? buildSeoImageUrl(seo.image, siteUrl) : (defaultOgImage ?? null);
121
122
 
122
123
  // Canonical: explicit > path-based > null
123
124
  let canonical: string | null = null;
@@ -159,30 +160,3 @@ export function getSeoMeta<T>(content: SeoContentInput<T>, options: SeoMetaOptio
159
160
  export function getContentSeo<T>(content: SeoContentInput<T>): ContentSeo | undefined {
160
161
  return content.seo ?? content.data.seo;
161
162
  }
162
-
163
- /**
164
- * Build a media URL from a media reference ID.
165
- * If it's already an absolute URL, return as-is.
166
- */
167
- function buildMediaUrl(imageRef: string, siteUrl?: string): string {
168
- // If already an absolute URL, return as-is
169
- if (ABSOLUTE_URL_RE.test(imageRef)) {
170
- return imageRef;
171
- }
172
-
173
- // Root-relative path — the CMS SEO panel stores seo_image as
174
- // "/_emdash/api/media/file/01KS....svg" (already includes the API
175
- // prefix). Without this branch we'd re-prefix and produce
176
- // "${siteUrl}/_emdash/api/media/file//_emdash/api/media/file/<id>"
177
- // which 404s and breaks <meta property="og:image">.
178
- if (imageRef.startsWith("/")) {
179
- return siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, "")}${imageRef}` : imageRef;
180
- }
181
-
182
- // Bare media_id — build the full media API path
183
- const mediaPath = `/_emdash/api/media/file/${imageRef}`;
184
- if (siteUrl) {
185
- return `${siteUrl.replace(TRAILING_SLASH_RE, "")}${mediaPath}`;
186
- }
187
- return mediaPath;
188
- }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolve a stored SEO image reference to a URL.
3
+ *
4
+ * The CMS SEO panel stores `seo_image` in one of these shapes:
5
+ * - an absolute URL (`https://...`) — returned as-is;
6
+ * - a root-relative path that already includes the media API prefix
7
+ * (`/_emdash/api/media/file/01KS....webp`) — prefixed with `siteUrl`;
8
+ * - a bare media id (`01KS...`) — expanded to the media API path, then
9
+ * prefixed with `siteUrl`.
10
+ *
11
+ * Shared by the SEO meta builder (`og:image`) and the sitemap route
12
+ * (`<image:image>`) so both resolve image references identically.
13
+ */
14
+ const TRAILING_SLASH_RE = /\/$/;
15
+ const ABSOLUTE_URL_RE = /^https?:\/\//i;
16
+
17
+ export function buildSeoImageUrl(imageRef: string, siteUrl?: string): string {
18
+ // Already absolute — use as-is.
19
+ if (ABSOLUTE_URL_RE.test(imageRef)) {
20
+ return imageRef;
21
+ }
22
+
23
+ // Root-relative path (already includes the media API prefix). Without
24
+ // this branch we'd re-prefix and produce a doubled path that 404s.
25
+ if (imageRef.startsWith("/")) {
26
+ return siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, "")}${imageRef}` : imageRef;
27
+ }
28
+
29
+ // Bare media id — build the full media API path.
30
+ const mediaPath = `/_emdash/api/media/file/${imageRef}`;
31
+ return siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, "")}${mediaPath}` : mediaPath;
32
+ }
@@ -7,12 +7,19 @@
7
7
 
8
8
  import type { Kysely } from "kysely";
9
9
 
10
+ import { after } from "../after.js";
10
11
  import { MediaRepository } from "../database/repositories/media.js";
11
12
  import { OptionsRepository } from "../database/repositories/options.js";
12
13
  import type { Database } from "../database/types.js";
13
14
  import { getDb } from "../loader.js";
14
15
  import { peekRequestCache, requestCached } from "../request-cache.js";
15
16
  import type { Storage } from "../storage/types.js";
17
+ import {
18
+ createIsolateCache,
19
+ type IsolateCache,
20
+ invalidateIsolateCache,
21
+ isolateCachedAsync,
22
+ } from "../utils/isolate-cache.js";
16
23
  import type { SiteSettings, SiteSettingKey, MediaReference, SeoSettings } from "./types.js";
17
24
 
18
25
  /** Prefix for site settings in the options table */
@@ -27,29 +34,22 @@ const SETTINGS_PREFIX = "site:";
27
34
  * once-per-isolate. Cross-isolate staleness is bounded by isolate lifetime
28
35
  * (workerd typically recycles within minutes); acceptable for chrome.
29
36
  *
30
- * Stored on globalThis with a Symbol.for key so Vite SSR chunk duplication
31
- * doesn't produce two independent caches (same pattern as request-context.ts).
32
- *
33
- * Invalidation: every `site:*` write bumps `version`. Reads compare the
34
- * cached promise's version against the current version and refetch on
35
- * mismatch. Caching the promise (not the resolved value) lets concurrent
36
- * cold-isolate readers share the in-flight query.
37
+ * Backed by isolate-cache.ts: concurrent cold-isolate reads coalesce onto one
38
+ * query via a reclaimable single-flight lock and the resolved *value* is
39
+ * cached — never a shared in-flight promise, so a cancelled request can't
40
+ * poison the isolate (see that file's header). Stored on globalThis with a
41
+ * Symbol.for key so Vite SSR chunk duplication doesn't produce two
42
+ * independent caches (same pattern as request-context.ts).
37
43
  */
38
- interface SiteSettingsHolder {
39
- version: number;
40
- cached: Promise<Partial<SiteSettings>> | null;
41
- cachedVersion: number;
42
- }
43
-
44
44
  const SITE_SETTINGS_CACHE_KEY = Symbol.for("emdash:site-settings");
45
45
  const g = globalThis as Record<symbol, unknown>;
46
- const holder: SiteSettingsHolder =
46
+ const settingsCache: IsolateCache<Partial<SiteSettings>> =
47
47
  // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts)
48
- (g[SITE_SETTINGS_CACHE_KEY] as SiteSettingsHolder | undefined) ??
48
+ (g[SITE_SETTINGS_CACHE_KEY] as IsolateCache<Partial<SiteSettings>> | undefined) ??
49
49
  (() => {
50
- const h: SiteSettingsHolder = { version: 0, cached: null, cachedVersion: -1 };
51
- g[SITE_SETTINGS_CACHE_KEY] = h;
52
- return h;
50
+ const c = createIsolateCache<Partial<SiteSettings>>();
51
+ g[SITE_SETTINGS_CACHE_KEY] = c;
52
+ return c;
53
53
  })();
54
54
 
55
55
  /**
@@ -60,9 +60,7 @@ const holder: SiteSettingsHolder =
60
60
  * own cached copy until they expire — staleness bounded by isolate lifetime.
61
61
  */
62
62
  export function invalidateSiteSettingsCache(): void {
63
- holder.version++;
64
- holder.cached = null;
65
- holder.cachedVersion = -1;
63
+ invalidateIsolateCache(settingsCache);
66
64
  }
67
65
 
68
66
  /**
@@ -210,25 +208,19 @@ export async function getSiteSettingWithDb<K extends SiteSettingKey>(
210
208
  * ```
211
209
  */
212
210
  export function getSiteSettings(): Promise<Partial<SiteSettings>> {
213
- return requestCached("siteSettings", () => {
214
- const versionAtCall = holder.version;
215
- if (holder.cached && holder.cachedVersion === versionAtCall) {
216
- return holder.cached;
217
- }
218
- const fetchPromise = (async () => {
219
- const db = await getDb();
220
- return getSiteSettingsWithDb(db);
221
- })().catch((error) => {
222
- if (holder.cached === fetchPromise) {
223
- holder.cached = null;
224
- holder.cachedVersion = -1;
225
- }
226
- throw error;
227
- });
228
- holder.cached = fetchPromise;
229
- holder.cachedVersion = versionAtCall;
230
- return fetchPromise;
231
- });
211
+ // requestCached dedupes within a single request; isolateCachedAsync
212
+ // coalesces across requests and caches the resolved value for the
213
+ // isolate's lifetime without ever sharing an awaitable promise.
214
+ return requestCached("siteSettings", () =>
215
+ isolateCachedAsync(
216
+ settingsCache,
217
+ async () => {
218
+ const db = await getDb();
219
+ return getSiteSettingsWithDb(db);
220
+ },
221
+ { anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },
222
+ ),
223
+ );
232
224
  }
233
225
 
234
226
  /**
@@ -120,15 +120,10 @@ export async function getTaxonomyTerms(
120
120
  if (locale !== undefined) termsQuery = termsQuery.where("locale", "=", locale);
121
121
  const rows = await termsQuery.execute();
122
122
 
123
- // Counts are keyed by translation_group (what the pivot stores).
124
- const countsResult = await db
125
- .selectFrom("content_taxonomies")
126
- .select(["taxonomy_id"])
127
- .select((eb) => eb.fn.count<number>("entry_id").as("count"))
128
- .groupBy("taxonomy_id")
129
- .execute();
130
- const counts = new Map<string, number>();
131
- for (const row of countsResult) counts.set(row.taxonomy_id, row.count);
123
+ // Counts are keyed by translation_group (what the pivot stores) and are
124
+ // locale-independent, so the aggregate is shared across every taxonomy
125
+ // rendered in this request (Categories + Tags widgets, etc.).
126
+ const counts = await getTaxonomyTermCounts();
132
127
 
133
128
  const flatTerms: TaxonomyTermRow[] = rows.map((row) => ({
134
129
  id: row.id,
@@ -157,6 +152,27 @@ export async function getTaxonomyTerms(
157
152
  });
158
153
  }
159
154
 
155
+ /**
156
+ * Per-translation-group usage counts across all taxonomies, in one aggregate
157
+ * scan of `content_taxonomies`. Counts are locale-independent (the pivot stores
158
+ * translation_group), so a single request-cached entry serves every taxonomy
159
+ * that renders during the request.
160
+ */
161
+ function getTaxonomyTermCounts(): Promise<Map<string, number>> {
162
+ return requestCached("taxonomy-term-counts", async () => {
163
+ const db = await getDb();
164
+ const countsResult = await db
165
+ .selectFrom("content_taxonomies")
166
+ .select(["taxonomy_id"])
167
+ .select((eb) => eb.fn.count<number>("entry_id").as("count"))
168
+ .groupBy("taxonomy_id")
169
+ .execute();
170
+ const counts = new Map<string, number>();
171
+ for (const row of countsResult) counts.set(row.taxonomy_id, row.count);
172
+ return counts;
173
+ });
174
+ }
175
+
160
176
  /**
161
177
  * Get a single term by (taxonomy, slug). Honours the fallback chain — if the
162
178
  * slug exists in a fallback locale, we return that row (useful for deep-linking
@@ -290,10 +306,57 @@ export async function getTermsForEntries(
290
306
  for (const id of uniqueIds) result.set(id, []);
291
307
  if (uniqueIds.length === 0) return result;
292
308
 
293
- const db = await getDb();
294
309
  const locale = resolveLocale(options.locale);
310
+ const localeKey = locale ?? "*";
295
311
 
296
- for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
312
+ // Entry-term hydration (getAllTermsForEntries -> primeEntryTermsCache)
313
+ // seeds the per-entry cache under the same key getEntryTerms uses:
314
+ // `terms:${collection}:${entryId}:${taxonomyName}:${localeKey}`, storing a
315
+ // TaxonomyTerm[] (including `[]` for entries with no terms). Satisfy those
316
+ // from cache and run the batched query only for the ids that missed.
317
+ const missedIds: string[] = [];
318
+ type CacheRead = { id: string; terms: TaxonomyTerm[] } | { id: string; miss: true };
319
+ const cacheReads: Array<Promise<CacheRead>> = [];
320
+ for (const id of uniqueIds) {
321
+ const cached = peekRequestCache<TaxonomyTerm[]>(
322
+ `terms:${collection}:${id}:${taxonomyName}:${localeKey}`,
323
+ );
324
+ if (cached) {
325
+ // A peeked promise can reject (e.g. a sibling getEntryTerms hit a
326
+ // missing table). Treat a rejection as a cache miss so the batched
327
+ // query path -- and its isMissingTableError guard below -- still runs,
328
+ // rather than propagating an uncaught error.
329
+ cacheReads.push(
330
+ cached.then(
331
+ (terms): CacheRead => ({ id, terms }),
332
+ (): CacheRead => ({ id, miss: true }),
333
+ ),
334
+ );
335
+ } else {
336
+ missedIds.push(id);
337
+ }
338
+ }
339
+ for (const read of await Promise.all(cacheReads)) {
340
+ if ("miss" in read) {
341
+ missedIds.push(read.id);
342
+ continue;
343
+ }
344
+ // Return a private copy. The cached array and its term objects are shared
345
+ // with getEntryTerms/getAllTermsForEntries (primeEntryTermsCache stores
346
+ // the same references), so a caller that mutates the result -- sorting in
347
+ // place, pushing into `children` -- must not poison the cache. The
348
+ // pre-cache implementation always returned freshly built arrays.
349
+ result.set(
350
+ read.id,
351
+ read.terms.map((t) => ({ ...t, children: [...t.children] })),
352
+ );
353
+ }
354
+
355
+ if (missedIds.length === 0) return result;
356
+
357
+ const db = await getDb();
358
+
359
+ for (const chunk of chunks(missedIds, SQL_BATCH_SIZE)) {
297
360
  let rows;
298
361
  try {
299
362
  let query = db
@@ -311,7 +374,10 @@ export async function getTermsForEntries(
311
374
  ])
312
375
  .where("content_taxonomies.collection", "=", collection)
313
376
  .where("content_taxonomies.entry_id", "in", chunk)
314
- .where("taxonomies.name", "=", taxonomyName);
377
+ .where("taxonomies.name", "=", taxonomyName)
378
+ // Match the order getAllTermsForEntries (the cache primer) uses, so
379
+ // cache-hit and DB-miss entries in one result are ordered consistently.
380
+ .orderBy("taxonomies.label", "asc");
315
381
  if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
316
382
  rows = await query.execute();
317
383
  } catch (error) {