emdash 0.24.0 → 0.25.1

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 (340) hide show
  1. package/dist/api/route-utils.d.mts +2 -2
  2. package/dist/api/route-utils.mjs +9 -8
  3. package/dist/api/route-utils.mjs.map +1 -1
  4. package/dist/api/schemas/index.d.mts +1 -1
  5. package/dist/api/schemas/index.mjs +2 -2
  6. package/dist/{api-d_0wi9fW.mjs → api-D2_sbavn.mjs} +16 -12
  7. package/dist/api-D2_sbavn.mjs.map +1 -0
  8. package/dist/{apply-BE9UaWX0.mjs → apply-Bk2mAuLn.mjs} +11 -10
  9. package/dist/apply-Bk2mAuLn.mjs.map +1 -0
  10. package/dist/astro/index.d.mts +6 -6
  11. package/dist/astro/index.mjs +2 -2
  12. package/dist/astro/middleware/auth.d.mts +5 -5
  13. package/dist/astro/middleware/auth.mjs +1 -1
  14. package/dist/astro/middleware/redirect.mjs +2 -2
  15. package/dist/astro/middleware.d.mts.map +1 -1
  16. package/dist/astro/middleware.mjs +66 -37
  17. package/dist/astro/middleware.mjs.map +1 -1
  18. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +4 -4
  19. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +4 -4
  20. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +1 -1
  21. package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
  22. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
  23. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +5 -5
  24. package/dist/astro/routes/api/admin/byline-fields/index.mjs +5 -5
  25. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +5 -5
  26. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +7 -7
  27. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +7 -7
  28. package/dist/astro/routes/api/admin/bylines/index.mjs +7 -7
  29. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +5 -5
  30. package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
  31. package/dist/astro/routes/api/admin/comments/bulk.mjs +5 -5
  32. package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
  33. package/dist/astro/routes/api/admin/comments/index.mjs +5 -5
  34. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +3 -3
  35. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +2 -2
  36. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +2 -2
  37. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +2 -2
  38. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +20 -20
  39. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +20 -20
  40. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +20 -20
  41. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +20 -20
  42. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +20 -20
  43. package/dist/astro/routes/api/admin/plugins/index.mjs +20 -20
  44. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
  45. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +20 -20
  46. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +20 -20
  47. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +20 -20
  48. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +20 -20
  49. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +21 -21
  50. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +20 -20
  51. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +21 -21
  52. package/dist/astro/routes/api/admin/plugins/updates.mjs +20 -20
  53. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +20 -20
  54. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
  55. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +20 -20
  56. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +1 -1
  57. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
  58. package/dist/astro/routes/api/admin/users/_id_/index.mjs +4 -4
  59. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
  60. package/dist/astro/routes/api/admin/users/index.mjs +4 -4
  61. package/dist/astro/routes/api/auth/dev-bypass.mjs +1 -1
  62. package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
  63. package/dist/astro/routes/api/auth/invite/complete.mjs +4 -4
  64. package/dist/astro/routes/api/auth/invite/index.mjs +4 -4
  65. package/dist/astro/routes/api/auth/invite/register-options.mjs +4 -4
  66. package/dist/astro/routes/api/auth/logout.mjs +1 -1
  67. package/dist/astro/routes/api/auth/magic-link/send.mjs +5 -5
  68. package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
  69. package/dist/astro/routes/api/auth/me.mjs +4 -4
  70. package/dist/astro/routes/api/auth/passkey/_id_.mjs +4 -4
  71. package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
  72. package/dist/astro/routes/api/auth/passkey/options.mjs +5 -5
  73. package/dist/astro/routes/api/auth/passkey/register/options.mjs +4 -4
  74. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +4 -4
  75. package/dist/astro/routes/api/auth/passkey/verify.mjs +4 -4
  76. package/dist/astro/routes/api/auth/signup/complete.mjs +4 -4
  77. package/dist/astro/routes/api/auth/signup/request.mjs +5 -5
  78. package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
  79. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +4 -4
  80. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +4 -4
  81. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
  82. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +2 -2
  83. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +2 -2
  84. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
  85. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +5 -5
  86. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +5 -5
  87. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +2 -2
  88. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
  89. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +5 -5
  90. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
  91. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
  92. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +2 -2
  93. package/dist/astro/routes/api/content/_collection_/_id_.mjs +5 -5
  94. package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
  95. package/dist/astro/routes/api/content/_collection_/index.mjs +5 -5
  96. package/dist/astro/routes/api/content/_collection_/trash.mjs +5 -5
  97. package/dist/astro/routes/api/dashboard.mjs +5 -5
  98. package/dist/astro/routes/api/dev/emails.mjs +1 -1
  99. package/dist/astro/routes/api/import/probe.mjs +5 -5
  100. package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
  101. package/dist/astro/routes/api/import/wordpress/execute.d.mts +5 -5
  102. package/dist/astro/routes/api/import/wordpress/execute.mjs +4 -4
  103. package/dist/astro/routes/api/import/wordpress/media.d.mts +13 -1
  104. package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
  105. package/dist/astro/routes/api/import/wordpress/media.mjs +12 -8
  106. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  107. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  108. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
  109. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +5 -5
  110. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
  111. package/dist/astro/routes/api/manifest.mjs +2 -2
  112. package/dist/astro/routes/api/mcp.mjs +20 -20
  113. package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -1
  114. package/dist/astro/routes/api/media/_id_/confirm.mjs +43 -7
  115. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  116. package/dist/astro/routes/api/media/_id_.mjs +5 -5
  117. package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
  118. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
  119. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
  120. package/dist/astro/routes/api/media/providers/index.mjs +2 -2
  121. package/dist/astro/routes/api/media/upload-url.mjs +7 -7
  122. package/dist/astro/routes/api/media.mjs +21 -20
  123. package/dist/astro/routes/api/media.mjs.map +1 -1
  124. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  125. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  126. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  127. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  128. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  129. package/dist/astro/routes/api/menus/index.mjs +5 -5
  130. package/dist/astro/routes/api/oauth/device/authorize.mjs +2 -2
  131. package/dist/astro/routes/api/oauth/device/code.mjs +3 -3
  132. package/dist/astro/routes/api/oauth/device/token.mjs +3 -3
  133. package/dist/astro/routes/api/oauth/register.mjs +1 -1
  134. package/dist/astro/routes/api/oauth/token/refresh.mjs +2 -2
  135. package/dist/astro/routes/api/oauth/token/revoke.mjs +2 -2
  136. package/dist/astro/routes/api/oauth/token.mjs +1 -1
  137. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  138. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -2
  139. package/dist/astro/routes/api/redirects/404s/index.mjs +5 -5
  140. package/dist/astro/routes/api/redirects/404s/summary.mjs +5 -5
  141. package/dist/astro/routes/api/redirects/_id_.mjs +6 -6
  142. package/dist/astro/routes/api/redirects/index.mjs +6 -6
  143. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
  144. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +2 -2
  145. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +20 -20
  146. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +20 -20
  147. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +20 -20
  148. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +20 -20
  149. package/dist/astro/routes/api/schema/collections/index.mjs +20 -20
  150. package/dist/astro/routes/api/schema/index.mjs +3 -3
  151. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +20 -20
  152. package/dist/astro/routes/api/schema/orphans/index.mjs +20 -20
  153. package/dist/astro/routes/api/search/enable.mjs +7 -7
  154. package/dist/astro/routes/api/search/index.mjs +6 -6
  155. package/dist/astro/routes/api/search/rebuild.mjs +7 -7
  156. package/dist/astro/routes/api/search/stats.mjs +4 -4
  157. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  158. package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
  159. package/dist/astro/routes/api/sections/index.mjs +7 -7
  160. package/dist/astro/routes/api/settings/email.mjs +3 -3
  161. package/dist/astro/routes/api/settings.mjs +9 -9
  162. package/dist/astro/routes/api/setup/admin-verify.mjs +4 -4
  163. package/dist/astro/routes/api/setup/admin.mjs +4 -4
  164. package/dist/astro/routes/api/setup/dev-bypass.mjs +7 -7
  165. package/dist/astro/routes/api/setup/dev-reset.mjs +1 -1
  166. package/dist/astro/routes/api/setup/index.mjs +10 -10
  167. package/dist/astro/routes/api/setup/status.mjs +1 -1
  168. package/dist/astro/routes/api/snapshot.mjs +2 -2
  169. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +8 -8
  170. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +8 -8
  171. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +8 -8
  172. package/dist/astro/routes/api/taxonomies/index.mjs +8 -8
  173. package/dist/astro/routes/api/themes/preview.mjs +2 -2
  174. package/dist/astro/routes/api/typegen.mjs +3 -3
  175. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +5 -5
  176. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
  177. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
  178. package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -4
  179. package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
  180. package/dist/astro/routes/api/widget-components.mjs +1 -1
  181. package/dist/astro/routes/robots.txt.mjs +3 -3
  182. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  183. package/dist/astro/routes/sitemap.xml.mjs +4 -4
  184. package/dist/astro/types.d.mts +7 -7
  185. package/dist/{authorize-B__u-iZJ.mjs → authorize-CuxZ0i2t.mjs} +2 -2
  186. package/dist/{authorize-B__u-iZJ.mjs.map → authorize-CuxZ0i2t.mjs.map} +1 -1
  187. package/dist/{byline-fields-RFPkDLRw.d.mts → byline-fields-KD3JFRuH.d.mts} +19 -1
  188. package/dist/byline-fields-KD3JFRuH.d.mts.map +1 -0
  189. package/dist/{byline-fields-B-KXkg5R.mjs → byline-fields-eyJMblSr.mjs} +2 -2
  190. package/dist/{byline-fields-B-KXkg5R.mjs.map → byline-fields-eyJMblSr.mjs.map} +1 -1
  191. package/dist/{bylines-BMtJnPnG.mjs → bylines-CPc_q2Vd.mjs} +4 -4
  192. package/dist/{bylines-BMtJnPnG.mjs.map → bylines-CPc_q2Vd.mjs.map} +1 -1
  193. package/dist/{cache-BUJe2w6Z.mjs → cache-C35VWD5D.mjs} +1 -1
  194. package/dist/{cache-BUJe2w6Z.mjs.map → cache-C35VWD5D.mjs.map} +1 -1
  195. package/dist/cli/index.mjs +8 -8
  196. package/dist/{content-PoVGKjp0.mjs → content-B43-YrIq.mjs} +7 -5
  197. package/dist/content-B43-YrIq.mjs.map +1 -0
  198. package/dist/{context-Dvws41_A.mjs → context-BW3N7wyt.mjs} +12 -6
  199. package/dist/context-BW3N7wyt.mjs.map +1 -0
  200. package/dist/{dashboard-DsYU8JKS.mjs → dashboard-FIe0S512.mjs} +3 -3
  201. package/dist/{dashboard-DsYU8JKS.mjs.map → dashboard-FIe0S512.mjs.map} +1 -1
  202. package/dist/{db-errors-UXB1CK42.mjs → db-errors-CK46D-ly.mjs} +1 -1
  203. package/dist/{db-errors-UXB1CK42.mjs.map → db-errors-CK46D-ly.mjs.map} +1 -1
  204. package/dist/{placeholder-B0s2mWu5.mjs → enrich-BQ0mxJRs.mjs} +61 -10
  205. package/dist/enrich-BQ0mxJRs.mjs.map +1 -0
  206. package/dist/{error-lq_fL-RS.mjs → error-DXnK2wF4.mjs} +2 -1
  207. package/dist/error-DXnK2wF4.mjs.map +1 -0
  208. package/dist/{index-BroXT-vD.d.mts → index-Cz6_JBL3.d.mts} +21 -8
  209. package/dist/index-Cz6_JBL3.d.mts.map +1 -0
  210. package/dist/index.d.mts +8 -8
  211. package/dist/index.mjs +28 -28
  212. package/dist/{loader-BR2SgfEc.mjs → loader-DrpofRon.mjs} +2 -2
  213. package/dist/{loader-BR2SgfEc.mjs.map → loader-DrpofRon.mjs.map} +1 -1
  214. package/dist/{manifest-schema-BWKnQYQF.mjs → manifest-schema-BOfPXse0.mjs} +2 -1
  215. package/dist/manifest-schema-BOfPXse0.mjs.map +1 -0
  216. package/dist/media/image-endpoint.mjs +1 -1
  217. package/dist/media/index.d.mts +45 -2
  218. package/dist/media/index.d.mts.map +1 -1
  219. package/dist/media/index.mjs +5 -3
  220. package/dist/media/index.mjs.map +1 -1
  221. package/dist/media/local-runtime.d.mts +6 -6
  222. package/dist/media/local-runtime.d.mts.map +1 -1
  223. package/dist/media/local-runtime.mjs +15 -3
  224. package/dist/media/local-runtime.mjs.map +1 -1
  225. package/dist/{media-CTjJoJTh.mjs → media-BjKxjzk8.mjs} +3 -1
  226. package/dist/media-BjKxjzk8.mjs.map +1 -0
  227. package/dist/{media-allowlist-B77I5_bw.mjs → media-allowlist-Du8t1u6a.mjs} +2 -2
  228. package/dist/{media-allowlist-B77I5_bw.mjs.map → media-allowlist-Du8t1u6a.mjs.map} +1 -1
  229. package/dist/{menus-GvRSpgIA.mjs → menus-CxqJ-z-N.mjs} +46 -4
  230. package/dist/menus-CxqJ-z-N.mjs.map +1 -0
  231. package/dist/{mime-BTzvzJ8M.mjs → mime-CCEzze7W.mjs} +1 -1
  232. package/dist/{mime-BTzvzJ8M.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
  233. package/dist/{normalize-ClbNIenm.mjs → normalize-CeglrBT9.mjs} +11 -2
  234. package/dist/normalize-CeglrBT9.mjs.map +1 -0
  235. package/dist/{options-CqECG0gM.d.mts → options-BvClpP5Q.d.mts} +2 -2
  236. package/dist/{options-CqECG0gM.d.mts.map → options-BvClpP5Q.d.mts.map} +1 -1
  237. package/dist/page/index.d.mts +1 -1
  238. package/dist/{parse-DHI873qU.mjs → parse-DqMWHQ8x.mjs} +2 -2
  239. package/dist/{parse-DHI873qU.mjs.map → parse-DqMWHQ8x.mjs.map} +1 -1
  240. package/dist/{placeholder-QoI4gEzP.d.mts → placeholder-Cuce9U-m.d.mts} +20 -5
  241. package/dist/placeholder-Cuce9U-m.d.mts.map +1 -0
  242. package/dist/plugin-types.d.mts +2 -1
  243. package/dist/plugin-types.d.mts.map +1 -1
  244. package/dist/plugin-utils.d.mts +5 -5
  245. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  246. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  247. package/dist/{query-DZdlS6Cu.mjs → query-34pfm02Q.mjs} +11 -11
  248. package/dist/{query-DZdlS6Cu.mjs.map → query-34pfm02Q.mjs.map} +1 -1
  249. package/dist/{rate-limit-D1Y6VFVP.mjs → rate-limit-CBQK4ygJ.mjs} +2 -2
  250. package/dist/{rate-limit-D1Y6VFVP.mjs.map → rate-limit-CBQK4ygJ.mjs.map} +1 -1
  251. package/dist/{redirects-x9eFPeyi.mjs → redirects-Dcbuisoj.mjs} +2 -1
  252. package/dist/redirects-Dcbuisoj.mjs.map +1 -0
  253. package/dist/{registry-CJkCubOJ.mjs → registry-3aOAS4My.mjs} +11 -1
  254. package/dist/registry-3aOAS4My.mjs.map +1 -0
  255. package/dist/runtime.d.mts +6 -6
  256. package/dist/runtime.mjs +1 -1
  257. package/dist/{schema-DMjXuvNU.mjs → schema-BGwx8AQY.mjs} +3 -3
  258. package/dist/{schema-DMjXuvNU.mjs.map → schema-BGwx8AQY.mjs.map} +1 -1
  259. package/dist/{search-D1wKk3lc.mjs → search-Gl747zMP.mjs} +2 -2
  260. package/dist/{search-D1wKk3lc.mjs.map → search-Gl747zMP.mjs.map} +1 -1
  261. package/dist/{sections-CCMic8dB.mjs → sections-C-umuuLj.mjs} +2 -2
  262. package/dist/{sections-CCMic8dB.mjs.map → sections-C-umuuLj.mjs.map} +1 -1
  263. package/dist/seed/index.d.mts +1 -1
  264. package/dist/seed/index.mjs +6 -6
  265. package/dist/seo/index.d.mts +1 -1
  266. package/dist/{seo-3QrZ1EPT.mjs → seo-BNzkiK_W.mjs} +1 -1
  267. package/dist/{seo-3QrZ1EPT.mjs.map → seo-BNzkiK_W.mjs.map} +1 -1
  268. package/dist/{seo-DCEF-AVr.mjs → seo-BkhuuaaE.mjs} +25 -3
  269. package/dist/seo-BkhuuaaE.mjs.map +1 -0
  270. package/dist/{settings-D9rvaLuY.mjs → settings-C4tXRYiG.mjs} +3 -3
  271. package/dist/{settings-D9rvaLuY.mjs.map → settings-C4tXRYiG.mjs.map} +1 -1
  272. package/dist/{settings-C3Yi5Co3.mjs → settings-fPSR6dJa.mjs} +2 -2
  273. package/dist/{settings-C3Yi5Co3.mjs.map → settings-fPSR6dJa.mjs.map} +1 -1
  274. package/dist/{taxonomies-DhLaNJji.mjs → taxonomies-DbAtL6m4.mjs} +2 -2
  275. package/dist/{taxonomies-DhLaNJji.mjs.map → taxonomies-DbAtL6m4.mjs.map} +1 -1
  276. package/dist/{taxonomies-CurpLtNC.mjs → taxonomies-n8w72lLQ.mjs} +4 -4
  277. package/dist/{taxonomies-CurpLtNC.mjs.map → taxonomies-n8w72lLQ.mjs.map} +1 -1
  278. package/dist/{types-CTLJYMhN.d.mts → types-CNlaBFzx.d.mts} +11 -1
  279. package/dist/{types-CTLJYMhN.d.mts.map → types-CNlaBFzx.d.mts.map} +1 -1
  280. package/dist/{types-BWh2MjmA.d.mts → types-ETmO_jQr.d.mts} +2 -2
  281. package/dist/{types-BWh2MjmA.d.mts.map → types-ETmO_jQr.d.mts.map} +1 -1
  282. package/dist/{types-TTx8uP7V.d.mts → types-h2Cwbzi2.d.mts} +6 -3
  283. package/dist/{types-TTx8uP7V.d.mts.map → types-h2Cwbzi2.d.mts.map} +1 -1
  284. package/dist/types-tM44hEcf.mjs.map +1 -1
  285. package/dist/{validate-DOFrIb-c.d.mts → validate-Va_An3qb.d.mts} +2 -2
  286. package/dist/{validate-DOFrIb-c.d.mts.map → validate-Va_An3qb.d.mts.map} +1 -1
  287. package/dist/{validation-CY1gk4Ar.mjs → validation-B6DKPTtU.mjs} +3 -3
  288. package/dist/{validation-CY1gk4Ar.mjs.map → validation-B6DKPTtU.mjs.map} +1 -1
  289. package/dist/version-DHdCA24u.mjs +7 -0
  290. package/dist/{version-B5nHxI5Y.mjs.map → version-DHdCA24u.mjs.map} +1 -1
  291. package/dist/{widgets-Dri0spI4.mjs → widgets-C2RbFN0G.mjs} +2 -2
  292. package/dist/{widgets-Dri0spI4.mjs.map → widgets-C2RbFN0G.mjs.map} +1 -1
  293. package/package.json +5 -5
  294. package/src/api/errors.ts +1 -0
  295. package/src/api/handlers/content.ts +4 -4
  296. package/src/api/handlers/seo.ts +30 -2
  297. package/src/api/schemas/schema.ts +1 -0
  298. package/src/astro/middleware.ts +14 -1
  299. package/src/astro/routes/api/import/wordpress/media.ts +9 -3
  300. package/src/astro/routes/api/media/[id]/confirm.ts +62 -2
  301. package/src/astro/routes/api/media.ts +17 -19
  302. package/src/components/EmDashImage.astro +4 -7
  303. package/src/components/Image.astro +14 -4
  304. package/src/components/InlinePortableTextEditor.tsx +48 -1
  305. package/src/database/repositories/content.ts +8 -5
  306. package/src/database/repositories/media.ts +9 -1
  307. package/src/emdash-runtime.ts +39 -1
  308. package/src/media/enrich.ts +78 -0
  309. package/src/media/index.ts +1 -0
  310. package/src/media/local-runtime.ts +19 -0
  311. package/src/media/normalize.ts +20 -1
  312. package/src/media/placeholder.ts +29 -9
  313. package/src/media/types.ts +14 -0
  314. package/src/plugin-types.ts +2 -0
  315. package/src/plugins/context.ts +8 -0
  316. package/src/plugins/hooks.ts +45 -0
  317. package/src/plugins/index.ts +1 -0
  318. package/src/plugins/manager.ts +11 -0
  319. package/src/plugins/manifest-schema.ts +1 -0
  320. package/src/plugins/types.ts +7 -0
  321. package/src/schema/registry.ts +25 -0
  322. package/src/schema/types.ts +10 -0
  323. package/src/seed/apply.ts +1 -0
  324. package/dist/api-d_0wi9fW.mjs.map +0 -1
  325. package/dist/apply-BE9UaWX0.mjs.map +0 -1
  326. package/dist/byline-fields-RFPkDLRw.d.mts.map +0 -1
  327. package/dist/content-PoVGKjp0.mjs.map +0 -1
  328. package/dist/context-Dvws41_A.mjs.map +0 -1
  329. package/dist/error-lq_fL-RS.mjs.map +0 -1
  330. package/dist/index-BroXT-vD.d.mts.map +0 -1
  331. package/dist/manifest-schema-BWKnQYQF.mjs.map +0 -1
  332. package/dist/media-CTjJoJTh.mjs.map +0 -1
  333. package/dist/menus-GvRSpgIA.mjs.map +0 -1
  334. package/dist/normalize-ClbNIenm.mjs.map +0 -1
  335. package/dist/placeholder-B0s2mWu5.mjs.map +0 -1
  336. package/dist/placeholder-QoI4gEzP.d.mts.map +0 -1
  337. package/dist/redirects-x9eFPeyi.mjs.map +0 -1
  338. package/dist/registry-CJkCubOJ.mjs.map +0 -1
  339. package/dist/seo-DCEF-AVr.mjs.map +0 -1
  340. package/dist/version-B5nHxI5Y.mjs +0 -7
@@ -1 +1 @@
1
- {"version":3,"file":"types-tM44hEcf.mjs","names":[],"sources":["../src/schema/types.ts"],"sourcesContent":["/**\n * Schema Registry Types\n *\n * These types represent the schema definitions stored in D1.\n * They are the source of truth for all collections and fields.\n */\n\n/**\n * Supported field types\n */\nexport type FieldType =\n\t| \"string\"\n\t| \"text\"\n\t| \"url\"\n\t| \"number\"\n\t| \"integer\"\n\t| \"boolean\"\n\t| \"datetime\"\n\t| \"select\"\n\t| \"multiSelect\"\n\t| \"portableText\"\n\t| \"image\"\n\t| \"file\"\n\t| \"reference\"\n\t| \"json\"\n\t| \"slug\"\n\t| \"repeater\";\n\n/**\n * Array of all field types for validation\n */\nexport const FIELD_TYPES: readonly FieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n\t\"repeater\",\n] as const;\n\n/**\n * SQLite column types that map from field types\n */\nexport type ColumnType = \"TEXT\" | \"REAL\" | \"INTEGER\" | \"JSON\";\n\n/**\n * Map field types to their SQLite column types\n */\nexport const FIELD_TYPE_TO_COLUMN: Record<FieldType, ColumnType> = {\n\tstring: \"TEXT\",\n\ttext: \"TEXT\",\n\tnumber: \"REAL\",\n\tinteger: \"INTEGER\",\n\tboolean: \"INTEGER\",\n\tdatetime: \"TEXT\",\n\tselect: \"TEXT\",\n\tmultiSelect: \"JSON\",\n\tportableText: \"JSON\",\n\timage: \"TEXT\",\n\tfile: \"TEXT\",\n\treference: \"TEXT\",\n\tjson: \"JSON\",\n\tslug: \"TEXT\",\n\turl: \"TEXT\",\n\trepeater: \"JSON\",\n};\n\n/**\n * Features a collection can support\n */\nexport type CollectionSupport =\n\t| \"drafts\"\n\t| \"revisions\"\n\t| \"preview\"\n\t| \"scheduling\"\n\t| \"search\"\n\t| \"seo\";\n\n/**\n * Sources for how a collection was created\n */\nexport type CollectionSource =\n\t| `template:${string}`\n\t| `import:${string}`\n\t| \"manual\"\n\t| \"discovered\"\n\t| \"seed\";\n\n/**\n * Validation rules for a field\n */\n/** Sub-field definition for repeater fields */\nexport interface RepeaterSubField {\n\tslug: string;\n\ttype:\n\t\t| \"string\"\n\t\t| \"text\"\n\t\t| \"url\"\n\t\t| \"number\"\n\t\t| \"integer\"\n\t\t| \"boolean\"\n\t\t| \"datetime\"\n\t\t| \"select\"\n\t\t| \"image\";\n\tlabel: string;\n\trequired?: boolean;\n\toptions?: string[]; // For select sub-fields\n}\n\n/** Allowed types for repeater sub-fields (no nesting, no complex types) */\nexport const REPEATER_SUB_FIELD_TYPES = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"image\",\n] as const;\n\nexport interface FieldValidation {\n\trequired?: boolean;\n\tmin?: number;\n\tmax?: number;\n\tminLength?: number;\n\tmaxLength?: number;\n\tpattern?: string;\n\toptions?: string[]; // For select/multiSelect\n\tsubFields?: RepeaterSubField[]; // For repeater fields\n\tminItems?: number; // For repeater fields\n\tmaxItems?: number; // For repeater fields\n\tallowedMimeTypes?: string[];\n}\n\n/**\n * Widget options for field rendering\n */\nexport interface FieldWidgetOptions {\n\trows?: number; // For textarea\n\tshowPreview?: boolean; // For image/file\n\tcollection?: string; // For reference - which collection to reference\n\tallowMultiple?: boolean; // For reference\n\t[key: string]: unknown;\n}\n\n/**\n * A collection definition\n */\nexport interface Collection {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports: CollectionSupport[];\n\tsource?: CollectionSource;\n\t/** Whether this collection has SEO metadata fields enabled */\n\thasSeo: boolean;\n\t/** URL pattern with {slug} placeholder (e.g. \"/{slug}\", \"/blog/{slug}\") */\n\turlPattern?: string;\n\t/** Whether comments are enabled for this collection */\n\tcommentsEnabled: boolean;\n\t/** Moderation strategy: \"all\" | \"first_time\" | \"none\" */\n\tcommentsModeration: \"all\" | \"first_time\" | \"none\";\n\t/** Auto-close comments after N days. 0 = never close. */\n\tcommentsClosedAfterDays: number;\n\t/** Auto-approve comments from authenticated CMS users */\n\tcommentsAutoApproveUsers: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * A field definition\n */\nexport interface Field {\n\tid: string;\n\tcollectionId: string;\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\tcolumnType: ColumnType;\n\trequired: boolean;\n\tunique: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder: number;\n\tsearchable: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable: boolean;\n\tcreatedAt: string;\n}\n\n/**\n * Input for creating a collection\n */\nexport interface CreateCollectionInput {\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\tsource?: CollectionSource;\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n}\n\n/**\n * Input for updating a collection\n */\nexport interface UpdateCollectionInput {\n\tlabel?: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n\tcommentsModeration?: \"all\" | \"first_time\" | \"none\";\n\tcommentsClosedAfterDays?: number;\n\tcommentsAutoApproveUsers?: boolean;\n}\n\n/**\n * Input for creating a field\n */\nexport interface CreateFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * Input for updating a field\n */\nexport interface UpdateFieldInput {\n\tlabel?: string;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * A collection with its fields\n */\nexport interface CollectionWithFields extends Collection {\n\tfields: Field[];\n}\n\n/**\n * Reserved field slugs that cannot be used.\n *\n * Includes names reserved for runtime hydration (`terms`, `bylines`, `byline`)\n * so user-defined fields never shadow the auto-hydrated values on entry.data.\n */\nexport const RESERVED_FIELD_SLUGS = [\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t// Runtime-hydrated fields\n\t\"terms\",\n\t\"bylines\",\n\t\"byline\",\n];\n\n/**\n * Reserved collection slugs that cannot be used\n */\nexport const RESERVED_COLLECTION_SLUGS = [\n\t\"content\",\n\t\"media\",\n\t\"users\",\n\t\"revisions\",\n\t\"taxonomies\",\n\t\"options\",\n\t\"audit_logs\",\n];\n\n/**\n * Byline custom fields (Discussion #1174).\n *\n * Sites declare site-specific byline metadata (`job_title`, `pronouns`,\n * `twitter_handle`, `company`, …) without touching emdash core. Definitions\n * live in `_emdash_byline_fields`; values in either\n * `_emdash_byline_field_values` (translatable, keyed by `byline_id`) or\n * `_emdash_byline_field_group_values` (non-translatable, keyed by\n * `translation_group`). The per-field `translatable` flag decides which\n * value table is used. See migration 041.\n */\n\n/**\n * The five v1 field types supported on byline custom fields. Deliberately\n * narrower than the content `FieldType` union — bylines don't need\n * `portableText`, `reference`, `image`, etc. v2 may extend this; v1 keeps\n * the storage and UI surfaces small.\n */\nexport type BylineFieldType = \"string\" | \"text\" | \"url\" | \"boolean\" | \"select\";\n\nexport const BYLINE_FIELD_TYPES: readonly BylineFieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"boolean\",\n\t\"select\",\n] as const;\n\n/**\n * Validation rules for a byline custom field. v1 only exposes `options`\n * (the choice list for `select` fields). The shape mirrors the content-\n * field convention so the admin UI patterns transfer.\n */\nexport interface BylineFieldValidation {\n\t/** Choices for `select`-type fields. Ignored for other types. */\n\toptions?: string[];\n}\n\n/**\n * Runtime shape of a registered byline custom field. Stored in\n * `_emdash_byline_fields` (see migration 041).\n */\nexport interface BylineFieldDefinition {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired: boolean;\n\t/**\n\t * Whether values are stored per-locale (`true`, in\n\t * `_emdash_byline_field_values` keyed by `byline_id`) or shared across\n\t * every locale variant of the same byline identity (`false`, in\n\t * `_emdash_byline_field_group_values` keyed by `translation_group`).\n\t * Defaults to `true` at the DB level.\n\t */\n\ttranslatable: boolean;\n\tvalidation: BylineFieldValidation | null;\n\tsortOrder: number;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * Input for creating a byline custom field. `slug` and `type` are not\n * updatable post-create — changing either would invalidate stored values.\n */\nexport interface CreateBylineFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Input for updating a byline custom field. `slug` and `type` are\n * intentionally not present — see `CreateBylineFieldInput`.\n */\nexport interface UpdateBylineFieldInput {\n\tlabel?: string;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Runtime value type for a byline custom field. The narrow union mirrors\n * what the five v1 field types can produce: `string`/`text`/`url`/`select`\n * → string, `boolean` → boolean, plus `null` for cleared values.\n */\nexport type CustomFieldValue = string | boolean | null;\n\n/**\n * Reserved byline-field slugs. Two reasons a slug ends up here:\n *\n * 1. **Column collision.** Slugs that match a fixed column on\n * `_emdash_bylines` (migrations 031 + 040) would shadow that column\n * on hydration. The first 12 entries cover this.\n * 2. **Route collision.** Static file routes under\n * `/_emdash/api/admin/byline-fields/` take precedence over the\n * `[slug].ts` dynamic route in Astro, so a custom field whose slug\n * matches a sibling static file (e.g. `reorder.ts`) is unreachable\n * via single-field CRUD — the static route handles only its own\n * method (POST for `reorder`) and 405s everything else.\n * `reorder` is the only such sibling today; new sibling routes\n * (e.g. a hypothetical `import.ts`) must be added here.\n * `[slug]/usage.ts` lives a level deeper so a slug of `usage` does\n * not collide — it resolves cleanly to `[slug].ts`.\n *\n * Enforced at the registry layer (Phase 2) and the admin API zod layer\n * (Phase 4) so non-HTTP callers (seeds, scripts) get the same guarantee.\n */\nexport const RESERVED_BYLINE_FIELD_SLUGS = [\n\t// 1. Column-collision slugs (matches `_emdash_bylines` fixed columns).\n\t\"id\",\n\t\"slug\",\n\t\"display_name\",\n\t\"bio\",\n\t\"avatar_media_id\",\n\t\"website_url\",\n\t\"user_id\",\n\t\"is_guest\",\n\t\"locale\",\n\t\"translation_group\",\n\t\"created_at\",\n\t\"updated_at\",\n\t// 2. Route-collision slugs (matches static sibling files of `[slug].ts`).\n\t\"reorder\",\n] as const;\n"],"mappings":";;;;AA+BA,MAAa,cAAoC;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;AAUD,MAAa,uBAAsD;CAClE,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;CACR,aAAa;CACb,cAAc;CACd,OAAO;CACP,MAAM;CACN,WAAW;CACX,MAAM;CACN,MAAM;CACN,KAAK;CACL,UAAU;CACV;;;;;;;AAyND,MAAa,uBAAuB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;;;;AAKD,MAAa,4BAA4B;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAsBD,MAAa,qBAAiD;CAC7D;CACA;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;AAyFD,MAAa,8BAA8B;CAE1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA"}
1
+ {"version":3,"file":"types-tM44hEcf.mjs","names":[],"sources":["../src/schema/types.ts"],"sourcesContent":["/**\n * Schema Registry Types\n *\n * These types represent the schema definitions stored in D1.\n * They are the source of truth for all collections and fields.\n */\n\n/**\n * Supported field types\n */\nexport type FieldType =\n\t| \"string\"\n\t| \"text\"\n\t| \"url\"\n\t| \"number\"\n\t| \"integer\"\n\t| \"boolean\"\n\t| \"datetime\"\n\t| \"select\"\n\t| \"multiSelect\"\n\t| \"portableText\"\n\t| \"image\"\n\t| \"file\"\n\t| \"reference\"\n\t| \"json\"\n\t| \"slug\"\n\t| \"repeater\";\n\n/**\n * Array of all field types for validation\n */\nexport const FIELD_TYPES: readonly FieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n\t\"repeater\",\n] as const;\n\n/**\n * SQLite column types that map from field types\n */\nexport type ColumnType = \"TEXT\" | \"REAL\" | \"INTEGER\" | \"JSON\";\n\n/**\n * Map field types to their SQLite column types\n */\nexport const FIELD_TYPE_TO_COLUMN: Record<FieldType, ColumnType> = {\n\tstring: \"TEXT\",\n\ttext: \"TEXT\",\n\tnumber: \"REAL\",\n\tinteger: \"INTEGER\",\n\tboolean: \"INTEGER\",\n\tdatetime: \"TEXT\",\n\tselect: \"TEXT\",\n\tmultiSelect: \"JSON\",\n\tportableText: \"JSON\",\n\timage: \"TEXT\",\n\tfile: \"TEXT\",\n\treference: \"TEXT\",\n\tjson: \"JSON\",\n\tslug: \"TEXT\",\n\turl: \"TEXT\",\n\trepeater: \"JSON\",\n};\n\n/**\n * Features a collection can support\n */\nexport type CollectionSupport =\n\t| \"drafts\"\n\t| \"revisions\"\n\t| \"preview\"\n\t| \"scheduling\"\n\t| \"search\"\n\t| \"seo\";\n\n/**\n * Sources for how a collection was created\n */\nexport type CollectionSource =\n\t| `template:${string}`\n\t| `import:${string}`\n\t| \"manual\"\n\t| \"discovered\"\n\t| \"seed\";\n\n/**\n * Validation rules for a field\n */\n/** Sub-field definition for repeater fields */\nexport interface RepeaterSubField {\n\tslug: string;\n\ttype:\n\t\t| \"string\"\n\t\t| \"text\"\n\t\t| \"url\"\n\t\t| \"number\"\n\t\t| \"integer\"\n\t\t| \"boolean\"\n\t\t| \"datetime\"\n\t\t| \"select\"\n\t\t| \"image\";\n\tlabel: string;\n\trequired?: boolean;\n\toptions?: string[]; // For select sub-fields\n}\n\n/** Allowed types for repeater sub-fields (no nesting, no complex types) */\nexport const REPEATER_SUB_FIELD_TYPES = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"image\",\n] as const;\n\nexport interface FieldValidation {\n\trequired?: boolean;\n\tmin?: number;\n\tmax?: number;\n\tminLength?: number;\n\tmaxLength?: number;\n\tpattern?: string;\n\toptions?: string[]; // For select/multiSelect\n\tsubFields?: RepeaterSubField[]; // For repeater fields\n\tminItems?: number; // For repeater fields\n\tmaxItems?: number; // For repeater fields\n\tallowedMimeTypes?: string[];\n}\n\n/**\n * Widget options for field rendering\n */\nexport interface FieldWidgetOptions {\n\trows?: number; // For textarea\n\tshowPreview?: boolean; // For image/file\n\tcollection?: string; // For reference - which collection to reference\n\tallowMultiple?: boolean; // For reference\n\t[key: string]: unknown;\n}\n\n/**\n * A collection definition\n */\nexport interface Collection {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports: CollectionSupport[];\n\tsource?: CollectionSource;\n\t/** Whether this collection has SEO metadata fields enabled */\n\thasSeo: boolean;\n\t/** URL pattern with {slug} placeholder (e.g. \"/{slug}\", \"/blog/{slug}\") */\n\turlPattern?: string;\n\t/** Whether comments are enabled for this collection */\n\tcommentsEnabled: boolean;\n\t/** Moderation strategy: \"all\" | \"first_time\" | \"none\" */\n\tcommentsModeration: \"all\" | \"first_time\" | \"none\";\n\t/** Auto-close comments after N days. 0 = never close. */\n\tcommentsClosedAfterDays: number;\n\t/** Auto-approve comments from authenticated CMS users */\n\tcommentsAutoApproveUsers: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * A field definition\n */\nexport interface Field {\n\tid: string;\n\tcollectionId: string;\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\tcolumnType: ColumnType;\n\trequired: boolean;\n\tunique: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder: number;\n\tsearchable: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable: boolean;\n\tcreatedAt: string;\n}\n\n/**\n * Input for creating a collection\n */\nexport interface CreateCollectionInput {\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\tsource?: CollectionSource;\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n}\n\n/**\n * Input for updating a collection\n */\nexport interface UpdateCollectionInput {\n\tlabel?: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n\tcommentsModeration?: \"all\" | \"first_time\" | \"none\";\n\tcommentsClosedAfterDays?: number;\n\tcommentsAutoApproveUsers?: boolean;\n}\n\n/**\n * Input for creating a field\n */\nexport interface CreateFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * Input for updating a field\n */\nexport interface UpdateFieldInput {\n\tlabel?: string;\n\t/**\n\t * Change the field's type. Only type changes that keep the same underlying\n\t * column type (per `FIELD_TYPE_TO_COLUMN`) are allowed — e.g. `string` to\n\t * `slug` (both TEXT). A change that would alter the column affinity (e.g.\n\t * `text` TEXT to `portableText` JSON) is rejected, because there is no\n\t * in-place column migration and silently rewriting the metadata would\n\t * desync `column_type` from the real `ec_*` column. Omit to keep the\n\t * current type.\n\t */\n\ttype?: FieldType;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * A collection with its fields\n */\nexport interface CollectionWithFields extends Collection {\n\tfields: Field[];\n}\n\n/**\n * Reserved field slugs that cannot be used.\n *\n * Includes names reserved for runtime hydration (`terms`, `bylines`, `byline`)\n * so user-defined fields never shadow the auto-hydrated values on entry.data.\n */\nexport const RESERVED_FIELD_SLUGS = [\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t// Runtime-hydrated fields\n\t\"terms\",\n\t\"bylines\",\n\t\"byline\",\n];\n\n/**\n * Reserved collection slugs that cannot be used\n */\nexport const RESERVED_COLLECTION_SLUGS = [\n\t\"content\",\n\t\"media\",\n\t\"users\",\n\t\"revisions\",\n\t\"taxonomies\",\n\t\"options\",\n\t\"audit_logs\",\n];\n\n/**\n * Byline custom fields (Discussion #1174).\n *\n * Sites declare site-specific byline metadata (`job_title`, `pronouns`,\n * `twitter_handle`, `company`, …) without touching emdash core. Definitions\n * live in `_emdash_byline_fields`; values in either\n * `_emdash_byline_field_values` (translatable, keyed by `byline_id`) or\n * `_emdash_byline_field_group_values` (non-translatable, keyed by\n * `translation_group`). The per-field `translatable` flag decides which\n * value table is used. See migration 041.\n */\n\n/**\n * The five v1 field types supported on byline custom fields. Deliberately\n * narrower than the content `FieldType` union — bylines don't need\n * `portableText`, `reference`, `image`, etc. v2 may extend this; v1 keeps\n * the storage and UI surfaces small.\n */\nexport type BylineFieldType = \"string\" | \"text\" | \"url\" | \"boolean\" | \"select\";\n\nexport const BYLINE_FIELD_TYPES: readonly BylineFieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"boolean\",\n\t\"select\",\n] as const;\n\n/**\n * Validation rules for a byline custom field. v1 only exposes `options`\n * (the choice list for `select` fields). The shape mirrors the content-\n * field convention so the admin UI patterns transfer.\n */\nexport interface BylineFieldValidation {\n\t/** Choices for `select`-type fields. Ignored for other types. */\n\toptions?: string[];\n}\n\n/**\n * Runtime shape of a registered byline custom field. Stored in\n * `_emdash_byline_fields` (see migration 041).\n */\nexport interface BylineFieldDefinition {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired: boolean;\n\t/**\n\t * Whether values are stored per-locale (`true`, in\n\t * `_emdash_byline_field_values` keyed by `byline_id`) or shared across\n\t * every locale variant of the same byline identity (`false`, in\n\t * `_emdash_byline_field_group_values` keyed by `translation_group`).\n\t * Defaults to `true` at the DB level.\n\t */\n\ttranslatable: boolean;\n\tvalidation: BylineFieldValidation | null;\n\tsortOrder: number;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * Input for creating a byline custom field. `slug` and `type` are not\n * updatable post-create — changing either would invalidate stored values.\n */\nexport interface CreateBylineFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Input for updating a byline custom field. `slug` and `type` are\n * intentionally not present — see `CreateBylineFieldInput`.\n */\nexport interface UpdateBylineFieldInput {\n\tlabel?: string;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Runtime value type for a byline custom field. The narrow union mirrors\n * what the five v1 field types can produce: `string`/`text`/`url`/`select`\n * → string, `boolean` → boolean, plus `null` for cleared values.\n */\nexport type CustomFieldValue = string | boolean | null;\n\n/**\n * Reserved byline-field slugs. Two reasons a slug ends up here:\n *\n * 1. **Column collision.** Slugs that match a fixed column on\n * `_emdash_bylines` (migrations 031 + 040) would shadow that column\n * on hydration. The first 12 entries cover this.\n * 2. **Route collision.** Static file routes under\n * `/_emdash/api/admin/byline-fields/` take precedence over the\n * `[slug].ts` dynamic route in Astro, so a custom field whose slug\n * matches a sibling static file (e.g. `reorder.ts`) is unreachable\n * via single-field CRUD — the static route handles only its own\n * method (POST for `reorder`) and 405s everything else.\n * `reorder` is the only such sibling today; new sibling routes\n * (e.g. a hypothetical `import.ts`) must be added here.\n * `[slug]/usage.ts` lives a level deeper so a slug of `usage` does\n * not collide — it resolves cleanly to `[slug].ts`.\n *\n * Enforced at the registry layer (Phase 2) and the admin API zod layer\n * (Phase 4) so non-HTTP callers (seeds, scripts) get the same guarantee.\n */\nexport const RESERVED_BYLINE_FIELD_SLUGS = [\n\t// 1. Column-collision slugs (matches `_emdash_bylines` fixed columns).\n\t\"id\",\n\t\"slug\",\n\t\"display_name\",\n\t\"bio\",\n\t\"avatar_media_id\",\n\t\"website_url\",\n\t\"user_id\",\n\t\"is_guest\",\n\t\"locale\",\n\t\"translation_group\",\n\t\"created_at\",\n\t\"updated_at\",\n\t// 2. Route-collision slugs (matches static sibling files of `[slug].ts`).\n\t\"reorder\",\n] as const;\n"],"mappings":";;;;AA+BA,MAAa,cAAoC;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;AAUD,MAAa,uBAAsD;CAClE,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;CACR,aAAa;CACb,cAAc;CACd,OAAO;CACP,MAAM;CACN,WAAW;CACX,MAAM;CACN,MAAM;CACN,KAAK;CACL,UAAU;CACV;;;;;;;AAmOD,MAAa,uBAAuB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;;;;AAKD,MAAa,4BAA4B;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAsBD,MAAa,qBAAiD;CAC7D;CACA;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;AAyFD,MAAa,8BAA8B;CAE1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA"}
@@ -1,4 +1,4 @@
1
- import { d as FieldType } from "./types-CTLJYMhN.mjs";
1
+ import { d as FieldType } from "./types-CNlaBFzx.mjs";
2
2
  import { t as Database } from "./types-B-gIxFCE.mjs";
3
3
  import { i as SiteSettings } from "./types-BG8_XTLj.mjs";
4
4
  import { d as Storage } from "./types-DrYMm9s5.mjs";
@@ -374,4 +374,4 @@ declare function loadUserSeed(): Promise<SeedFile | null>;
374
374
  declare function validateSeed(data: unknown): ValidationResult;
375
375
  //#endregion
376
376
  export { SeedTaxonomyTerm as _, applySeed as a, ValidationResult as b, SeedCollection as c, SeedFile as d, SeedMenu as f, SeedTaxonomy as g, SeedSection as h, defaultSeed as i, SeedContentEntry as l, SeedRedirect as m, loadSeed as n, SeedApplyOptions as o, SeedMenuItem as p, loadUserSeed as r, SeedApplyResult as s, validateSeed as t, SeedField as u, SeedWidget as v, SeedWidgetArea as y };
377
- //# sourceMappingURL=validate-DOFrIb-c.d.mts.map
377
+ //# sourceMappingURL=validate-Va_An3qb.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-DOFrIb-c.d.mts","names":[],"sources":["../src/seed/types.ts","../src/seed/apply.ts","../src/seed/default.ts","../src/seed/load.ts","../src/seed/validate.ts"],"mappings":"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"}
1
+ {"version":3,"file":"validate-Va_An3qb.d.mts","names":[],"sources":["../src/seed/types.ts","../src/seed/apply.ts","../src/seed/default.ts","../src/seed/load.ts","../src/seed/validate.ts"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
2
2
  import "./dialect-helpers-DRI5pyY3.mjs";
3
3
  import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BHgcHh2C.mjs";
4
- import { n as isMissingTableError } from "./db-errors-UXB1CK42.mjs";
4
+ import { n as isMissingTableError } from "./db-errors-CK46D-ly.mjs";
5
5
  import "./fts-manager-DW_NR-Fs.mjs";
6
- import { n as SchemaRegistry } from "./registry-CJkCubOJ.mjs";
6
+ import { n as SchemaRegistry } from "./registry-3aOAS4My.mjs";
7
7
  import { n as generateZodSchema } from "./zod-generator-xEHODMJf.mjs";
8
8
  import { sql } from "kysely";
9
9
 
@@ -142,4 +142,4 @@ async function validateContentData(db, collection, data, options = {}) {
142
142
 
143
143
  //#endregion
144
144
  export { validateContentData };
145
- //# sourceMappingURL=validation-CY1gk4Ar.mjs.map
145
+ //# sourceMappingURL=validation-B6DKPTtU.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validation-CY1gk4Ar.mjs","names":[],"sources":["../src/api/handlers/validation.ts"],"sourcesContent":["/**\n * Field-level validation for content create / update.\n *\n * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)\n * into the handler boundary so REST and MCP both get the same enforcement:\n *\n * - required fields must be present and non-empty\n * - select / multiSelect values must match the configured options\n * - reference fields must resolve to a real, non-trashed target\n *\n * Errors surface as `{ code: \"VALIDATION_ERROR\", message }` with all\n * offending fields listed in one message so callers can fix everything in\n * a single round trip.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { Field } from \"../../schema/types.js\";\nimport { generateZodSchema } from \"../../schema/zod-generator.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\n\ntype ValidationResult =\n\t| { ok: true }\n\t| { ok: false; error: { code: \"VALIDATION_ERROR\" | \"COLLECTION_NOT_FOUND\"; message: string } };\n\n/** Treat `undefined`, `null`, and `\"\"` as \"not set\". */\nfunction isMissing(value: unknown): boolean {\n\treturn value === undefined || value === null || value === \"\";\n}\n\n/**\n * Resolve the target collection slug for a reference field.\n *\n * Schema-defined reference fields (the static `reference()` factory in\n * `fields/reference.ts`) put the target in `options.collection`. The MCP\n * `schema_create_field` tool also puts it there. Tests and some admin paths\n * stash it inside `validation.collection` directly; we accept both.\n */\nfunction getReferenceTargetCollection(field: Field): string | undefined {\n\tconst fromOptions = field.options?.collection;\n\tif (typeof fromOptions === \"string\" && fromOptions.length > 0) return fromOptions;\n\tconst validation = field.validation;\n\tif (validation && \"collection\" in validation) {\n\t\tconst fromValidation: unknown = (validation as { collection?: unknown }).collection;\n\t\tif (typeof fromValidation === \"string\" && fromValidation.length > 0) return fromValidation;\n\t}\n\treturn undefined;\n}\n\n/**\n * Format a Zod issue path into a human-readable field reference, e.g.\n * `tags`, `tags.1`, `image.alt`.\n */\nfunction formatIssuePath(path: ReadonlyArray<PropertyKey>): string {\n\tif (path.length === 0) return \"(root)\";\n\treturn path.map((seg) => String(seg)).join(\".\");\n}\n\n/**\n * Validate `data` against the collection's field definitions.\n *\n * `partial: true` switches Zod into partial mode so updates can include\n * only the fields being changed without tripping required-field errors on\n * fields the caller didn't touch. Required fields that ARE present in\n * partial-mode data still get the empty-string check below.\n */\nexport async function validateContentData(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n\toptions: { partial?: boolean } = {},\n): Promise<ValidationResult> {\n\tconst registry = new SchemaRegistry(db);\n\tconst collectionWithFields = await registry.getCollectionWithFields(collection);\n\tif (!collectionWithFields) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst issues: string[] = [];\n\n\t// Detect unknown keys explicitly so callers get a useful error rather\n\t// than silently dropped data. Leading-underscore keys (e.g. `_slug`,\n\t// `_rev`) are reserved for internal handler/runtime use and aren't real\n\t// fields; skip them.\n\tconst knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));\n\tfor (const key of Object.keys(data)) {\n\t\tif (key.startsWith(\"_\")) continue;\n\t\tif (!knownFields.has(key)) {\n\t\t\tissues.push(`${key}: unknown field on collection '${collection}'`);\n\t\t}\n\t}\n\n\t// Zod handles type, enum, length and missing-required (in non-partial\n\t// mode) checks. Empty-string handling for required string fields is\n\t// done as a separate pass below since Zod's `z.string()` accepts \"\".\n\tconst baseSchema = generateZodSchema(collectionWithFields);\n\tconst schema = options.partial ? baseSchema.partial() : baseSchema;\n\tconst parsed = schema.safeParse(data);\n\tif (!parsed.success) {\n\t\tfor (const issue of parsed.error.issues) {\n\t\t\tissues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);\n\t\t}\n\t}\n\n\t// Empty-string-on-required check. In create mode (partial=false) Zod\n\t// already catches missing/null for required fields, but `z.string()`\n\t// happily accepts \"\". In update mode (partial=true) the field is only\n\t// checked if it's present in `data`.\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (!field.required) continue;\n\t\tconst present = Object.hasOwn(data, field.slug);\n\t\tif (options.partial && !present) continue;\n\t\tif (data[field.slug] === \"\") {\n\t\t\tissues.push(`${field.slug}: required (empty value not allowed)`);\n\t\t}\n\t}\n\n\t// Reference target existence. Only check fields that:\n\t// - have a value (non-missing) in `data`\n\t// - have a resolvable target collection\n\t// - in partial mode: are present in `data`\n\t// Batch one IN-query per target collection to keep round-trips low.\n\tconst refsByTarget = new Map<string, { field: string; id: string }[]>();\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (field.type !== \"reference\") continue;\n\t\tif (options.partial && !Object.hasOwn(data, field.slug)) continue;\n\t\tconst value = data[field.slug];\n\t\tif (isMissing(value)) continue;\n\t\tif (typeof value !== \"string\") continue; // Zod will have flagged this already\n\t\tconst target = getReferenceTargetCollection(field);\n\t\tif (!target) continue;\n\t\tconst list = refsByTarget.get(target) ?? [];\n\t\tlist.push({ field: field.slug, id: value });\n\t\trefsByTarget.set(target, list);\n\t}\n\n\tfor (const [target, refs] of refsByTarget) {\n\t\t// Validate the target collection slug before interpolating into raw\n\t\t// SQL — defense-in-depth even though slugs are already validated at\n\t\t// schema-create time.\n\t\ttry {\n\t\t\tvalidateIdentifier(target, \"reference target collection\");\n\t\t} catch {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: invalid reference target collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst ids = [...new Set(refs.map((r) => r.id))];\n\t\tconst tableName = `ec_${target}`;\n\n\t\t// Chunk the IN clause to stay below D1's bind-parameter limit. One\n\t\t// reference per request is the common case today; chunking makes the\n\t\t// helper safe if a future multiSelect-of-references is added.\n\t\tconst found = new Set<string>();\n\t\tlet targetTableMissing = false;\n\t\tfor (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{ id: string }>`\n\t\t\t\t\tSELECT id FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(idChunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(db);\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tfound.add(row.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Missing table = the target collection table doesn't exist\n\t\t\t\t// (orphan reference). Treat all those references as missing.\n\t\t\t\t// Any other DB error (permissions, connection, syntax) must\n\t\t\t\t// propagate — silently dropping data integrity errors as\n\t\t\t\t// \"not found\" is exactly the bug F5 fixes.\n\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\ttargetTableMissing = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\tif (targetTableMissing) {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const ref of refs) {\n\t\t\tif (!found.has(ref.id)) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length === 0) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\terror: {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: issues.join(\"; \"),\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAS,UAAU,OAAyB;AAC3C,QAAO,UAAU,UAAa,UAAU,QAAQ,UAAU;;;;;;;;;;AAW3D,SAAS,6BAA6B,OAAkC;CACvE,MAAM,cAAc,MAAM,SAAS;AACnC,KAAI,OAAO,gBAAgB,YAAY,YAAY,SAAS,EAAG,QAAO;CACtE,MAAM,aAAa,MAAM;AACzB,KAAI,cAAc,gBAAgB,YAAY;EAC7C,MAAM,iBAA2B,WAAwC;AACzE,MAAI,OAAO,mBAAmB,YAAY,eAAe,SAAS,EAAG,QAAO;;;;;;;AAS9E,SAAS,gBAAgB,MAA0C;AAClE,KAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAO,KAAK,KAAK,QAAQ,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI;;;;;;;;;;AAWhD,eAAsB,oBACrB,IACA,YACA,MACA,UAAiC,EAAE,EACP;CAE5B,MAAM,uBAAuB,MADZ,IAAI,eAAe,GAAG,CACK,wBAAwB,WAAW;AAC/E,KAAI,CAAC,qBACJ,QAAO;EACN,IAAI;EACJ,OAAO;GACN,MAAM;GACN,SAAS,eAAe,WAAW;GACnC;EACD;CAGF,MAAM,SAAmB,EAAE;CAM3B,MAAM,cAAc,IAAI,IAAI,qBAAqB,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC;AAC3E,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AACpC,MAAI,IAAI,WAAW,IAAI,CAAE;AACzB,MAAI,CAAC,YAAY,IAAI,IAAI,CACxB,QAAO,KAAK,GAAG,IAAI,iCAAiC,WAAW,GAAG;;CAOpE,MAAM,aAAa,kBAAkB,qBAAqB;CAE1D,MAAM,UADS,QAAQ,UAAU,WAAW,SAAS,GAAG,YAClC,UAAU,KAAK;AACrC,KAAI,CAAC,OAAO,QACX,MAAK,MAAM,SAAS,OAAO,MAAM,OAChC,QAAO,KAAK,GAAG,gBAAgB,MAAM,KAAK,CAAC,IAAI,MAAM,UAAU;AAQjE,MAAK,MAAM,SAAS,qBAAqB,QAAQ;AAChD,MAAI,CAAC,MAAM,SAAU;EACrB,MAAM,UAAU,OAAO,OAAO,MAAM,MAAM,KAAK;AAC/C,MAAI,QAAQ,WAAW,CAAC,QAAS;AACjC,MAAI,KAAK,MAAM,UAAU,GACxB,QAAO,KAAK,GAAG,MAAM,KAAK,sCAAsC;;CASlE,MAAM,+BAAe,IAAI,KAA8C;AACvE,MAAK,MAAM,SAAS,qBAAqB,QAAQ;AAChD,MAAI,MAAM,SAAS,YAAa;AAChC,MAAI,QAAQ,WAAW,CAAC,OAAO,OAAO,MAAM,MAAM,KAAK,CAAE;EACzD,MAAM,QAAQ,KAAK,MAAM;AACzB,MAAI,UAAU,MAAM,CAAE;AACtB,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,SAAS,6BAA6B,MAAM;AAClD,MAAI,CAAC,OAAQ;EACb,MAAM,OAAO,aAAa,IAAI,OAAO,IAAI,EAAE;AAC3C,OAAK,KAAK;GAAE,OAAO,MAAM;GAAM,IAAI;GAAO,CAAC;AAC3C,eAAa,IAAI,QAAQ,KAAK;;AAG/B,MAAK,MAAM,CAAC,QAAQ,SAAS,cAAc;AAI1C,MAAI;AACH,sBAAmB,QAAQ,8BAA8B;UAClD;AACP,QAAK,MAAM,OAAO,KACjB,QAAO,KAAK,GAAG,IAAI,MAAM,yCAAyC,OAAO,GAAG;AAE7E;;EAGD,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC;EAC/C,MAAM,YAAY,MAAM;EAKxB,MAAM,wBAAQ,IAAI,KAAa;EAC/B,IAAI,qBAAqB;AACzB,OAAK,MAAM,WAAW,OAAO,KAAK,eAAe,CAChD,KAAI;GACH,MAAM,OAAO,MAAM,GAAmB;sBACpB,IAAI,IAAI,UAAU,CAAC;oBACrB,IAAI,KAAK,QAAQ,CAAC;;MAEhC,QAAQ,GAAG;AACb,QAAK,MAAM,OAAO,KAAK,KACtB,OAAM,IAAI,IAAI,GAAG;WAEV,OAAO;AAMf,OAAI,oBAAoB,MAAM,EAAE;AAC/B,yBAAqB;AACrB;;AAED,SAAM;;AAGR,MAAI,oBAAoB;AACvB,QAAK,MAAM,OAAO,KACjB,QAAO,KAAK,GAAG,IAAI,MAAM,YAAY,IAAI,GAAG,6BAA6B,OAAO,GAAG;AAEpF;;AAED,OAAK,MAAM,OAAO,KACjB,KAAI,CAAC,MAAM,IAAI,IAAI,GAAG,CACrB,QAAO,KAAK,GAAG,IAAI,MAAM,YAAY,IAAI,GAAG,6BAA6B,OAAO,GAAG;;AAKtF,KAAI,OAAO,WAAW,EAAG,QAAO,EAAE,IAAI,MAAM;AAC5C,QAAO;EACN,IAAI;EACJ,OAAO;GACN,MAAM;GACN,SAAS,OAAO,KAAK,KAAK;GAC1B;EACD"}
1
+ {"version":3,"file":"validation-B6DKPTtU.mjs","names":[],"sources":["../src/api/handlers/validation.ts"],"sourcesContent":["/**\n * Field-level validation for content create / update.\n *\n * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)\n * into the handler boundary so REST and MCP both get the same enforcement:\n *\n * - required fields must be present and non-empty\n * - select / multiSelect values must match the configured options\n * - reference fields must resolve to a real, non-trashed target\n *\n * Errors surface as `{ code: \"VALIDATION_ERROR\", message }` with all\n * offending fields listed in one message so callers can fix everything in\n * a single round trip.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { Field } from \"../../schema/types.js\";\nimport { generateZodSchema } from \"../../schema/zod-generator.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\n\ntype ValidationResult =\n\t| { ok: true }\n\t| { ok: false; error: { code: \"VALIDATION_ERROR\" | \"COLLECTION_NOT_FOUND\"; message: string } };\n\n/** Treat `undefined`, `null`, and `\"\"` as \"not set\". */\nfunction isMissing(value: unknown): boolean {\n\treturn value === undefined || value === null || value === \"\";\n}\n\n/**\n * Resolve the target collection slug for a reference field.\n *\n * Schema-defined reference fields (the static `reference()` factory in\n * `fields/reference.ts`) put the target in `options.collection`. The MCP\n * `schema_create_field` tool also puts it there. Tests and some admin paths\n * stash it inside `validation.collection` directly; we accept both.\n */\nfunction getReferenceTargetCollection(field: Field): string | undefined {\n\tconst fromOptions = field.options?.collection;\n\tif (typeof fromOptions === \"string\" && fromOptions.length > 0) return fromOptions;\n\tconst validation = field.validation;\n\tif (validation && \"collection\" in validation) {\n\t\tconst fromValidation: unknown = (validation as { collection?: unknown }).collection;\n\t\tif (typeof fromValidation === \"string\" && fromValidation.length > 0) return fromValidation;\n\t}\n\treturn undefined;\n}\n\n/**\n * Format a Zod issue path into a human-readable field reference, e.g.\n * `tags`, `tags.1`, `image.alt`.\n */\nfunction formatIssuePath(path: ReadonlyArray<PropertyKey>): string {\n\tif (path.length === 0) return \"(root)\";\n\treturn path.map((seg) => String(seg)).join(\".\");\n}\n\n/**\n * Validate `data` against the collection's field definitions.\n *\n * `partial: true` switches Zod into partial mode so updates can include\n * only the fields being changed without tripping required-field errors on\n * fields the caller didn't touch. Required fields that ARE present in\n * partial-mode data still get the empty-string check below.\n */\nexport async function validateContentData(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n\toptions: { partial?: boolean } = {},\n): Promise<ValidationResult> {\n\tconst registry = new SchemaRegistry(db);\n\tconst collectionWithFields = await registry.getCollectionWithFields(collection);\n\tif (!collectionWithFields) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst issues: string[] = [];\n\n\t// Detect unknown keys explicitly so callers get a useful error rather\n\t// than silently dropped data. Leading-underscore keys (e.g. `_slug`,\n\t// `_rev`) are reserved for internal handler/runtime use and aren't real\n\t// fields; skip them.\n\tconst knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));\n\tfor (const key of Object.keys(data)) {\n\t\tif (key.startsWith(\"_\")) continue;\n\t\tif (!knownFields.has(key)) {\n\t\t\tissues.push(`${key}: unknown field on collection '${collection}'`);\n\t\t}\n\t}\n\n\t// Zod handles type, enum, length and missing-required (in non-partial\n\t// mode) checks. Empty-string handling for required string fields is\n\t// done as a separate pass below since Zod's `z.string()` accepts \"\".\n\tconst baseSchema = generateZodSchema(collectionWithFields);\n\tconst schema = options.partial ? baseSchema.partial() : baseSchema;\n\tconst parsed = schema.safeParse(data);\n\tif (!parsed.success) {\n\t\tfor (const issue of parsed.error.issues) {\n\t\t\tissues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);\n\t\t}\n\t}\n\n\t// Empty-string-on-required check. In create mode (partial=false) Zod\n\t// already catches missing/null for required fields, but `z.string()`\n\t// happily accepts \"\". In update mode (partial=true) the field is only\n\t// checked if it's present in `data`.\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (!field.required) continue;\n\t\tconst present = Object.hasOwn(data, field.slug);\n\t\tif (options.partial && !present) continue;\n\t\tif (data[field.slug] === \"\") {\n\t\t\tissues.push(`${field.slug}: required (empty value not allowed)`);\n\t\t}\n\t}\n\n\t// Reference target existence. Only check fields that:\n\t// - have a value (non-missing) in `data`\n\t// - have a resolvable target collection\n\t// - in partial mode: are present in `data`\n\t// Batch one IN-query per target collection to keep round-trips low.\n\tconst refsByTarget = new Map<string, { field: string; id: string }[]>();\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (field.type !== \"reference\") continue;\n\t\tif (options.partial && !Object.hasOwn(data, field.slug)) continue;\n\t\tconst value = data[field.slug];\n\t\tif (isMissing(value)) continue;\n\t\tif (typeof value !== \"string\") continue; // Zod will have flagged this already\n\t\tconst target = getReferenceTargetCollection(field);\n\t\tif (!target) continue;\n\t\tconst list = refsByTarget.get(target) ?? [];\n\t\tlist.push({ field: field.slug, id: value });\n\t\trefsByTarget.set(target, list);\n\t}\n\n\tfor (const [target, refs] of refsByTarget) {\n\t\t// Validate the target collection slug before interpolating into raw\n\t\t// SQL — defense-in-depth even though slugs are already validated at\n\t\t// schema-create time.\n\t\ttry {\n\t\t\tvalidateIdentifier(target, \"reference target collection\");\n\t\t} catch {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: invalid reference target collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst ids = [...new Set(refs.map((r) => r.id))];\n\t\tconst tableName = `ec_${target}`;\n\n\t\t// Chunk the IN clause to stay below D1's bind-parameter limit. One\n\t\t// reference per request is the common case today; chunking makes the\n\t\t// helper safe if a future multiSelect-of-references is added.\n\t\tconst found = new Set<string>();\n\t\tlet targetTableMissing = false;\n\t\tfor (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{ id: string }>`\n\t\t\t\t\tSELECT id FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(idChunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(db);\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tfound.add(row.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Missing table = the target collection table doesn't exist\n\t\t\t\t// (orphan reference). Treat all those references as missing.\n\t\t\t\t// Any other DB error (permissions, connection, syntax) must\n\t\t\t\t// propagate — silently dropping data integrity errors as\n\t\t\t\t// \"not found\" is exactly the bug F5 fixes.\n\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\ttargetTableMissing = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\tif (targetTableMissing) {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const ref of refs) {\n\t\t\tif (!found.has(ref.id)) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length === 0) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\terror: {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: issues.join(\"; \"),\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAS,UAAU,OAAyB;AAC3C,QAAO,UAAU,UAAa,UAAU,QAAQ,UAAU;;;;;;;;;;AAW3D,SAAS,6BAA6B,OAAkC;CACvE,MAAM,cAAc,MAAM,SAAS;AACnC,KAAI,OAAO,gBAAgB,YAAY,YAAY,SAAS,EAAG,QAAO;CACtE,MAAM,aAAa,MAAM;AACzB,KAAI,cAAc,gBAAgB,YAAY;EAC7C,MAAM,iBAA2B,WAAwC;AACzE,MAAI,OAAO,mBAAmB,YAAY,eAAe,SAAS,EAAG,QAAO;;;;;;;AAS9E,SAAS,gBAAgB,MAA0C;AAClE,KAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAO,KAAK,KAAK,QAAQ,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI;;;;;;;;;;AAWhD,eAAsB,oBACrB,IACA,YACA,MACA,UAAiC,EAAE,EACP;CAE5B,MAAM,uBAAuB,MADZ,IAAI,eAAe,GAAG,CACK,wBAAwB,WAAW;AAC/E,KAAI,CAAC,qBACJ,QAAO;EACN,IAAI;EACJ,OAAO;GACN,MAAM;GACN,SAAS,eAAe,WAAW;GACnC;EACD;CAGF,MAAM,SAAmB,EAAE;CAM3B,MAAM,cAAc,IAAI,IAAI,qBAAqB,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC;AAC3E,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AACpC,MAAI,IAAI,WAAW,IAAI,CAAE;AACzB,MAAI,CAAC,YAAY,IAAI,IAAI,CACxB,QAAO,KAAK,GAAG,IAAI,iCAAiC,WAAW,GAAG;;CAOpE,MAAM,aAAa,kBAAkB,qBAAqB;CAE1D,MAAM,UADS,QAAQ,UAAU,WAAW,SAAS,GAAG,YAClC,UAAU,KAAK;AACrC,KAAI,CAAC,OAAO,QACX,MAAK,MAAM,SAAS,OAAO,MAAM,OAChC,QAAO,KAAK,GAAG,gBAAgB,MAAM,KAAK,CAAC,IAAI,MAAM,UAAU;AAQjE,MAAK,MAAM,SAAS,qBAAqB,QAAQ;AAChD,MAAI,CAAC,MAAM,SAAU;EACrB,MAAM,UAAU,OAAO,OAAO,MAAM,MAAM,KAAK;AAC/C,MAAI,QAAQ,WAAW,CAAC,QAAS;AACjC,MAAI,KAAK,MAAM,UAAU,GACxB,QAAO,KAAK,GAAG,MAAM,KAAK,sCAAsC;;CASlE,MAAM,+BAAe,IAAI,KAA8C;AACvE,MAAK,MAAM,SAAS,qBAAqB,QAAQ;AAChD,MAAI,MAAM,SAAS,YAAa;AAChC,MAAI,QAAQ,WAAW,CAAC,OAAO,OAAO,MAAM,MAAM,KAAK,CAAE;EACzD,MAAM,QAAQ,KAAK,MAAM;AACzB,MAAI,UAAU,MAAM,CAAE;AACtB,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,SAAS,6BAA6B,MAAM;AAClD,MAAI,CAAC,OAAQ;EACb,MAAM,OAAO,aAAa,IAAI,OAAO,IAAI,EAAE;AAC3C,OAAK,KAAK;GAAE,OAAO,MAAM;GAAM,IAAI;GAAO,CAAC;AAC3C,eAAa,IAAI,QAAQ,KAAK;;AAG/B,MAAK,MAAM,CAAC,QAAQ,SAAS,cAAc;AAI1C,MAAI;AACH,sBAAmB,QAAQ,8BAA8B;UAClD;AACP,QAAK,MAAM,OAAO,KACjB,QAAO,KAAK,GAAG,IAAI,MAAM,yCAAyC,OAAO,GAAG;AAE7E;;EAGD,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC;EAC/C,MAAM,YAAY,MAAM;EAKxB,MAAM,wBAAQ,IAAI,KAAa;EAC/B,IAAI,qBAAqB;AACzB,OAAK,MAAM,WAAW,OAAO,KAAK,eAAe,CAChD,KAAI;GACH,MAAM,OAAO,MAAM,GAAmB;sBACpB,IAAI,IAAI,UAAU,CAAC;oBACrB,IAAI,KAAK,QAAQ,CAAC;;MAEhC,QAAQ,GAAG;AACb,QAAK,MAAM,OAAO,KAAK,KACtB,OAAM,IAAI,IAAI,GAAG;WAEV,OAAO;AAMf,OAAI,oBAAoB,MAAM,EAAE;AAC/B,yBAAqB;AACrB;;AAED,SAAM;;AAGR,MAAI,oBAAoB;AACvB,QAAK,MAAM,OAAO,KACjB,QAAO,KAAK,GAAG,IAAI,MAAM,YAAY,IAAI,GAAG,6BAA6B,OAAO,GAAG;AAEpF;;AAED,OAAK,MAAM,OAAO,KACjB,KAAI,CAAC,MAAM,IAAI,IAAI,GAAG,CACrB,QAAO,KAAK,GAAG,IAAI,MAAM,YAAY,IAAI,GAAG,6BAA6B,OAAO,GAAG;;AAKtF,KAAI,OAAO,WAAW,EAAG,QAAO,EAAE,IAAI,MAAM;AAC5C,QAAO;EACN,IAAI;EACJ,OAAO;GACN,MAAM;GACN,SAAS,OAAO,KAAK,KAAK;GAC1B;EACD"}
@@ -0,0 +1,7 @@
1
+ //#region src/version.ts
2
+ const VERSION = "0.25.1";
3
+ const COMMIT = "bc9f1b61";
4
+
5
+ //#endregion
6
+ export { VERSION as n, COMMIT as t };
7
+ //# sourceMappingURL=version-DHdCA24u.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-B5nHxI5Y.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
1
+ {"version":3,"file":"version-DHdCA24u.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
@@ -1,5 +1,5 @@
1
1
  import { r as requestCached } from "./request-cache-KCNHp_RJ.mjs";
2
- import { a as getDb } from "./loader-BR2SgfEc.mjs";
2
+ import { a as getDb } from "./loader-DrpofRon.mjs";
3
3
  import { t as getWidgetComponents$1 } from "./components-CYt4uVK9.mjs";
4
4
 
5
5
  //#region src/widgets/index.ts
@@ -106,4 +106,4 @@ function rowToWidget(row) {
106
106
 
107
107
  //#endregion
108
108
  export { rowToWidget as i, getWidgetAreas as n, getWidgetComponents as r, getWidgetArea as t };
109
- //# sourceMappingURL=widgets-Dri0spI4.mjs.map
109
+ //# sourceMappingURL=widgets-C2RbFN0G.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"widgets-Dri0spI4.mjs","names":["getComponentRegistry"],"sources":["../src/widgets/index.ts"],"sourcesContent":["import { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { getWidgetComponents as getComponentRegistry } from \"./components.js\";\nimport type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from \"./types.js\";\n\nexport type {\n\tWidget,\n\tWidgetArea,\n\tWidgetType,\n\tWidgetComponentDef,\n\tPropDef,\n\tCreateWidgetAreaInput,\n\tCreateWidgetInput,\n\tUpdateWidgetInput,\n\tReorderWidgetsInput,\n} from \"./types.js\";\n\n/**\n * Get a widget area by name, with all its widgets.\n *\n * Single query with a left join rather than area-then-widgets so the\n * common case costs one round-trip. An area with no widgets yields one\n * row with null widget columns, which we skip when mapping.\n */\nexport async function getWidgetArea(name: string): Promise<WidgetArea | null> {\n\treturn requestCached(`widget-area:${name}`, async () => {\n\t\tconst db = await getDb();\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_widget_areas as a\")\n\t\t\t.leftJoin(\"_emdash_widgets as w\", \"w.area_id\", \"a.id\")\n\t\t\t.select([\n\t\t\t\t\"a.id as a_id\",\n\t\t\t\t\"a.name as a_name\",\n\t\t\t\t\"a.label as a_label\",\n\t\t\t\t\"a.description as a_description\",\n\t\t\t\t\"w.id as w_id\",\n\t\t\t\t\"w.type as w_type\",\n\t\t\t\t\"w.title as w_title\",\n\t\t\t\t\"w.content as w_content\",\n\t\t\t\t\"w.menu_name as w_menu_name\",\n\t\t\t\t\"w.component_id as w_component_id\",\n\t\t\t\t\"w.component_props as w_component_props\",\n\t\t\t\t\"w.area_id as w_area_id\",\n\t\t\t\t\"w.sort_order as w_sort_order\",\n\t\t\t\t\"w.created_at as w_created_at\",\n\t\t\t])\n\t\t\t.where(\"a.name\", \"=\", name)\n\t\t\t.orderBy(\"w.sort_order\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst first = rows[0];\n\t\tif (!first) return null;\n\t\tconst widgets: Widget[] = [];\n\t\tfor (const row of rows) {\n\t\t\tif (row.w_id === null) continue; // area has no widgets (left-join null row)\n\t\t\t// Left-join makes every w_* column nullable in the type; at runtime\n\t\t\t// they're all non-null once w_id is (we match on widgets.area_id, so\n\t\t\t// a widget row always has the not-null columns filled). Cast is the\n\t\t\t// price of that structural fact.\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- left-join row is non-null when w_id is set; see above\n\t\t\tconst widgetRow = {\n\t\t\t\tid: row.w_id,\n\t\t\t\ttype: row.w_type,\n\t\t\t\ttitle: row.w_title,\n\t\t\t\tcontent: row.w_content,\n\t\t\t\tmenu_name: row.w_menu_name,\n\t\t\t\tcomponent_id: row.w_component_id,\n\t\t\t\tcomponent_props: row.w_component_props,\n\t\t\t\tarea_id: row.w_area_id,\n\t\t\t\tsort_order: row.w_sort_order,\n\t\t\t\tcreated_at: row.w_created_at,\n\t\t\t} as WidgetRow;\n\t\t\twidgets.push(rowToWidget(widgetRow));\n\t\t}\n\n\t\treturn {\n\t\t\tid: first.a_id,\n\t\t\tname: first.a_name,\n\t\t\tlabel: first.a_label,\n\t\t\tdescription: first.a_description ?? undefined,\n\t\t\twidgets,\n\t\t};\n\t});\n}\n\n/**\n * Get all widget areas with their widgets\n */\nexport async function getWidgetAreas(): Promise<WidgetArea[]> {\n\tconst db = await getDb();\n\t// Get all areas\n\tconst areaRows = await db.selectFrom(\"_emdash_widget_areas\").selectAll().execute();\n\n\t// Get all widgets\n\tconst widgetRows = await db\n\t\t.selectFrom(\"_emdash_widgets\")\n\t\t.selectAll()\n\t\t.$castTo<WidgetRow>()\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\n\t// Group widgets by area\n\tconst widgetsByArea = new Map<string, Widget[]>();\n\tfor (const row of widgetRows) {\n\t\tif (!widgetsByArea.has(row.area_id)) {\n\t\t\twidgetsByArea.set(row.area_id, []);\n\t\t}\n\t\twidgetsByArea.get(row.area_id)!.push(rowToWidget(row));\n\t}\n\n\t// Combine\n\treturn areaRows.map((areaRow) => ({\n\t\tid: areaRow.id,\n\t\tname: areaRow.name,\n\t\tlabel: areaRow.label,\n\t\tdescription: areaRow.description ?? undefined,\n\t\twidgets: widgetsByArea.get(areaRow.id) || [],\n\t}));\n}\n\n/**\n * Get available widget components (for admin UI)\n */\nexport function getWidgetComponents(): WidgetComponentDef[] {\n\treturn getComponentRegistry();\n}\n\n/**\n * Convert a widget row to the API type\n */\nexport function rowToWidget(row: WidgetRow): Widget {\n\tconst widget: Widget = {\n\t\tid: row.id,\n\t\ttype: row.type,\n\t\ttitle: row.title ?? undefined,\n\t};\n\n\t// Type-specific fields\n\tif (row.type === \"content\" && row.content) {\n\t\ttry {\n\t\t\twidget.content = JSON.parse(row.content);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\tif (row.type === \"menu\" && row.menu_name) {\n\t\twidget.menuName = row.menu_name;\n\t}\n\n\tif (row.type === \"component\" && row.component_id) {\n\t\twidget.componentId = row.component_id;\n\t\tif (row.component_props) {\n\t\t\ttry {\n\t\t\t\twidget.componentProps = JSON.parse(row.component_props);\n\t\t\t} catch {\n\t\t\t\t// Invalid JSON, ignore\n\t\t\t}\n\t\t}\n\t}\n\n\treturn widget;\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,eAAsB,cAAc,MAA0C;AAC7E,QAAO,cAAc,eAAe,QAAQ,YAAY;EAEvD,MAAM,OAAO,OADF,MAAM,OAAO,EAEtB,WAAW,4BAA4B,CACvC,SAAS,wBAAwB,aAAa,OAAO,CACrD,OAAO;GACP;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,CAAC,CACD,MAAM,UAAU,KAAK,KAAK,CAC1B,QAAQ,gBAAgB,MAAM,CAC9B,SAAS;EAEX,MAAM,QAAQ,KAAK;AACnB,MAAI,CAAC,MAAO,QAAO;EACnB,MAAM,UAAoB,EAAE;AAC5B,OAAK,MAAM,OAAO,MAAM;AACvB,OAAI,IAAI,SAAS,KAAM;GAMvB,MAAM,YAAY;IACjB,IAAI,IAAI;IACR,MAAM,IAAI;IACV,OAAO,IAAI;IACX,SAAS,IAAI;IACb,WAAW,IAAI;IACf,cAAc,IAAI;IAClB,iBAAiB,IAAI;IACrB,SAAS,IAAI;IACb,YAAY,IAAI;IAChB,YAAY,IAAI;IAChB;AACD,WAAQ,KAAK,YAAY,UAAU,CAAC;;AAGrC,SAAO;GACN,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,aAAa,MAAM,iBAAiB;GACpC;GACA;GACA;;;;;AAMH,eAAsB,iBAAwC;CAC7D,MAAM,KAAK,MAAM,OAAO;CAExB,MAAM,WAAW,MAAM,GAAG,WAAW,uBAAuB,CAAC,WAAW,CAAC,SAAS;CAGlF,MAAM,aAAa,MAAM,GACvB,WAAW,kBAAkB,CAC7B,WAAW,CACX,SAAoB,CACpB,QAAQ,cAAc,MAAM,CAC5B,SAAS;CAGX,MAAM,gCAAgB,IAAI,KAAuB;AACjD,MAAK,MAAM,OAAO,YAAY;AAC7B,MAAI,CAAC,cAAc,IAAI,IAAI,QAAQ,CAClC,eAAc,IAAI,IAAI,SAAS,EAAE,CAAC;AAEnC,gBAAc,IAAI,IAAI,QAAQ,CAAE,KAAK,YAAY,IAAI,CAAC;;AAIvD,QAAO,SAAS,KAAK,aAAa;EACjC,IAAI,QAAQ;EACZ,MAAM,QAAQ;EACd,OAAO,QAAQ;EACf,aAAa,QAAQ,eAAe;EACpC,SAAS,cAAc,IAAI,QAAQ,GAAG,IAAI,EAAE;EAC5C,EAAE;;;;;AAMJ,SAAgB,sBAA4C;AAC3D,QAAOA,uBAAsB;;;;;AAM9B,SAAgB,YAAY,KAAwB;CACnD,MAAM,SAAiB;EACtB,IAAI,IAAI;EACR,MAAM,IAAI;EACV,OAAO,IAAI,SAAS;EACpB;AAGD,KAAI,IAAI,SAAS,aAAa,IAAI,QACjC,KAAI;AACH,SAAO,UAAU,KAAK,MAAM,IAAI,QAAQ;SACjC;AAKT,KAAI,IAAI,SAAS,UAAU,IAAI,UAC9B,QAAO,WAAW,IAAI;AAGvB,KAAI,IAAI,SAAS,eAAe,IAAI,cAAc;AACjD,SAAO,cAAc,IAAI;AACzB,MAAI,IAAI,gBACP,KAAI;AACH,UAAO,iBAAiB,KAAK,MAAM,IAAI,gBAAgB;UAChD;;AAMV,QAAO"}
1
+ {"version":3,"file":"widgets-C2RbFN0G.mjs","names":["getComponentRegistry"],"sources":["../src/widgets/index.ts"],"sourcesContent":["import { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { getWidgetComponents as getComponentRegistry } from \"./components.js\";\nimport type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from \"./types.js\";\n\nexport type {\n\tWidget,\n\tWidgetArea,\n\tWidgetType,\n\tWidgetComponentDef,\n\tPropDef,\n\tCreateWidgetAreaInput,\n\tCreateWidgetInput,\n\tUpdateWidgetInput,\n\tReorderWidgetsInput,\n} from \"./types.js\";\n\n/**\n * Get a widget area by name, with all its widgets.\n *\n * Single query with a left join rather than area-then-widgets so the\n * common case costs one round-trip. An area with no widgets yields one\n * row with null widget columns, which we skip when mapping.\n */\nexport async function getWidgetArea(name: string): Promise<WidgetArea | null> {\n\treturn requestCached(`widget-area:${name}`, async () => {\n\t\tconst db = await getDb();\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_widget_areas as a\")\n\t\t\t.leftJoin(\"_emdash_widgets as w\", \"w.area_id\", \"a.id\")\n\t\t\t.select([\n\t\t\t\t\"a.id as a_id\",\n\t\t\t\t\"a.name as a_name\",\n\t\t\t\t\"a.label as a_label\",\n\t\t\t\t\"a.description as a_description\",\n\t\t\t\t\"w.id as w_id\",\n\t\t\t\t\"w.type as w_type\",\n\t\t\t\t\"w.title as w_title\",\n\t\t\t\t\"w.content as w_content\",\n\t\t\t\t\"w.menu_name as w_menu_name\",\n\t\t\t\t\"w.component_id as w_component_id\",\n\t\t\t\t\"w.component_props as w_component_props\",\n\t\t\t\t\"w.area_id as w_area_id\",\n\t\t\t\t\"w.sort_order as w_sort_order\",\n\t\t\t\t\"w.created_at as w_created_at\",\n\t\t\t])\n\t\t\t.where(\"a.name\", \"=\", name)\n\t\t\t.orderBy(\"w.sort_order\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst first = rows[0];\n\t\tif (!first) return null;\n\t\tconst widgets: Widget[] = [];\n\t\tfor (const row of rows) {\n\t\t\tif (row.w_id === null) continue; // area has no widgets (left-join null row)\n\t\t\t// Left-join makes every w_* column nullable in the type; at runtime\n\t\t\t// they're all non-null once w_id is (we match on widgets.area_id, so\n\t\t\t// a widget row always has the not-null columns filled). Cast is the\n\t\t\t// price of that structural fact.\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- left-join row is non-null when w_id is set; see above\n\t\t\tconst widgetRow = {\n\t\t\t\tid: row.w_id,\n\t\t\t\ttype: row.w_type,\n\t\t\t\ttitle: row.w_title,\n\t\t\t\tcontent: row.w_content,\n\t\t\t\tmenu_name: row.w_menu_name,\n\t\t\t\tcomponent_id: row.w_component_id,\n\t\t\t\tcomponent_props: row.w_component_props,\n\t\t\t\tarea_id: row.w_area_id,\n\t\t\t\tsort_order: row.w_sort_order,\n\t\t\t\tcreated_at: row.w_created_at,\n\t\t\t} as WidgetRow;\n\t\t\twidgets.push(rowToWidget(widgetRow));\n\t\t}\n\n\t\treturn {\n\t\t\tid: first.a_id,\n\t\t\tname: first.a_name,\n\t\t\tlabel: first.a_label,\n\t\t\tdescription: first.a_description ?? undefined,\n\t\t\twidgets,\n\t\t};\n\t});\n}\n\n/**\n * Get all widget areas with their widgets\n */\nexport async function getWidgetAreas(): Promise<WidgetArea[]> {\n\tconst db = await getDb();\n\t// Get all areas\n\tconst areaRows = await db.selectFrom(\"_emdash_widget_areas\").selectAll().execute();\n\n\t// Get all widgets\n\tconst widgetRows = await db\n\t\t.selectFrom(\"_emdash_widgets\")\n\t\t.selectAll()\n\t\t.$castTo<WidgetRow>()\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\n\t// Group widgets by area\n\tconst widgetsByArea = new Map<string, Widget[]>();\n\tfor (const row of widgetRows) {\n\t\tif (!widgetsByArea.has(row.area_id)) {\n\t\t\twidgetsByArea.set(row.area_id, []);\n\t\t}\n\t\twidgetsByArea.get(row.area_id)!.push(rowToWidget(row));\n\t}\n\n\t// Combine\n\treturn areaRows.map((areaRow) => ({\n\t\tid: areaRow.id,\n\t\tname: areaRow.name,\n\t\tlabel: areaRow.label,\n\t\tdescription: areaRow.description ?? undefined,\n\t\twidgets: widgetsByArea.get(areaRow.id) || [],\n\t}));\n}\n\n/**\n * Get available widget components (for admin UI)\n */\nexport function getWidgetComponents(): WidgetComponentDef[] {\n\treturn getComponentRegistry();\n}\n\n/**\n * Convert a widget row to the API type\n */\nexport function rowToWidget(row: WidgetRow): Widget {\n\tconst widget: Widget = {\n\t\tid: row.id,\n\t\ttype: row.type,\n\t\ttitle: row.title ?? undefined,\n\t};\n\n\t// Type-specific fields\n\tif (row.type === \"content\" && row.content) {\n\t\ttry {\n\t\t\twidget.content = JSON.parse(row.content);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\tif (row.type === \"menu\" && row.menu_name) {\n\t\twidget.menuName = row.menu_name;\n\t}\n\n\tif (row.type === \"component\" && row.component_id) {\n\t\twidget.componentId = row.component_id;\n\t\tif (row.component_props) {\n\t\t\ttry {\n\t\t\t\twidget.componentProps = JSON.parse(row.component_props);\n\t\t\t} catch {\n\t\t\t\t// Invalid JSON, ignore\n\t\t\t}\n\t\t}\n\t}\n\n\treturn widget;\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,eAAsB,cAAc,MAA0C;AAC7E,QAAO,cAAc,eAAe,QAAQ,YAAY;EAEvD,MAAM,OAAO,OADF,MAAM,OAAO,EAEtB,WAAW,4BAA4B,CACvC,SAAS,wBAAwB,aAAa,OAAO,CACrD,OAAO;GACP;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,CAAC,CACD,MAAM,UAAU,KAAK,KAAK,CAC1B,QAAQ,gBAAgB,MAAM,CAC9B,SAAS;EAEX,MAAM,QAAQ,KAAK;AACnB,MAAI,CAAC,MAAO,QAAO;EACnB,MAAM,UAAoB,EAAE;AAC5B,OAAK,MAAM,OAAO,MAAM;AACvB,OAAI,IAAI,SAAS,KAAM;GAMvB,MAAM,YAAY;IACjB,IAAI,IAAI;IACR,MAAM,IAAI;IACV,OAAO,IAAI;IACX,SAAS,IAAI;IACb,WAAW,IAAI;IACf,cAAc,IAAI;IAClB,iBAAiB,IAAI;IACrB,SAAS,IAAI;IACb,YAAY,IAAI;IAChB,YAAY,IAAI;IAChB;AACD,WAAQ,KAAK,YAAY,UAAU,CAAC;;AAGrC,SAAO;GACN,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,aAAa,MAAM,iBAAiB;GACpC;GACA;GACA;;;;;AAMH,eAAsB,iBAAwC;CAC7D,MAAM,KAAK,MAAM,OAAO;CAExB,MAAM,WAAW,MAAM,GAAG,WAAW,uBAAuB,CAAC,WAAW,CAAC,SAAS;CAGlF,MAAM,aAAa,MAAM,GACvB,WAAW,kBAAkB,CAC7B,WAAW,CACX,SAAoB,CACpB,QAAQ,cAAc,MAAM,CAC5B,SAAS;CAGX,MAAM,gCAAgB,IAAI,KAAuB;AACjD,MAAK,MAAM,OAAO,YAAY;AAC7B,MAAI,CAAC,cAAc,IAAI,IAAI,QAAQ,CAClC,eAAc,IAAI,IAAI,SAAS,EAAE,CAAC;AAEnC,gBAAc,IAAI,IAAI,QAAQ,CAAE,KAAK,YAAY,IAAI,CAAC;;AAIvD,QAAO,SAAS,KAAK,aAAa;EACjC,IAAI,QAAQ;EACZ,MAAM,QAAQ;EACd,OAAO,QAAQ;EACf,aAAa,QAAQ,eAAe;EACpC,SAAS,cAAc,IAAI,QAAQ,GAAG,IAAI,EAAE;EAC5C,EAAE;;;;;AAMJ,SAAgB,sBAA4C;AAC3D,QAAOA,uBAAsB;;;;;AAM9B,SAAgB,YAAY,KAAwB;CACnD,MAAM,SAAiB;EACtB,IAAI,IAAI;EACR,MAAM,IAAI;EACV,OAAO,IAAI,SAAS;EACpB;AAGD,KAAI,IAAI,SAAS,aAAa,IAAI,QACjC,KAAI;AACH,SAAO,UAAU,KAAK,MAAM,IAAI,QAAQ;SACjC;AAKT,KAAI,IAAI,SAAS,UAAU,IAAI,UAC9B,QAAO,WAAW,IAAI;AAGvB,KAAI,IAAI,SAAS,eAAe,IAAI,cAAc;AACjD,SAAO,cAAc,IAAI;AACzB,MAAI,IAAI,gBACP,KAAI;AACH,UAAO,iBAAiB,KAAK,MAAM,IAAI,gBAAgB;UAChD;;AAMV,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emdash",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "Astro-native CMS with WordPress migration support",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -230,9 +230,9 @@
230
230
  "upng-js": "^2.1.0",
231
231
  "zod": "^4.4.1",
232
232
  "@atcute/client": "^5.0.0",
233
- "@emdash-cms/admin": "0.24.0",
234
- "@emdash-cms/gutenberg-to-portable-text": "0.24.0",
235
- "@emdash-cms/auth": "0.24.0",
233
+ "@emdash-cms/admin": "0.25.1",
234
+ "@emdash-cms/auth": "0.25.1",
235
+ "@emdash-cms/gutenberg-to-portable-text": "0.25.1",
236
236
  "@emdash-cms/plugin-types": "0.1.0",
237
237
  "@emdash-cms/registry-client": "0.3.2"
238
238
  },
@@ -267,7 +267,7 @@
267
267
  "vite": "^6.0.0",
268
268
  "vitest": "^4.1.5",
269
269
  "zod-openapi": "^5.4.6",
270
- "@emdash-cms/blocks": "0.24.0"
270
+ "@emdash-cms/blocks": "0.25.1"
271
271
  },
272
272
  "repository": {
273
273
  "type": "git",
package/src/api/errors.ts CHANGED
@@ -72,6 +72,7 @@ export const ErrorCode = {
72
72
  COLLECTION_NOT_FOUND: "COLLECTION_NOT_FOUND",
73
73
  TABLE_NOT_FOUND: "TABLE_NOT_FOUND",
74
74
  FIELD_EXISTS: "FIELD_EXISTS",
75
+ FIELD_TYPE_COLUMN_CHANGE: "FIELD_TYPE_COLUMN_CHANGE",
75
76
  RESERVED_SLUG: "RESERVED_SLUG",
76
77
  INVALID_SLUG: "INVALID_SLUG",
77
78
  CREATE_FAILED: "CREATE_FAILED",
@@ -1087,15 +1087,15 @@ export async function handleContentRestore(
1087
1087
  db: Kysely<Database>,
1088
1088
  collection: string,
1089
1089
  id: string,
1090
- ): Promise<ApiResult<{ restored: true }>> {
1090
+ ): Promise<ApiResult<{ restored: true; item: ContentItem }>> {
1091
1091
  try {
1092
- const restored = await withTransaction(db, async (trx) => {
1092
+ const item = await withTransaction(db, async (trx) => {
1093
1093
  const repo = new ContentRepository(trx);
1094
1094
  const resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;
1095
1095
  return repo.restore(collection, resolvedId);
1096
1096
  });
1097
1097
 
1098
- if (!restored) {
1098
+ if (!item) {
1099
1099
  return {
1100
1100
  success: false,
1101
1101
  error: {
@@ -1107,7 +1107,7 @@ export async function handleContentRestore(
1107
1107
 
1108
1108
  return {
1109
1109
  success: true,
1110
- data: { restored: true },
1110
+ data: { restored: true, item },
1111
1111
  };
1112
1112
  } catch (error) {
1113
1113
  console.error("Content restore error:", error);
@@ -56,6 +56,34 @@ export interface SitemapDataResponse {
56
56
  /** Maximum entries per sitemap (per spec) */
57
57
  const SITEMAP_MAX_ENTRIES = 50_000;
58
58
 
59
+ /** Matches a trailing timezone designator (`Z` or `±HH`, `±HHMM`, `±HH:MM`). */
60
+ const TZ_SUFFIX_RE = /([zZ]|[+-]\d{2}(:?\d{2})?)$/;
61
+
62
+ /**
63
+ * Normalize a stored timestamp to W3C Datetime (ISO 8601) for sitemaps.
64
+ *
65
+ * `updated_at` is not guaranteed to be ISO: the column default is
66
+ * `datetime('now')` on SQLite and `CURRENT_TIMESTAMP` on Postgres, both of
67
+ * which store a space-separated `YYYY-MM-DD HH:MM:SS` string (and imported
68
+ * content can carry other shapes). The sitemap `<lastmod>` field requires
69
+ * W3C Datetime, and Google Search Console rejects the space-separated form
70
+ * as "Invalid date". Normalize defensively, assuming UTC when no offset is
71
+ * present (matches SQLite's `datetime('now')`). Valid date strings are
72
+ * normalized to UTC ISO 8601; unparseable values are returned as-is.
73
+ */
74
+ function toW3CDate(value: string): string {
75
+ if (!value) return value;
76
+ let normalized = value.trim();
77
+ if (normalized.includes(" ") && !normalized.includes("T")) {
78
+ normalized = normalized.replace(" ", "T");
79
+ }
80
+ if (!TZ_SUFFIX_RE.test(normalized)) {
81
+ normalized += "Z";
82
+ }
83
+ const parsed = Date.parse(normalized);
84
+ return Number.isNaN(parsed) ? value : new Date(parsed).toISOString();
85
+ }
86
+
59
87
  /**
60
88
  * Collect all published, indexable content across SEO-enabled collections
61
89
  * for sitemap generation, grouped by collection.
@@ -133,7 +161,7 @@ export async function handleSitemapData(
133
161
  entries.push({
134
162
  id: row.id,
135
163
  slug: row.slug,
136
- updatedAt: row.updated_at,
164
+ updatedAt: toW3CDate(row.updated_at),
137
165
  locale: row.locale,
138
166
  translationGroup: row.translation_group,
139
167
  image: row.seo_image ?? null,
@@ -144,7 +172,7 @@ export async function handleSitemapData(
144
172
  collection: col.slug,
145
173
  urlPattern: col.url_pattern,
146
174
  // Rows are ordered by updated_at DESC, so first row is the latest
147
- lastmod: rows.rows[0].updated_at,
175
+ lastmod: toW3CDate(rows.rows[0].updated_at),
148
176
  entries,
149
177
  });
150
178
  } catch (err) {
@@ -125,6 +125,7 @@ export const createFieldBody = z
125
125
  export const updateFieldBody = z
126
126
  .object({
127
127
  label: z.string().min(1).optional(),
128
+ type: fieldTypeValues.optional(),
128
129
  required: z.boolean().optional(),
129
130
  unique: z.boolean().optional(),
130
131
  defaultValue: z.unknown().optional(),
@@ -490,6 +490,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
490
490
  const sessionUser =
491
491
  context.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);
492
492
 
493
+ // Credentialed API requests (API tokens `ec_pat_*`, OAuth tokens
494
+ // `ec_oat_*`, and other Bearer credentials) carry no `astro-session`
495
+ // cookie, so `sessionUser` is null for them -- yet they still expect
496
+ // read-your-writes. The auth middleware that resolves the token runs
497
+ // *after* this one, so `locals.user` isn't populated here; detect the
498
+ // credential directly on the request. Request-scoped adapters use this to
499
+ // keep such requests on the primary/uncached connection (not a lagging
500
+ // read replica or the Hyperdrive query cache).
501
+ const hasBearerAuth = (request.headers.get("authorization") ?? "")
502
+ .toLowerCase()
503
+ .startsWith("bearer ");
504
+
493
505
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
494
506
  if (!sessionUser && !playgroundDb) {
495
507
  const timings: Array<{ name: string; dur: number; desc?: string }> = [];
@@ -668,6 +680,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
668
680
  // Content handlers
669
681
  handleContentList: runtime.handleContentList.bind(runtime),
670
682
  handleContentGet: runtime.handleContentGet.bind(runtime),
683
+ handleContentAuthors: runtime.handleContentAuthors.bind(runtime),
671
684
  handleContentCreate: runtime.handleContentCreate.bind(runtime),
672
685
  handleContentUpdate: runtime.handleContentUpdate.bind(runtime),
673
686
  handleContentDelete: runtime.handleContentDelete.bind(runtime),
@@ -775,7 +788,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
775
788
  // per-request state (e.g. a D1 bookmark cookie for read-your-writes).
776
789
  const scoped = createRequestScopedDb({
777
790
  config: config?.database?.config,
778
- isAuthenticated: !!sessionUser,
791
+ isAuthenticated: !!sessionUser || hasBearerAuth,
779
792
  isWrite: request.method !== "GET" && request.method !== "HEAD",
780
793
  cookies: context.cookies,
781
794
  url,
@@ -20,6 +20,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
20
20
  import { isParseError, parseBody } from "#api/parse.js";
21
21
  import { wpMediaImportBody } from "#api/schemas.js";
22
22
  import { validateExternalUrl, ssrfSafeFetch, SsrfError } from "#import/ssrf.js";
23
+ import { enrichImageMetadata } from "#media/enrich.js";
23
24
  import type { EmDashHandlers } from "#types";
24
25
 
25
26
  import type { AttachmentInfo } from "./analyze.js";
@@ -125,7 +126,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
125
126
  }
126
127
  };
127
128
 
128
- async function importMediaWithProgress(
129
+ export async function importMediaWithProgress(
129
130
  attachments: AttachmentInfo[],
130
131
  db: NonNullable<EmDashHandlers["db"]>,
131
132
  storage: NonNullable<EmDashHandlers["storage"]>,
@@ -273,6 +274,9 @@ async function importMediaWithProgress(
273
274
  contentType,
274
275
  });
275
276
 
277
+ // Derive dimensions + LQIP placeholders (no-op for non-images).
278
+ const enriched = await enrichImageMetadata(new Uint8Array(buffer), contentType);
279
+
276
280
  // Create media record with content hash
277
281
  const mediaItem = await repo.create({
278
282
  filename: attachment.filename || `media-${attachment.id}${ext}`,
@@ -280,8 +284,10 @@ async function importMediaWithProgress(
280
284
  size,
281
285
  storageKey,
282
286
  contentHash,
283
- width: undefined,
284
- height: undefined,
287
+ width: enriched.width,
288
+ height: enriched.height,
289
+ blurhash: enriched.blurhash,
290
+ dominantColor: enriched.dominantColor,
285
291
  });
286
292
 
287
293
  // Build the new URL
@@ -14,10 +14,20 @@ import { requireOwnerPerm, requirePerm } from "#api/authorize.js";
14
14
  import { apiError, apiSuccess, handleError } from "#api/error.js";
15
15
  import { isParseError, parseOptionalBody } from "#api/parse.js";
16
16
  import { mediaConfirmBody } from "#api/schemas.js";
17
+ import { enrichImageMetadata } from "#media/enrich.js";
17
18
  import type { MediaItem } from "#types";
18
19
 
19
20
  export const prerender = false;
20
21
 
22
+ /**
23
+ * Max raw bytes to buffer for server-side LQIP generation at confirm time. The
24
+ * signed-URL upload flow exists so large files bypass server buffering — re-reading
25
+ * the whole object into a Worker's 128 MB heap to compute a blurhash would OOM
26
+ * on the very uploads that flow was designed for. LQIP is progressive
27
+ * enhancement: large images simply ship without a server-generated placeholder.
28
+ */
29
+ const MAX_PLACEHOLDER_DOWNLOAD_BYTES = 8 * 1024 * 1024;
30
+
21
31
  /**
22
32
  * Add URL to media item (relative URL for portability)
23
33
  */
@@ -81,11 +91,61 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
81
91
  }
82
92
  }
83
93
 
94
+ // For images, read the just-uploaded bytes back from storage once to
95
+ // generate LQIP placeholders (and server-side dimensions as a fallback).
96
+ // The signed-URL flow uploads directly to storage, so this confirm is the
97
+ // only point at which the server sees the bytes. Best-effort: a decode
98
+ // failure must not block the upload from being marked ready. We also cap
99
+ // the download size — buffering a large original into a Worker heap to
100
+ // compute a 32px blurhash would OOM on the uploads the signed-URL path
101
+ // exists to support, so oversized files skip the server-side placeholder.
102
+ let blurhash: string | undefined;
103
+ let dominantColor: string | undefined;
104
+ let width = body.width;
105
+ let height = body.height;
106
+ if (emdash.storage && existing.mimeType.startsWith("image/")) {
107
+ const knownSize = body.size ?? existing.size ?? undefined;
108
+ const tooLarge = knownSize != null && knownSize > MAX_PLACEHOLDER_DOWNLOAD_BYTES;
109
+ if (!tooLarge) {
110
+ try {
111
+ const { body: stream } = await emdash.storage.download(existing.storageKey);
112
+ const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
113
+ // Defense-in-depth for the unknown-size case: even though we
114
+ // already buffered it, refuse the decode so we don't also pay
115
+ // the (larger) RGBA allocation.
116
+ if (bytes.byteLength > MAX_PLACEHOLDER_DOWNLOAD_BYTES) {
117
+ console.warn(
118
+ `[media] confirm skipping placeholder: object ${existing.storageKey} is ${bytes.byteLength} bytes (> ${MAX_PLACEHOLDER_DOWNLOAD_BYTES})`,
119
+ );
120
+ } else {
121
+ const enriched = await enrichImageMetadata(bytes, existing.mimeType, {
122
+ knownDimensions:
123
+ body.width != null && body.height != null
124
+ ? { width: body.width, height: body.height }
125
+ : undefined,
126
+ });
127
+ blurhash = enriched.blurhash;
128
+ dominantColor = enriched.dominantColor;
129
+ width = width ?? enriched.width;
130
+ height = height ?? enriched.height;
131
+ }
132
+ } catch (error) {
133
+ console.error("[media] confirm placeholder generation failed:", error);
134
+ }
135
+ } else {
136
+ console.warn(
137
+ `[media] confirm skipping placeholder: object ${existing.storageKey} reported size ${knownSize} bytes (> ${MAX_PLACEHOLDER_DOWNLOAD_BYTES})`,
138
+ );
139
+ }
140
+ }
141
+
84
142
  // Confirm the upload
85
143
  const item = await repo.confirmUpload(id, {
86
144
  size: body.size,
87
- width: body.width,
88
- height: body.height,
145
+ width,
146
+ height,
147
+ blurhash,
148
+ dominantColor,
89
149
  });
90
150
 
91
151
  if (!item) {
@@ -16,8 +16,8 @@ import { GLOBAL_UPLOAD_ALLOWLIST, resolveFieldAllowlist } from "#api/handlers/me
16
16
  import { isParseError, parseQuery } from "#api/parse.js";
17
17
  import { DEFAULT_MAX_UPLOAD_SIZE, formatFileSize, mediaListQuery } from "#api/schemas.js";
18
18
  import { MediaRepository } from "#db/repositories/media.js";
19
+ import { enrichImageMetadata } from "#media/enrich.js";
19
20
  import { matchesMimeAllowlist, normalizeMime } from "#media/mime.js";
20
- import { generatePlaceholder } from "#media/placeholder.js";
21
21
  import { computeContentHash } from "#utils/hash.js";
22
22
 
23
23
  import type { MediaItem } from "../../types.js";
@@ -163,34 +163,32 @@ export const POST: APIRoute = async ({ request, locals }) => {
163
163
  const width = widthStr ? parseInt(widthStr, 10) : undefined;
164
164
  const height = heightStr ? parseInt(heightStr, 10) : undefined;
165
165
 
166
- // Generate placeholder data for images.
167
- // If the client sent a thumbnail (small pre-resized image), use that
168
- // instead of the full buffer to avoid OOM on memory-constrained runtimes.
166
+ // Derive dimensions + LQIP placeholders via the shared helper.
167
+ // If the client sent a downscaled thumbnail, decode that for the blurhash
168
+ // (avoids OOM on large originals on memory-constrained runtimes).
169
169
  const thumbnailEntry = formData.get("thumbnail");
170
170
  const thumbnail = thumbnailEntry instanceof File ? thumbnailEntry : null;
171
-
172
- let placeholder: Awaited<ReturnType<typeof generatePlaceholder>> = null;
173
- if (file.type.startsWith("image/")) {
174
- if (thumbnail) {
175
- const thumbBuffer = new Uint8Array(await thumbnail.arrayBuffer());
176
- placeholder = await generatePlaceholder(thumbBuffer, thumbnail.type);
177
- } else {
178
- const clientDims = width && height ? { width, height } : undefined;
179
- placeholder = await generatePlaceholder(buffer, file.type, clientDims);
180
- }
181
- }
171
+ const enriched = await enrichImageMetadata(buffer, file.type, {
172
+ knownDimensions: width != null && height != null ? { width, height } : undefined,
173
+ placeholder: thumbnail
174
+ ? { bytes: new Uint8Array(await thumbnail.arrayBuffer()), contentType: thumbnail.type }
175
+ : undefined,
176
+ });
182
177
 
183
178
  // Create media record
184
179
  const result = await emdash.handleMediaCreate({
185
180
  filename: file.name,
186
181
  mimeType: normalizeMime(file.type),
187
182
  size: file.size,
188
- width,
189
- height,
183
+ // Client dimensions win over server header dimensions: the browser's
184
+ // naturalWidth/Height apply EXIF orientation, while image-size reports
185
+ // raw (pre-orientation) header dims — swapped for 90°/270° JPEGs.
186
+ width: width ?? enriched.width,
187
+ height: height ?? enriched.height,
190
188
  storageKey,
191
189
  contentHash,
192
- blurhash: placeholder?.blurhash,
193
- dominantColor: placeholder?.dominantColor,
190
+ blurhash: enriched.blurhash,
191
+ dominantColor: enriched.dominantColor,
194
192
  authorId: user?.id,
195
193
  });
196
194
 
@@ -158,15 +158,12 @@ if (img) {
158
158
  }
159
159
  }
160
160
 
161
- // Build placeholder background style
161
+ // Build placeholder background style. Prefer the first-class MediaValue fields;
162
+ // fall back to `meta` for snapshots stored before LQIP was promoted off `meta`.
162
163
  const blurhash =
163
- typeof image === "object"
164
- ? (image?.meta?.blurhash as string | undefined)
165
- : undefined;
164
+ img?.blurhash ?? (img?.meta?.blurhash as string | undefined);
166
165
  const dominantColor =
167
- typeof image === "object"
168
- ? (image?.meta?.dominantColor as string | undefined)
169
- : undefined;
166
+ img?.dominantColor ?? (img?.meta?.dominantColor as string | undefined);
170
167
 
171
168
  let placeholderStyle = "";
172
169
  if (blurhash) {