emdash 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (605) hide show
  1. package/dist/{adapters-9DybjTO6.d.mts → adapters-C4yd_UJR.d.mts} +1 -1
  2. package/dist/{adapters-9DybjTO6.d.mts.map → adapters-C4yd_UJR.d.mts.map} +1 -1
  3. package/dist/{allowed-origins-CDdG-4Gd.mjs → allowed-origins-D0fFk9a6.mjs} +2 -2
  4. package/dist/{allowed-origins-CDdG-4Gd.mjs.map → allowed-origins-D0fFk9a6.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +15 -15
  7. package/dist/api/schemas/index.d.mts +2 -2
  8. package/dist/api/schemas/index.mjs +3 -3
  9. package/dist/{api-ayIQ7rIe.mjs → api-CLwG_3dh.mjs} +523 -59
  10. package/dist/api-CLwG_3dh.mjs.map +1 -0
  11. package/dist/{api-tokens-eYymBhIT.mjs → api-tokens-ucpcNXDt.mjs} +2 -2
  12. package/dist/{api-tokens-eYymBhIT.mjs.map → api-tokens-ucpcNXDt.mjs.map} +1 -1
  13. package/dist/{apply-v4DBgjPw.mjs → apply-wJhM_bwU.mjs} +17 -17
  14. package/dist/{apply-v4DBgjPw.mjs.map → apply-wJhM_bwU.mjs.map} +1 -1
  15. package/dist/astro/index.d.mts +10 -10
  16. package/dist/astro/index.mjs +21 -5
  17. package/dist/astro/index.mjs.map +1 -1
  18. package/dist/astro/middleware/auth.d.mts +9 -9
  19. package/dist/astro/middleware/auth.mjs +6 -6
  20. package/dist/astro/middleware/auth.mjs.map +1 -1
  21. package/dist/astro/middleware/redirect.mjs +4 -4
  22. package/dist/astro/middleware/request-context.mjs +2 -2
  23. package/dist/astro/middleware/request-context.mjs.map +1 -1
  24. package/dist/astro/middleware/setup.mjs +1 -1
  25. package/dist/astro/middleware.d.mts.map +1 -1
  26. package/dist/astro/middleware.mjs +353 -71
  27. package/dist/astro/middleware.mjs.map +1 -1
  28. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
  29. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
  30. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
  31. package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
  32. package/dist/astro/routes/api/admin/bylines/_id_/index.d.mts.map +1 -1
  33. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +14 -17
  34. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  35. package/dist/astro/routes/api/admin/bylines/_id_/translations.d.mts +9 -0
  36. package/dist/astro/routes/api/admin/bylines/_id_/translations.d.mts.map +1 -0
  37. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +70 -0
  38. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -0
  39. package/dist/astro/routes/api/admin/bylines/index.d.mts.map +1 -1
  40. package/dist/astro/routes/api/admin/bylines/index.mjs +25 -16
  41. package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
  42. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +10 -10
  43. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  44. package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
  45. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  46. package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
  47. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  48. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  49. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
  50. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
  51. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +32 -31
  52. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +32 -31
  54. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  55. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +31 -30
  56. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  57. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +31 -30
  58. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +33 -31
  60. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  61. package/dist/astro/routes/api/admin/plugins/index.mjs +31 -30
  62. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  63. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  64. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +31 -30
  65. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  66. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +33 -31
  67. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  68. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +31 -30
  69. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  70. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts +8 -0
  71. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -0
  72. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +59 -0
  73. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -0
  74. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts +8 -0
  75. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -0
  76. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +72 -0
  77. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -0
  78. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +31 -30
  79. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/updates.d.mts.map +1 -1
  81. package/dist/astro/routes/api/admin/plugins/updates.mjs +44 -31
  82. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  83. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +31 -30
  84. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  85. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  86. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +31 -30
  87. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  89. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  90. package/dist/astro/routes/api/admin/users/_id_/index.mjs +5 -5
  91. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  92. package/dist/astro/routes/api/admin/users/index.mjs +5 -5
  93. package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
  94. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  95. package/dist/astro/routes/api/auth/invite/complete.mjs +9 -9
  96. package/dist/astro/routes/api/auth/invite/index.mjs +6 -6
  97. package/dist/astro/routes/api/auth/invite/register-options.mjs +8 -8
  98. package/dist/astro/routes/api/auth/logout.mjs +3 -3
  99. package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
  100. package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
  101. package/dist/astro/routes/api/auth/me.mjs +5 -5
  102. package/dist/astro/routes/api/auth/mode.mjs +1 -1
  103. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  104. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs.map +1 -1
  105. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  106. package/dist/astro/routes/api/auth/oauth/_provider_.mjs.map +1 -1
  107. package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
  108. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  109. package/dist/astro/routes/api/auth/passkey/options.mjs +10 -10
  110. package/dist/astro/routes/api/auth/passkey/register/options.mjs +8 -8
  111. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +9 -9
  112. package/dist/astro/routes/api/auth/passkey/verify.mjs +9 -9
  113. package/dist/astro/routes/api/auth/signup/complete.mjs +9 -9
  114. package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
  115. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  116. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
  117. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  118. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  119. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  120. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  121. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs.map +1 -1
  122. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  123. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +9 -9
  124. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +6 -6
  125. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  126. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  127. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs.map +1 -1
  128. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  129. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +6 -6
  130. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  131. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +10 -9
  132. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  133. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  134. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs.map +1 -1
  135. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  136. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  137. package/dist/astro/routes/api/content/_collection_/_id_.mjs +6 -6
  138. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  139. package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
  140. package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
  141. package/dist/astro/routes/api/dashboard.mjs +7 -7
  142. package/dist/astro/routes/api/dev/emails.mjs +3 -3
  143. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  144. package/dist/astro/routes/api/import/probe.mjs +10 -10
  145. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  146. package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
  147. package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -8
  148. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  149. package/dist/astro/routes/api/import/wordpress/media.mjs +8 -8
  150. package/dist/astro/routes/api/import/wordpress/prepare.mjs +8 -8
  151. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  152. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
  153. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
  154. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  155. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +10 -10
  156. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  157. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +11 -11
  158. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  159. package/dist/astro/routes/api/manifest.mjs +4 -4
  160. package/dist/astro/routes/api/mcp.mjs +29 -29
  161. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  162. package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
  163. package/dist/astro/routes/api/media/_id_.mjs +6 -6
  164. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  165. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  166. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  167. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  168. package/dist/astro/routes/api/media/upload-url.mjs +7 -7
  169. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  170. package/dist/astro/routes/api/media.mjs +8 -8
  171. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
  172. package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
  173. package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
  174. package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
  175. package/dist/astro/routes/api/menus/_name_.mjs +7 -7
  176. package/dist/astro/routes/api/menus/index.mjs +7 -7
  177. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  178. package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
  179. package/dist/astro/routes/api/oauth/device/code.mjs +9 -9
  180. package/dist/astro/routes/api/oauth/device/token.mjs +8 -8
  181. package/dist/astro/routes/api/oauth/register.mjs +3 -3
  182. package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
  183. package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
  184. package/dist/astro/routes/api/oauth/token.mjs +6 -6
  185. package/dist/astro/routes/api/openapi.json.mjs +3 -3
  186. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  187. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  188. package/dist/astro/routes/api/redirects/404s/index.mjs +8 -8
  189. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  190. package/dist/astro/routes/api/redirects/404s/summary.mjs +8 -8
  191. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  192. package/dist/astro/routes/api/redirects/_id_.mjs +9 -9
  193. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  194. package/dist/astro/routes/api/redirects/index.mjs +9 -9
  195. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  196. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  197. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  198. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +31 -30
  199. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  200. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +31 -30
  201. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  202. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +31 -30
  203. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  204. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +31 -30
  205. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  206. package/dist/astro/routes/api/schema/collections/index.mjs +31 -30
  207. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  208. package/dist/astro/routes/api/schema/index.mjs +6 -6
  209. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  210. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +31 -30
  211. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  212. package/dist/astro/routes/api/schema/orphans/index.mjs +31 -30
  213. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  214. package/dist/astro/routes/api/search/enable.mjs +9 -9
  215. package/dist/astro/routes/api/search/index.mjs +8 -8
  216. package/dist/astro/routes/api/search/rebuild.mjs +9 -9
  217. package/dist/astro/routes/api/search/stats.mjs +6 -6
  218. package/dist/astro/routes/api/search/suggest.mjs +8 -8
  219. package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
  220. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  221. package/dist/astro/routes/api/sections/index.mjs +8 -8
  222. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  223. package/dist/astro/routes/api/settings/email.mjs +4 -4
  224. package/dist/astro/routes/api/settings.mjs +10 -10
  225. package/dist/astro/routes/api/setup/admin-verify.mjs +10 -10
  226. package/dist/astro/routes/api/setup/admin.mjs +9 -9
  227. package/dist/astro/routes/api/setup/dev-bypass.mjs +22 -22
  228. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  229. package/dist/astro/routes/api/setup/index.mjs +22 -22
  230. package/dist/astro/routes/api/setup/status.mjs +4 -4
  231. package/dist/astro/routes/api/snapshot.mjs +5 -5
  232. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -10
  233. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  234. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -10
  235. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  236. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -10
  237. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  238. package/dist/astro/routes/api/taxonomies/index.mjs +11 -10
  239. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  240. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  241. package/dist/astro/routes/api/typegen.mjs +5 -5
  242. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  243. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  244. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  245. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
  246. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
  247. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
  248. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  249. package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
  250. package/dist/astro/routes/api/widget-components.mjs +3 -3
  251. package/dist/astro/routes/robots.txt.mjs +5 -5
  252. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  253. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  254. package/dist/astro/types.d.mts +13 -12
  255. package/dist/astro/types.d.mts.map +1 -1
  256. package/dist/auth/providers/github.d.mts +1 -1
  257. package/dist/auth/providers/google.d.mts +1 -1
  258. package/dist/{authorize-BlyCH-96.mjs → authorize-Bkwe8kuL.mjs} +2 -2
  259. package/dist/{authorize-BlyCH-96.mjs.map → authorize-Bkwe8kuL.mjs.map} +1 -1
  260. package/dist/byline-CTaWkMh5.mjs +404 -0
  261. package/dist/byline-CTaWkMh5.mjs.map +1 -0
  262. package/dist/bylines-BYHWU3T7.mjs +174 -0
  263. package/dist/bylines-BYHWU3T7.mjs.map +1 -0
  264. package/dist/{bylines-C6eYUWlZ.d.mts → bylines-DtDRNF1n.d.mts} +63 -18
  265. package/dist/bylines-DtDRNF1n.d.mts.map +1 -0
  266. package/dist/bylines-H0Xh5TMy.mjs +118 -0
  267. package/dist/bylines-H0Xh5TMy.mjs.map +1 -0
  268. package/dist/{cache-CXCpjWiL.mjs → cache-CNk1jIxp.mjs} +2 -2
  269. package/dist/{cache-CXCpjWiL.mjs.map → cache-CNk1jIxp.mjs.map} +1 -1
  270. package/dist/{challenge-store-CJ0OOHOr.mjs → challenge-store-Dng1SxKT.mjs} +1 -1
  271. package/dist/{challenge-store-CJ0OOHOr.mjs.map → challenge-store-Dng1SxKT.mjs.map} +1 -1
  272. package/dist/{chunks-DyGtu1Bv.mjs → chunks-BkfVdD-3.mjs} +2 -2
  273. package/dist/{chunks-DyGtu1Bv.mjs.map → chunks-BkfVdD-3.mjs.map} +1 -1
  274. package/dist/cli/index.mjs +21 -29
  275. package/dist/cli/index.mjs.map +1 -1
  276. package/dist/client/cf-access.d.mts +1 -1
  277. package/dist/client/index.d.mts +1 -1
  278. package/dist/client/index.mjs +1 -1
  279. package/dist/client/index.mjs.map +1 -1
  280. package/dist/{comment-Dd9MI82-.mjs → comment-_yzlBYPx.mjs} +2 -2
  281. package/dist/{comment-Dd9MI82-.mjs.map → comment-_yzlBYPx.mjs.map} +1 -1
  282. package/dist/{comments-koGI0FrK.mjs → comments-DxID-rsd.mjs} +3 -3
  283. package/dist/{comments-koGI0FrK.mjs.map → comments-DxID-rsd.mjs.map} +1 -1
  284. package/dist/{components-mZem7pbe.mjs → components-Dx3DM0gg.mjs} +1 -1
  285. package/dist/{components-mZem7pbe.mjs.map → components-Dx3DM0gg.mjs.map} +1 -1
  286. package/dist/config-CVssduLe.mjs.map +1 -1
  287. package/dist/{content-D6YG26WG.mjs → content-C0ooIs-f.mjs} +3 -3
  288. package/dist/{content-D6YG26WG.mjs.map → content-C0ooIs-f.mjs.map} +1 -1
  289. package/dist/{context-qF8d3IPR.mjs → context-sAnCaUIR.mjs} +10 -10
  290. package/dist/context-sAnCaUIR.mjs.map +1 -0
  291. package/dist/{cron-H8eJ46dv.mjs → cron-Bd3b3iuj.mjs} +1 -1
  292. package/dist/{cron-H8eJ46dv.mjs.map → cron-Bd3b3iuj.mjs.map} +1 -1
  293. package/dist/{dashboard-BmWSIUwY.mjs → dashboard-Cqw3ay2X.mjs} +4 -4
  294. package/dist/{dashboard-BmWSIUwY.mjs.map → dashboard-Cqw3ay2X.mjs.map} +1 -1
  295. package/dist/db/index.d.mts +3 -3
  296. package/dist/db/index.mjs +1 -1
  297. package/dist/db/libsql.d.mts +1 -1
  298. package/dist/db/postgres.d.mts +1 -1
  299. package/dist/db/sqlite.d.mts +1 -1
  300. package/dist/{default-Dbs22Gg4.mjs → default-BvTAYCzx.mjs} +1 -1
  301. package/dist/{default-Dbs22Gg4.mjs.map → default-BvTAYCzx.mjs.map} +1 -1
  302. package/dist/{device-flow-BqJRxa0Q.mjs → device-flow-B9oG8PwP.mjs} +4 -4
  303. package/dist/{device-flow-BqJRxa0Q.mjs.map → device-flow-B9oG8PwP.mjs.map} +1 -1
  304. package/dist/{email-console-Dmp5Q-P2.mjs → email-console-CubRll9q.mjs} +1 -1
  305. package/dist/email-console-CubRll9q.mjs.map +1 -0
  306. package/dist/{error-tSQWIl5U.mjs → error-CPh_8eLq.mjs} +16 -8
  307. package/dist/error-CPh_8eLq.mjs.map +1 -0
  308. package/dist/{escape-B8bdIryO.mjs → escape-Cg6kMELH.mjs} +1 -1
  309. package/dist/{escape-B8bdIryO.mjs.map → escape-Cg6kMELH.mjs.map} +1 -1
  310. package/dist/{fts-manager-B633C-kQ.mjs → fts-manager-Mnrtn-r2.mjs} +2 -2
  311. package/dist/{fts-manager-B633C-kQ.mjs.map → fts-manager-Mnrtn-r2.mjs.map} +1 -1
  312. package/dist/{import-CNfLOgDE.mjs → import-DG80rC_I.mjs} +3 -3
  313. package/dist/{import-CNfLOgDE.mjs.map → import-DG80rC_I.mjs.map} +1 -1
  314. package/dist/{index-UmOMt9T-.d.mts → index-Bv1Wf1zB.d.mts} +235 -18
  315. package/dist/index-Bv1Wf1zB.d.mts.map +1 -0
  316. package/dist/{index-D2gvztOP.d.mts → index-CC42STEm.d.mts} +3 -3
  317. package/dist/{index-D2gvztOP.d.mts.map → index-CC42STEm.d.mts.map} +1 -1
  318. package/dist/index.d.mts +17 -17
  319. package/dist/index.mjs +50 -49
  320. package/dist/{load-QzYRpVN3.mjs → load-DmXNVhst.mjs} +2 -2
  321. package/dist/{load-QzYRpVN3.mjs.map → load-DmXNVhst.mjs.map} +1 -1
  322. package/dist/{loader-Cs6-Bqe6.mjs → loader-Chm5h7Gr.mjs} +3 -3
  323. package/dist/loader-Chm5h7Gr.mjs.map +1 -0
  324. package/dist/{manifest-schema-HCtSh4Jq.mjs → manifest-schema-Czqf0TLu.mjs} +1 -1
  325. package/dist/{manifest-schema-HCtSh4Jq.mjs.map → manifest-schema-Czqf0TLu.mjs.map} +1 -1
  326. package/dist/media/index.d.mts +1 -1
  327. package/dist/media/local-runtime.d.mts +11 -11
  328. package/dist/media/local-runtime.mjs +4 -4
  329. package/dist/{media-allowlist-B8EX01DH.mjs → media-allowlist-BNloC69x.mjs} +1 -1
  330. package/dist/{media-allowlist-B8EX01DH.mjs.map → media-allowlist-BNloC69x.mjs.map} +1 -1
  331. package/dist/{media-Dg7he9uK.mjs → media-oqRcNiQf.mjs} +2 -2
  332. package/dist/media-oqRcNiQf.mjs.map +1 -0
  333. package/dist/{menus-DOzIecHi.mjs → menus-Bjf5R1Qq.mjs} +2 -2
  334. package/dist/menus-Bjf5R1Qq.mjs.map +1 -0
  335. package/dist/{menus-X4Z-eBA1.mjs → menus-C75SSmRy.mjs} +30 -11
  336. package/dist/menus-C75SSmRy.mjs.map +1 -0
  337. package/dist/mime-KV5TqkMN.mjs.map +1 -1
  338. package/dist/{mode-DPRPvJYm.mjs → mode-CaaiebZI.mjs} +1 -1
  339. package/dist/{mode-DPRPvJYm.mjs.map → mode-CaaiebZI.mjs.map} +1 -1
  340. package/dist/{oauth-authorization-62GmpGIH.mjs → oauth-authorization-CTMeVfvj.mjs} +4 -4
  341. package/dist/{oauth-authorization-62GmpGIH.mjs.map → oauth-authorization-CTMeVfvj.mjs.map} +1 -1
  342. package/dist/{oauth-clients-D_B0_-Bz.mjs → oauth-clients-eJCbkVSG.mjs} +1 -1
  343. package/dist/oauth-clients-eJCbkVSG.mjs.map +1 -0
  344. package/dist/{oauth-state-store-DpsZViTu.mjs → oauth-state-store-vOSdOeGe.mjs} +1 -1
  345. package/dist/{oauth-state-store-DpsZViTu.mjs.map → oauth-state-store-vOSdOeGe.mjs.map} +1 -1
  346. package/dist/{oauth-user-lookup-meyS2oB1.mjs → oauth-user-lookup-3JwsVw6N.mjs} +1 -1
  347. package/dist/{oauth-user-lookup-meyS2oB1.mjs.map → oauth-user-lookup-3JwsVw6N.mjs.map} +1 -1
  348. package/dist/options-BL4X94qY.mjs.map +1 -1
  349. package/dist/{options-Cq64Wx0O.d.mts → options-DhV-gwJb.d.mts} +4 -4
  350. package/dist/options-DhV-gwJb.d.mts.map +1 -0
  351. package/dist/page/index.d.mts +2 -2
  352. package/dist/{parse-BFTPon-J.mjs → parse-3-caTKgt.mjs} +2 -2
  353. package/dist/{parse-BFTPon-J.mjs.map → parse-3-caTKgt.mjs.map} +1 -1
  354. package/dist/{passkey-config-Cg86_ISa.mjs → passkey-config-BloQOT3y.mjs} +1 -1
  355. package/dist/{passkey-config-Cg86_ISa.mjs.map → passkey-config-BloQOT3y.mjs.map} +1 -1
  356. package/dist/{placeholder-D3cFCU9y.d.mts → placeholder-KCkkCtgQ.d.mts} +1 -1
  357. package/dist/{placeholder-D3cFCU9y.d.mts.map → placeholder-KCkkCtgQ.d.mts.map} +1 -1
  358. package/dist/plugin-types.d.mts +1 -1
  359. package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
  360. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  361. package/dist/plugins/adapt-sandbox-entry.mjs +26 -15
  362. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  363. package/dist/{preview-C1LOEbWZ.mjs → preview-D4z0WONU.mjs} +2 -2
  364. package/dist/{preview-C1LOEbWZ.mjs.map → preview-D4z0WONU.mjs.map} +1 -1
  365. package/dist/{public-url-CseXl9Fv.mjs → public-url-CUWWFME2.mjs} +1 -1
  366. package/dist/{public-url-CseXl9Fv.mjs.map → public-url-CUWWFME2.mjs.map} +1 -1
  367. package/dist/{query-axZmO6Tn.mjs → query-BJn8TOPk.mjs} +16 -13
  368. package/dist/{query-axZmO6Tn.mjs.map → query-BJn8TOPk.mjs.map} +1 -1
  369. package/dist/{rate-limit-t5CVjCO6.mjs → rate-limit-D_-gAeJ0.mjs} +2 -2
  370. package/dist/{rate-limit-t5CVjCO6.mjs.map → rate-limit-D_-gAeJ0.mjs.map} +1 -1
  371. package/dist/{redirect-DGRsLO2I.mjs → redirect-BINiRYq4.mjs} +1 -1
  372. package/dist/{redirect-DGRsLO2I.mjs.map → redirect-BINiRYq4.mjs.map} +1 -1
  373. package/dist/{redirect-DkaDxq8e.mjs → redirect-CNv4mHX2.mjs} +2 -2
  374. package/dist/{redirect-DkaDxq8e.mjs.map → redirect-CNv4mHX2.mjs.map} +1 -1
  375. package/dist/{redirects-D1fdd68T.mjs → redirects-B-CUZ1Xh.mjs} +3 -3
  376. package/dist/{redirects-D1fdd68T.mjs.map → redirects-B-CUZ1Xh.mjs.map} +1 -1
  377. package/dist/{redirects-Dmj6KRU3.mjs → redirects-COMLwsV5.mjs} +19 -5
  378. package/dist/redirects-COMLwsV5.mjs.map +1 -0
  379. package/dist/{registry-BnCeHYsf.mjs → registry-DqrAQDXH.mjs} +4 -4
  380. package/dist/{registry-BnCeHYsf.mjs.map → registry-DqrAQDXH.mjs.map} +1 -1
  381. package/dist/request-cache-dzCt8TZB.mjs.map +1 -1
  382. package/dist/request-context.mjs.map +1 -1
  383. package/dist/{request-meta-CLCwSQOS.mjs → request-meta-C_Cjii-T.mjs} +2 -2
  384. package/dist/{request-meta-CLCwSQOS.mjs.map → request-meta-C_Cjii-T.mjs.map} +1 -1
  385. package/dist/resolve-Cj98DuqN.mjs +39 -0
  386. package/dist/resolve-Cj98DuqN.mjs.map +1 -0
  387. package/dist/{runner-DdnQIwz_.mjs → runner-CGlojznK.mjs} +472 -165
  388. package/dist/runner-CGlojznK.mjs.map +1 -0
  389. package/dist/{runner-DcfZewkO.d.mts → runner-CNHRo1mT.d.mts} +2 -2
  390. package/dist/{runner-DcfZewkO.d.mts.map → runner-CNHRo1mT.d.mts.map} +1 -1
  391. package/dist/runtime.d.mts +10 -10
  392. package/dist/runtime.mjs +2 -2
  393. package/dist/{schema-BmqagCwG.mjs → schema-Djdlfi5G.mjs} +4 -4
  394. package/dist/{schema-BmqagCwG.mjs.map → schema-Djdlfi5G.mjs.map} +1 -1
  395. package/dist/{search-CPrvO5u8.mjs → search-By-NN3da.mjs} +4 -4
  396. package/dist/{search-CPrvO5u8.mjs.map → search-By-NN3da.mjs.map} +1 -1
  397. package/dist/{secrets-6pgZyq0K.mjs → secrets-rPdhEBkD.mjs} +1 -1
  398. package/dist/{secrets-6pgZyq0K.mjs.map → secrets-rPdhEBkD.mjs.map} +1 -1
  399. package/dist/{sections-Cm-zb-gZ.mjs → sections-DcBIlOq1.mjs} +3 -3
  400. package/dist/{sections-Cm-zb-gZ.mjs.map → sections-DcBIlOq1.mjs.map} +1 -1
  401. package/dist/seed/index.d.mts +2 -2
  402. package/dist/seed/index.mjs +16 -16
  403. package/dist/seo/index.d.mts +1 -1
  404. package/dist/{seo-DRq9-EPP.mjs → seo-bjDoq9Eg.mjs} +2 -2
  405. package/dist/{seo-DRq9-EPP.mjs.map → seo-bjDoq9Eg.mjs.map} +1 -1
  406. package/dist/{service-vByySp-2.mjs → service-BuuTdGAT.mjs} +3 -3
  407. package/dist/{service-vByySp-2.mjs.map → service-BuuTdGAT.mjs.map} +1 -1
  408. package/dist/{settings-CBBj7HUd.mjs → settings-CJnKiWuR.mjs} +3 -3
  409. package/dist/{settings-CBBj7HUd.mjs.map → settings-CJnKiWuR.mjs.map} +1 -1
  410. package/dist/{settings-xQKsWnzQ.mjs → settings-hcubRfkr.mjs} +3 -3
  411. package/dist/settings-hcubRfkr.mjs.map +1 -0
  412. package/dist/{setup-BGAJ2uXs.mjs → setup-Cf_TyOv5.mjs} +2 -2
  413. package/dist/{setup-BGAJ2uXs.mjs.map → setup-Cf_TyOv5.mjs.map} +1 -1
  414. package/dist/{setup-complete-C6ZCLhKo.mjs → setup-complete-MzzN9u0b.mjs} +1 -1
  415. package/dist/{setup-complete-C6ZCLhKo.mjs.map → setup-complete-MzzN9u0b.mjs.map} +1 -1
  416. package/dist/{setup-nonce-CY1gQiAU.mjs → setup-nonce-DXuriHsg.mjs} +1 -1
  417. package/dist/{setup-nonce-CY1gQiAU.mjs.map → setup-nonce-DXuriHsg.mjs.map} +1 -1
  418. package/dist/{site-url-D-M4Fd8O.mjs → site-url-xkhw1tcz.mjs} +1 -1
  419. package/dist/{site-url-D-M4Fd8O.mjs.map → site-url-xkhw1tcz.mjs.map} +1 -1
  420. package/dist/{ssrf-DzFN_qV-.mjs → ssrf-MZ-zrG6-.mjs} +1 -1
  421. package/dist/{ssrf-DzFN_qV-.mjs.map → ssrf-MZ-zrG6-.mjs.map} +1 -1
  422. package/dist/storage/local.d.mts +1 -1
  423. package/dist/storage/local.mjs +1 -1
  424. package/dist/storage/local.mjs.map +1 -1
  425. package/dist/storage/s3.d.mts +1 -1
  426. package/dist/storage/s3.mjs +1 -1
  427. package/dist/storage/s3.mjs.map +1 -1
  428. package/dist/{taxonomies-Dc0mzlms.mjs → taxonomies-CLs9HPE2.mjs} +4 -4
  429. package/dist/{taxonomies-Dc0mzlms.mjs.map → taxonomies-CLs9HPE2.mjs.map} +1 -1
  430. package/dist/{taxonomies-Cn9UpaR2.mjs → taxonomies-WamPVA2x.mjs} +7 -42
  431. package/dist/taxonomies-WamPVA2x.mjs.map +1 -0
  432. package/dist/{taxonomy-wPfusMK9.mjs → taxonomy-D4Uc2LsZ.mjs} +3 -3
  433. package/dist/{taxonomy-wPfusMK9.mjs.map → taxonomy-D4Uc2LsZ.mjs.map} +1 -1
  434. package/dist/{tokens-DILYNZMi.mjs → tokens-N8otWMmj.mjs} +1 -1
  435. package/dist/{tokens-DILYNZMi.mjs.map → tokens-N8otWMmj.mjs.map} +1 -1
  436. package/dist/{transport-fw-mKJzT.mjs → transport-B6CHddbu.mjs} +1 -1
  437. package/dist/{transport-fw-mKJzT.mjs.map → transport-B6CHddbu.mjs.map} +1 -1
  438. package/dist/{transport-GeXlLscf.d.mts → transport-DOxLfUir.d.mts} +1 -1
  439. package/dist/{transport-GeXlLscf.d.mts.map → transport-DOxLfUir.d.mts.map} +1 -1
  440. package/dist/{trusted-proxy-CJhQIk65.mjs → trusted-proxy-97pajC2f.mjs} +1 -1
  441. package/dist/{trusted-proxy-CJhQIk65.mjs.map → trusted-proxy-97pajC2f.mjs.map} +1 -1
  442. package/dist/{types-CwXMEPRr.mjs → types-ByV5sgsv.mjs} +2 -2
  443. package/dist/types-ByV5sgsv.mjs.map +1 -0
  444. package/dist/{types-Dz9CGX_d.mjs → types-Cd9UCu3t.mjs} +1 -1
  445. package/dist/{types-Dz9CGX_d.mjs.map → types-Cd9UCu3t.mjs.map} +1 -1
  446. package/dist/{types-DmxPPXGf.d.mts → types-CkDSF81F.d.mts} +1 -1
  447. package/dist/{types-DmxPPXGf.d.mts.map → types-CkDSF81F.d.mts.map} +1 -1
  448. package/dist/{types-BWhaSS7U.d.mts → types-CpUuGcd5.d.mts} +1 -1
  449. package/dist/{types-BWhaSS7U.d.mts.map → types-CpUuGcd5.d.mts.map} +1 -1
  450. package/dist/{types-DFowNO60.d.mts → types-D599-ruj.d.mts} +1 -1
  451. package/dist/{types-DFowNO60.d.mts.map → types-D599-ruj.d.mts.map} +1 -1
  452. package/dist/{types-B05e2naf.d.mts → types-DGHWRQgr.d.mts} +3 -3
  453. package/dist/{types-B05e2naf.d.mts.map → types-DGHWRQgr.d.mts.map} +1 -1
  454. package/dist/{types-CzvJd1ND.d.mts → types-DaYDYW6g.d.mts} +14 -1
  455. package/dist/types-DaYDYW6g.d.mts.map +1 -0
  456. package/dist/{types-C1KKK4VP.d.mts → types-DaqNzqVt.d.mts} +16 -1
  457. package/dist/{types-C1KKK4VP.d.mts.map → types-DaqNzqVt.d.mts.map} +1 -1
  458. package/dist/{types-DW1l0gCv.d.mts → types-Dgo6y-Ut.d.mts} +1 -1
  459. package/dist/{types-DW1l0gCv.d.mts.map → types-Dgo6y-Ut.d.mts.map} +1 -1
  460. package/dist/{types-Cb2UCDJg.d.mts → types-bYmRn_Uy.d.mts} +1 -1
  461. package/dist/{types-Cb2UCDJg.d.mts.map → types-bYmRn_Uy.d.mts.map} +1 -1
  462. package/dist/{user-Dr1bOCqS.mjs → user-D3BD5zdT.mjs} +2 -2
  463. package/dist/{user-Dr1bOCqS.mjs.map → user-D3BD5zdT.mjs.map} +1 -1
  464. package/dist/{utils-_F-rWBTN.mjs → utils-C3wTAP-P.mjs} +1 -1
  465. package/dist/{utils-_F-rWBTN.mjs.map → utils-C3wTAP-P.mjs.map} +1 -1
  466. package/dist/{validate-BpQGsmd7.d.mts → validate-DQtHw9NT.d.mts} +5 -5
  467. package/dist/{validate-BpQGsmd7.d.mts.map → validate-DQtHw9NT.d.mts.map} +1 -1
  468. package/dist/{validate-DlFxcVVK.mjs → validate-mz87i8_1.mjs} +2 -2
  469. package/dist/{validate-DlFxcVVK.mjs.map → validate-mz87i8_1.mjs.map} +1 -1
  470. package/dist/{validation-BiFJqUp5.mjs → validation-DKHhXjPr.mjs} +5 -5
  471. package/dist/{validation-BiFJqUp5.mjs.map → validation-DKHhXjPr.mjs.map} +1 -1
  472. package/dist/version-Ct7C6RSo.mjs +7 -0
  473. package/dist/{version-Dw7Z5PVU.mjs.map → version-Ct7C6RSo.mjs.map} +1 -1
  474. package/dist/{widgets-B9j_yzlk.mjs → widgets-lShIQXU5.mjs} +3 -3
  475. package/dist/widgets-lShIQXU5.mjs.map +1 -0
  476. package/dist/{zod-generator-DSyz01KE.mjs → zod-generator-dvxgmd1M.mjs} +2 -2
  477. package/dist/{zod-generator-DSyz01KE.mjs.map → zod-generator-dvxgmd1M.mjs.map} +1 -1
  478. package/package.json +10 -8
  479. package/src/api/error.ts +18 -3
  480. package/src/api/errors.ts +6 -0
  481. package/src/api/handlers/bylines.ts +161 -0
  482. package/src/api/handlers/content.ts +125 -43
  483. package/src/api/handlers/index.ts +6 -0
  484. package/src/api/handlers/marketplace.ts +27 -5
  485. package/src/api/handlers/oauth-clients.ts +1 -1
  486. package/src/api/handlers/registry.ts +568 -22
  487. package/src/api/openapi/document.ts +1 -1
  488. package/src/api/schemas/bylines.ts +46 -0
  489. package/src/astro/integration/index.ts +1 -1
  490. package/src/astro/integration/routes.ts +5 -0
  491. package/src/astro/integration/runtime.ts +12 -1
  492. package/src/astro/integration/virtual-modules.ts +19 -2
  493. package/src/astro/integration/vite-config.ts +2 -2
  494. package/src/astro/middleware/auth.ts +7 -7
  495. package/src/astro/middleware/request-context.ts +1 -1
  496. package/src/astro/middleware.ts +31 -20
  497. package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -12
  498. package/src/astro/routes/api/admin/bylines/[id]/translations.ts +99 -0
  499. package/src/astro/routes/api/admin/bylines/index.ts +22 -11
  500. package/src/astro/routes/api/admin/plugins/[id]/update.ts +1 -0
  501. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +6 -1
  502. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +51 -0
  503. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +79 -0
  504. package/src/astro/routes/api/admin/plugins/updates.ts +43 -6
  505. package/src/astro/routes/api/admin/themes/marketplace/index.ts +1 -1
  506. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +2 -2
  507. package/src/astro/routes/api/auth/oauth/[provider].ts +2 -2
  508. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +2 -2
  509. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +2 -2
  510. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +2 -2
  511. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +2 -2
  512. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +2 -2
  513. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +6 -6
  514. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
  515. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +2 -2
  516. package/src/astro/routes/api/content/[collection]/[id].ts +6 -6
  517. package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
  518. package/src/astro/routes/api/import/wordpress/prepare.ts +2 -2
  519. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +3 -3
  520. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +2 -2
  521. package/src/astro/routes/api/media/upload-url.ts +1 -1
  522. package/src/astro/routes/api/redirects/404s/index.ts +3 -3
  523. package/src/astro/routes/api/redirects/404s/summary.ts +1 -1
  524. package/src/astro/routes/api/redirects/[id].ts +3 -3
  525. package/src/astro/routes/api/redirects/index.ts +2 -2
  526. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +4 -4
  527. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +2 -6
  528. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -1
  529. package/src/astro/routes/api/schema/collections/[slug]/index.ts +6 -6
  530. package/src/astro/routes/api/schema/collections/index.ts +4 -4
  531. package/src/astro/routes/api/schema/index.ts +1 -1
  532. package/src/astro/routes/api/schema/orphans/[slug].ts +1 -1
  533. package/src/astro/routes/api/schema/orphans/index.ts +1 -1
  534. package/src/astro/routes/api/sections/[slug].ts +3 -3
  535. package/src/astro/routes/api/sections/index.ts +2 -2
  536. package/src/astro/types.ts +4 -0
  537. package/src/auth/rate-limit.ts +1 -1
  538. package/src/auth/trusted-proxy.ts +1 -1
  539. package/src/bylines/index.ts +154 -55
  540. package/src/cli/commands/init.ts +4 -8
  541. package/src/client/index.ts +1 -1
  542. package/src/components/InlinePortableTextEditor.tsx +5 -1
  543. package/src/components/inline-code-block.tsx +343 -0
  544. package/src/config/secrets.ts +3 -3
  545. package/src/database/migrations/006_taxonomy_defs.ts +1 -1
  546. package/src/database/migrations/014_draft_revisions.ts +6 -6
  547. package/src/database/migrations/040_byline_i18n.ts +497 -0
  548. package/src/database/migrations/runner.ts +4 -1
  549. package/src/database/repositories/audit.ts +2 -2
  550. package/src/database/repositories/byline.ts +320 -50
  551. package/src/database/repositories/media.ts +2 -2
  552. package/src/database/repositories/menu.ts +1 -1
  553. package/src/database/repositories/options.ts +3 -3
  554. package/src/database/repositories/plugin-storage.ts +3 -3
  555. package/src/database/repositories/types.ts +13 -0
  556. package/src/database/types.ts +15 -0
  557. package/src/emdash-runtime.ts +492 -20
  558. package/src/i18n/config.ts +1 -1
  559. package/src/index.ts +7 -0
  560. package/src/loader.ts +1 -1
  561. package/src/mcp/server.ts +3 -3
  562. package/src/media/mime.ts +1 -1
  563. package/src/page/absolute-url.ts +1 -1
  564. package/src/plugins/adapt-sandbox-entry.ts +45 -40
  565. package/src/plugins/email-console.ts +1 -1
  566. package/src/plugins/index.ts +1 -0
  567. package/src/plugins/marketplace.ts +1 -1
  568. package/src/plugins/sandbox/index.ts +1 -0
  569. package/src/plugins/sandbox/noop.ts +11 -3
  570. package/src/plugins/sandbox/types.ts +28 -0
  571. package/src/query.ts +17 -2
  572. package/src/registry/config.ts +1 -1
  573. package/src/request-cache.ts +3 -3
  574. package/src/request-context.ts +1 -1
  575. package/src/settings/index.ts +4 -4
  576. package/src/storage/local.ts +1 -1
  577. package/src/storage/s3.ts +3 -3
  578. package/src/widgets/index.ts +1 -1
  579. package/dist/api-ayIQ7rIe.mjs.map +0 -1
  580. package/dist/byline-D09BaS4j.mjs +0 -220
  581. package/dist/byline-D09BaS4j.mjs.map +0 -1
  582. package/dist/bylines-BTM2xtP8.mjs +0 -113
  583. package/dist/bylines-BTM2xtP8.mjs.map +0 -1
  584. package/dist/bylines-C6eYUWlZ.d.mts.map +0 -1
  585. package/dist/context-qF8d3IPR.mjs.map +0 -1
  586. package/dist/email-console-Dmp5Q-P2.mjs.map +0 -1
  587. package/dist/error-tSQWIl5U.mjs.map +0 -1
  588. package/dist/index-UmOMt9T-.d.mts.map +0 -1
  589. package/dist/loader-Cs6-Bqe6.mjs.map +0 -1
  590. package/dist/media-Dg7he9uK.mjs.map +0 -1
  591. package/dist/menus-DOzIecHi.mjs.map +0 -1
  592. package/dist/menus-X4Z-eBA1.mjs.map +0 -1
  593. package/dist/oauth-clients-D_B0_-Bz.mjs.map +0 -1
  594. package/dist/options-Cq64Wx0O.d.mts.map +0 -1
  595. package/dist/redirects-Dmj6KRU3.mjs.map +0 -1
  596. package/dist/runner-DdnQIwz_.mjs.map +0 -1
  597. package/dist/settings-xQKsWnzQ.mjs.map +0 -1
  598. package/dist/taxonomies-Cn9UpaR2.mjs.map +0 -1
  599. package/dist/types-CwXMEPRr.mjs.map +0 -1
  600. package/dist/types-CzvJd1ND.d.mts.map +0 -1
  601. package/dist/version-Dw7Z5PVU.mjs +0 -7
  602. package/dist/widgets-B9j_yzlk.mjs.map +0 -1
  603. /package/dist/{api-tokens-D3C9v02m.mjs → api-tokens-iPIHAY8N.mjs} +0 -0
  604. /package/dist/{ssrf-CTul4uQi.mjs → ssrf-BIcd-aXW.mjs} +0 -0
  605. /package/dist/{types-Db67HHlU.mjs → types-1NNkmTIn.mjs} +0 -0
@@ -85,4 +85,4 @@ function getEnvAllowedOrigins() {
85
85
 
86
86
  //#endregion
87
87
  export { getPublicOrigin as n, getEnvAllowedOrigins as t };
88
- //# sourceMappingURL=public-url-CseXl9Fv.mjs.map
88
+ //# sourceMappingURL=public-url-CUWWFME2.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"public-url-CseXl9Fv.mjs","names":[],"sources":["../src/api/public-url.ts"],"sourcesContent":["/**\n * Public URL helpers for reverse-proxy deployments.\n *\n * Behind a TLS-terminating proxy the internal request URL\n * (`http://localhost:4321`) differs from the browser-facing origin\n * (`https://mysite.example.com`). These pure helpers resolve the\n * correct public origin from config, falling back to the request URL.\n *\n * Workers-safe: no Node.js imports.\n */\n\n/** Minimal config shape — avoids importing the full EmDashConfig type tree. */\ninterface SiteUrlConfig {\n\tsiteUrl?: string;\n}\n\n/**\n * Resolve siteUrl from runtime environment variables.\n *\n * Uses process.env (not import.meta.env) because Vite statically replaces\n * import.meta.env at build time, baking out any env vars not present during\n * the build. Container deployments set env vars at runtime, so we must read\n * process.env which Vite leaves untouched.\n *\n * On Cloudflare Workers process.env is unavailable (returns undefined),\n * so the fallback chain continues to url.origin.\n *\n * Caches after first call.\n */\nlet _envSiteUrl: string | undefined | null = null;\n\n/** @internal Reset cached env values — test-only. */\nexport function _resetEnvCache(): void {\n\t_envSiteUrl = null;\n\t_envAllowedOrigins = null;\n}\n\nfunction getEnvSiteUrl(): string | undefined {\n\tif (_envSiteUrl !== null) return _envSiteUrl || undefined;\n\ttry {\n\t\t// process.env is available on Node.js; undefined on Workers\n\t\tconst value =\n\t\t\t(typeof process !== \"undefined\" && process.env?.EMDASH_SITE_URL) ||\n\t\t\t(typeof process !== \"undefined\" && process.env?.SITE_URL) ||\n\t\t\t\"\";\n\t\tif (value) {\n\t\t\tconst parsed = new URL(value);\n\t\t\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\t\t\t_envSiteUrl = \"\";\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\t_envSiteUrl = parsed.origin;\n\t\t} else {\n\t\t\t_envSiteUrl = \"\";\n\t\t}\n\t} catch {\n\t\t_envSiteUrl = \"\";\n\t}\n\treturn _envSiteUrl || undefined;\n}\n\n/**\n * Return the public-facing origin for the site.\n *\n * Resolution order:\n * 1. `config.siteUrl` (set in astro.config.mjs, origin-normalized at startup)\n * 2. `EMDASH_SITE_URL` or `SITE_URL` env var (resolved at runtime for containers)\n * 3. `url.origin` (internal request URL — correct when no proxy)\n *\n * @param url The request URL (`new URL(request.url)` or `Astro.url`)\n * @param config The EmDash config (from `locals.emdash?.config`)\n * @returns Origin string, e.g. `\"https://mysite.example.com\"`\n */\nexport function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {\n\treturn config?.siteUrl || getEnvSiteUrl() || url.origin;\n}\n\n/**\n * Resolve additional accepted passkey origins from runtime environment.\n *\n * Reads `EMDASH_ALLOWED_ORIGINS` (comma-separated list of origins) for\n * multi-origin deployments where the same RP is reachable under several\n * hostnames sharing the registrable parent domain (e.g. apex + preview).\n *\n * Each entry is parsed via `new URL()` and reduced to its `origin`. Unlike\n * `getEnvSiteUrl` (which silently falls back to `url.origin` on bad input),\n * this throws on any unparseable or non-http(s) entry — `EMDASH_ALLOWED_ORIGINS`\n * is an allowlist for passkey verification, so silently dropping a typo would\n * surface as \"I can't authenticate on this origin\" with no diagnostic. Fail\n * loud at first read.\n *\n * Uses `process.env` (Vite leaves it untouched at runtime). Result is cached\n * on success.\n */\nlet _envAllowedOrigins: string[] | null = null;\n\nexport function getEnvAllowedOrigins(): string[] {\n\tif (_envAllowedOrigins !== null) return _envAllowedOrigins;\n\tconst raw = typeof process !== \"undefined\" ? process.env?.EMDASH_ALLOWED_ORIGINS || \"\" : \"\";\n\tconst parsed: string[] = [];\n\tfor (const entry of raw.split(\",\")) {\n\t\tconst trimmed = entry.trim();\n\t\tif (!trimmed) continue;\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(trimmed);\n\t\t} catch (e) {\n\t\t\tthrow new Error(`EmDash config error in EMDASH_ALLOWED_ORIGINS: invalid URL: \"${trimmed}\"`, {\n\t\t\t\tcause: e,\n\t\t\t});\n\t\t}\n\t\tif (u.protocol !== \"http:\" && u.protocol !== \"https:\") {\n\t\t\tthrow new Error(\n\t\t\t\t`EmDash config error in EMDASH_ALLOWED_ORIGINS: origin must be http or https: \"${trimmed}\" (got ${u.protocol})`,\n\t\t\t);\n\t\t}\n\t\tparsed.push(u.origin);\n\t}\n\t_envAllowedOrigins = parsed;\n\treturn parsed;\n}\n\n/**\n * Build a full public URL by appending a path to the public origin.\n *\n * @param url The request URL\n * @param config The EmDash config\n * @param path Path to append (must start with `/`)\n * @returns Full URL string, e.g. `\"https://mysite.example.com/_emdash/admin/login\"`\n */\nexport function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {\n\treturn `${getPublicOrigin(url, config)}${path}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,IAAI,cAAyC;AAQ7C,SAAS,gBAAoC;AAC5C,KAAI,gBAAgB,KAAM,QAAO,eAAe;AAChD,KAAI;EAEH,MAAM,QACJ,OAAO,YAAY,eAAe,QAAQ,KAAK,mBAC/C,OAAO,YAAY,eAAe,QAAQ,KAAK,YAChD;AACD,MAAI,OAAO;GACV,MAAM,SAAS,IAAI,IAAI,MAAM;AAC7B,OAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAChE,kBAAc;AACd;;AAED,iBAAc,OAAO;QAErB,eAAc;SAER;AACP,gBAAc;;AAEf,QAAO,eAAe;;;;;;;;;;;;;;AAevB,SAAgB,gBAAgB,KAAU,QAAgC;AACzE,QAAO,QAAQ,WAAW,eAAe,IAAI,IAAI;;;;;;;;;;;;;;;;;;;AAoBlD,IAAI,qBAAsC;AAE1C,SAAgB,uBAAiC;AAChD,KAAI,uBAAuB,KAAM,QAAO;CACxC,MAAM,MAAM,OAAO,YAAY,cAAc,QAAQ,KAAK,0BAA0B,KAAK;CACzF,MAAM,SAAmB,EAAE;AAC3B,MAAK,MAAM,SAAS,IAAI,MAAM,IAAI,EAAE;EACnC,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,QAAS;EACd,IAAI;AACJ,MAAI;AACH,OAAI,IAAI,IAAI,QAAQ;WACZ,GAAG;AACX,SAAM,IAAI,MAAM,gEAAgE,QAAQ,IAAI,EAC3F,OAAO,GACP,CAAC;;AAEH,MAAI,EAAE,aAAa,WAAW,EAAE,aAAa,SAC5C,OAAM,IAAI,MACT,iFAAiF,QAAQ,SAAS,EAAE,SAAS,GAC7G;AAEF,SAAO,KAAK,EAAE,OAAO;;AAEtB,sBAAqB;AACrB,QAAO"}
1
+ {"version":3,"file":"public-url-CUWWFME2.mjs","names":[],"sources":["../src/api/public-url.ts"],"sourcesContent":["/**\n * Public URL helpers for reverse-proxy deployments.\n *\n * Behind a TLS-terminating proxy the internal request URL\n * (`http://localhost:4321`) differs from the browser-facing origin\n * (`https://mysite.example.com`). These pure helpers resolve the\n * correct public origin from config, falling back to the request URL.\n *\n * Workers-safe: no Node.js imports.\n */\n\n/** Minimal config shape — avoids importing the full EmDashConfig type tree. */\ninterface SiteUrlConfig {\n\tsiteUrl?: string;\n}\n\n/**\n * Resolve siteUrl from runtime environment variables.\n *\n * Uses process.env (not import.meta.env) because Vite statically replaces\n * import.meta.env at build time, baking out any env vars not present during\n * the build. Container deployments set env vars at runtime, so we must read\n * process.env which Vite leaves untouched.\n *\n * On Cloudflare Workers process.env is unavailable (returns undefined),\n * so the fallback chain continues to url.origin.\n *\n * Caches after first call.\n */\nlet _envSiteUrl: string | undefined | null = null;\n\n/** @internal Reset cached env values — test-only. */\nexport function _resetEnvCache(): void {\n\t_envSiteUrl = null;\n\t_envAllowedOrigins = null;\n}\n\nfunction getEnvSiteUrl(): string | undefined {\n\tif (_envSiteUrl !== null) return _envSiteUrl || undefined;\n\ttry {\n\t\t// process.env is available on Node.js; undefined on Workers\n\t\tconst value =\n\t\t\t(typeof process !== \"undefined\" && process.env?.EMDASH_SITE_URL) ||\n\t\t\t(typeof process !== \"undefined\" && process.env?.SITE_URL) ||\n\t\t\t\"\";\n\t\tif (value) {\n\t\t\tconst parsed = new URL(value);\n\t\t\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\t\t\t_envSiteUrl = \"\";\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\t_envSiteUrl = parsed.origin;\n\t\t} else {\n\t\t\t_envSiteUrl = \"\";\n\t\t}\n\t} catch {\n\t\t_envSiteUrl = \"\";\n\t}\n\treturn _envSiteUrl || undefined;\n}\n\n/**\n * Return the public-facing origin for the site.\n *\n * Resolution order:\n * 1. `config.siteUrl` (set in astro.config.mjs, origin-normalized at startup)\n * 2. `EMDASH_SITE_URL` or `SITE_URL` env var (resolved at runtime for containers)\n * 3. `url.origin` (internal request URL — correct when no proxy)\n *\n * @param url The request URL (`new URL(request.url)` or `Astro.url`)\n * @param config The EmDash config (from `locals.emdash?.config`)\n * @returns Origin string, e.g. `\"https://mysite.example.com\"`\n */\nexport function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {\n\treturn config?.siteUrl || getEnvSiteUrl() || url.origin;\n}\n\n/**\n * Resolve additional accepted passkey origins from runtime environment.\n *\n * Reads `EMDASH_ALLOWED_ORIGINS` (comma-separated list of origins) for\n * multi-origin deployments where the same RP is reachable under several\n * hostnames sharing the registrable parent domain (e.g. apex + preview).\n *\n * Each entry is parsed via `new URL()` and reduced to its `origin`. Unlike\n * `getEnvSiteUrl` (which silently falls back to `url.origin` on bad input),\n * this throws on any unparseable or non-http(s) entry — `EMDASH_ALLOWED_ORIGINS`\n * is an allowlist for passkey verification, so silently dropping a typo would\n * surface as \"I can't authenticate on this origin\" with no diagnostic. Fail\n * loud at first read.\n *\n * Uses `process.env` (Vite leaves it untouched at runtime). Result is cached\n * on success.\n */\nlet _envAllowedOrigins: string[] | null = null;\n\nexport function getEnvAllowedOrigins(): string[] {\n\tif (_envAllowedOrigins !== null) return _envAllowedOrigins;\n\tconst raw = typeof process !== \"undefined\" ? process.env?.EMDASH_ALLOWED_ORIGINS || \"\" : \"\";\n\tconst parsed: string[] = [];\n\tfor (const entry of raw.split(\",\")) {\n\t\tconst trimmed = entry.trim();\n\t\tif (!trimmed) continue;\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(trimmed);\n\t\t} catch (e) {\n\t\t\tthrow new Error(`EmDash config error in EMDASH_ALLOWED_ORIGINS: invalid URL: \"${trimmed}\"`, {\n\t\t\t\tcause: e,\n\t\t\t});\n\t\t}\n\t\tif (u.protocol !== \"http:\" && u.protocol !== \"https:\") {\n\t\t\tthrow new Error(\n\t\t\t\t`EmDash config error in EMDASH_ALLOWED_ORIGINS: origin must be http or https: \"${trimmed}\" (got ${u.protocol})`,\n\t\t\t);\n\t\t}\n\t\tparsed.push(u.origin);\n\t}\n\t_envAllowedOrigins = parsed;\n\treturn parsed;\n}\n\n/**\n * Build a full public URL by appending a path to the public origin.\n *\n * @param url The request URL\n * @param config The EmDash config\n * @param path Path to append (must start with `/`)\n * @returns Full URL string, e.g. `\"https://mysite.example.com/_emdash/admin/login\"`\n */\nexport function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {\n\treturn `${getPublicOrigin(url, config)}${path}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,IAAI,cAAyC;AAQ7C,SAAS,gBAAoC;AAC5C,KAAI,gBAAgB,KAAM,QAAO,eAAe;AAChD,KAAI;EAEH,MAAM,QACJ,OAAO,YAAY,eAAe,QAAQ,KAAK,mBAC/C,OAAO,YAAY,eAAe,QAAQ,KAAK,YAChD;AACD,MAAI,OAAO;GACV,MAAM,SAAS,IAAI,IAAI,MAAM;AAC7B,OAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAChE,kBAAc;AACd;;AAED,iBAAc,OAAO;QAErB,eAAc;SAER;AACP,gBAAc;;AAEf,QAAO,eAAe;;;;;;;;;;;;;;AAevB,SAAgB,gBAAgB,KAAU,QAAgC;AACzE,QAAO,QAAQ,WAAW,eAAe,IAAI,IAAI;;;;;;;;;;;;;;;;;;;AAoBlD,IAAI,qBAAsC;AAE1C,SAAgB,uBAAiC;AAChD,KAAI,uBAAuB,KAAM,QAAO;CACxC,MAAM,MAAM,OAAO,YAAY,cAAc,QAAQ,KAAK,0BAA0B,KAAK;CACzF,MAAM,SAAmB,EAAE;AAC3B,MAAK,MAAM,SAAS,IAAI,MAAM,IAAI,EAAE;EACnC,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,QAAS;EACd,IAAI;AACJ,MAAI;AACH,OAAI,IAAI,IAAI,QAAQ;WACZ,GAAG;AACX,SAAM,IAAI,MAAM,gEAAgE,QAAQ,IAAI,EAC3F,OAAO,GACP,CAAC;;AAEH,MAAI,EAAE,aAAa,WAAW,EAAE,aAAa,SAC5C,OAAM,IAAI,MACT,iFAAiF,QAAQ,SAAS,EAAE,SAAS,GAC7G;AAEF,SAAO,KAAK,EAAE,OAAO;;AAEtB,sBAAqB;AACrB,QAAO"}
@@ -1,10 +1,10 @@
1
- import { i as __exportAll } from "./runner-DdnQIwz_.mjs";
1
+ import { i as __exportAll } from "./runner-CGlojznK.mjs";
2
2
  import { getRequestContext } from "./request-context.mjs";
3
3
  import { n as getI18nConfig, r as isI18nEnabled, t as getFallbackChain } from "./config-CVssduLe.mjs";
4
- import { i as encodeCursor } from "./types-CwXMEPRr.mjs";
4
+ import { i as encodeCursor } from "./types-ByV5sgsv.mjs";
5
5
  import { t as isMissingTableError } from "./db-errors-CGN9kJfo.mjs";
6
6
  import { n as requestCached } from "./request-cache-dzCt8TZB.mjs";
7
- import { t as CURSOR_RAW_VALUES } from "./loader-Cs6-Bqe6.mjs";
7
+ import { t as CURSOR_RAW_VALUES } from "./loader-Chm5h7Gr.mjs";
8
8
 
9
9
  //#region src/visual-editing/editable.ts
10
10
  /**
@@ -483,14 +483,17 @@ async function getEmDashEntry(type, id, options) {
483
483
  async function hydrateEntryBylines(type, entries) {
484
484
  if (entries.length === 0) return;
485
485
  try {
486
- const { getBylinesForEntries } = await import("./bylines-BTM2xtP8.mjs").then((n) => n.t);
486
+ const { getBylinesForEntries } = await import("./bylines-BYHWU3T7.mjs").then((n) => n.t);
487
487
  const refs = entries.map((e) => {
488
488
  const data = entryData(e);
489
489
  const id = dataStr(data, "id");
490
- return id ? {
490
+ if (!id) return null;
491
+ return {
491
492
  id,
492
- authorId: dataStr(data, "authorId") || null
493
- } : null;
493
+ authorId: dataStr(data, "authorId") || null,
494
+ primaryBylineId: dataStr(data, "primaryBylineId") || null,
495
+ locale: dataStr(data, "locale") || null
496
+ };
494
497
  }).filter((r) => r !== null);
495
498
  if (refs.length === 0) return;
496
499
  const bylinesMap = await getBylinesForEntries(type, refs);
@@ -526,7 +529,7 @@ async function hydrateEntryBylines(type, entries) {
526
529
  async function hydrateEntryTerms(type, entries) {
527
530
  if (entries.length === 0) return;
528
531
  try {
529
- const { getAllTermsForEntries } = await import("./taxonomies-Cn9UpaR2.mjs").then((n) => n.u);
532
+ const { getAllTermsForEntries } = await import("./taxonomies-WamPVA2x.mjs").then((n) => n.u);
530
533
  const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
531
534
  if (ids.length === 0) return;
532
535
  const termsMap = await getAllTermsForEntries(type, ids);
@@ -560,9 +563,9 @@ async function hydrateEntryTerms(type, entries) {
560
563
  */
561
564
  async function getTranslations(type, id) {
562
565
  try {
563
- const db = (await import("./loader-Cs6-Bqe6.mjs").then((n) => n.i)).getDb;
566
+ const db = (await import("./loader-Chm5h7Gr.mjs").then((n) => n.i)).getDb;
564
567
  const dbInstance = await db();
565
- const { ContentRepository } = await import("./content-D6YG26WG.mjs").then((n) => n.n);
568
+ const { ContentRepository } = await import("./content-C0ooIs-f.mjs").then((n) => n.n);
566
569
  const repo = new ContentRepository(dbInstance);
567
570
  const item = await repo.findByIdOrSlug(type, id);
568
571
  if (!item) return {
@@ -631,8 +634,8 @@ function invalidateUrlPatternCache() {
631
634
  */
632
635
  async function resolveEmDashPath(path) {
633
636
  if (!cachedUrlPatterns) {
634
- const { getDb } = await import("./loader-Cs6-Bqe6.mjs").then((n) => n.i);
635
- const { SchemaRegistry } = await import("./registry-BnCeHYsf.mjs").then((n) => n.r);
637
+ const { getDb } = await import("./loader-Chm5h7Gr.mjs").then((n) => n.i);
638
+ const { SchemaRegistry } = await import("./registry-DqrAQDXH.mjs").then((n) => n.r);
636
639
  const collections = await new SchemaRegistry(await getDb()).listCollections();
637
640
  cachedUrlPatterns = [];
638
641
  for (const collection of collections) {
@@ -664,4 +667,4 @@ async function resolveEmDashPath(path) {
664
667
 
665
668
  //#endregion
666
669
  export { invalidateUrlPatternCache as a, createEditable as c, getTranslations as i, createNoop as l, getEmDashCollection as n, query_exports as o, getEmDashEntry as r, resolveEmDashPath as s, getEditMeta as t };
667
- //# sourceMappingURL=query-axZmO6Tn.mjs.map
670
+ //# sourceMappingURL=query-BJn8TOPk.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"query-axZmO6Tn.mjs","names":[],"sources":["../src/visual-editing/editable.ts","../src/query.ts"],"sourcesContent":["/**\n * Visual editing annotation system\n *\n * Creates Proxy objects that emit data-emdash-ref attributes when spread onto elements.\n */\n\nexport interface CMSAnnotation {\n\tcollection: string;\n\tid: string;\n\tfield?: string;\n\t/** Entry status — only present on entry-level annotations (not field-level) */\n\tstatus?: string;\n\t/** Whether the entry has unpublished draft changes */\n\thasDraft?: boolean;\n}\n\n/** The shape returned when spreading an edit annotation onto an element */\nexport interface FieldAnnotation {\n\t\"data-emdash-ref\": string;\n}\n\nexport interface EditableOptions {\n\t/** Entry status: \"draft\", \"published\", \"scheduled\" */\n\tstatus?: string;\n\t/** true when draftRevisionId exists and differs from liveRevisionId */\n\thasDraft?: boolean;\n}\n\n/**\n * Create an editable proxy for an entry.\n *\n * Usage:\n * - `{...entry.edit}` - entry-level annotation (includes status/hasDraft)\n * - `{...entry.edit.title}` - field-level annotation\n * - `{...entry.edit['nested.field']}` - nested field (bracket notation)\n */\nexport function createEditable(\n\tcollection: string,\n\tid: string,\n\toptions?: EditableOptions,\n): EditProxy {\n\tconst base: CMSAnnotation = {\n\t\tcollection,\n\t\tid,\n\t\t...(options?.status && { status: options.status }),\n\t\t...(options?.hasDraft && { hasDraft: true }),\n\t};\n\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (prop === \"toJSON\") return () => ({ \"data-emdash-ref\": JSON.stringify(base) });\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\n\t\t\t// data-emdash-ref access returns the entry-level string\n\t\t\tif (prop === \"data-emdash-ref\") return JSON.stringify(base);\n\n\t\t\t// Field-level: return a FieldAnnotation for the specific field\n\t\t\treturn {\n\t\t\t\t\"data-emdash-ref\": JSON.stringify({ ...base, field: String(prop) }),\n\t\t\t} satisfies FieldAnnotation;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [\"data-emdash-ref\"];\n\t\t},\n\t\tgetOwnPropertyDescriptor(_, prop) {\n\t\t\tif (prop === \"data-emdash-ref\") {\n\t\t\t\treturn {\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tvalue: JSON.stringify(base),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Create a noop proxy for production mode.\n * Spreading this produces no attributes.\n */\nexport function createNoop(): EditProxy {\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\t\t\t// All property access returns undefined in noop mode\n\t\t\treturn undefined;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [];\n\t\t},\n\t\tgetOwnPropertyDescriptor() {\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Visual editing proxy type.\n *\n * Spread directly onto elements for entry-level annotations: `{...entry.edit}`\n * Access a field for field-level annotations: `{...entry.edit.title}`\n *\n * In production, spreading produces no attributes (noop).\n */\nexport type EditProxy = {\n\treadonly [field: string]: Partial<FieldAnnotation>;\n};\n","/// <reference types=\"astro/client\" />\n/**\n * Query functions for EmDash content\n *\n * These wrap Astro's getLiveCollection/getLiveEntry with type filtering.\n * Use these instead of calling Astro's functions directly.\n *\n * Error handling follows Astro's pattern - returns { entries/entry, error }\n * so callers can gracefully handle errors (including 404s).\n *\n * Preview mode is handled implicitly via ALS request context —\n * no parameters needed. The middleware verifies the preview token\n * and sets the context; query functions read it automatically.\n *\n * The triple-slash directive above pulls in the ambient declaration for\n * `astro:content` (used by the dynamic imports below) so this source\n * file typechecks even when reached transitively by a sibling package\n * whose tsconfig doesn't list `astro/client` in `compilerOptions.types`.\n *\n * Note: the directive is stripped from the compiled output (`dist/*`)\n * by tsdown, so it does not propagate to downstream consumers of the\n * published package. Consumers are Astro sites and already provide their\n * own `astro/client` ambient surface anyway, so the runtime dynamic\n * import resolves there at typecheck time without our help.\n */\n\nimport { encodeCursor } from \"./database/repositories/types.js\";\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./i18n/config.js\";\nimport { CURSOR_RAW_VALUES } from \"./loader.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { isMissingTableError } from \"./utils/db-errors.js\";\nimport {\n\tcreateEditable,\n\tcreateNoop,\n\ttype EditProxy,\n\ttype EditableOptions,\n} from \"./visual-editing/editable.js\";\n\n/**\n * Collection type registry for type-safe queries.\n *\n * This interface is extended by the generated emdash-env.d.ts file\n * to provide type inference for collection names and their data shapes.\n *\n * @example\n * ```ts\n * // In emdash-env.d.ts (generated):\n * declare module \"emdash\" {\n * interface EmDashCollections {\n * posts: { title: string; content: PortableTextBlock[]; };\n * pages: { title: string; body: PortableTextBlock[]; };\n * }\n * }\n *\n * // Then in your code:\n * const { entries } = await getEmDashCollection(\"posts\");\n * // entries[0].data.title is typed as string\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface EmDashCollections {}\n\n/**\n * Helper type to infer the data type for a collection.\n * Returns the registered type if known, otherwise falls back to Record<string, unknown>.\n */\nexport type InferCollectionData<T extends string> = T extends keyof EmDashCollections\n\t? EmDashCollections[T]\n\t: Record<string, unknown>;\n\n/**\n * Sort direction\n */\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Order by specification - field name to direction\n * @example { created_at: \"desc\" } - Sort by created_at descending\n * @example { title: \"asc\" } - Sort by title ascending\n * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n */\nexport type OrderBySpec = Record<string, SortDirection>;\n\nexport interface CollectionFilter {\n\tstatus?: \"draft\" | \"published\" | \"archived\";\n\tlimit?: number;\n\t/**\n\t * Opaque cursor for keyset pagination.\n\t * Pass the `nextCursor` value from a previous result to fetch the next page.\n\t * @example\n\t * ```ts\n\t * const cursor = Astro.url.searchParams.get(\"cursor\") ?? undefined;\n\t * const { entries, nextCursor } = await getEmDashCollection(\"posts\", {\n\t * limit: 10,\n\t * cursor,\n\t * });\n\t * ```\n\t */\n\tcursor?: string;\n\t/**\n\t * Filter by field values or taxonomy terms\n\t * @example { category: 'news' } - Filter by taxonomy term\n\t * @example { category: ['news', 'featured'] } - Filter by multiple terms (OR)\n\t */\n\twhere?: Record<string, string | string[]>;\n\t/**\n\t * Order results by field(s)\n\t * @default { created_at: \"desc\" }\n\t * @example { created_at: \"desc\" } - Sort by created_at descending (default)\n\t * @example { title: \"asc\" } - Sort by title ascending\n\t * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n\t */\n\torderBy?: OrderBySpec;\n\t/**\n\t * Filter by locale. When set, only returns entries in this locale.\n\t * Only relevant when i18n is configured.\n\t * @example \"en\" — English entries only\n\t * @example \"fr\" — French entries only\n\t */\n\tlocale?: string;\n}\n\nexport interface ContentEntry<T = Record<string, unknown>> {\n\tid: string;\n\tdata: T;\n\t/** Visual editing annotations. Spread onto elements: {...entry.edit.title} */\n\tedit: EditProxy;\n}\n\n/** Cache hint returned by the content loader for route caching */\nexport interface CacheHint {\n\ttags?: string[];\n\tlastModified?: Date;\n}\n\n/**\n * Result from getEmDashCollection\n */\nexport interface CollectionResult<T> {\n\t/** The entries (empty array if error or none found) */\n\tentries: ContentEntry<T>[];\n\t/** Error if the query failed */\n\terror?: Error;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n\t/**\n\t * Opaque cursor for the next page.\n\t * Undefined when there are no more results.\n\t * Pass this as `cursor` in the next query to get the next page.\n\t */\n\tnextCursor?: string;\n}\n\n/**\n * Result from getEmDashEntry\n */\nexport interface EntryResult<T> {\n\t/** The entry, or null if not found */\n\tentry: ContentEntry<T> | null;\n\t/** Error if the query failed (not set for \"not found\", only for actual errors) */\n\terror?: Error;\n\t/** Whether we're in preview mode (valid token was provided) */\n\tisPreview: boolean;\n\t/** Set when a fallback locale was used instead of the requested locale */\n\tfallbackLocale?: string;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n}\n\nconst COLLECTION_NAME = \"_emdash\";\n\n/** Symbol key for edit metadata on PT arrays — avoids collision with user data */\nconst EMDASH_EDIT = Symbol.for(\"__emdash\");\n\n/** Edit metadata attached to PT arrays in edit mode */\nexport interface EditFieldMeta {\n\tcollection: string;\n\tid: string;\n\tfield: string;\n}\n\n/** Type guard for EditFieldMeta */\nfunction isEditFieldMeta(value: unknown): value is EditFieldMeta {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"collection\" in value) || !(\"id\" in value) || !(\"field\" in value)) return false;\n\t// After `in` checks, TS narrows to Record<\"collection\" | \"id\" | \"field\", unknown>\n\tconst { collection, id, field } = value;\n\treturn typeof collection === \"string\" && typeof id === \"string\" && typeof field === \"string\";\n}\n\n/**\n * Read edit metadata from a value (returns undefined if not tagged).\n * Uses Object.getOwnPropertyDescriptor to access Symbol-keyed property\n * without an unsafe type assertion.\n */\nexport function getEditMeta(value: unknown): EditFieldMeta | undefined {\n\tif (value && typeof value === \"object\") {\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, EMDASH_EDIT);\n\t\tconst meta: unknown = desc?.value;\n\t\tif (isEditFieldMeta(meta)) {\n\t\t\treturn meta;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Tag PT-like arrays in entry data with edit metadata (non-enumerable).\n * A PT array is identified by: is an array, first element has _type property.\n */\nfunction tagEditableFields(data: Record<string, unknown>, collection: string, id: string): void {\n\tfor (const [field, value] of Object.entries(data)) {\n\t\tif (\n\t\t\tArray.isArray(value) &&\n\t\t\tvalue.length > 0 &&\n\t\t\tvalue[0] &&\n\t\t\ttypeof value[0] === \"object\" &&\n\t\t\t\"_type\" in value[0]\n\t\t) {\n\t\t\tObject.defineProperty(value, EMDASH_EDIT, {\n\t\t\t\tvalue: { collection, id, field } satisfies EditFieldMeta,\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/** Safely read a string field from a Record, with optional fallback */\nfunction dataStr(data: Record<string, unknown>, key: string, fallback = \"\"): string {\n\tconst val = data[key];\n\treturn typeof val === \"string\" ? val : fallback;\n}\n\n/** Type guard for Record<string, unknown> */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Extract data as Record from an Astro entry (which is any-typed) */\nfunction entryData(entry: { data?: unknown }): Record<string, unknown> {\n\treturn isRecord(entry.data) ? entry.data : {};\n}\n\n/** Extract the database ID from entry data (data.id is the ULID, entry.id is the slug) */\nfunction entryDatabaseId(entry: { id: string; data?: unknown }): string {\n\tconst d = entryData(entry);\n\treturn dataStr(d, \"id\") || entry.id;\n}\n\n/** Extract edit options from entry data for the proxy */\nfunction entryEditOptions(entry: { data?: unknown }): EditableOptions {\n\tconst data = entryData(entry);\n\tconst status = dataStr(data, \"status\", \"draft\");\n\tconst draftRevisionId = dataStr(data, \"draftRevisionId\") || undefined;\n\tconst liveRevisionId = dataStr(data, \"liveRevisionId\") || undefined;\n\tconst hasDraft = !!draftRevisionId && draftRevisionId !== liveRevisionId;\n\treturn { status, hasDraft };\n}\n\n/**\n * Get all entries of a content type\n *\n * Returns { entries, error } for graceful error handling.\n *\n * When emdash-env.d.ts is generated, the collection name will be\n * type-checked and the return type will be inferred automatically.\n *\n * @example\n * ```ts\n * import { getEmDashCollection } from \"emdash\";\n *\n * const { entries: posts, error } = await getEmDashCollection(\"posts\");\n * if (error) {\n * console.error(\"Failed to load posts:\", error);\n * return;\n * }\n * // posts[0].data.title is typed (if emdash-env.d.ts exists)\n *\n * // With filters\n * const { entries: drafts } = await getEmDashCollection(\"posts\", { status: \"draft\" });\n * ```\n */\nexport async function getEmDashCollection<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Cache per (type, filter) within a single request. Edit mode and\n\t// preview are request-scoped and stable, so they don't need to be\n\t// part of the key. Widgets and layouts frequently request the same\n\t// collection shape as the page itself (e.g. a \"recent posts\" list\n\t// appears on the home page AND in the sidebar) — caching collapses\n\t// those duplicate queries, along with the bylines and taxonomy-term\n\t// hydration each call would otherwise re-do.\n\t//\n\t// Bucket small limits to a shared minimum so a page with several\n\t// \"recent N posts\" widgets at slightly different limits (e.g. a\n\t// post-detail page asking for 4 in the body and 5 in the sidebar)\n\t// shares one fetch + hydration round-trip rather than running two.\n\t// Cursor-paginated calls are exempt: their limit is part of the\n\t// pagination contract.\n\tconst bucketed = bucketFilter(filter);\n\tconst cached = await requestCached(collectionCacheKey(type, bucketed.fetchFilter), () =>\n\t\tgetEmDashCollectionUncached<T, D>(type, bucketed.fetchFilter),\n\t);\n\treturn bucketed.requestedLimit === undefined\n\t\t? cached\n\t\t: sliceCollectionResult(cached, bucketed.requestedLimit, filter?.orderBy);\n}\n\n/**\n * Threshold for limit bucketing. Page templates routinely render small\n * \"recent posts\" widgets at limits 3-8; rounding those up to a single\n * shared bucket lets one fetch satisfy several widgets within a request.\n * Above this, the requested limit is honoured exactly — bucketing limit:50\n * to limit:64 would waste hydration work for callers fetching real pages.\n */\nconst BUCKET_LIMIT_THRESHOLD = 10;\n\ninterface BucketedFilter {\n\t/** Filter to pass to the loader (with limit possibly raised). */\n\tfetchFilter: CollectionFilter | undefined;\n\t/** Original limit; defined only when bucketing was applied. */\n\trequestedLimit: number | undefined;\n}\n\n/** @internal exported for unit tests; not part of the public API. */\nexport function bucketFilter(filter: CollectionFilter | undefined): BucketedFilter {\n\tconst limit = filter?.limit;\n\tif (\n\t\tlimit === undefined ||\n\t\tlimit >= BUCKET_LIMIT_THRESHOLD ||\n\t\tlimit <= 0 ||\n\t\tfilter?.cursor !== undefined\n\t) {\n\t\treturn { fetchFilter: filter, requestedLimit: undefined };\n\t}\n\treturn {\n\t\tfetchFilter: { ...filter, limit: BUCKET_LIMIT_THRESHOLD },\n\t\trequestedLimit: limit,\n\t};\n}\n\n/**\n * Slice a cached bucketed result down to the originally-requested limit\n * and recompute `nextCursor` from the row that would have been the\n * over-fetch detector for that limit. When truncation is needed, returns\n * a shallow-copied result with a new `entries` array; otherwise returns\n * the cached result unchanged (including error results and results\n * already within the requested limit).\n */\n/** @internal exported for unit tests; not part of the public API. */\nexport function sliceCollectionResult<D>(\n\tcached: CollectionResult<D>,\n\tlimit: number,\n\torderBy: OrderBySpec | undefined,\n): CollectionResult<D> {\n\tif (cached.error) return cached;\n\tif (cached.entries.length <= limit) return cached;\n\tconst sliced = cached.entries.slice(0, limit);\n\t// Mirror the loader's encoding: cursor points at the last returned row,\n\t// so \"next page\" picks up at the row immediately after it. See\n\t// buildCursorCondition in loader.ts — it filters strictly past this row.\n\tconst lastEntry = sliced.at(-1);\n\tconst nextCursor = lastEntry ? encodeEntryCursor(lastEntry, orderBy) : undefined;\n\treturn { ...cached, entries: sliced, nextCursor };\n}\n\n/** Map of database column names to camelCase keys present on entry.data. */\nconst ENTRY_DATA_KEY_MAP: Record<string, string> = {\n\tcreated_at: \"createdAt\",\n\tupdated_at: \"updatedAt\",\n\tpublished_at: \"publishedAt\",\n\tscheduled_at: \"scheduledAt\",\n\tauthor_id: \"authorId\",\n\tprimary_byline_id: \"primaryBylineId\",\n};\n\n// Mirror loader.ts FIELD_NAME_PATTERN. Kept in sync intentionally — diverging\n// would let the encoder accept a field name the loader's getPrimarySort then\n// rejected, producing a cursor that paginates against a different column.\nconst FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Encode a `nextCursor` from a content entry, mirroring the loader's\n * encoding scheme: `(orderValue, id)` where `orderValue` is the primary\n * sort field's stringified value. For date columns, reads the raw DB\n * string the loader stashed via CURSOR_RAW_VALUES — round-tripping the\n * parsed Date through `toISOString()` would lose precision for stored\n * values that aren't already ISO-with-milliseconds.\n */\nfunction encodeEntryCursor<D>(\n\tentry: ContentEntry<D>,\n\torderBy: OrderBySpec | undefined,\n): string | undefined {\n\tconst data = entryData(entry);\n\tconst id = dataStr(data, \"id\");\n\tif (!id) return undefined;\n\n\t// Match loader.ts getPrimarySort: take the first valid field, default to created_at.\n\tlet dbField = \"created_at\";\n\tif (orderBy) {\n\t\tfor (const field of Object.keys(orderBy)) {\n\t\t\tif (FIELD_NAME_PATTERN.test(field)) {\n\t\t\t\tdbField = field;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Date columns: prefer the raw stored string captured by the loader so\n\t// the cursor matches what a direct loader fetch would emit, regardless\n\t// of how the DB stored the timestamp.\n\tconst rawDateValuesRaw = Reflect.get(data, CURSOR_RAW_VALUES);\n\tif (rawDateValuesRaw !== null && typeof rawDateValuesRaw === \"object\") {\n\t\tconst raw = Reflect.get(rawDateValuesRaw, dbField);\n\t\tif (typeof raw === \"string\") return encodeCursor(raw, id);\n\t}\n\n\tconst dataKey = ENTRY_DATA_KEY_MAP[dbField] ?? dbField;\n\tconst value = data[dataKey];\n\tlet orderValue: string;\n\tif (value instanceof Date) {\n\t\torderValue = value.toISOString();\n\t} else if (typeof value === \"string\" || typeof value === \"number\") {\n\t\torderValue = String(value);\n\t} else {\n\t\t// Match the loader's empty-string fallback for null/undefined order\n\t\t// values so cursor decoding stays valid even at the boundary.\n\t\torderValue = \"\";\n\t}\n\treturn encodeCursor(orderValue, id);\n}\n\n/**\n * Build a canonical cache key for `getEmDashCollection`.\n *\n * `JSON.stringify` is insertion-order-sensitive, so two callers passing\n * semantically identical filters with different key orders would miss\n * the cache. We fix the top-level field order and sort `where` keys\n * (order there is irrelevant), while preserving `orderBy` key order\n * because that's the sort priority.\n */\nfunction collectionCacheKey(type: string, filter?: CollectionFilter): string {\n\tif (!filter) return `collection:${type}:`;\n\tconst parts = [\n\t\tfilter.status ?? \"\",\n\t\tfilter.limit ?? \"\",\n\t\tfilter.cursor ?? \"\",\n\t\tfilter.where ? stableStringify(filter.where) : \"\",\n\t\tfilter.orderBy ? JSON.stringify(filter.orderBy) : \"\",\n\t\tfilter.locale ?? \"\",\n\t];\n\treturn `collection:${type}:${parts.join(\"|\")}`;\n}\n\nfunction stableStringify(value: Record<string, unknown>): string {\n\tconst keys = Object.keys(value).toSorted();\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const k of keys) ordered[k] = value[k];\n\treturn JSON.stringify(ordered);\n}\n\nasync function getEmDashCollectionUncached<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveCollection } = await import(\"astro:content\");\n\n\t// Resolve locale: explicit filter > ALS context > defaultLocale (when i18n enabled)\n\t// Without this, queries return all locale rows, producing broken IDs\n\tconst ctx = getRequestContext();\n\tconst i18nConfig = getI18nConfig();\n\tconst resolvedLocale =\n\t\tfilter?.locale ?? ctx?.locale ?? (isI18nEnabled() ? i18nConfig!.defaultLocale : undefined);\n\n\tconst result = await getLiveCollection(COLLECTION_NAME, {\n\t\ttype,\n\t\tstatus: filter?.status,\n\t\tlimit: filter?.limit,\n\t\tcursor: filter?.cursor,\n\t\twhere: filter?.where,\n\t\torderBy: filter?.orderBy,\n\t\tlocale: resolvedLocale,\n\t});\n\n\tconst { entries, error, cacheHint } = result;\n\t// nextCursor is returned by the emdash loader but not part of Astro's base\n\t// LiveLoader return type. Extract it safely via property descriptor to avoid\n\t// an unsafe type assertion on the `any`-typed result object.\n\tconst rawCursor = Object.getOwnPropertyDescriptor(result, \"nextCursor\")?.value;\n\tconst nextCursor: string | undefined = typeof rawCursor === \"string\" ? rawCursor : undefined;\n\n\tif (error) {\n\t\treturn { entries: [], error, cacheHint: {} };\n\t}\n\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst entriesWithEdit = entries.map((entry: ContentEntry<D>) => {\n\t\tconst dbId = entryDatabaseId(entry);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(entry), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...entry,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(entry)) : createNoop(),\n\t\t};\n\t});\n\n\t// Eagerly hydrate bylines and taxonomy terms for all entries in parallel.\n\t// Both are independent queries, so running them concurrently halves the\n\t// round-trip cost on remote databases (D1 replicas, etc.).\n\tawait Promise.all([\n\t\thydrateEntryBylines(type, entriesWithEdit),\n\t\thydrateEntryTerms(type, entriesWithEdit),\n\t]);\n\n\treturn { entries: entriesWithEdit, nextCursor, cacheHint: cacheHint ?? {} };\n}\n\n/**\n * Get a single entry by type and ID/slug\n *\n * Returns { entry, error, isPreview } for graceful error handling.\n * - entry is null if not found (not an error)\n * - error is set only for actual errors (db issues, etc.)\n *\n * Preview mode is detected automatically from request context (ALS).\n * When the URL has a valid `_preview` token, the middleware sets preview\n * context and this function serves draft revision data if available.\n *\n * @example\n * ```ts\n * import { getEmDashEntry } from \"emdash\";\n *\n * // Simple usage — preview just works via middleware\n * const { entry: post, isPreview, error } = await getEmDashEntry(\"posts\", \"my-slug\");\n * if (!post) return Astro.redirect(\"/404\");\n * ```\n */\nexport async function getEmDashEntry<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tid: string,\n\toptions?: { locale?: string },\n): Promise<EntryResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveEntry } = await import(\"astro:content\");\n\n\t// Check ALS for preview and edit mode context\n\tconst ctx = getRequestContext();\n\tconst preview = ctx?.preview;\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst isPreviewMode = !!preview && preview.collection === type;\n\t// Edit mode implies preview — editors should see draft content\n\tconst serveDrafts = isPreviewMode || isEditMode;\n\n\t// Resolve locale: explicit option > ALS context > undefined (no filter)\n\tconst requestedLocale = options?.locale ?? ctx?.locale;\n\n\t/** Wrap a raw Astro entry with edit proxy, tagging editable fields if needed */\n\tfunction wrapEntry(raw: ContentEntry<D>): ContentEntry<D> {\n\t\tconst dbId = entryDatabaseId(raw);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(raw), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...raw,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(raw)) : createNoop(),\n\t\t};\n\t}\n\n\t/** Check if an entry is publicly visible (published or scheduled past its time) */\n\tfunction isVisible(entry: ContentEntry<D>): boolean {\n\t\tconst data = entryData(entry);\n\t\tconst status = dataStr(data, \"status\");\n\t\tconst scheduledAt = dataStr(data, \"scheduledAt\") || undefined;\n\t\tconst isPublished = status === \"published\";\n\t\tconst isScheduledAndReady =\n\t\t\tstatus === \"scheduled\" && scheduledAt && new Date(scheduledAt) <= new Date();\n\t\treturn isPublished || !!isScheduledAndReady;\n\t}\n\n\t// Build the fallback chain: [requestedLocale, fallback1, ..., defaultLocale]\n\t// When i18n is disabled or no locale requested, just use a single-element chain\n\tconst localeChain =\n\t\trequestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];\n\n\t/** Return a successful EntryResult with bylines and taxonomy terms hydrated */\n\tasync function successResult(\n\t\twrapped: ContentEntry<D>,\n\t\topts: { isPreview: boolean; fallbackLocale?: string; cacheHint: CacheHint },\n\t): Promise<EntryResult<D>> {\n\t\tawait Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);\n\t\treturn {\n\t\t\tentry: wrapped,\n\t\t\tisPreview: opts.isPreview,\n\t\t\tfallbackLocale: opts.fallbackLocale,\n\t\t\tcacheHint: opts.cacheHint,\n\t\t};\n\t}\n\n\tif (serveDrafts) {\n\t\t// Draft mode: try each locale in the fallback chain\n\t\tfor (let i = 0; i < localeChain.length; i++) {\n\t\t\tconst locale = localeChain[i];\n\t\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\t\tconst {\n\t\t\t\tentry: baseEntry,\n\t\t\t\terror: baseError,\n\t\t\t\tcacheHint,\n\t\t\t} = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\ttype,\n\t\t\t\tid,\n\t\t\t\tlocale,\n\t\t\t});\n\n\t\t\tif (baseError) {\n\t\t\t\treturn { entry: null, error: baseError, isPreview: serveDrafts, cacheHint: {} };\n\t\t\t}\n\n\t\t\tif (!baseEntry) continue; // Try next locale in chain\n\n\t\t\t// Preview tokens are item-scoped: verify the resolved entry matches.\n\t\t\t// Edit mode (authenticated editors) has collection-wide draft access.\n\t\t\tif (isPreviewMode && !isEditMode) {\n\t\t\t\tconst dbId = entryDatabaseId(baseEntry);\n\t\t\t\tif (preview.id !== dbId && preview.id !== id) {\n\t\t\t\t\t// Token doesn't match — serve only if publicly visible, without draft access\n\t\t\t\t\tif (isVisible(baseEntry)) {\n\t\t\t\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\t\t\t\tisPreview: false,\n\t\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// Not visible — try next locale in fallback chain\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if entry has a draft revision — if so, re-fetch with revision data\n\t\t\tconst baseData = entryData(baseEntry);\n\t\t\tconst draftRevisionId = dataStr(baseData, \"draftRevisionId\") || undefined;\n\n\t\t\tif (draftRevisionId) {\n\t\t\t\tconst { entry: draftEntry, error: draftError } = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\t\ttype,\n\t\t\t\t\tid,\n\t\t\t\t\trevisionId: draftRevisionId,\n\t\t\t\t\tlocale,\n\t\t\t\t});\n\n\t\t\t\tif (!draftError && draftEntry) {\n\t\t\t\t\treturn successResult(wrapEntry(draftEntry), {\n\t\t\t\t\t\tisPreview: serveDrafts,\n\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\tisPreview: serveDrafts,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\n\t\t// No entry found in any locale\n\t\treturn { entry: null, isPreview: serveDrafts, cacheHint: {} };\n\t}\n\n\t// Normal mode: try each locale in the fallback chain, only return published content\n\tfor (let i = 0; i < localeChain.length; i++) {\n\t\tconst locale = localeChain[i];\n\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\tconst { entry, error, cacheHint } = await getLiveEntry(COLLECTION_NAME, { type, id, locale });\n\t\tif (error) {\n\t\t\treturn { entry: null, error, isPreview: false, cacheHint: {} };\n\t\t}\n\n\t\tif (entry && isVisible(entry)) {\n\t\t\treturn successResult(wrapEntry(entry), {\n\t\t\t\tisPreview: false,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\t\t// Entry not found or not visible in this locale — try next\n\t}\n\n\treturn { entry: null, isPreview: false, cacheHint: {} };\n}\n\n/**\n * Eagerly hydrate byline data onto entry.data for one or more entries.\n *\n * Attaches `bylines` (array of ContentBylineCredit) and `byline`\n * (primary BylineSummary or null) to each entry's data object.\n * Uses batch queries to avoid N+1.\n *\n * Fails silently if the byline tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getBylinesForEntries } = await import(\"./bylines/index.js\");\n\n\t\tconst refs = entries\n\t\t\t.map((e) => {\n\t\t\t\tconst data = entryData(e);\n\t\t\t\tconst id = dataStr(data, \"id\");\n\t\t\t\treturn id ? { id, authorId: dataStr(data, \"authorId\") || null } : null;\n\t\t\t})\n\t\t\t.filter((r): r is { id: string; authorId: string | null } => r !== null);\n\t\tif (refs.length === 0) return;\n\n\t\tconst bylinesMap = await getBylinesForEntries(type, refs);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tconst credits = bylinesMap.get(dbId) ?? [];\n\t\t\tdata.bylines = credits;\n\t\t\tdata.byline = credits[0]?.byline ?? null;\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate bylines:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Eagerly hydrate taxonomy term data onto entry.data for one or more entries.\n *\n * Attaches `terms` (Record keyed by taxonomy name with an array of TaxonomyTerm\n * values) to each entry's data object. Uses a single batched JOIN query across\n * all taxonomies so the cost is O(1) regardless of the number of entries or\n * taxonomies on the site.\n *\n * This eliminates the common N+1 pattern where templates loop over list\n * results and call getEntryTerms() per entry. With hydration, the list page\n * stays at a single round-trip for term data.\n *\n * Fails silently if the taxonomy tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getAllTermsForEntries } = await import(\"./taxonomies/index.js\");\n\n\t\tconst ids = entries.map((e) => dataStr(entryData(e), \"id\")).filter(Boolean);\n\t\tif (ids.length === 0) return;\n\n\t\tconst termsMap = await getAllTermsForEntries(type, ids);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tdata.terms = termsMap.get(dbId) ?? {};\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate terms:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Translation summary for a single locale variant\n */\nexport interface TranslationSummary {\n\t/** Content item ID */\n\tid: string;\n\t/** Locale code (e.g. \"en\", \"fr\") */\n\tlocale: string;\n\t/** URL slug */\n\tslug: string | null;\n\t/** Current status */\n\tstatus: string;\n}\n\n/**\n * Result from getTranslations\n */\nexport interface TranslationsResult {\n\t/** The translation group ID (shared across locales) */\n\ttranslationGroup: string;\n\t/** All locale variants in this group */\n\ttranslations: TranslationSummary[];\n\t/** Error if the query failed */\n\terror?: Error;\n}\n\n/**\n * Get all translations of a content item.\n *\n * Given a content entry, returns all locale variants that share the same\n * translation group. This is useful for building language switcher UI.\n *\n * @example\n * ```ts\n * import { getEmDashEntry, getTranslations } from \"emdash\";\n *\n * const { entry: post } = await getEmDashEntry(\"posts\", \"hello-world\", { locale: \"en\" });\n * const { translations } = await getTranslations(\"posts\", post.data.id);\n * // translations = [{ id: \"...\", locale: \"en\", slug: \"hello-world\", status: \"published\" }, ...]\n * ```\n */\nexport async function getTranslations(type: string, id: string): Promise<TranslationsResult> {\n\ttry {\n\t\tconst db = (await import(\"./loader.js\")).getDb;\n\t\tconst dbInstance = await db();\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(dbInstance);\n\n\t\t// Find the item to get its translation group\n\t\tconst item = await repo.findByIdOrSlug(type, id);\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\ttranslationGroup: \"\",\n\t\t\t\ttranslations: [],\n\t\t\t\terror: new Error(`Content item not found: ${id}`),\n\t\t\t};\n\t\t}\n\n\t\tconst group = item.translationGroup || item.id;\n\t\tconst translations = await repo.findTranslations(type, group);\n\n\t\treturn {\n\t\t\ttranslationGroup: group,\n\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\tid: t.id,\n\t\t\t\tlocale: t.locale || \"en\",\n\t\t\t\tslug: t.slug,\n\t\t\t\tstatus: t.status,\n\t\t\t})),\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\ttranslationGroup: \"\",\n\t\t\ttranslations: [],\n\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t};\n\t}\n}\n\n/**\n * Result from resolveEmDashPath\n */\nexport interface ResolvePathResult<T = Record<string, unknown>> {\n\t/** The matched entry */\n\tentry: ContentEntry<T>;\n\t/** The collection slug that matched */\n\tcollection: string;\n\t/** Extracted parameters from the URL pattern (e.g. { slug: \"my-post\" }) */\n\tparams: Record<string, string>;\n}\n\n/** Matches `{paramName}` placeholders in URL patterns */\nconst URL_PARAM_PATTERN = /\\{(\\w+)\\}/g;\n\n/** Convert a URL pattern like \"/blog/{slug}\" to a regex and param name list */\nfunction patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {\n\tconst paramNames: string[] = [];\n\tconst regexStr = pattern.replace(URL_PARAM_PATTERN, (_match, name: string) => {\n\t\tparamNames.push(name);\n\t\treturn \"([^/]+)\";\n\t});\n\treturn { regex: new RegExp(`^${regexStr}$`), paramNames };\n}\n\n/** Cached compiled URL patterns for resolveEmDashPath */\ninterface CachedPattern {\n\tslug: string;\n\tregex: RegExp;\n\tparamNames: string[];\n}\nlet cachedUrlPatterns: CachedPattern[] | null = null;\n\n/**\n * Invalidate the cached URL patterns used by resolveEmDashPath.\n * Call when collection URL patterns change (schema updates).\n */\nexport function invalidateUrlPatternCache(): void {\n\tcachedUrlPatterns = null;\n}\n\n/**\n * Resolve a URL path to a content entry by matching against collection URL patterns.\n *\n * Loads all collections with a `urlPattern` set, converts each pattern to a regex,\n * and tests the given path. On match, extracts the slug and fetches the entry.\n *\n * @example\n * ```ts\n * import { resolveEmDashPath } from \"emdash\";\n *\n * // Given pages with urlPattern \"/{slug}\" and posts with \"/blog/{slug}\":\n * const result = await resolveEmDashPath(\"/blog/hello-world\");\n * if (result) {\n * console.log(result.collection); // \"posts\"\n * console.log(result.params.slug); // \"hello-world\"\n * console.log(result.entry.data); // post data\n * }\n * ```\n */\nexport async function resolveEmDashPath<T = Record<string, unknown>>(\n\tpath: string,\n): Promise<ResolvePathResult<T> | null> {\n\t// Build and cache compiled patterns on first call\n\tif (!cachedUrlPatterns) {\n\t\tconst { getDb } = await import(\"./loader.js\");\n\t\tconst { SchemaRegistry } = await import(\"./schema/registry.js\");\n\t\tconst db = await getDb();\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst collections = await registry.listCollections();\n\n\t\tcachedUrlPatterns = [];\n\t\tfor (const collection of collections) {\n\t\t\tif (!collection.urlPattern) continue;\n\t\t\tconst { regex, paramNames } = patternToRegex(collection.urlPattern);\n\t\t\tcachedUrlPatterns.push({ slug: collection.slug, regex, paramNames });\n\t\t}\n\t}\n\n\tfor (const pattern of cachedUrlPatterns) {\n\t\tconst match = path.match(pattern.regex);\n\t\tif (!match) continue;\n\n\t\t// Extract params\n\t\tconst params: Record<string, string> = {};\n\t\tfor (let i = 0; i < pattern.paramNames.length; i++) {\n\t\t\tparams[pattern.paramNames[i]] = match[i + 1];\n\t\t}\n\n\t\t// Look up entry by slug (most common pattern)\n\t\tconst slug = params.slug;\n\t\tif (!slug) continue;\n\n\t\tconst { entry } = await getEmDashEntry<string, T>(pattern.slug, slug);\n\t\tif (entry) {\n\t\t\treturn { entry, collection: pattern.slug, params };\n\t\t}\n\t}\n\n\treturn null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-BJn8TOPk.mjs","names":[],"sources":["../src/visual-editing/editable.ts","../src/query.ts"],"sourcesContent":["/**\n * Visual editing annotation system\n *\n * Creates Proxy objects that emit data-emdash-ref attributes when spread onto elements.\n */\n\nexport interface CMSAnnotation {\n\tcollection: string;\n\tid: string;\n\tfield?: string;\n\t/** Entry status — only present on entry-level annotations (not field-level) */\n\tstatus?: string;\n\t/** Whether the entry has unpublished draft changes */\n\thasDraft?: boolean;\n}\n\n/** The shape returned when spreading an edit annotation onto an element */\nexport interface FieldAnnotation {\n\t\"data-emdash-ref\": string;\n}\n\nexport interface EditableOptions {\n\t/** Entry status: \"draft\", \"published\", \"scheduled\" */\n\tstatus?: string;\n\t/** true when draftRevisionId exists and differs from liveRevisionId */\n\thasDraft?: boolean;\n}\n\n/**\n * Create an editable proxy for an entry.\n *\n * Usage:\n * - `{...entry.edit}` - entry-level annotation (includes status/hasDraft)\n * - `{...entry.edit.title}` - field-level annotation\n * - `{...entry.edit['nested.field']}` - nested field (bracket notation)\n */\nexport function createEditable(\n\tcollection: string,\n\tid: string,\n\toptions?: EditableOptions,\n): EditProxy {\n\tconst base: CMSAnnotation = {\n\t\tcollection,\n\t\tid,\n\t\t...(options?.status && { status: options.status }),\n\t\t...(options?.hasDraft && { hasDraft: true }),\n\t};\n\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (prop === \"toJSON\") return () => ({ \"data-emdash-ref\": JSON.stringify(base) });\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\n\t\t\t// data-emdash-ref access returns the entry-level string\n\t\t\tif (prop === \"data-emdash-ref\") return JSON.stringify(base);\n\n\t\t\t// Field-level: return a FieldAnnotation for the specific field\n\t\t\treturn {\n\t\t\t\t\"data-emdash-ref\": JSON.stringify({ ...base, field: String(prop) }),\n\t\t\t} satisfies FieldAnnotation;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [\"data-emdash-ref\"];\n\t\t},\n\t\tgetOwnPropertyDescriptor(_, prop) {\n\t\t\tif (prop === \"data-emdash-ref\") {\n\t\t\t\treturn {\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tvalue: JSON.stringify(base),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Create a noop proxy for production mode.\n * Spreading this produces no attributes.\n */\nexport function createNoop(): EditProxy {\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\t\t\t// All property access returns undefined in noop mode\n\t\t\treturn undefined;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [];\n\t\t},\n\t\tgetOwnPropertyDescriptor() {\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Visual editing proxy type.\n *\n * Spread directly onto elements for entry-level annotations: `{...entry.edit}`\n * Access a field for field-level annotations: `{...entry.edit.title}`\n *\n * In production, spreading produces no attributes (noop).\n */\nexport type EditProxy = {\n\treadonly [field: string]: Partial<FieldAnnotation>;\n};\n","/// <reference types=\"astro/client\" />\n/**\n * Query functions for EmDash content\n *\n * These wrap Astro's getLiveCollection/getLiveEntry with type filtering.\n * Use these instead of calling Astro's functions directly.\n *\n * Error handling follows Astro's pattern - returns { entries/entry, error }\n * so callers can gracefully handle errors (including 404s).\n *\n * Preview mode is handled implicitly via ALS request context —\n * no parameters needed. The middleware verifies the preview token\n * and sets the context; query functions read it automatically.\n *\n * The triple-slash directive above pulls in the ambient declaration for\n * `astro:content` (used by the dynamic imports below) so this source\n * file typechecks even when reached transitively by a sibling package\n * whose tsconfig doesn't list `astro/client` in `compilerOptions.types`.\n *\n * Note: the directive is stripped from the compiled output (`dist/*`)\n * by tsdown, so it does not propagate to downstream consumers of the\n * published package. Consumers are Astro sites and already provide their\n * own `astro/client` ambient surface anyway, so the runtime dynamic\n * import resolves there at typecheck time without our help.\n */\n\nimport { encodeCursor } from \"./database/repositories/types.js\";\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./i18n/config.js\";\nimport { CURSOR_RAW_VALUES } from \"./loader.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { isMissingTableError } from \"./utils/db-errors.js\";\nimport {\n\tcreateEditable,\n\tcreateNoop,\n\ttype EditProxy,\n\ttype EditableOptions,\n} from \"./visual-editing/editable.js\";\n\n/**\n * Collection type registry for type-safe queries.\n *\n * This interface is extended by the generated emdash-env.d.ts file\n * to provide type inference for collection names and their data shapes.\n *\n * @example\n * ```ts\n * // In emdash-env.d.ts (generated):\n * declare module \"emdash\" {\n * interface EmDashCollections {\n * posts: { title: string; content: PortableTextBlock[]; };\n * pages: { title: string; body: PortableTextBlock[]; };\n * }\n * }\n *\n * // Then in your code:\n * const { entries } = await getEmDashCollection(\"posts\");\n * // entries[0].data.title is typed as string\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface EmDashCollections {}\n\n/**\n * Helper type to infer the data type for a collection.\n * Returns the registered type if known, otherwise falls back to Record<string, unknown>.\n */\nexport type InferCollectionData<T extends string> = T extends keyof EmDashCollections\n\t? EmDashCollections[T]\n\t: Record<string, unknown>;\n\n/**\n * Sort direction\n */\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Order by specification - field name to direction\n * @example { created_at: \"desc\" } - Sort by created_at descending\n * @example { title: \"asc\" } - Sort by title ascending\n * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n */\nexport type OrderBySpec = Record<string, SortDirection>;\n\nexport interface CollectionFilter {\n\tstatus?: \"draft\" | \"published\" | \"archived\";\n\tlimit?: number;\n\t/**\n\t * Opaque cursor for keyset pagination.\n\t * Pass the `nextCursor` value from a previous result to fetch the next page.\n\t * @example\n\t * ```ts\n\t * const cursor = Astro.url.searchParams.get(\"cursor\") ?? undefined;\n\t * const { entries, nextCursor } = await getEmDashCollection(\"posts\", {\n\t * limit: 10,\n\t * cursor,\n\t * });\n\t * ```\n\t */\n\tcursor?: string;\n\t/**\n\t * Filter by field values or taxonomy terms\n\t * @example { category: 'news' } - Filter by taxonomy term\n\t * @example { category: ['news', 'featured'] } - Filter by multiple terms (OR)\n\t */\n\twhere?: Record<string, string | string[]>;\n\t/**\n\t * Order results by field(s)\n\t * @default { created_at: \"desc\" }\n\t * @example { created_at: \"desc\" } - Sort by created_at descending (default)\n\t * @example { title: \"asc\" } - Sort by title ascending\n\t * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n\t */\n\torderBy?: OrderBySpec;\n\t/**\n\t * Filter by locale. When set, only returns entries in this locale.\n\t * Only relevant when i18n is configured.\n\t * @example \"en\" — English entries only\n\t * @example \"fr\" — French entries only\n\t */\n\tlocale?: string;\n}\n\nexport interface ContentEntry<T = Record<string, unknown>> {\n\tid: string;\n\tdata: T;\n\t/** Visual editing annotations. Spread onto elements: {...entry.edit.title} */\n\tedit: EditProxy;\n}\n\n/** Cache hint returned by the content loader for route caching */\nexport interface CacheHint {\n\ttags?: string[];\n\tlastModified?: Date;\n}\n\n/**\n * Result from getEmDashCollection\n */\nexport interface CollectionResult<T> {\n\t/** The entries (empty array if error or none found) */\n\tentries: ContentEntry<T>[];\n\t/** Error if the query failed */\n\terror?: Error;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n\t/**\n\t * Opaque cursor for the next page.\n\t * Undefined when there are no more results.\n\t * Pass this as `cursor` in the next query to get the next page.\n\t */\n\tnextCursor?: string;\n}\n\n/**\n * Result from getEmDashEntry\n */\nexport interface EntryResult<T> {\n\t/** The entry, or null if not found */\n\tentry: ContentEntry<T> | null;\n\t/** Error if the query failed (not set for \"not found\", only for actual errors) */\n\terror?: Error;\n\t/** Whether we're in preview mode (valid token was provided) */\n\tisPreview: boolean;\n\t/** Set when a fallback locale was used instead of the requested locale */\n\tfallbackLocale?: string;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n}\n\nconst COLLECTION_NAME = \"_emdash\";\n\n/** Symbol key for edit metadata on PT arrays — avoids collision with user data */\nconst EMDASH_EDIT = Symbol.for(\"__emdash\");\n\n/** Edit metadata attached to PT arrays in edit mode */\nexport interface EditFieldMeta {\n\tcollection: string;\n\tid: string;\n\tfield: string;\n}\n\n/** Type guard for EditFieldMeta */\nfunction isEditFieldMeta(value: unknown): value is EditFieldMeta {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"collection\" in value) || !(\"id\" in value) || !(\"field\" in value)) return false;\n\t// After `in` checks, TS narrows to Record<\"collection\" | \"id\" | \"field\", unknown>\n\tconst { collection, id, field } = value;\n\treturn typeof collection === \"string\" && typeof id === \"string\" && typeof field === \"string\";\n}\n\n/**\n * Read edit metadata from a value (returns undefined if not tagged).\n * Uses Object.getOwnPropertyDescriptor to access Symbol-keyed property\n * without an unsafe type assertion.\n */\nexport function getEditMeta(value: unknown): EditFieldMeta | undefined {\n\tif (value && typeof value === \"object\") {\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, EMDASH_EDIT);\n\t\tconst meta: unknown = desc?.value;\n\t\tif (isEditFieldMeta(meta)) {\n\t\t\treturn meta;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Tag PT-like arrays in entry data with edit metadata (non-enumerable).\n * A PT array is identified by: is an array, first element has _type property.\n */\nfunction tagEditableFields(data: Record<string, unknown>, collection: string, id: string): void {\n\tfor (const [field, value] of Object.entries(data)) {\n\t\tif (\n\t\t\tArray.isArray(value) &&\n\t\t\tvalue.length > 0 &&\n\t\t\tvalue[0] &&\n\t\t\ttypeof value[0] === \"object\" &&\n\t\t\t\"_type\" in value[0]\n\t\t) {\n\t\t\tObject.defineProperty(value, EMDASH_EDIT, {\n\t\t\t\tvalue: { collection, id, field } satisfies EditFieldMeta,\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/** Safely read a string field from a Record, with optional fallback */\nfunction dataStr(data: Record<string, unknown>, key: string, fallback = \"\"): string {\n\tconst val = data[key];\n\treturn typeof val === \"string\" ? val : fallback;\n}\n\n/** Type guard for Record<string, unknown> */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Extract data as Record from an Astro entry (which is any-typed) */\nfunction entryData(entry: { data?: unknown }): Record<string, unknown> {\n\treturn isRecord(entry.data) ? entry.data : {};\n}\n\n/** Extract the database ID from entry data (data.id is the ULID, entry.id is the slug) */\nfunction entryDatabaseId(entry: { id: string; data?: unknown }): string {\n\tconst d = entryData(entry);\n\treturn dataStr(d, \"id\") || entry.id;\n}\n\n/** Extract edit options from entry data for the proxy */\nfunction entryEditOptions(entry: { data?: unknown }): EditableOptions {\n\tconst data = entryData(entry);\n\tconst status = dataStr(data, \"status\", \"draft\");\n\tconst draftRevisionId = dataStr(data, \"draftRevisionId\") || undefined;\n\tconst liveRevisionId = dataStr(data, \"liveRevisionId\") || undefined;\n\tconst hasDraft = !!draftRevisionId && draftRevisionId !== liveRevisionId;\n\treturn { status, hasDraft };\n}\n\n/**\n * Get all entries of a content type\n *\n * Returns { entries, error } for graceful error handling.\n *\n * When emdash-env.d.ts is generated, the collection name will be\n * type-checked and the return type will be inferred automatically.\n *\n * @example\n * ```ts\n * import { getEmDashCollection } from \"emdash\";\n *\n * const { entries: posts, error } = await getEmDashCollection(\"posts\");\n * if (error) {\n * console.error(\"Failed to load posts:\", error);\n * return;\n * }\n * // posts[0].data.title is typed (if emdash-env.d.ts exists)\n *\n * // With filters\n * const { entries: drafts } = await getEmDashCollection(\"posts\", { status: \"draft\" });\n * ```\n */\nexport async function getEmDashCollection<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Cache per (type, filter) within a single request. Edit mode and\n\t// preview are request-scoped and stable, so they don't need to be\n\t// part of the key. Widgets and layouts frequently request the same\n\t// collection shape as the page itself (e.g. a \"recent posts\" list\n\t// appears on the home page AND in the sidebar) — caching collapses\n\t// those duplicate queries, along with the bylines and taxonomy-term\n\t// hydration each call would otherwise re-do.\n\t//\n\t// Bucket small limits to a shared minimum so a page with several\n\t// \"recent N posts\" widgets at slightly different limits (e.g. a\n\t// post-detail page asking for 4 in the body and 5 in the sidebar)\n\t// shares one fetch + hydration round-trip rather than running two.\n\t// Cursor-paginated calls are exempt: their limit is part of the\n\t// pagination contract.\n\tconst bucketed = bucketFilter(filter);\n\tconst cached = await requestCached(collectionCacheKey(type, bucketed.fetchFilter), () =>\n\t\tgetEmDashCollectionUncached<T, D>(type, bucketed.fetchFilter),\n\t);\n\treturn bucketed.requestedLimit === undefined\n\t\t? cached\n\t\t: sliceCollectionResult(cached, bucketed.requestedLimit, filter?.orderBy);\n}\n\n/**\n * Threshold for limit bucketing. Page templates routinely render small\n * \"recent posts\" widgets at limits 3-8; rounding those up to a single\n * shared bucket lets one fetch satisfy several widgets within a request.\n * Above this, the requested limit is honoured exactly — bucketing limit:50\n * to limit:64 would waste hydration work for callers fetching real pages.\n */\nconst BUCKET_LIMIT_THRESHOLD = 10;\n\ninterface BucketedFilter {\n\t/** Filter to pass to the loader (with limit possibly raised). */\n\tfetchFilter: CollectionFilter | undefined;\n\t/** Original limit; defined only when bucketing was applied. */\n\trequestedLimit: number | undefined;\n}\n\n/** @internal exported for unit tests; not part of the public API. */\nexport function bucketFilter(filter: CollectionFilter | undefined): BucketedFilter {\n\tconst limit = filter?.limit;\n\tif (\n\t\tlimit === undefined ||\n\t\tlimit >= BUCKET_LIMIT_THRESHOLD ||\n\t\tlimit <= 0 ||\n\t\tfilter?.cursor !== undefined\n\t) {\n\t\treturn { fetchFilter: filter, requestedLimit: undefined };\n\t}\n\treturn {\n\t\tfetchFilter: { ...filter, limit: BUCKET_LIMIT_THRESHOLD },\n\t\trequestedLimit: limit,\n\t};\n}\n\n/**\n * Slice a cached bucketed result down to the originally-requested limit\n * and recompute `nextCursor` from the row that would have been the\n * over-fetch detector for that limit. When truncation is needed, returns\n * a shallow-copied result with a new `entries` array; otherwise returns\n * the cached result unchanged (including error results and results\n * already within the requested limit).\n */\n/** @internal exported for unit tests; not part of the public API. */\nexport function sliceCollectionResult<D>(\n\tcached: CollectionResult<D>,\n\tlimit: number,\n\torderBy: OrderBySpec | undefined,\n): CollectionResult<D> {\n\tif (cached.error) return cached;\n\tif (cached.entries.length <= limit) return cached;\n\tconst sliced = cached.entries.slice(0, limit);\n\t// Mirror the loader's encoding: cursor points at the last returned row,\n\t// so \"next page\" picks up at the row immediately after it. See\n\t// buildCursorCondition in loader.ts — it filters strictly past this row.\n\tconst lastEntry = sliced.at(-1);\n\tconst nextCursor = lastEntry ? encodeEntryCursor(lastEntry, orderBy) : undefined;\n\treturn { ...cached, entries: sliced, nextCursor };\n}\n\n/** Map of database column names to camelCase keys present on entry.data. */\nconst ENTRY_DATA_KEY_MAP: Record<string, string> = {\n\tcreated_at: \"createdAt\",\n\tupdated_at: \"updatedAt\",\n\tpublished_at: \"publishedAt\",\n\tscheduled_at: \"scheduledAt\",\n\tauthor_id: \"authorId\",\n\tprimary_byline_id: \"primaryBylineId\",\n};\n\n// Mirror loader.ts FIELD_NAME_PATTERN. Kept in sync intentionally — diverging\n// would let the encoder accept a field name the loader's getPrimarySort then\n// rejected, producing a cursor that paginates against a different column.\nconst FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Encode a `nextCursor` from a content entry, mirroring the loader's\n * encoding scheme: `(orderValue, id)` where `orderValue` is the primary\n * sort field's stringified value. For date columns, reads the raw DB\n * string the loader stashed via CURSOR_RAW_VALUES — round-tripping the\n * parsed Date through `toISOString()` would lose precision for stored\n * values that aren't already ISO-with-milliseconds.\n */\nfunction encodeEntryCursor<D>(\n\tentry: ContentEntry<D>,\n\torderBy: OrderBySpec | undefined,\n): string | undefined {\n\tconst data = entryData(entry);\n\tconst id = dataStr(data, \"id\");\n\tif (!id) return undefined;\n\n\t// Match loader.ts getPrimarySort: take the first valid field, default to created_at.\n\tlet dbField = \"created_at\";\n\tif (orderBy) {\n\t\tfor (const field of Object.keys(orderBy)) {\n\t\t\tif (FIELD_NAME_PATTERN.test(field)) {\n\t\t\t\tdbField = field;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Date columns: prefer the raw stored string captured by the loader so\n\t// the cursor matches what a direct loader fetch would emit, regardless\n\t// of how the DB stored the timestamp.\n\tconst rawDateValuesRaw = Reflect.get(data, CURSOR_RAW_VALUES);\n\tif (rawDateValuesRaw !== null && typeof rawDateValuesRaw === \"object\") {\n\t\tconst raw = Reflect.get(rawDateValuesRaw, dbField);\n\t\tif (typeof raw === \"string\") return encodeCursor(raw, id);\n\t}\n\n\tconst dataKey = ENTRY_DATA_KEY_MAP[dbField] ?? dbField;\n\tconst value = data[dataKey];\n\tlet orderValue: string;\n\tif (value instanceof Date) {\n\t\torderValue = value.toISOString();\n\t} else if (typeof value === \"string\" || typeof value === \"number\") {\n\t\torderValue = String(value);\n\t} else {\n\t\t// Match the loader's empty-string fallback for null/undefined order\n\t\t// values so cursor decoding stays valid even at the boundary.\n\t\torderValue = \"\";\n\t}\n\treturn encodeCursor(orderValue, id);\n}\n\n/**\n * Build a canonical cache key for `getEmDashCollection`.\n *\n * `JSON.stringify` is insertion-order-sensitive, so two callers passing\n * semantically identical filters with different key orders would miss\n * the cache. We fix the top-level field order and sort `where` keys\n * (order there is irrelevant), while preserving `orderBy` key order\n * because that's the sort priority.\n */\nfunction collectionCacheKey(type: string, filter?: CollectionFilter): string {\n\tif (!filter) return `collection:${type}:`;\n\tconst parts = [\n\t\tfilter.status ?? \"\",\n\t\tfilter.limit ?? \"\",\n\t\tfilter.cursor ?? \"\",\n\t\tfilter.where ? stableStringify(filter.where) : \"\",\n\t\tfilter.orderBy ? JSON.stringify(filter.orderBy) : \"\",\n\t\tfilter.locale ?? \"\",\n\t];\n\treturn `collection:${type}:${parts.join(\"|\")}`;\n}\n\nfunction stableStringify(value: Record<string, unknown>): string {\n\tconst keys = Object.keys(value).toSorted();\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const k of keys) ordered[k] = value[k];\n\treturn JSON.stringify(ordered);\n}\n\nasync function getEmDashCollectionUncached<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveCollection } = await import(\"astro:content\");\n\n\t// Resolve locale: explicit filter > ALS context > defaultLocale (when i18n enabled)\n\t// Without this, queries return all locale rows, producing broken IDs\n\tconst ctx = getRequestContext();\n\tconst i18nConfig = getI18nConfig();\n\tconst resolvedLocale =\n\t\tfilter?.locale ?? ctx?.locale ?? (isI18nEnabled() ? i18nConfig!.defaultLocale : undefined);\n\n\tconst result = await getLiveCollection(COLLECTION_NAME, {\n\t\ttype,\n\t\tstatus: filter?.status,\n\t\tlimit: filter?.limit,\n\t\tcursor: filter?.cursor,\n\t\twhere: filter?.where,\n\t\torderBy: filter?.orderBy,\n\t\tlocale: resolvedLocale,\n\t});\n\n\tconst { entries, error, cacheHint } = result;\n\t// nextCursor is returned by the emdash loader but not part of Astro's base\n\t// LiveLoader return type. Extract it safely via property descriptor to avoid\n\t// an unsafe type assertion on the `any`-typed result object.\n\tconst rawCursor = Object.getOwnPropertyDescriptor(result, \"nextCursor\")?.value;\n\tconst nextCursor: string | undefined = typeof rawCursor === \"string\" ? rawCursor : undefined;\n\n\tif (error) {\n\t\treturn { entries: [], error, cacheHint: {} };\n\t}\n\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst entriesWithEdit = entries.map((entry: ContentEntry<D>) => {\n\t\tconst dbId = entryDatabaseId(entry);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(entry), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...entry,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(entry)) : createNoop(),\n\t\t};\n\t});\n\n\t// Eagerly hydrate bylines and taxonomy terms for all entries in parallel.\n\t// Both are independent queries, so running them concurrently halves the\n\t// round-trip cost on remote databases (D1 replicas, etc.).\n\tawait Promise.all([\n\t\thydrateEntryBylines(type, entriesWithEdit),\n\t\thydrateEntryTerms(type, entriesWithEdit),\n\t]);\n\n\treturn { entries: entriesWithEdit, nextCursor, cacheHint: cacheHint ?? {} };\n}\n\n/**\n * Get a single entry by type and ID/slug\n *\n * Returns { entry, error, isPreview } for graceful error handling.\n * - entry is null if not found (not an error)\n * - error is set only for actual errors (db issues, etc.)\n *\n * Preview mode is detected automatically from request context (ALS).\n * When the URL has a valid `_preview` token, the middleware sets preview\n * context and this function serves draft revision data if available.\n *\n * @example\n * ```ts\n * import { getEmDashEntry } from \"emdash\";\n *\n * // Simple usage — preview just works via middleware\n * const { entry: post, isPreview, error } = await getEmDashEntry(\"posts\", \"my-slug\");\n * if (!post) return Astro.redirect(\"/404\");\n * ```\n */\nexport async function getEmDashEntry<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tid: string,\n\toptions?: { locale?: string },\n): Promise<EntryResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveEntry } = await import(\"astro:content\");\n\n\t// Check ALS for preview and edit mode context\n\tconst ctx = getRequestContext();\n\tconst preview = ctx?.preview;\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst isPreviewMode = !!preview && preview.collection === type;\n\t// Edit mode implies preview — editors should see draft content\n\tconst serveDrafts = isPreviewMode || isEditMode;\n\n\t// Resolve locale: explicit option > ALS context > undefined (no filter)\n\tconst requestedLocale = options?.locale ?? ctx?.locale;\n\n\t/** Wrap a raw Astro entry with edit proxy, tagging editable fields if needed */\n\tfunction wrapEntry(raw: ContentEntry<D>): ContentEntry<D> {\n\t\tconst dbId = entryDatabaseId(raw);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(raw), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...raw,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(raw)) : createNoop(),\n\t\t};\n\t}\n\n\t/** Check if an entry is publicly visible (published or scheduled past its time) */\n\tfunction isVisible(entry: ContentEntry<D>): boolean {\n\t\tconst data = entryData(entry);\n\t\tconst status = dataStr(data, \"status\");\n\t\tconst scheduledAt = dataStr(data, \"scheduledAt\") || undefined;\n\t\tconst isPublished = status === \"published\";\n\t\tconst isScheduledAndReady =\n\t\t\tstatus === \"scheduled\" && scheduledAt && new Date(scheduledAt) <= new Date();\n\t\treturn isPublished || !!isScheduledAndReady;\n\t}\n\n\t// Build the fallback chain: [requestedLocale, fallback1, ..., defaultLocale]\n\t// When i18n is disabled or no locale requested, just use a single-element chain\n\tconst localeChain =\n\t\trequestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];\n\n\t/** Return a successful EntryResult with bylines and taxonomy terms hydrated */\n\tasync function successResult(\n\t\twrapped: ContentEntry<D>,\n\t\topts: { isPreview: boolean; fallbackLocale?: string; cacheHint: CacheHint },\n\t): Promise<EntryResult<D>> {\n\t\tawait Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);\n\t\treturn {\n\t\t\tentry: wrapped,\n\t\t\tisPreview: opts.isPreview,\n\t\t\tfallbackLocale: opts.fallbackLocale,\n\t\t\tcacheHint: opts.cacheHint,\n\t\t};\n\t}\n\n\tif (serveDrafts) {\n\t\t// Draft mode: try each locale in the fallback chain\n\t\tfor (let i = 0; i < localeChain.length; i++) {\n\t\t\tconst locale = localeChain[i];\n\t\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\t\tconst {\n\t\t\t\tentry: baseEntry,\n\t\t\t\terror: baseError,\n\t\t\t\tcacheHint,\n\t\t\t} = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\ttype,\n\t\t\t\tid,\n\t\t\t\tlocale,\n\t\t\t});\n\n\t\t\tif (baseError) {\n\t\t\t\treturn { entry: null, error: baseError, isPreview: serveDrafts, cacheHint: {} };\n\t\t\t}\n\n\t\t\tif (!baseEntry) continue; // Try next locale in chain\n\n\t\t\t// Preview tokens are item-scoped: verify the resolved entry matches.\n\t\t\t// Edit mode (authenticated editors) has collection-wide draft access.\n\t\t\tif (isPreviewMode && !isEditMode) {\n\t\t\t\tconst dbId = entryDatabaseId(baseEntry);\n\t\t\t\tif (preview.id !== dbId && preview.id !== id) {\n\t\t\t\t\t// Token doesn't match — serve only if publicly visible, without draft access\n\t\t\t\t\tif (isVisible(baseEntry)) {\n\t\t\t\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\t\t\t\tisPreview: false,\n\t\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// Not visible — try next locale in fallback chain\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if entry has a draft revision — if so, re-fetch with revision data\n\t\t\tconst baseData = entryData(baseEntry);\n\t\t\tconst draftRevisionId = dataStr(baseData, \"draftRevisionId\") || undefined;\n\n\t\t\tif (draftRevisionId) {\n\t\t\t\tconst { entry: draftEntry, error: draftError } = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\t\ttype,\n\t\t\t\t\tid,\n\t\t\t\t\trevisionId: draftRevisionId,\n\t\t\t\t\tlocale,\n\t\t\t\t});\n\n\t\t\t\tif (!draftError && draftEntry) {\n\t\t\t\t\treturn successResult(wrapEntry(draftEntry), {\n\t\t\t\t\t\tisPreview: serveDrafts,\n\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\tisPreview: serveDrafts,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\n\t\t// No entry found in any locale\n\t\treturn { entry: null, isPreview: serveDrafts, cacheHint: {} };\n\t}\n\n\t// Normal mode: try each locale in the fallback chain, only return published content\n\tfor (let i = 0; i < localeChain.length; i++) {\n\t\tconst locale = localeChain[i];\n\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\tconst { entry, error, cacheHint } = await getLiveEntry(COLLECTION_NAME, { type, id, locale });\n\t\tif (error) {\n\t\t\treturn { entry: null, error, isPreview: false, cacheHint: {} };\n\t\t}\n\n\t\tif (entry && isVisible(entry)) {\n\t\t\treturn successResult(wrapEntry(entry), {\n\t\t\t\tisPreview: false,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\t\t// Entry not found or not visible in this locale — try next\n\t}\n\n\treturn { entry: null, isPreview: false, cacheHint: {} };\n}\n\n/**\n * Eagerly hydrate byline data onto entry.data for one or more entries.\n *\n * Attaches `bylines` (array of ContentBylineCredit) and `byline`\n * (primary BylineSummary or null) to each entry's data object.\n * Uses batch queries to avoid N+1.\n *\n * Fails silently if the byline tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getBylinesForEntries } = await import(\"./bylines/index.js\");\n\n\t\tconst refs = entries\n\t\t\t.map((e) => {\n\t\t\t\tconst data = entryData(e);\n\t\t\t\tconst id = dataStr(data, \"id\");\n\t\t\t\tif (!id) return null;\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tauthorId: dataStr(data, \"authorId\") || null,\n\t\t\t\t\tprimaryBylineId: dataStr(data, \"primaryBylineId\") || null,\n\t\t\t\t\tlocale: dataStr(data, \"locale\") || null,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter(\n\t\t\t\t(\n\t\t\t\t\tr,\n\t\t\t\t): r is {\n\t\t\t\t\tid: string;\n\t\t\t\t\tauthorId: string | null;\n\t\t\t\t\tprimaryBylineId: string | null;\n\t\t\t\t\tlocale: string | null;\n\t\t\t\t} => r !== null,\n\t\t\t);\n\t\tif (refs.length === 0) return;\n\n\t\tconst bylinesMap = await getBylinesForEntries(type, refs);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tconst credits = bylinesMap.get(dbId) ?? [];\n\t\t\tdata.bylines = credits;\n\t\t\tdata.byline = credits[0]?.byline ?? null;\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate bylines:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Eagerly hydrate taxonomy term data onto entry.data for one or more entries.\n *\n * Attaches `terms` (Record keyed by taxonomy name with an array of TaxonomyTerm\n * values) to each entry's data object. Uses a single batched JOIN query across\n * all taxonomies so the cost is O(1) regardless of the number of entries or\n * taxonomies on the site.\n *\n * This eliminates the common N+1 pattern where templates loop over list\n * results and call getEntryTerms() per entry. With hydration, the list page\n * stays at a single round-trip for term data.\n *\n * Fails silently if the taxonomy tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getAllTermsForEntries } = await import(\"./taxonomies/index.js\");\n\n\t\tconst ids = entries.map((e) => dataStr(entryData(e), \"id\")).filter(Boolean);\n\t\tif (ids.length === 0) return;\n\n\t\tconst termsMap = await getAllTermsForEntries(type, ids);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tdata.terms = termsMap.get(dbId) ?? {};\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate terms:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Translation summary for a single locale variant\n */\nexport interface TranslationSummary {\n\t/** Content item ID */\n\tid: string;\n\t/** Locale code (e.g. \"en\", \"fr\") */\n\tlocale: string;\n\t/** URL slug */\n\tslug: string | null;\n\t/** Current status */\n\tstatus: string;\n}\n\n/**\n * Result from getTranslations\n */\nexport interface TranslationsResult {\n\t/** The translation group ID (shared across locales) */\n\ttranslationGroup: string;\n\t/** All locale variants in this group */\n\ttranslations: TranslationSummary[];\n\t/** Error if the query failed */\n\terror?: Error;\n}\n\n/**\n * Get all translations of a content item.\n *\n * Given a content entry, returns all locale variants that share the same\n * translation group. This is useful for building language switcher UI.\n *\n * @example\n * ```ts\n * import { getEmDashEntry, getTranslations } from \"emdash\";\n *\n * const { entry: post } = await getEmDashEntry(\"posts\", \"hello-world\", { locale: \"en\" });\n * const { translations } = await getTranslations(\"posts\", post.data.id);\n * // translations = [{ id: \"...\", locale: \"en\", slug: \"hello-world\", status: \"published\" }, ...]\n * ```\n */\nexport async function getTranslations(type: string, id: string): Promise<TranslationsResult> {\n\ttry {\n\t\tconst db = (await import(\"./loader.js\")).getDb;\n\t\tconst dbInstance = await db();\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(dbInstance);\n\n\t\t// Find the item to get its translation group\n\t\tconst item = await repo.findByIdOrSlug(type, id);\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\ttranslationGroup: \"\",\n\t\t\t\ttranslations: [],\n\t\t\t\terror: new Error(`Content item not found: ${id}`),\n\t\t\t};\n\t\t}\n\n\t\tconst group = item.translationGroup || item.id;\n\t\tconst translations = await repo.findTranslations(type, group);\n\n\t\treturn {\n\t\t\ttranslationGroup: group,\n\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\tid: t.id,\n\t\t\t\tlocale: t.locale || \"en\",\n\t\t\t\tslug: t.slug,\n\t\t\t\tstatus: t.status,\n\t\t\t})),\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\ttranslationGroup: \"\",\n\t\t\ttranslations: [],\n\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t};\n\t}\n}\n\n/**\n * Result from resolveEmDashPath\n */\nexport interface ResolvePathResult<T = Record<string, unknown>> {\n\t/** The matched entry */\n\tentry: ContentEntry<T>;\n\t/** The collection slug that matched */\n\tcollection: string;\n\t/** Extracted parameters from the URL pattern (e.g. { slug: \"my-post\" }) */\n\tparams: Record<string, string>;\n}\n\n/** Matches `{paramName}` placeholders in URL patterns */\nconst URL_PARAM_PATTERN = /\\{(\\w+)\\}/g;\n\n/** Convert a URL pattern like \"/blog/{slug}\" to a regex and param name list */\nfunction patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {\n\tconst paramNames: string[] = [];\n\tconst regexStr = pattern.replace(URL_PARAM_PATTERN, (_match, name: string) => {\n\t\tparamNames.push(name);\n\t\treturn \"([^/]+)\";\n\t});\n\treturn { regex: new RegExp(`^${regexStr}$`), paramNames };\n}\n\n/** Cached compiled URL patterns for resolveEmDashPath */\ninterface CachedPattern {\n\tslug: string;\n\tregex: RegExp;\n\tparamNames: string[];\n}\nlet cachedUrlPatterns: CachedPattern[] | null = null;\n\n/**\n * Invalidate the cached URL patterns used by resolveEmDashPath.\n * Call when collection URL patterns change (schema updates).\n */\nexport function invalidateUrlPatternCache(): void {\n\tcachedUrlPatterns = null;\n}\n\n/**\n * Resolve a URL path to a content entry by matching against collection URL patterns.\n *\n * Loads all collections with a `urlPattern` set, converts each pattern to a regex,\n * and tests the given path. On match, extracts the slug and fetches the entry.\n *\n * @example\n * ```ts\n * import { resolveEmDashPath } from \"emdash\";\n *\n * // Given pages with urlPattern \"/{slug}\" and posts with \"/blog/{slug}\":\n * const result = await resolveEmDashPath(\"/blog/hello-world\");\n * if (result) {\n * console.log(result.collection); // \"posts\"\n * console.log(result.params.slug); // \"hello-world\"\n * console.log(result.entry.data); // post data\n * }\n * ```\n */\nexport async function resolveEmDashPath<T = Record<string, unknown>>(\n\tpath: string,\n): Promise<ResolvePathResult<T> | null> {\n\t// Build and cache compiled patterns on first call\n\tif (!cachedUrlPatterns) {\n\t\tconst { getDb } = await import(\"./loader.js\");\n\t\tconst { SchemaRegistry } = await import(\"./schema/registry.js\");\n\t\tconst db = await getDb();\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst collections = await registry.listCollections();\n\n\t\tcachedUrlPatterns = [];\n\t\tfor (const collection of collections) {\n\t\t\tif (!collection.urlPattern) continue;\n\t\t\tconst { regex, paramNames } = patternToRegex(collection.urlPattern);\n\t\t\tcachedUrlPatterns.push({ slug: collection.slug, regex, paramNames });\n\t\t}\n\t}\n\n\tfor (const pattern of cachedUrlPatterns) {\n\t\tconst match = path.match(pattern.regex);\n\t\tif (!match) continue;\n\n\t\t// Extract params\n\t\tconst params: Record<string, string> = {};\n\t\tfor (let i = 0; i < pattern.paramNames.length; i++) {\n\t\t\tparams[pattern.paramNames[i]] = match[i + 1];\n\t\t}\n\n\t\t// Look up entry by slug (most common pattern)\n\t\tconst slug = params.slug;\n\t\tif (!slug) continue;\n\n\t\tconst { entry } = await getEmDashEntry<string, T>(pattern.slug, slug);\n\t\tif (entry) {\n\t\t\treturn { entry, collection: pattern.slug, params };\n\t\t}\n\t}\n\n\treturn null;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as apiError } from "./error-tSQWIl5U.mjs";
1
+ import { t as apiError } from "./error-CPh_8eLq.mjs";
2
2
  import { sql } from "kysely";
3
3
 
4
4
  //#region src/auth/rate-limit.ts
@@ -117,4 +117,4 @@ async function cleanupExpiredRateLimits(db, maxAgeSeconds = 3600) {
117
117
 
118
118
  //#endregion
119
119
  export { getClientIp as n, rateLimitResponse as r, checkRateLimit as t };
120
- //# sourceMappingURL=rate-limit-t5CVjCO6.mjs.map
120
+ //# sourceMappingURL=rate-limit-D_-gAeJ0.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit-t5CVjCO6.mjs","names":[],"sources":["../src/auth/rate-limit.ts"],"sourcesContent":["/**\n * Database-backed rate limiter for unauthenticated endpoints.\n *\n * Uses a `_emdash_rate_limits` table with composite primary key (key, window).\n * Each call to `checkRateLimit` atomically upserts a counter and returns\n * whether the request is within the allowed limit.\n *\n * Key format: `{ip}:{endpoint}` — limits are per-IP, per-endpoint.\n * Window format: ISO timestamp truncated to the window size.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { apiError } from \"../api/error.js\";\nimport type { Database } from \"../database/types.js\";\n\n/** Loose validation for IPv4 and IPv6 addresses. */\nconst IP_PATTERN = /^[\\da-fA-F.:]+$/;\n\n/**\n * Result of a rate limit check.\n */\nexport interface RateLimitResult {\n\t/** Whether the request is allowed (within limit). */\n\tallowed: boolean;\n\t/** Current request count in this window. */\n\tcount: number;\n\t/** Maximum requests allowed in this window. */\n\tlimit: number;\n}\n\n/**\n * Check (and increment) the rate limit for a given IP + endpoint.\n *\n * If `ip` is null (no trusted IP available), rate limiting is skipped\n * and the request is allowed. There's no meaningful key to rate limit\n * on when the IP is unknown.\n *\n * Returns whether the request is allowed. The counter is always\n * incremented — even when the limit is exceeded — so that repeated\n * abuse doesn't reset the window.\n *\n * Piggybacks cleanup of expired entries with a 1% probability\n * to prevent unbounded table growth.\n */\nexport async function checkRateLimit(\n\tdb: Kysely<Database>,\n\tip: string | null,\n\tendpoint: string,\n\tmaxRequests: number,\n\twindowSeconds: number,\n): Promise<RateLimitResult> {\n\t// No trusted IP — skip rate limiting entirely\n\tif (!ip) {\n\t\treturn { allowed: true, count: 0, limit: maxRequests };\n\t}\n\n\tconst windowStart = new Date(\n\t\tMath.floor(Date.now() / (windowSeconds * 1000)) * windowSeconds * 1000,\n\t).toISOString();\n\tconst key = `${ip}:${endpoint}`;\n\n\t// Atomic upsert: insert or increment, return current count\n\tconst result = await sql<{ count: number }>`\n\t\tINSERT INTO _emdash_rate_limits (key, \"window\", count)\n\t\tVALUES (${key}, ${windowStart}, 1)\n\t\tON CONFLICT (key, \"window\")\n\t\tDO UPDATE SET count = _emdash_rate_limits.count + 1\n\t\tRETURNING count\n\t`.execute(db);\n\n\tconst count = result.rows[0]?.count ?? 1;\n\n\t// Piggyback cleanup: 1% chance per request to clean expired entries\n\tif (Math.random() < 0.01) {\n\t\tcleanupExpiredRateLimits(db).catch(() => {\n\t\t\t// Swallow errors — cleanup is best-effort\n\t\t});\n\t}\n\n\treturn {\n\t\tallowed: count <= maxRequests,\n\t\tcount,\n\t\tlimit: maxRequests,\n\t};\n}\n\n/**\n * Build a 429 Too Many Requests response with standard headers.\n */\nexport function rateLimitResponse(retryAfterSeconds: number): Response {\n\tconst response = apiError(\"RATE_LIMITED\", \"Too many requests. Please try again later.\", 429);\n\tresponse.headers.set(\"Retry-After\", String(retryAfterSeconds));\n\treturn response;\n}\n\n/**\n * Extract client IP from a Request.\n *\n * Resolution order:\n * 1. `CF-Connecting-IP` — trusted only when the Cloudflare `cf` object is\n * present. CF edge overwrites any client-supplied value, so this is the\n * cryptographically trustworthy path on Workers. Operator-declared\n * trusted headers cannot override it.\n * 2. `X-Forwarded-For` (first entry) — trusted only when the `cf` object\n * is present (CF sets this reliably).\n * 3. Operator-declared trusted proxy headers (ordered list) — used as a\n * fallback for non-CF deployments behind a reverse proxy the operator\n * controls. Also applies as a fill-in on CF when the CF headers are\n * absent (e.g. internal cron handlers).\n * 4. `null` — no trusted IP available. Callers must handle this gracefully\n * (e.g. skip rate limiting).\n *\n * Pass `trustedHeaders` from `getTrustedProxyHeaders(emdash.config)` so\n * self-hosted non-CF deployments can opt into reading a specific header.\n *\n * Aligned with `extractRequestMeta` in `plugins/request-meta.ts`.\n */\nexport function getClientIp(request: Request, trustedHeaders: string[] = []): string | null {\n\tconst headers = request.headers;\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CF Workers runtime shape\n\tconst cf = (request as unknown as { cf?: Record<string, unknown> }).cf;\n\n\t// On Cloudflare, prefer the cryptographically trustworthy headers. An\n\t// attacker can't spoof these — the CF edge strips/overwrites them.\n\tif (cf) {\n\t\tconst cfIp = headers.get(\"cf-connecting-ip\")?.trim();\n\t\tif (cfIp && IP_PATTERN.test(cfIp)) {\n\t\t\treturn cfIp;\n\t\t}\n\n\t\tconst xff = headers.get(\"x-forwarded-for\");\n\t\tif (xff) {\n\t\t\tconst first = xff.split(\",\")[0]?.trim();\n\t\t\tif (first && IP_PATTERN.test(first)) {\n\t\t\t\treturn first;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fall through to operator-declared trusted headers. On CF this fills\n\t// in when the CF headers are absent; off-CF it's the primary source.\n\tfor (const name of trustedHeaders) {\n\t\tconst value = readIpFromHeader(headers, name);\n\t\tif (value) return value;\n\t}\n\n\treturn null;\n}\n\n/**\n * Read an IP from an operator-declared trusted header. XFF-style headers\n * are parsed as comma-separated lists and the first entry is used.\n */\nfunction readIpFromHeader(headers: Headers, name: string): string | null {\n\tconst value = headers.get(name);\n\tif (!value) return null;\n\tif (name.toLowerCase().endsWith(\"forwarded-for\")) {\n\t\tconst first = value.split(\",\")[0]?.trim();\n\t\tif (!first) return null;\n\t\treturn IP_PATTERN.test(first) ? first : null;\n\t}\n\tconst trimmed = value.trim();\n\tif (!trimmed) return null;\n\treturn IP_PATTERN.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Delete expired rate limit entries.\n *\n * Entries with a window timestamp older than `maxAgeSeconds` are removed.\n * Safe to call periodically (e.g., from cron cleanup or on-request piggyback).\n */\nexport async function cleanupExpiredRateLimits(\n\tdb: Kysely<Database>,\n\tmaxAgeSeconds = 3600,\n): Promise<number> {\n\tconst cutoff = new Date(Date.now() - maxAgeSeconds * 1000).toISOString();\n\n\tconst result = await sql`\n\t\tDELETE FROM _emdash_rate_limits WHERE \"window\" < ${cutoff}\n\t`.execute(db);\n\n\treturn Number(result.numAffectedRows ?? 0);\n}\n"],"mappings":";;;;;AAkBA,MAAM,aAAa;;;;;;;;;;;;;;;AA4BnB,eAAsB,eACrB,IACA,IACA,UACA,aACA,eAC2B;AAE3B,KAAI,CAAC,GACJ,QAAO;EAAE,SAAS;EAAM,OAAO;EAAG,OAAO;EAAa;CAGvD,MAAM,+BAAc,IAAI,KACvB,KAAK,MAAM,KAAK,KAAK,IAAI,gBAAgB,KAAM,GAAG,gBAAgB,IAClE,EAAC,aAAa;CAYf,MAAM,SARS,MAAM,GAAsB;;YAH/B,GAAG,GAAG,GAAG,WAKN,IAAI,YAAY;;;;GAI7B,QAAQ,GAAG,EAEQ,KAAK,IAAI,SAAS;AAGvC,KAAI,KAAK,QAAQ,GAAG,IACnB,0BAAyB,GAAG,CAAC,YAAY,GAEvC;AAGH,QAAO;EACN,SAAS,SAAS;EAClB;EACA,OAAO;EACP;;;;;AAMF,SAAgB,kBAAkB,mBAAqC;CACtE,MAAM,WAAW,SAAS,gBAAgB,8CAA8C,IAAI;AAC5F,UAAS,QAAQ,IAAI,eAAe,OAAO,kBAAkB,CAAC;AAC9D,QAAO;;;;;;;;;;;;;;;;;;;;;;;;AAyBR,SAAgB,YAAY,SAAkB,iBAA2B,EAAE,EAAiB;CAC3F,MAAM,UAAU,QAAQ;AAMxB,KAJY,QAAwD,IAI5D;EACP,MAAM,OAAO,QAAQ,IAAI,mBAAmB,EAAE,MAAM;AACpD,MAAI,QAAQ,WAAW,KAAK,KAAK,CAChC,QAAO;EAGR,MAAM,MAAM,QAAQ,IAAI,kBAAkB;AAC1C,MAAI,KAAK;GACR,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM;AACvC,OAAI,SAAS,WAAW,KAAK,MAAM,CAClC,QAAO;;;AAOV,MAAK,MAAM,QAAQ,gBAAgB;EAClC,MAAM,QAAQ,iBAAiB,SAAS,KAAK;AAC7C,MAAI,MAAO,QAAO;;AAGnB,QAAO;;;;;;AAOR,SAAS,iBAAiB,SAAkB,MAA6B;CACxE,MAAM,QAAQ,QAAQ,IAAI,KAAK;AAC/B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI,KAAK,aAAa,CAAC,SAAS,gBAAgB,EAAE;EACjD,MAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,IAAI,MAAM;AACzC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,WAAW,KAAK,MAAM,GAAG,QAAQ;;CAEzC,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO,WAAW,KAAK,QAAQ,GAAG,UAAU;;;;;;;;AAS7C,eAAsB,yBACrB,IACA,gBAAgB,MACE;CAGlB,MAAM,SAAS,MAAM,GAAG;sEAFT,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,IAAK,EAAC,aAAa,CAGb;GACzD,QAAQ,GAAG;AAEb,QAAO,OAAO,OAAO,mBAAmB,EAAE"}
1
+ {"version":3,"file":"rate-limit-D_-gAeJ0.mjs","names":[],"sources":["../src/auth/rate-limit.ts"],"sourcesContent":["/**\n * Database-backed rate limiter for unauthenticated endpoints.\n *\n * Uses a `_emdash_rate_limits` table with composite primary key (key, window).\n * Each call to `checkRateLimit` atomically upserts a counter and returns\n * whether the request is within the allowed limit.\n *\n * Key format: `{ip}:{endpoint}` — limits are per-IP, per-endpoint.\n * Window format: ISO timestamp truncated to the window size.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { apiError } from \"../api/error.js\";\nimport type { Database } from \"../database/types.js\";\n\n/** Loose validation for IPv4 and IPv6 addresses. */\nconst IP_PATTERN = /^[\\da-fA-F.:]+$/;\n\n/**\n * Result of a rate limit check.\n */\nexport interface RateLimitResult {\n\t/** Whether the request is allowed (within limit). */\n\tallowed: boolean;\n\t/** Current request count in this window. */\n\tcount: number;\n\t/** Maximum requests allowed in this window. */\n\tlimit: number;\n}\n\n/**\n * Check (and increment) the rate limit for a given IP + endpoint.\n *\n * If `ip` is null (no trusted IP available), rate limiting is skipped\n * and the request is allowed. There's no meaningful key to rate limit\n * on when the IP is unknown.\n *\n * Returns whether the request is allowed. The counter is always\n * incremented — even when the limit is exceeded — so that repeated\n * abuse doesn't reset the window.\n *\n * Piggybacks cleanup of expired entries with a 1% probability\n * to prevent unbounded table growth.\n */\nexport async function checkRateLimit(\n\tdb: Kysely<Database>,\n\tip: string | null,\n\tendpoint: string,\n\tmaxRequests: number,\n\twindowSeconds: number,\n): Promise<RateLimitResult> {\n\t// No trusted IP — skip rate limiting entirely\n\tif (!ip) {\n\t\treturn { allowed: true, count: 0, limit: maxRequests };\n\t}\n\n\tconst windowStart = new Date(\n\t\tMath.floor(Date.now() / (windowSeconds * 1000)) * windowSeconds * 1000,\n\t).toISOString();\n\tconst key = `${ip}:${endpoint}`;\n\n\t// Atomic upsert: insert or increment, return current count\n\tconst result = await sql<{ count: number }>`\n\t\tINSERT INTO _emdash_rate_limits (key, \"window\", count)\n\t\tVALUES (${key}, ${windowStart}, 1)\n\t\tON CONFLICT (key, \"window\")\n\t\tDO UPDATE SET count = _emdash_rate_limits.count + 1\n\t\tRETURNING count\n\t`.execute(db);\n\n\tconst count = result.rows[0]?.count ?? 1;\n\n\t// Piggyback cleanup: 1% chance per request to clean expired entries\n\tif (Math.random() < 0.01) {\n\t\tcleanupExpiredRateLimits(db).catch(() => {\n\t\t\t// Swallow errors — cleanup is best-effort\n\t\t});\n\t}\n\n\treturn {\n\t\tallowed: count <= maxRequests,\n\t\tcount,\n\t\tlimit: maxRequests,\n\t};\n}\n\n/**\n * Build a 429 Too Many Requests response with standard headers.\n */\nexport function rateLimitResponse(retryAfterSeconds: number): Response {\n\tconst response = apiError(\"RATE_LIMITED\", \"Too many requests. Please try again later.\", 429);\n\tresponse.headers.set(\"Retry-After\", String(retryAfterSeconds));\n\treturn response;\n}\n\n/**\n * Extract client IP from a Request.\n *\n * Resolution order:\n * 1. `CF-Connecting-IP` — trusted only when the Cloudflare `cf` object is\n * present. CF edge overwrites any client-supplied value, so this is the\n * cryptographically trustworthy path on Workers. Operator-declared\n * trusted headers cannot override it.\n * 2. `X-Forwarded-For` (first entry) — trusted only when the `cf` object\n * is present (CF sets this reliably).\n * 3. Operator-declared trusted proxy headers (ordered list) — used as a\n * fallback for non-CF deployments behind a reverse proxy the operator\n * controls. Also applies as a fill-in on CF when the CF headers are\n * absent (e.g. internal cron handlers).\n * 4. `null` — no trusted IP available. Callers must handle this gracefully\n * (e.g. skip rate limiting).\n *\n * Pass `trustedHeaders` from `getTrustedProxyHeaders(emdash.config)` so\n * self-hosted non-CF deployments can opt into reading a specific header.\n *\n * Aligned with `extractRequestMeta` in `plugins/request-meta.ts`.\n */\nexport function getClientIp(request: Request, trustedHeaders: string[] = []): string | null {\n\tconst headers = request.headers;\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- CF Workers runtime shape\n\tconst cf = (request as unknown as { cf?: Record<string, unknown> }).cf;\n\n\t// On Cloudflare, prefer the cryptographically trustworthy headers. An\n\t// attacker can't spoof these — the CF edge strips/overwrites them.\n\tif (cf) {\n\t\tconst cfIp = headers.get(\"cf-connecting-ip\")?.trim();\n\t\tif (cfIp && IP_PATTERN.test(cfIp)) {\n\t\t\treturn cfIp;\n\t\t}\n\n\t\tconst xff = headers.get(\"x-forwarded-for\");\n\t\tif (xff) {\n\t\t\tconst first = xff.split(\",\")[0]?.trim();\n\t\t\tif (first && IP_PATTERN.test(first)) {\n\t\t\t\treturn first;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fall through to operator-declared trusted headers. On CF this fills\n\t// in when the CF headers are absent; off-CF it's the primary source.\n\tfor (const name of trustedHeaders) {\n\t\tconst value = readIpFromHeader(headers, name);\n\t\tif (value) return value;\n\t}\n\n\treturn null;\n}\n\n/**\n * Read an IP from an operator-declared trusted header. XFF-style headers\n * are parsed as comma-separated lists and the first entry is used.\n */\nfunction readIpFromHeader(headers: Headers, name: string): string | null {\n\tconst value = headers.get(name);\n\tif (!value) return null;\n\tif (name.toLowerCase().endsWith(\"forwarded-for\")) {\n\t\tconst first = value.split(\",\")[0]?.trim();\n\t\tif (!first) return null;\n\t\treturn IP_PATTERN.test(first) ? first : null;\n\t}\n\tconst trimmed = value.trim();\n\tif (!trimmed) return null;\n\treturn IP_PATTERN.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Delete expired rate limit entries.\n *\n * Entries with a window timestamp older than `maxAgeSeconds` are removed.\n * Safe to call periodically (e.g., from cron cleanup or on-request piggyback).\n */\nexport async function cleanupExpiredRateLimits(\n\tdb: Kysely<Database>,\n\tmaxAgeSeconds = 3600,\n): Promise<number> {\n\tconst cutoff = new Date(Date.now() - maxAgeSeconds * 1000).toISOString();\n\n\tconst result = await sql`\n\t\tDELETE FROM _emdash_rate_limits WHERE \"window\" < ${cutoff}\n\t`.execute(db);\n\n\treturn Number(result.numAffectedRows ?? 0);\n}\n"],"mappings":";;;;;AAkBA,MAAM,aAAa;;;;;;;;;;;;;;;AA4BnB,eAAsB,eACrB,IACA,IACA,UACA,aACA,eAC2B;AAE3B,KAAI,CAAC,GACJ,QAAO;EAAE,SAAS;EAAM,OAAO;EAAG,OAAO;EAAa;CAGvD,MAAM,+BAAc,IAAI,KACvB,KAAK,MAAM,KAAK,KAAK,IAAI,gBAAgB,KAAM,GAAG,gBAAgB,IAClE,EAAC,aAAa;CAYf,MAAM,SARS,MAAM,GAAsB;;YAH/B,GAAG,GAAG,GAAG,WAKN,IAAI,YAAY;;;;GAI7B,QAAQ,GAAG,EAEQ,KAAK,IAAI,SAAS;AAGvC,KAAI,KAAK,QAAQ,GAAG,IACnB,0BAAyB,GAAG,CAAC,YAAY,GAEvC;AAGH,QAAO;EACN,SAAS,SAAS;EAClB;EACA,OAAO;EACP;;;;;AAMF,SAAgB,kBAAkB,mBAAqC;CACtE,MAAM,WAAW,SAAS,gBAAgB,8CAA8C,IAAI;AAC5F,UAAS,QAAQ,IAAI,eAAe,OAAO,kBAAkB,CAAC;AAC9D,QAAO;;;;;;;;;;;;;;;;;;;;;;;;AAyBR,SAAgB,YAAY,SAAkB,iBAA2B,EAAE,EAAiB;CAC3F,MAAM,UAAU,QAAQ;AAMxB,KAJY,QAAwD,IAI5D;EACP,MAAM,OAAO,QAAQ,IAAI,mBAAmB,EAAE,MAAM;AACpD,MAAI,QAAQ,WAAW,KAAK,KAAK,CAChC,QAAO;EAGR,MAAM,MAAM,QAAQ,IAAI,kBAAkB;AAC1C,MAAI,KAAK;GACR,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM;AACvC,OAAI,SAAS,WAAW,KAAK,MAAM,CAClC,QAAO;;;AAOV,MAAK,MAAM,QAAQ,gBAAgB;EAClC,MAAM,QAAQ,iBAAiB,SAAS,KAAK;AAC7C,MAAI,MAAO,QAAO;;AAGnB,QAAO;;;;;;AAOR,SAAS,iBAAiB,SAAkB,MAA6B;CACxE,MAAM,QAAQ,QAAQ,IAAI,KAAK;AAC/B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI,KAAK,aAAa,CAAC,SAAS,gBAAgB,EAAE;EACjD,MAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,IAAI,MAAM;AACzC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,WAAW,KAAK,MAAM,GAAG,QAAQ;;CAEzC,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO,WAAW,KAAK,QAAQ,GAAG,UAAU;;;;;;;;AAS7C,eAAsB,yBACrB,IACA,gBAAgB,MACE;CAGlB,MAAM,SAAS,MAAM,GAAG;sEAFT,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,IAAK,EAAC,aAAa,CAGb;GACzD,QAAQ,GAAG;AAEb,QAAO,OAAO,OAAO,mBAAmB,EAAE"}
@@ -14,4 +14,4 @@ function isSafeRedirect(url) {
14
14
 
15
15
  //#endregion
16
16
  export { isSafeRedirect as t };
17
- //# sourceMappingURL=redirect-DGRsLO2I.mjs.map
17
+ //# sourceMappingURL=redirect-BINiRYq4.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-DGRsLO2I.mjs","names":[],"sources":["../src/api/redirect.ts"],"sourcesContent":["/**\n * Validate that a redirect URL is a safe local path.\n *\n * Rejects:\n * - Protocol-relative URLs (`//evil.com`)\n * - Backslash bypass (`/\\evil.com` — browsers normalize `\\` to `/` in Location headers)\n * - Absolute URLs (`https://evil.com`)\n * - Empty / nullish values\n */\nexport function isSafeRedirect(url: string | null | undefined): url is string {\n\treturn (\n\t\ttypeof url === \"string\" && url.startsWith(\"/\") && !url.startsWith(\"//\") && !url.includes(\"\\\\\")\n\t);\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,eAAe,KAA+C;AAC7E,QACC,OAAO,QAAQ,YAAY,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,IAAI,CAAC,IAAI,SAAS,KAAK"}
1
+ {"version":3,"file":"redirect-BINiRYq4.mjs","names":[],"sources":["../src/api/redirect.ts"],"sourcesContent":["/**\n * Validate that a redirect URL is a safe local path.\n *\n * Rejects:\n * - Protocol-relative URLs (`//evil.com`)\n * - Backslash bypass (`/\\evil.com` — browsers normalize `\\` to `/` in Location headers)\n * - Absolute URLs (`https://evil.com`)\n * - Empty / nullish values\n */\nexport function isSafeRedirect(url: string | null | undefined): url is string {\n\treturn (\n\t\ttypeof url === \"string\" && url.startsWith(\"/\") && !url.startsWith(\"//\") && !url.includes(\"\\\\\")\n\t);\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,eAAe,KAA+C;AAC7E,QACC,OAAO,QAAQ,YAAY,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,IAAI,CAAC,IAAI,SAAS,KAAK"}
@@ -1,5 +1,5 @@
1
1
  import { i as currentTimestampValue } from "./dialect-helpers-BKCvISIQ.mjs";
2
- import { i as encodeCursor, r as decodeCursor } from "./types-CwXMEPRr.mjs";
2
+ import { i as encodeCursor, r as decodeCursor } from "./types-ByV5sgsv.mjs";
3
3
  import { i as matchPattern, n as interpolateDestination, r as isPattern, t as compilePattern } from "./patterns-CqG5Ya3i.mjs";
4
4
  import { sql } from "kysely";
5
5
  import { ulid } from "ulidx";
@@ -304,4 +304,4 @@ var RedirectRepository = class {
304
304
 
305
305
  //#endregion
306
306
  export { RedirectRepository as t };
307
- //# sourceMappingURL=redirect-DkaDxq8e.mjs.map
307
+ //# sourceMappingURL=redirect-CNv4mHX2.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-DkaDxq8e.mjs","names":[],"sources":["../src/database/repositories/redirect.ts"],"sourcesContent":["import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tcompilePattern,\n\tmatchPattern,\n\tinterpolateDestination,\n\tisPattern,\n} from \"../../redirects/patterns.js\";\nimport { currentTimestampValue } from \"../dialect-helpers.js\";\nimport type { Database, RedirectTable } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Bounded 404 logging\n// ---------------------------------------------------------------------------\n\n/**\n * Hard cap on rows stored in `_emdash_404_log`. When exceeded, the oldest\n * rows (by `last_seen_at`) are evicted on insert. Prevents an unauthenticated\n * attacker from growing the table without bound by requesting unique URLs.\n */\nexport const MAX_404_LOG_ROWS = 10_000;\n\n/** Max stored length for the `Referer` header — truncated on insert. */\nexport const REFERRER_MAX_LENGTH = 512;\n\n/** Max stored length for the `User-Agent` header — truncated on insert. */\nexport const USER_AGENT_MAX_LENGTH = 256;\n\n/** Pattern to escape LIKE wildcards: %, _, and backslash */\nconst LIKE_ESCAPE_RE = /[\\\\%_]/g;\n\n/**\n * Truncate a header-derived string to `max` chars, preserving `null`/`undefined`\n * as `null`. Empty strings stay empty (the caller decides whether to coerce).\n */\nfunction truncateOrNull(value: string | null | undefined, max: number): string | null {\n\tif (value === null || value === undefined) return null;\n\treturn value.length > max ? value.slice(0, max) : value;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface Redirect {\n\tid: string;\n\tsource: string;\n\tdestination: string;\n\ttype: number;\n\tisPattern: boolean;\n\tenabled: boolean;\n\thits: number;\n\tlastHitAt: string | null;\n\tgroupName: string | null;\n\tauto: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface CreateRedirectInput {\n\tsource: string;\n\tdestination: string;\n\ttype?: number;\n\tisPattern?: boolean;\n\tenabled?: boolean;\n\tgroupName?: string | null;\n\tauto?: boolean;\n}\n\nexport interface UpdateRedirectInput {\n\tsource?: string;\n\tdestination?: string;\n\ttype?: number;\n\tisPattern?: boolean;\n\tenabled?: boolean;\n\tgroupName?: string | null;\n}\n\nexport interface NotFoundEntry {\n\tid: string;\n\tpath: string;\n\treferrer: string | null;\n\tuserAgent: string | null;\n\tip: string | null;\n\tcreatedAt: string;\n}\n\nexport interface NotFoundSummary {\n\tpath: string;\n\tcount: number;\n\tlastSeen: string;\n\ttopReferrer: string | null;\n}\n\nexport interface RedirectMatch {\n\tredirect: Redirect;\n\tresolvedDestination: string;\n}\n\n// ---------------------------------------------------------------------------\n// Row mapping\n// ---------------------------------------------------------------------------\n\nfunction rowToRedirect(row: RedirectTable): Redirect {\n\treturn {\n\t\tid: row.id,\n\t\tsource: row.source,\n\t\tdestination: row.destination,\n\t\ttype: row.type,\n\t\tisPattern: row.is_pattern === 1,\n\t\tenabled: row.enabled === 1,\n\t\thits: row.hits,\n\t\tlastHitAt: row.last_hit_at,\n\t\tgroupName: row.group_name,\n\t\tauto: row.auto === 1,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class RedirectRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t// --- CRUD ---------------------------------------------------------------\n\n\tasync findById(id: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findBySource(source: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", source)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findMany(opts: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tsearch?: string;\n\t\tgroup?: string;\n\t\tenabled?: boolean;\n\t\tauto?: boolean;\n\t}): Promise<FindManyResult<Redirect>> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (opts.search) {\n\t\t\t// Escape LIKE wildcards in the search term to prevent injection.\n\t\t\t// Must include ESCAPE clause for SQLite to recognize backslash as escape char.\n\t\t\tconst escaped = opts.search.replace(LIKE_ESCAPE_RE, (c) => `\\\\${c}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`source LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`destination LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tif (opts.group !== undefined) {\n\t\t\tquery = query.where(\"group_name\", \"=\", opts.group);\n\t\t}\n\n\t\tif (opts.enabled !== undefined) {\n\t\t\tquery = query.where(\"enabled\", \"=\", opts.enabled ? 1 : 0);\n\t\t}\n\n\t\tif (opts.auto !== undefined) {\n\t\t\tquery = query.where(\"auto\", \"=\", opts.auto ? 1 : 0);\n\t\t}\n\n\t\tif (opts.cursor) {\n\t\t\tconst decoded = decodeCursor(opts.cursor);\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items = rows.slice(0, limit).map(rowToRedirect);\n\t\tconst result: FindManyResult<Redirect> = { items };\n\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync create(input: CreateRedirectInput): Promise<Redirect> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\t\tconst patternFlag = input.isPattern ?? isPattern(input.source);\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_redirects\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tsource: input.source,\n\t\t\t\tdestination: input.destination,\n\t\t\t\ttype: input.type ?? 301,\n\t\t\t\tis_pattern: patternFlag ? 1 : 0,\n\t\t\t\tenabled: input.enabled !== false ? 1 : 0,\n\t\t\t\thits: 0,\n\t\t\t\tlast_hit_at: null,\n\t\t\t\tgroup_name: input.groupName ?? null,\n\t\t\t\tauto: input.auto ? 1 : 0,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\tasync update(id: string, input: UpdateRedirectInput): Promise<Redirect | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return null;\n\n\t\tconst now = new Date().toISOString();\n\t\tconst values: Record<string, unknown> = { updated_at: now };\n\n\t\tif (input.source !== undefined) {\n\t\t\tvalues.source = input.source;\n\t\t\tvalues.is_pattern =\n\t\t\t\tinput.isPattern !== undefined ? (input.isPattern ? 1 : 0) : isPattern(input.source) ? 1 : 0;\n\t\t} else if (input.isPattern !== undefined) {\n\t\t\tvalues.is_pattern = input.isPattern ? 1 : 0;\n\t\t}\n\n\t\tif (input.destination !== undefined) values.destination = input.destination;\n\t\tif (input.type !== undefined) values.type = input.type;\n\t\tif (input.enabled !== undefined) values.enabled = input.enabled ? 1 : 0;\n\t\tif (input.groupName !== undefined) values.group_name = input.groupName;\n\n\t\tawait this.db.updateTable(\"_emdash_redirects\").set(values).where(\"id\", \"=\", id).execute();\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_redirects\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn BigInt(result.numDeletedRows) > 0n;\n\t}\n\n\t/**\n\t * Fetch all enabled redirects (for loop detection graph building).\n\t * Not paginated — returns the full set.\n\t */\n\tasync findAllEnabled(): Promise<Redirect[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.execute();\n\t\treturn rows.map(rowToRedirect);\n\t}\n\n\t// --- Matching -----------------------------------------------------------\n\n\tasync findExactMatch(path: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", path)\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.where(\"is_pattern\", \"=\", 0)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findEnabledPatternRules(): Promise<Redirect[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.where(\"is_pattern\", \"=\", 1)\n\t\t\t.execute();\n\t\treturn rows.map(rowToRedirect);\n\t}\n\n\t/**\n\t * Match a request path against all enabled redirect rules.\n\t * Checks exact matches first (indexed), then pattern rules.\n\t * Returns the matched redirect and the resolved destination URL.\n\t */\n\tasync matchPath(path: string): Promise<RedirectMatch | null> {\n\t\t// 1. Exact match (fast, indexed)\n\t\tconst exact = await this.findExactMatch(path);\n\t\tif (exact) {\n\t\t\treturn { redirect: exact, resolvedDestination: exact.destination };\n\t\t}\n\n\t\t// 2. Pattern match\n\t\tconst patterns = await this.findEnabledPatternRules();\n\t\tfor (const redirect of patterns) {\n\t\t\tconst compiled = compilePattern(redirect.source);\n\t\t\tconst params = matchPattern(compiled, path);\n\t\t\tif (params) {\n\t\t\t\tconst resolved = interpolateDestination(redirect.destination, params);\n\t\t\t\treturn { redirect, resolvedDestination: resolved };\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// --- Hit tracking -------------------------------------------------------\n\n\tasync recordHit(id: string): Promise<void> {\n\t\tawait sql`\n\t\t\tUPDATE _emdash_redirects\n\t\t\tSET hits = hits + 1, last_hit_at = ${currentTimestampValue(this.db)}, updated_at = ${currentTimestampValue(this.db)}\n\t\t\tWHERE id = ${id}\n\t\t`.execute(this.db);\n\t}\n\n\t// --- Auto-redirects (slug change) ---------------------------------------\n\n\t/**\n\t * Create an auto-redirect when a content slug changes.\n\t * Uses the collection's URL pattern to compute old/new URLs.\n\t * Collapses existing redirect chains pointing to the old URL.\n\t */\n\tasync createAutoRedirect(\n\t\tcollection: string,\n\t\toldSlug: string,\n\t\tnewSlug: string,\n\t\tcontentId: string,\n\t\turlPattern: string | null,\n\t): Promise<Redirect> {\n\t\tconst oldUrl = urlPattern\n\t\t\t? urlPattern.replace(\"{slug}\", oldSlug).replace(\"{id}\", contentId)\n\t\t\t: `/${collection}/${oldSlug}`;\n\t\tconst newUrl = urlPattern\n\t\t\t? urlPattern.replace(\"{slug}\", newSlug).replace(\"{id}\", contentId)\n\t\t\t: `/${collection}/${newSlug}`;\n\n\t\t// Collapse chains: update any existing redirects pointing to the old URL\n\t\tawait this.collapseChains(oldUrl, newUrl);\n\n\t\t// Check if a redirect from this source already exists\n\t\tconst existing = await this.findBySource(oldUrl);\n\t\tif (existing) {\n\t\t\t// Update the existing redirect to point to the new URL\n\t\t\treturn (await this.update(existing.id, { destination: newUrl }))!;\n\t\t}\n\n\t\treturn this.create({\n\t\t\tsource: oldUrl,\n\t\t\tdestination: newUrl,\n\t\t\ttype: 301,\n\t\t\tisPattern: false,\n\t\t\tauto: true,\n\t\t\tgroupName: \"Auto: slug change\",\n\t\t});\n\t}\n\n\t/**\n\t * Update all redirects whose destination matches oldDestination\n\t * to point to newDestination instead. Prevents redirect chains.\n\t * Returns the number of updated rows.\n\t */\n\tasync collapseChains(oldDestination: string, newDestination: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_redirects\")\n\t\t\t.set({\n\t\t\t\tdestination: newDestination,\n\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t})\n\t\t\t.where(\"destination\", \"=\", oldDestination)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numUpdatedRows);\n\t}\n\n\t// --- 404 log ------------------------------------------------------------\n\n\t/**\n\t * Record a 404 hit for `entry.path`.\n\t *\n\t * Dedups by path: repeat hits increment `hits` and refresh `last_seen_at`\n\t * on the existing row instead of inserting a new one. Referrer and\n\t * user-agent are truncated to bounded lengths so a malicious client can't\n\t * blow up storage with huge headers. When the table would exceed\n\t * MAX_404_LOG_ROWS, the oldest entries (by `last_seen_at`) are evicted.\n\t *\n\t * This is called from the public redirect middleware on every 404 and\n\t * must never throw for an unauthenticated caller — failures bubble up to\n\t * the middleware, which swallows them.\n\t */\n\tasync log404(entry: {\n\t\tpath: string;\n\t\treferrer?: string | null;\n\t\tuserAgent?: string | null;\n\t\tip?: string | null;\n\t}): Promise<void> {\n\t\tconst now = new Date().toISOString();\n\t\tconst referrer = truncateOrNull(entry.referrer, REFERRER_MAX_LENGTH);\n\t\tconst userAgent = truncateOrNull(entry.userAgent, USER_AGENT_MAX_LENGTH);\n\t\tconst ip = entry.ip ?? null;\n\n\t\t// Atomic upsert by path. The UNIQUE index on `path` makes this safe\n\t\t// under concurrency: two requests for the same new path can't both\n\t\t// insert — the second one hits the conflict branch and increments\n\t\t// hits instead of failing with a uniqueness error.\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_404_log\")\n\t\t\t.values({\n\t\t\t\tid: ulid(),\n\t\t\t\tpath: entry.path,\n\t\t\t\treferrer,\n\t\t\t\tuser_agent: userAgent,\n\t\t\t\tip,\n\t\t\t\thits: 1,\n\t\t\t\tlast_seen_at: now,\n\t\t\t\tcreated_at: now,\n\t\t\t})\n\t\t\t.onConflict((oc) =>\n\t\t\t\toc.column(\"path\").doUpdateSet({\n\t\t\t\t\thits: sql`hits + 1`,\n\t\t\t\t\tlast_seen_at: now,\n\t\t\t\t\treferrer,\n\t\t\t\t\tuser_agent: userAgent,\n\t\t\t\t\tip,\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.execute();\n\n\t\t// Enforce the row cap. Cheap when the table is under cap (single\n\t\t// COUNT(*) query); evicts oldest rows if we're over. Updates (dedup\n\t\t// hits) don't grow the table so this is a no-op for repeat paths.\n\t\tawait this.enforce404Cap();\n\t}\n\n\t/**\n\t * Delete the oldest rows from `_emdash_404_log` if the row count exceeds\n\t * MAX_404_LOG_ROWS. \"Oldest\" is by `last_seen_at`, so a path that keeps\n\t * getting hit stays in the table even if it was first seen long ago.\n\t *\n\t * Private — callers use `log404`, which invokes this after every upsert.\n\t */\n\tprivate async enforce404Cap(): Promise<void> {\n\t\tconst countRow = await this.db\n\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"c\"))\n\t\t\t.executeTakeFirst();\n\t\tconst count = Number(countRow?.c ?? 0);\n\t\tif (count <= MAX_404_LOG_ROWS) return;\n\n\t\tconst excess = count - MAX_404_LOG_ROWS;\n\n\t\t// Evict the oldest rows in a single SQL statement. Using a subquery\n\t\t// (rather than materialising the victim IDs in JS and passing them\n\t\t// back as bind parameters) keeps the statement bounded regardless of\n\t\t// how far over cap the table is — important for existing installs\n\t\t// that crossed the threshold before this cap was introduced.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\n\t\t\t\t\"id\",\n\t\t\t\t\"in\",\n\t\t\t\tthis.db\n\t\t\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t\t\t.select(\"id\")\n\t\t\t\t\t.orderBy(\"last_seen_at\", \"asc\")\n\t\t\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t\t\t.limit(excess),\n\t\t\t)\n\t\t\t.execute();\n\t}\n\n\tasync find404s(opts: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tsearch?: string;\n\t}): Promise<FindManyResult<NotFoundEntry>> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (opts.search) {\n\t\t\tconst escaped = opts.search.replace(LIKE_ESCAPE_RE, (c) => `\\\\${c}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where(sql<boolean>`path LIKE ${term} ESCAPE '\\\\'`);\n\t\t}\n\n\t\tif (opts.cursor) {\n\t\t\tconst decoded = decodeCursor(opts.cursor);\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items: NotFoundEntry[] = rows.slice(0, limit).map((row) => ({\n\t\t\tid: row.id,\n\t\t\tpath: row.path,\n\t\t\treferrer: row.referrer,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tip: row.ip,\n\t\t\tcreatedAt: row.created_at,\n\t\t}));\n\n\t\tconst result: FindManyResult<NotFoundEntry> = { items };\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync get404Summary(limit = 50): Promise<NotFoundSummary[]> {\n\t\t// Since rows are now deduped by path, each path has exactly one row\n\t\t// with `hits` as the running count and `last_seen_at` as the latest\n\t\t// timestamp. The subquery for `top_referrer` collapses to a simple\n\t\t// pick of the row's stored referrer (the most recent one seen).\n\t\tconst rows = await sql<{\n\t\t\tpath: string;\n\t\t\tcount: number;\n\t\t\tlast_seen: string;\n\t\t\ttop_referrer: string | null;\n\t\t}>`\n\t\t\tSELECT\n\t\t\t\tpath,\n\t\t\t\tSUM(hits) as count,\n\t\t\t\tMAX(last_seen_at) as last_seen,\n\t\t\t\t(\n\t\t\t\t\tSELECT referrer FROM _emdash_404_log AS inner_log\n\t\t\t\t\tWHERE inner_log.path = _emdash_404_log.path\n\t\t\t\t\t\tAND referrer IS NOT NULL AND referrer != ''\n\t\t\t\t\tLIMIT 1\n\t\t\t\t) as top_referrer\n\t\t\tFROM _emdash_404_log\n\t\t\tGROUP BY path\n\t\t\tORDER BY count DESC\n\t\t\tLIMIT ${limit}\n\t\t`.execute(this.db);\n\n\t\treturn rows.rows.map((row) => ({\n\t\t\tpath: row.path,\n\t\t\tcount: Number(row.count),\n\t\t\tlastSeen: row.last_seen,\n\t\t\ttopReferrer: row.top_referrer,\n\t\t}));\n\t}\n\n\tasync delete404(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn BigInt(result.numDeletedRows) > 0n;\n\t}\n\n\tasync clear404s(): Promise<number> {\n\t\tconst result = await this.db.deleteFrom(\"_emdash_404_log\").executeTakeFirst();\n\t\treturn Number(result.numDeletedRows);\n\t}\n\n\tasync prune404s(olderThan: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\"created_at\", \"<\", olderThan)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numDeletedRows);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"redirect-CNv4mHX2.mjs","names":[],"sources":["../src/database/repositories/redirect.ts"],"sourcesContent":["import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tcompilePattern,\n\tmatchPattern,\n\tinterpolateDestination,\n\tisPattern,\n} from \"../../redirects/patterns.js\";\nimport { currentTimestampValue } from \"../dialect-helpers.js\";\nimport type { Database, RedirectTable } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Bounded 404 logging\n// ---------------------------------------------------------------------------\n\n/**\n * Hard cap on rows stored in `_emdash_404_log`. When exceeded, the oldest\n * rows (by `last_seen_at`) are evicted on insert. Prevents an unauthenticated\n * attacker from growing the table without bound by requesting unique URLs.\n */\nexport const MAX_404_LOG_ROWS = 10_000;\n\n/** Max stored length for the `Referer` header — truncated on insert. */\nexport const REFERRER_MAX_LENGTH = 512;\n\n/** Max stored length for the `User-Agent` header — truncated on insert. */\nexport const USER_AGENT_MAX_LENGTH = 256;\n\n/** Pattern to escape LIKE wildcards: %, _, and backslash */\nconst LIKE_ESCAPE_RE = /[\\\\%_]/g;\n\n/**\n * Truncate a header-derived string to `max` chars, preserving `null`/`undefined`\n * as `null`. Empty strings stay empty (the caller decides whether to coerce).\n */\nfunction truncateOrNull(value: string | null | undefined, max: number): string | null {\n\tif (value === null || value === undefined) return null;\n\treturn value.length > max ? value.slice(0, max) : value;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface Redirect {\n\tid: string;\n\tsource: string;\n\tdestination: string;\n\ttype: number;\n\tisPattern: boolean;\n\tenabled: boolean;\n\thits: number;\n\tlastHitAt: string | null;\n\tgroupName: string | null;\n\tauto: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface CreateRedirectInput {\n\tsource: string;\n\tdestination: string;\n\ttype?: number;\n\tisPattern?: boolean;\n\tenabled?: boolean;\n\tgroupName?: string | null;\n\tauto?: boolean;\n}\n\nexport interface UpdateRedirectInput {\n\tsource?: string;\n\tdestination?: string;\n\ttype?: number;\n\tisPattern?: boolean;\n\tenabled?: boolean;\n\tgroupName?: string | null;\n}\n\nexport interface NotFoundEntry {\n\tid: string;\n\tpath: string;\n\treferrer: string | null;\n\tuserAgent: string | null;\n\tip: string | null;\n\tcreatedAt: string;\n}\n\nexport interface NotFoundSummary {\n\tpath: string;\n\tcount: number;\n\tlastSeen: string;\n\ttopReferrer: string | null;\n}\n\nexport interface RedirectMatch {\n\tredirect: Redirect;\n\tresolvedDestination: string;\n}\n\n// ---------------------------------------------------------------------------\n// Row mapping\n// ---------------------------------------------------------------------------\n\nfunction rowToRedirect(row: RedirectTable): Redirect {\n\treturn {\n\t\tid: row.id,\n\t\tsource: row.source,\n\t\tdestination: row.destination,\n\t\ttype: row.type,\n\t\tisPattern: row.is_pattern === 1,\n\t\tenabled: row.enabled === 1,\n\t\thits: row.hits,\n\t\tlastHitAt: row.last_hit_at,\n\t\tgroupName: row.group_name,\n\t\tauto: row.auto === 1,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class RedirectRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t// --- CRUD ---------------------------------------------------------------\n\n\tasync findById(id: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findBySource(source: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", source)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findMany(opts: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tsearch?: string;\n\t\tgroup?: string;\n\t\tenabled?: boolean;\n\t\tauto?: boolean;\n\t}): Promise<FindManyResult<Redirect>> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (opts.search) {\n\t\t\t// Escape LIKE wildcards in the search term to prevent injection.\n\t\t\t// Must include ESCAPE clause for SQLite to recognize backslash as escape char.\n\t\t\tconst escaped = opts.search.replace(LIKE_ESCAPE_RE, (c) => `\\\\${c}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`source LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`destination LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tif (opts.group !== undefined) {\n\t\t\tquery = query.where(\"group_name\", \"=\", opts.group);\n\t\t}\n\n\t\tif (opts.enabled !== undefined) {\n\t\t\tquery = query.where(\"enabled\", \"=\", opts.enabled ? 1 : 0);\n\t\t}\n\n\t\tif (opts.auto !== undefined) {\n\t\t\tquery = query.where(\"auto\", \"=\", opts.auto ? 1 : 0);\n\t\t}\n\n\t\tif (opts.cursor) {\n\t\t\tconst decoded = decodeCursor(opts.cursor);\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items = rows.slice(0, limit).map(rowToRedirect);\n\t\tconst result: FindManyResult<Redirect> = { items };\n\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync create(input: CreateRedirectInput): Promise<Redirect> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\t\tconst patternFlag = input.isPattern ?? isPattern(input.source);\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_redirects\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tsource: input.source,\n\t\t\t\tdestination: input.destination,\n\t\t\t\ttype: input.type ?? 301,\n\t\t\t\tis_pattern: patternFlag ? 1 : 0,\n\t\t\t\tenabled: input.enabled !== false ? 1 : 0,\n\t\t\t\thits: 0,\n\t\t\t\tlast_hit_at: null,\n\t\t\t\tgroup_name: input.groupName ?? null,\n\t\t\t\tauto: input.auto ? 1 : 0,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\tasync update(id: string, input: UpdateRedirectInput): Promise<Redirect | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return null;\n\n\t\tconst now = new Date().toISOString();\n\t\tconst values: Record<string, unknown> = { updated_at: now };\n\n\t\tif (input.source !== undefined) {\n\t\t\tvalues.source = input.source;\n\t\t\tvalues.is_pattern =\n\t\t\t\tinput.isPattern !== undefined ? (input.isPattern ? 1 : 0) : isPattern(input.source) ? 1 : 0;\n\t\t} else if (input.isPattern !== undefined) {\n\t\t\tvalues.is_pattern = input.isPattern ? 1 : 0;\n\t\t}\n\n\t\tif (input.destination !== undefined) values.destination = input.destination;\n\t\tif (input.type !== undefined) values.type = input.type;\n\t\tif (input.enabled !== undefined) values.enabled = input.enabled ? 1 : 0;\n\t\tif (input.groupName !== undefined) values.group_name = input.groupName;\n\n\t\tawait this.db.updateTable(\"_emdash_redirects\").set(values).where(\"id\", \"=\", id).execute();\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_redirects\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn BigInt(result.numDeletedRows) > 0n;\n\t}\n\n\t/**\n\t * Fetch all enabled redirects (for loop detection graph building).\n\t * Not paginated — returns the full set.\n\t */\n\tasync findAllEnabled(): Promise<Redirect[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.execute();\n\t\treturn rows.map(rowToRedirect);\n\t}\n\n\t// --- Matching -----------------------------------------------------------\n\n\tasync findExactMatch(path: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", path)\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.where(\"is_pattern\", \"=\", 0)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findEnabledPatternRules(): Promise<Redirect[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.where(\"is_pattern\", \"=\", 1)\n\t\t\t.execute();\n\t\treturn rows.map(rowToRedirect);\n\t}\n\n\t/**\n\t * Match a request path against all enabled redirect rules.\n\t * Checks exact matches first (indexed), then pattern rules.\n\t * Returns the matched redirect and the resolved destination URL.\n\t */\n\tasync matchPath(path: string): Promise<RedirectMatch | null> {\n\t\t// 1. Exact match (fast, indexed)\n\t\tconst exact = await this.findExactMatch(path);\n\t\tif (exact) {\n\t\t\treturn { redirect: exact, resolvedDestination: exact.destination };\n\t\t}\n\n\t\t// 2. Pattern match\n\t\tconst patterns = await this.findEnabledPatternRules();\n\t\tfor (const redirect of patterns) {\n\t\t\tconst compiled = compilePattern(redirect.source);\n\t\t\tconst params = matchPattern(compiled, path);\n\t\t\tif (params) {\n\t\t\t\tconst resolved = interpolateDestination(redirect.destination, params);\n\t\t\t\treturn { redirect, resolvedDestination: resolved };\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// --- Hit tracking -------------------------------------------------------\n\n\tasync recordHit(id: string): Promise<void> {\n\t\tawait sql`\n\t\t\tUPDATE _emdash_redirects\n\t\t\tSET hits = hits + 1, last_hit_at = ${currentTimestampValue(this.db)}, updated_at = ${currentTimestampValue(this.db)}\n\t\t\tWHERE id = ${id}\n\t\t`.execute(this.db);\n\t}\n\n\t// --- Auto-redirects (slug change) ---------------------------------------\n\n\t/**\n\t * Create an auto-redirect when a content slug changes.\n\t * Uses the collection's URL pattern to compute old/new URLs.\n\t * Collapses existing redirect chains pointing to the old URL.\n\t */\n\tasync createAutoRedirect(\n\t\tcollection: string,\n\t\toldSlug: string,\n\t\tnewSlug: string,\n\t\tcontentId: string,\n\t\turlPattern: string | null,\n\t): Promise<Redirect> {\n\t\tconst oldUrl = urlPattern\n\t\t\t? urlPattern.replace(\"{slug}\", oldSlug).replace(\"{id}\", contentId)\n\t\t\t: `/${collection}/${oldSlug}`;\n\t\tconst newUrl = urlPattern\n\t\t\t? urlPattern.replace(\"{slug}\", newSlug).replace(\"{id}\", contentId)\n\t\t\t: `/${collection}/${newSlug}`;\n\n\t\t// Collapse chains: update any existing redirects pointing to the old URL\n\t\tawait this.collapseChains(oldUrl, newUrl);\n\n\t\t// Check if a redirect from this source already exists\n\t\tconst existing = await this.findBySource(oldUrl);\n\t\tif (existing) {\n\t\t\t// Update the existing redirect to point to the new URL\n\t\t\treturn (await this.update(existing.id, { destination: newUrl }))!;\n\t\t}\n\n\t\treturn this.create({\n\t\t\tsource: oldUrl,\n\t\t\tdestination: newUrl,\n\t\t\ttype: 301,\n\t\t\tisPattern: false,\n\t\t\tauto: true,\n\t\t\tgroupName: \"Auto: slug change\",\n\t\t});\n\t}\n\n\t/**\n\t * Update all redirects whose destination matches oldDestination\n\t * to point to newDestination instead. Prevents redirect chains.\n\t * Returns the number of updated rows.\n\t */\n\tasync collapseChains(oldDestination: string, newDestination: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_redirects\")\n\t\t\t.set({\n\t\t\t\tdestination: newDestination,\n\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t})\n\t\t\t.where(\"destination\", \"=\", oldDestination)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numUpdatedRows);\n\t}\n\n\t// --- 404 log ------------------------------------------------------------\n\n\t/**\n\t * Record a 404 hit for `entry.path`.\n\t *\n\t * Dedups by path: repeat hits increment `hits` and refresh `last_seen_at`\n\t * on the existing row instead of inserting a new one. Referrer and\n\t * user-agent are truncated to bounded lengths so a malicious client can't\n\t * blow up storage with huge headers. When the table would exceed\n\t * MAX_404_LOG_ROWS, the oldest entries (by `last_seen_at`) are evicted.\n\t *\n\t * This is called from the public redirect middleware on every 404 and\n\t * must never throw for an unauthenticated caller — failures bubble up to\n\t * the middleware, which swallows them.\n\t */\n\tasync log404(entry: {\n\t\tpath: string;\n\t\treferrer?: string | null;\n\t\tuserAgent?: string | null;\n\t\tip?: string | null;\n\t}): Promise<void> {\n\t\tconst now = new Date().toISOString();\n\t\tconst referrer = truncateOrNull(entry.referrer, REFERRER_MAX_LENGTH);\n\t\tconst userAgent = truncateOrNull(entry.userAgent, USER_AGENT_MAX_LENGTH);\n\t\tconst ip = entry.ip ?? null;\n\n\t\t// Atomic upsert by path. The UNIQUE index on `path` makes this safe\n\t\t// under concurrency: two requests for the same new path can't both\n\t\t// insert — the second one hits the conflict branch and increments\n\t\t// hits instead of failing with a uniqueness error.\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_404_log\")\n\t\t\t.values({\n\t\t\t\tid: ulid(),\n\t\t\t\tpath: entry.path,\n\t\t\t\treferrer,\n\t\t\t\tuser_agent: userAgent,\n\t\t\t\tip,\n\t\t\t\thits: 1,\n\t\t\t\tlast_seen_at: now,\n\t\t\t\tcreated_at: now,\n\t\t\t})\n\t\t\t.onConflict((oc) =>\n\t\t\t\toc.column(\"path\").doUpdateSet({\n\t\t\t\t\thits: sql`hits + 1`,\n\t\t\t\t\tlast_seen_at: now,\n\t\t\t\t\treferrer,\n\t\t\t\t\tuser_agent: userAgent,\n\t\t\t\t\tip,\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.execute();\n\n\t\t// Enforce the row cap. Cheap when the table is under cap (single\n\t\t// COUNT(*) query); evicts oldest rows if we're over. Updates (dedup\n\t\t// hits) don't grow the table so this is a no-op for repeat paths.\n\t\tawait this.enforce404Cap();\n\t}\n\n\t/**\n\t * Delete the oldest rows from `_emdash_404_log` if the row count exceeds\n\t * MAX_404_LOG_ROWS. \"Oldest\" is by `last_seen_at`, so a path that keeps\n\t * getting hit stays in the table even if it was first seen long ago.\n\t *\n\t * Private — callers use `log404`, which invokes this after every upsert.\n\t */\n\tprivate async enforce404Cap(): Promise<void> {\n\t\tconst countRow = await this.db\n\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"c\"))\n\t\t\t.executeTakeFirst();\n\t\tconst count = Number(countRow?.c ?? 0);\n\t\tif (count <= MAX_404_LOG_ROWS) return;\n\n\t\tconst excess = count - MAX_404_LOG_ROWS;\n\n\t\t// Evict the oldest rows in a single SQL statement. Using a subquery\n\t\t// (rather than materialising the victim IDs in JS and passing them\n\t\t// back as bind parameters) keeps the statement bounded regardless of\n\t\t// how far over cap the table is — important for existing installs\n\t\t// that crossed the threshold before this cap was introduced.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\n\t\t\t\t\"id\",\n\t\t\t\t\"in\",\n\t\t\t\tthis.db\n\t\t\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t\t\t.select(\"id\")\n\t\t\t\t\t.orderBy(\"last_seen_at\", \"asc\")\n\t\t\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t\t\t.limit(excess),\n\t\t\t)\n\t\t\t.execute();\n\t}\n\n\tasync find404s(opts: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tsearch?: string;\n\t}): Promise<FindManyResult<NotFoundEntry>> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (opts.search) {\n\t\t\tconst escaped = opts.search.replace(LIKE_ESCAPE_RE, (c) => `\\\\${c}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where(sql<boolean>`path LIKE ${term} ESCAPE '\\\\'`);\n\t\t}\n\n\t\tif (opts.cursor) {\n\t\t\tconst decoded = decodeCursor(opts.cursor);\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items: NotFoundEntry[] = rows.slice(0, limit).map((row) => ({\n\t\t\tid: row.id,\n\t\t\tpath: row.path,\n\t\t\treferrer: row.referrer,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tip: row.ip,\n\t\t\tcreatedAt: row.created_at,\n\t\t}));\n\n\t\tconst result: FindManyResult<NotFoundEntry> = { items };\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync get404Summary(limit = 50): Promise<NotFoundSummary[]> {\n\t\t// Since rows are now deduped by path, each path has exactly one row\n\t\t// with `hits` as the running count and `last_seen_at` as the latest\n\t\t// timestamp. The subquery for `top_referrer` collapses to a simple\n\t\t// pick of the row's stored referrer (the most recent one seen).\n\t\tconst rows = await sql<{\n\t\t\tpath: string;\n\t\t\tcount: number;\n\t\t\tlast_seen: string;\n\t\t\ttop_referrer: string | null;\n\t\t}>`\n\t\t\tSELECT\n\t\t\t\tpath,\n\t\t\t\tSUM(hits) as count,\n\t\t\t\tMAX(last_seen_at) as last_seen,\n\t\t\t\t(\n\t\t\t\t\tSELECT referrer FROM _emdash_404_log AS inner_log\n\t\t\t\t\tWHERE inner_log.path = _emdash_404_log.path\n\t\t\t\t\t\tAND referrer IS NOT NULL AND referrer != ''\n\t\t\t\t\tLIMIT 1\n\t\t\t\t) as top_referrer\n\t\t\tFROM _emdash_404_log\n\t\t\tGROUP BY path\n\t\t\tORDER BY count DESC\n\t\t\tLIMIT ${limit}\n\t\t`.execute(this.db);\n\n\t\treturn rows.rows.map((row) => ({\n\t\t\tpath: row.path,\n\t\t\tcount: Number(row.count),\n\t\t\tlastSeen: row.last_seen,\n\t\t\ttopReferrer: row.top_referrer,\n\t\t}));\n\t}\n\n\tasync delete404(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn BigInt(result.numDeletedRows) > 0n;\n\t}\n\n\tasync clear404s(): Promise<number> {\n\t\tconst result = await this.db.deleteFrom(\"_emdash_404_log\").executeTakeFirst();\n\t\treturn Number(result.numDeletedRows);\n\t}\n\n\tasync prune404s(olderThan: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\"created_at\", \"<\", olderThan)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numDeletedRows);\n\t}\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { n as InvalidCursorError } from "./types-CwXMEPRr.mjs";
1
+ import { n as InvalidCursorError } from "./types-ByV5sgsv.mjs";
2
2
  import { t as OptionsRepository } from "./options-BL4X94qY.mjs";
3
3
  import { a as validateDestinationParams, i as matchPattern, n as interpolateDestination, o as validatePattern, r as isPattern, t as compilePattern } from "./patterns-CqG5Ya3i.mjs";
4
- import { t as RedirectRepository } from "./redirect-DkaDxq8e.mjs";
4
+ import { t as RedirectRepository } from "./redirect-CNv4mHX2.mjs";
5
5
 
6
6
  //#region src/redirects/loops.ts
7
7
  /**
@@ -570,4 +570,4 @@ async function handleNotFoundPrune(db, olderThan) {
570
570
 
571
571
  //#endregion
572
572
  export { handleRedirectCreate as a, handleRedirectList as c, handleNotFoundSummary as i, handleRedirectUpdate as l, handleNotFoundList as n, handleRedirectDelete as o, handleNotFoundPrune as r, handleRedirectGet as s, handleNotFoundClear as t };
573
- //# sourceMappingURL=redirects-D1fdd68T.mjs.map
573
+ //# sourceMappingURL=redirects-B-CUZ1Xh.mjs.map