emdash 0.16.1 → 0.17.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 (562) hide show
  1. package/dist/{adapters-C4yd_UJR.d.mts → adapters-C5AWLJSD.d.mts} +1 -1
  2. package/dist/{adapters-C4yd_UJR.d.mts.map → adapters-C5AWLJSD.d.mts.map} +1 -1
  3. package/dist/{allowed-origins-D0fFk9a6.mjs → allowed-origins-CyYLEJkp.mjs} +2 -2
  4. package/dist/{allowed-origins-D0fFk9a6.mjs.map → allowed-origins-CyYLEJkp.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +16 -16
  7. package/dist/api/schemas/index.d.mts +2 -2
  8. package/dist/api/schemas/index.mjs +3 -3
  9. package/dist/{api-BNKqxyFX.mjs → api-Dmz40c2V.mjs} +44 -22
  10. package/dist/api-Dmz40c2V.mjs.map +1 -0
  11. package/dist/{api-tokens-ucpcNXDt.mjs → api-tokens-VrXNiNvV.mjs} +2 -2
  12. package/dist/{api-tokens-ucpcNXDt.mjs.map → api-tokens-VrXNiNvV.mjs.map} +1 -1
  13. package/dist/{apply-BOPaD-s9.mjs → apply-CgamLmed.mjs} +93 -31
  14. package/dist/apply-CgamLmed.mjs.map +1 -0
  15. package/dist/astro/index.d.mts +10 -10
  16. package/dist/astro/index.mjs +19 -3
  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/redirect.d.mts.map +1 -1
  21. package/dist/astro/middleware/redirect.mjs +9 -5
  22. package/dist/astro/middleware/redirect.mjs.map +1 -1
  23. package/dist/astro/middleware/request-context.mjs +2 -2
  24. package/dist/astro/middleware/setup.mjs +1 -1
  25. package/dist/astro/middleware.mjs +66 -65
  26. package/dist/astro/middleware.mjs.map +1 -1
  27. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
  28. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
  29. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
  30. package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
  31. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.d.mts +8 -0
  32. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.d.mts.map +1 -0
  33. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +23 -0
  34. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs.map +1 -0
  35. package/dist/astro/routes/api/admin/byline-fields/_slug_.d.mts +10 -0
  36. package/dist/astro/routes/api/admin/byline-fields/_slug_.d.mts.map +1 -0
  37. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +55 -0
  38. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -0
  39. package/dist/astro/routes/api/admin/byline-fields/index.d.mts +9 -0
  40. package/dist/astro/routes/api/admin/byline-fields/index.d.mts.map +1 -0
  41. package/dist/astro/routes/api/admin/byline-fields/index.mjs +43 -0
  42. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -0
  43. package/dist/astro/routes/api/admin/byline-fields/reorder.d.mts +8 -0
  44. package/dist/astro/routes/api/admin/byline-fields/reorder.d.mts.map +1 -0
  45. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +27 -0
  46. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -0
  47. package/dist/astro/routes/api/admin/bylines/_id_/index.d.mts.map +1 -1
  48. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +27 -28
  49. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
  50. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +13 -12
  51. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
  52. package/dist/astro/routes/api/admin/bylines/index.mjs +15 -13
  53. package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
  54. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +10 -10
  55. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  56. package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
  57. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  58. package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
  59. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  60. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  61. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
  62. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
  63. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +35 -34
  64. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  65. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +35 -34
  66. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  67. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +34 -33
  68. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  69. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +34 -33
  70. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  71. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +34 -33
  72. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  73. package/dist/astro/routes/api/admin/plugins/index.mjs +34 -33
  74. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  75. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  76. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +34 -33
  77. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  78. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +34 -33
  79. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  80. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +34 -33
  81. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +34 -33
  83. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +35 -34
  85. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  86. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +34 -33
  87. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  88. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +35 -34
  89. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  90. package/dist/astro/routes/api/admin/plugins/updates.mjs +34 -33
  91. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  92. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +34 -33
  93. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  94. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  95. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +34 -33
  96. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  97. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  98. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  99. package/dist/astro/routes/api/admin/users/_id_/index.mjs +5 -5
  100. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
  101. package/dist/astro/routes/api/admin/users/index.mjs +5 -5
  102. package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
  103. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  104. package/dist/astro/routes/api/auth/invite/complete.mjs +9 -9
  105. package/dist/astro/routes/api/auth/invite/index.mjs +6 -6
  106. package/dist/astro/routes/api/auth/invite/register-options.mjs +8 -8
  107. package/dist/astro/routes/api/auth/logout.mjs +3 -3
  108. package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
  109. package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
  110. package/dist/astro/routes/api/auth/me.d.mts.map +1 -1
  111. package/dist/astro/routes/api/auth/me.mjs +18 -11
  112. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  113. package/dist/astro/routes/api/auth/mode.mjs +1 -1
  114. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  115. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  116. package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
  117. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  118. package/dist/astro/routes/api/auth/passkey/options.mjs +10 -10
  119. package/dist/astro/routes/api/auth/passkey/register/options.mjs +8 -8
  120. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +9 -9
  121. package/dist/astro/routes/api/auth/passkey/verify.mjs +9 -9
  122. package/dist/astro/routes/api/auth/signup/complete.mjs +9 -9
  123. package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
  124. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  125. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
  126. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  127. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  128. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  129. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  130. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +9 -9
  131. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +6 -6
  132. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  133. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  134. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +6 -6
  135. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  136. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +18 -13
  137. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  138. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  139. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  140. package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
  141. package/dist/astro/routes/api/content/_collection_/_id_.mjs +9 -7
  142. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  143. package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
  144. package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
  145. package/dist/astro/routes/api/dashboard.mjs +7 -7
  146. package/dist/astro/routes/api/dev/emails.mjs +3 -3
  147. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  148. package/dist/astro/routes/api/import/probe.mjs +10 -10
  149. package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
  150. package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
  151. package/dist/astro/routes/api/import/wordpress/execute.mjs +11 -10
  152. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  153. package/dist/astro/routes/api/import/wordpress/media.mjs +8 -8
  154. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  155. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
  156. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  157. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +10 -10
  158. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  159. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +13 -11
  160. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  161. package/dist/astro/routes/api/manifest.mjs +4 -4
  162. package/dist/astro/routes/api/mcp.mjs +34 -30
  163. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  164. package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
  165. package/dist/astro/routes/api/media/_id_.mjs +6 -6
  166. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  167. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  168. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  169. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  170. package/dist/astro/routes/api/media/upload-url.mjs +8 -8
  171. package/dist/astro/routes/api/media.d.mts.map +1 -1
  172. package/dist/astro/routes/api/media.mjs +13 -12
  173. package/dist/astro/routes/api/media.mjs.map +1 -1
  174. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
  175. package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
  176. package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
  177. package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
  178. package/dist/astro/routes/api/menus/_name_.mjs +7 -7
  179. package/dist/astro/routes/api/menus/index.mjs +7 -7
  180. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  181. package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
  182. package/dist/astro/routes/api/oauth/device/code.mjs +9 -9
  183. package/dist/astro/routes/api/oauth/device/token.mjs +8 -8
  184. package/dist/astro/routes/api/oauth/register.mjs +3 -3
  185. package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
  186. package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
  187. package/dist/astro/routes/api/oauth/token.mjs +6 -6
  188. package/dist/astro/routes/api/openapi.json.mjs +10 -7
  189. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  190. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
  191. package/dist/astro/routes/api/redirects/404s/index.mjs +8 -8
  192. package/dist/astro/routes/api/redirects/404s/summary.mjs +8 -8
  193. package/dist/astro/routes/api/redirects/_id_.mjs +9 -9
  194. package/dist/astro/routes/api/redirects/index.mjs +9 -9
  195. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  196. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  197. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +34 -33
  198. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  199. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +34 -33
  200. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  201. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +34 -33
  202. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  203. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +34 -33
  204. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  205. package/dist/astro/routes/api/schema/collections/index.mjs +34 -33
  206. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  207. package/dist/astro/routes/api/schema/index.mjs +6 -6
  208. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +34 -33
  209. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  210. package/dist/astro/routes/api/schema/orphans/index.mjs +34 -33
  211. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  212. package/dist/astro/routes/api/search/enable.mjs +9 -9
  213. package/dist/astro/routes/api/search/index.mjs +8 -8
  214. package/dist/astro/routes/api/search/rebuild.mjs +9 -9
  215. package/dist/astro/routes/api/search/stats.mjs +6 -6
  216. package/dist/astro/routes/api/search/suggest.mjs +8 -8
  217. package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
  218. package/dist/astro/routes/api/sections/index.mjs +8 -8
  219. package/dist/astro/routes/api/settings/email.mjs +4 -4
  220. package/dist/astro/routes/api/settings.mjs +11 -11
  221. package/dist/astro/routes/api/setup/admin-verify.mjs +10 -10
  222. package/dist/astro/routes/api/setup/admin.mjs +9 -9
  223. package/dist/astro/routes/api/setup/dev-bypass.mjs +24 -23
  224. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  225. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  226. package/dist/astro/routes/api/setup/index.mjs +24 -23
  227. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  228. package/dist/astro/routes/api/setup/status.mjs +4 -4
  229. package/dist/astro/routes/api/snapshot.mjs +5 -5
  230. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +12 -12
  231. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +12 -12
  232. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +12 -12
  233. package/dist/astro/routes/api/taxonomies/index.mjs +12 -12
  234. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  235. package/dist/astro/routes/api/typegen.mjs +5 -5
  236. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  237. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  238. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  239. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
  240. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
  241. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
  242. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  243. package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
  244. package/dist/astro/routes/api/widget-components.mjs +3 -3
  245. package/dist/astro/routes/robots.txt.mjs +6 -6
  246. package/dist/astro/routes/sitemap-_collection_.xml.mjs +8 -8
  247. package/dist/astro/routes/sitemap.xml.mjs +7 -7
  248. package/dist/astro/types.d.mts +13 -12
  249. package/dist/astro/types.d.mts.map +1 -1
  250. package/dist/auth/providers/github.d.mts +1 -1
  251. package/dist/auth/providers/google.d.mts +1 -1
  252. package/dist/{authorize-Bn4S4DUT.mjs → authorize-_wWM_44T.mjs} +2 -2
  253. package/dist/{authorize-Bn4S4DUT.mjs.map → authorize-_wWM_44T.mjs.map} +1 -1
  254. package/dist/byline-BrIVWLm-.mjs +925 -0
  255. package/dist/byline-BrIVWLm-.mjs.map +1 -0
  256. package/dist/{bylines-DWLnr6-k.d.mts → byline-fields-BNy7Ng1U.d.mts} +151 -23
  257. package/dist/byline-fields-BNy7Ng1U.d.mts.map +1 -0
  258. package/dist/byline-fields-DC3Wkk-U.mjs +123 -0
  259. package/dist/byline-fields-DC3Wkk-U.mjs.map +1 -0
  260. package/dist/byline-fields-Dr-xcb6S.mjs +238 -0
  261. package/dist/byline-fields-Dr-xcb6S.mjs.map +1 -0
  262. package/dist/byline-registry-CxK5g559.mjs +406 -0
  263. package/dist/byline-registry-CxK5g559.mjs.map +1 -0
  264. package/dist/{bylines-n6nykUyI.mjs → bylines-C_POWmGT.mjs} +25 -11
  265. package/dist/{bylines-n6nykUyI.mjs.map → bylines-C_POWmGT.mjs.map} +1 -1
  266. package/dist/bylines-sqExMElV.mjs +204 -0
  267. package/dist/bylines-sqExMElV.mjs.map +1 -0
  268. package/dist/{cache-BcI1yUjR.mjs → cache-wsDkA8ru.mjs} +2 -2
  269. package/dist/{cache-BcI1yUjR.mjs.map → cache-wsDkA8ru.mjs.map} +1 -1
  270. package/dist/{challenge-store-Dng1SxKT.mjs → challenge-store-DGwuCc4R.mjs} +1 -1
  271. package/dist/{challenge-store-Dng1SxKT.mjs.map → challenge-store-DGwuCc4R.mjs.map} +1 -1
  272. package/dist/{chunks-cYG4SnIP.mjs → chunks-BAYkM-CF.mjs} +2 -2
  273. package/dist/{chunks-cYG4SnIP.mjs.map → chunks-BAYkM-CF.mjs.map} +1 -1
  274. package/dist/cli/index.mjs +29 -23
  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 +2 -1
  278. package/dist/client/index.d.mts.map +1 -1
  279. package/dist/client/index.mjs +4 -2
  280. package/dist/client/index.mjs.map +1 -1
  281. package/dist/{comment-C76G-9tz.mjs → comment-Cd29aktf.mjs} +2 -2
  282. package/dist/{comment-C76G-9tz.mjs.map → comment-Cd29aktf.mjs.map} +1 -1
  283. package/dist/{comments-CCxFFGY1.mjs → comments-B7ufhkxN.mjs} +3 -3
  284. package/dist/{comments-CCxFFGY1.mjs.map → comments-B7ufhkxN.mjs.map} +1 -1
  285. package/dist/{components-Dx3DM0gg.mjs → components-CTfpu3PZ.mjs} +1 -1
  286. package/dist/{components-Dx3DM0gg.mjs.map → components-CTfpu3PZ.mjs.map} +1 -1
  287. package/dist/{content-8voQNTXX.mjs → content-BbqKo3Kc.mjs} +22 -3
  288. package/dist/content-BbqKo3Kc.mjs.map +1 -0
  289. package/dist/{context-B7qiYrz2.mjs → context-BsF1rhoI.mjs} +9 -9
  290. package/dist/{context-B7qiYrz2.mjs.map → context-BsF1rhoI.mjs.map} +1 -1
  291. package/dist/{cron-Bd3b3iuj.mjs → cron-DZovZUnC.mjs} +1 -1
  292. package/dist/{cron-Bd3b3iuj.mjs.map → cron-DZovZUnC.mjs.map} +1 -1
  293. package/dist/{dashboard-BeaFSPpx.mjs → dashboard-BwIX9r-X.mjs} +4 -4
  294. package/dist/{dashboard-BeaFSPpx.mjs.map → dashboard-BwIX9r-X.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/{db-errors-BiYqoX-n.mjs → db-errors-CtzxKBxe.mjs} +1 -1
  301. package/dist/{db-errors-BiYqoX-n.mjs.map → db-errors-CtzxKBxe.mjs.map} +1 -1
  302. package/dist/{default-BvTAYCzx.mjs → default-xLFNSsZ9.mjs} +1 -1
  303. package/dist/{default-BvTAYCzx.mjs.map → default-xLFNSsZ9.mjs.map} +1 -1
  304. package/dist/{device-flow-B9oG8PwP.mjs → device-flow-ptLrVINd.mjs} +4 -4
  305. package/dist/{device-flow-B9oG8PwP.mjs.map → device-flow-ptLrVINd.mjs.map} +1 -1
  306. package/dist/{email-console-CubRll9q.mjs → email-console-DHT2Fbpj.mjs} +1 -1
  307. package/dist/{email-console-CubRll9q.mjs.map → email-console-DHT2Fbpj.mjs.map} +1 -1
  308. package/dist/{error-ChfADBuu.mjs → error-npZWBSb7.mjs} +7 -3
  309. package/dist/error-npZWBSb7.mjs.map +1 -0
  310. package/dist/{escape-Cg6kMELH.mjs → escape-bIyGoW5W.mjs} +1 -1
  311. package/dist/{escape-Cg6kMELH.mjs.map → escape-bIyGoW5W.mjs.map} +1 -1
  312. package/dist/{fts-manager-C_b-4x8u.mjs → fts-manager-DmUAk-kQ.mjs} +2 -2
  313. package/dist/{fts-manager-C_b-4x8u.mjs.map → fts-manager-DmUAk-kQ.mjs.map} +1 -1
  314. package/dist/{hash-DlUxGhQS.mjs → hash-9w3pd3-m.mjs} +1 -1
  315. package/dist/{hash-DlUxGhQS.mjs.map → hash-9w3pd3-m.mjs.map} +1 -1
  316. package/dist/{import-DG80rC_I.mjs → import-Dh8bWmyq.mjs} +3 -3
  317. package/dist/{import-DG80rC_I.mjs.map → import-Dh8bWmyq.mjs.map} +1 -1
  318. package/dist/{index-D_p_jIP1.d.mts → index-CjKdMZ3U.d.mts} +38 -16
  319. package/dist/index-CjKdMZ3U.d.mts.map +1 -0
  320. package/dist/{index-CC42STEm.d.mts → index-D60_SzHG.d.mts} +3 -3
  321. package/dist/{index-CC42STEm.d.mts.map → index-D60_SzHG.d.mts.map} +1 -1
  322. package/dist/index.d.mts +17 -17
  323. package/dist/index.mjs +55 -54
  324. package/dist/{load-CLFRjk9r.mjs → load-DsoLq7ex.mjs} +2 -2
  325. package/dist/{load-CLFRjk9r.mjs.map → load-DsoLq7ex.mjs.map} +1 -1
  326. package/dist/{loader-D-vIJjfY.mjs → loader-CJ6lWO0d.mjs} +75 -19
  327. package/dist/loader-CJ6lWO0d.mjs.map +1 -0
  328. package/dist/{manifest-schema-Czqf0TLu.mjs → manifest-schema-Cj-YrzrF.mjs} +1 -1
  329. package/dist/{manifest-schema-Czqf0TLu.mjs.map → manifest-schema-Cj-YrzrF.mjs.map} +1 -1
  330. package/dist/media/index.d.mts +1 -1
  331. package/dist/media/index.mjs +2 -2
  332. package/dist/media/local-runtime.d.mts +11 -11
  333. package/dist/media/local-runtime.mjs +5 -5
  334. package/dist/{media-allowlist-BNloC69x.mjs → media-allowlist-CMcoYIjQ.mjs} +2 -2
  335. package/dist/{media-allowlist-BNloC69x.mjs.map → media-allowlist-CMcoYIjQ.mjs.map} +1 -1
  336. package/dist/{media-CKQd8AYU.mjs → media-jk_HzzOl.mjs} +7 -2
  337. package/dist/media-jk_HzzOl.mjs.map +1 -0
  338. package/dist/{menus-arUNspyU.mjs → menus-B-5-3aon.mjs} +2 -2
  339. package/dist/{menus-arUNspyU.mjs.map → menus-B-5-3aon.mjs.map} +1 -1
  340. package/dist/{menus-C-nWT5Tu.mjs → menus-CyMO6GBx.mjs} +27 -11
  341. package/dist/menus-CyMO6GBx.mjs.map +1 -0
  342. package/dist/{mime-KV5TqkMN.mjs → mime-CCEzze7W.mjs} +1 -1
  343. package/dist/{mime-KV5TqkMN.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
  344. package/dist/{mode-CaaiebZI.mjs → mode-BjlXswIw.mjs} +1 -1
  345. package/dist/{mode-CaaiebZI.mjs.map → mode-BjlXswIw.mjs.map} +1 -1
  346. package/dist/{normalize-CN5kRSMC.mjs → normalize-DVV8nbrL.mjs} +1 -1
  347. package/dist/{normalize-CN5kRSMC.mjs.map → normalize-DVV8nbrL.mjs.map} +1 -1
  348. package/dist/{oauth-authorization-CTMeVfvj.mjs → oauth-authorization-DvBAL75d.mjs} +4 -4
  349. package/dist/{oauth-authorization-CTMeVfvj.mjs.map → oauth-authorization-DvBAL75d.mjs.map} +1 -1
  350. package/dist/{oauth-clients-eJCbkVSG.mjs → oauth-clients-8mPDStMv.mjs} +1 -1
  351. package/dist/{oauth-clients-eJCbkVSG.mjs.map → oauth-clients-8mPDStMv.mjs.map} +1 -1
  352. package/dist/{oauth-state-store-vOSdOeGe.mjs → oauth-state-store-BJ7YtrfD.mjs} +1 -1
  353. package/dist/{oauth-state-store-vOSdOeGe.mjs.map → oauth-state-store-BJ7YtrfD.mjs.map} +1 -1
  354. package/dist/{oauth-user-lookup-3JwsVw6N.mjs → oauth-user-lookup-BdDSDvjF.mjs} +1 -1
  355. package/dist/{oauth-user-lookup-3JwsVw6N.mjs.map → oauth-user-lookup-BdDSDvjF.mjs.map} +1 -1
  356. package/dist/{options-DhV-gwJb.d.mts → options-tb7DJROi.d.mts} +3 -3
  357. package/dist/{options-DhV-gwJb.d.mts.map → options-tb7DJROi.d.mts.map} +1 -1
  358. package/dist/page/index.d.mts +2 -2
  359. package/dist/{parse-DHbXfvxO.mjs → parse-4zO5Y2DL.mjs} +2 -2
  360. package/dist/{parse-DHbXfvxO.mjs.map → parse-4zO5Y2DL.mjs.map} +1 -1
  361. package/dist/{passkey-config-BloQOT3y.mjs → passkey-config-BDVM86Tj.mjs} +1 -1
  362. package/dist/{passkey-config-BloQOT3y.mjs.map → passkey-config-BDVM86Tj.mjs.map} +1 -1
  363. package/dist/{placeholder-KCkkCtgQ.d.mts → placeholder-B9lUUEmj.d.mts} +1 -1
  364. package/dist/{placeholder-KCkkCtgQ.d.mts.map → placeholder-B9lUUEmj.d.mts.map} +1 -1
  365. package/dist/{placeholder-LqmHqvBw.mjs → placeholder-BZxr8W1j.mjs} +1 -1
  366. package/dist/{placeholder-LqmHqvBw.mjs.map → placeholder-BZxr8W1j.mjs.map} +1 -1
  367. package/dist/plugin-types.d.mts +1 -1
  368. package/dist/plugin-utils.d.mts +9 -9
  369. package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
  370. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  371. package/dist/{preview-D4z0WONU.mjs → preview-BfuRkVKW.mjs} +2 -2
  372. package/dist/{preview-D4z0WONU.mjs.map → preview-BfuRkVKW.mjs.map} +1 -1
  373. package/dist/{public-url-CUWWFME2.mjs → public-url-egRHCy1m.mjs} +1 -1
  374. package/dist/{public-url-CUWWFME2.mjs.map → public-url-egRHCy1m.mjs.map} +1 -1
  375. package/dist/{query-7m6-l0f_.mjs → query-CuvjwhrE.mjs} +12 -12
  376. package/dist/{query-7m6-l0f_.mjs.map → query-CuvjwhrE.mjs.map} +1 -1
  377. package/dist/{rate-limit-D8RAXN8b.mjs → rate-limit-D6VQqBk_.mjs} +2 -2
  378. package/dist/{rate-limit-D8RAXN8b.mjs.map → rate-limit-D6VQqBk_.mjs.map} +1 -1
  379. package/dist/{redirect-CjfDGrTd.mjs → redirect-BZUJltlj.mjs} +2 -2
  380. package/dist/{redirect-CjfDGrTd.mjs.map → redirect-BZUJltlj.mjs.map} +1 -1
  381. package/dist/{redirect-BINiRYq4.mjs → redirect-Cw3JTlmj.mjs} +1 -1
  382. package/dist/{redirect-BINiRYq4.mjs.map → redirect-Cw3JTlmj.mjs.map} +1 -1
  383. package/dist/{redirects-COMLwsV5.mjs → redirects-C0L9JUk4.mjs} +19 -6
  384. package/dist/redirects-C0L9JUk4.mjs.map +1 -0
  385. package/dist/{redirects-CowoEHdE.mjs → redirects-DnYuqsEf.mjs} +3 -3
  386. package/dist/{redirects-CowoEHdE.mjs.map → redirects-DnYuqsEf.mjs.map} +1 -1
  387. package/dist/{registry-Cyp-dx6J.mjs → registry-Dn6gsx3L.mjs} +13 -5
  388. package/dist/{registry-Cyp-dx6J.mjs.map → registry-Dn6gsx3L.mjs.map} +1 -1
  389. package/dist/{request-cache-dzCt8TZB.mjs → request-cache-BYMs-BGX.mjs} +23 -2
  390. package/dist/{request-cache-dzCt8TZB.mjs.map → request-cache-BYMs-BGX.mjs.map} +1 -1
  391. package/dist/{request-meta-C_Cjii-T.mjs → request-meta-7ByVLxB-.mjs} +2 -2
  392. package/dist/{request-meta-C_Cjii-T.mjs.map → request-meta-7ByVLxB-.mjs.map} +1 -1
  393. package/dist/{resolve-D6sM-SgF.mjs → resolve-BqYMVG0D.mjs} +1 -1
  394. package/dist/{resolve-D6sM-SgF.mjs.map → resolve-BqYMVG0D.mjs.map} +1 -1
  395. package/dist/{runner-DSQBurMS.d.mts → runner-DM1yR5qd.d.mts} +2 -2
  396. package/dist/{runner-DSQBurMS.d.mts.map → runner-DM1yR5qd.d.mts.map} +1 -1
  397. package/dist/{runner-Drnvs96u.mjs → runner-eAgyIkeg.mjs} +284 -158
  398. package/dist/runner-eAgyIkeg.mjs.map +1 -0
  399. package/dist/runtime.d.mts +10 -10
  400. package/dist/runtime.mjs +2 -2
  401. package/dist/{schema-CI9mYPX3.mjs → schema--mYZX4D7.mjs} +5 -5
  402. package/dist/{schema-CI9mYPX3.mjs.map → schema--mYZX4D7.mjs.map} +1 -1
  403. package/dist/{search-DKz_mGBP.mjs → search-C6U_NvZI.mjs} +4 -4
  404. package/dist/{search-DKz_mGBP.mjs.map → search-C6U_NvZI.mjs.map} +1 -1
  405. package/dist/{secrets-rPdhEBkD.mjs → secrets-YYbTgB1w.mjs} +1 -1
  406. package/dist/{secrets-rPdhEBkD.mjs.map → secrets-YYbTgB1w.mjs.map} +1 -1
  407. package/dist/{sections-DBbCDIAT.mjs → sections-Ba-rJLKb.mjs} +3 -3
  408. package/dist/{sections-DBbCDIAT.mjs.map → sections-Ba-rJLKb.mjs.map} +1 -1
  409. package/dist/seed/index.d.mts +2 -2
  410. package/dist/seed/index.mjs +18 -17
  411. package/dist/seo/index.d.mts +1 -1
  412. package/dist/{seo-BGCyDlkb.mjs → seo-BTzb5ksq.mjs} +2 -2
  413. package/dist/{seo-BGCyDlkb.mjs.map → seo-BTzb5ksq.mjs.map} +1 -1
  414. package/dist/{seo-Dq707mNQ.mjs → seo-DfjLvu8i.mjs} +1 -1
  415. package/dist/{seo-Dq707mNQ.mjs.map → seo-DfjLvu8i.mjs.map} +1 -1
  416. package/dist/{service-B0H7U1Y9.mjs → service-Cn-kIfZn.mjs} +3 -3
  417. package/dist/{service-B0H7U1Y9.mjs.map → service-Cn-kIfZn.mjs.map} +1 -1
  418. package/dist/{settings-DfwNyQkf.mjs → settings-C65OSm41.mjs} +3 -3
  419. package/dist/{settings-DfwNyQkf.mjs.map → settings-C65OSm41.mjs.map} +1 -1
  420. package/dist/{settings-BSXRtTzk.mjs → settings-ChlQbwU0.mjs} +4 -4
  421. package/dist/{settings-BSXRtTzk.mjs.map → settings-ChlQbwU0.mjs.map} +1 -1
  422. package/dist/{setup-complete-MzzN9u0b.mjs → setup-complete-VoEZfasi.mjs} +1 -1
  423. package/dist/{setup-complete-MzzN9u0b.mjs.map → setup-complete-VoEZfasi.mjs.map} +1 -1
  424. package/dist/{setup-nonce-DXuriHsg.mjs → setup-nonce-Bm0uKqmf.mjs} +1 -1
  425. package/dist/{setup-nonce-DXuriHsg.mjs.map → setup-nonce-Bm0uKqmf.mjs.map} +1 -1
  426. package/dist/{site-url-xkhw1tcz.mjs → site-url-Cm8-sJy7.mjs} +1 -1
  427. package/dist/{site-url-xkhw1tcz.mjs.map → site-url-Cm8-sJy7.mjs.map} +1 -1
  428. package/dist/{ssrf-MZ-zrG6-.mjs → ssrf-BsVGIE0Z.mjs} +1 -1
  429. package/dist/{ssrf-MZ-zrG6-.mjs.map → ssrf-BsVGIE0Z.mjs.map} +1 -1
  430. package/dist/storage/local.d.mts +1 -1
  431. package/dist/storage/local.mjs +1 -1
  432. package/dist/storage/s3.d.mts +1 -1
  433. package/dist/storage/s3.mjs +1 -1
  434. package/dist/{taxonomies-CcvrMLbR.mjs → taxonomies-CgpzAU6F.mjs} +8 -8
  435. package/dist/{taxonomies-CcvrMLbR.mjs.map → taxonomies-CgpzAU6F.mjs.map} +1 -1
  436. package/dist/{taxonomies-4vx0nmMr.mjs → taxonomies-D72gTOg_.mjs} +4 -4
  437. package/dist/{taxonomies-4vx0nmMr.mjs.map → taxonomies-D72gTOg_.mjs.map} +1 -1
  438. package/dist/{taxonomy-zqGQUqgu.mjs → taxonomy-BBK-UAEo.mjs} +3 -3
  439. package/dist/{taxonomy-zqGQUqgu.mjs.map → taxonomy-BBK-UAEo.mjs.map} +1 -1
  440. package/dist/{tokens-N8otWMmj.mjs → tokens-Bx2afeT-.mjs} +1 -1
  441. package/dist/{tokens-N8otWMmj.mjs.map → tokens-Bx2afeT-.mjs.map} +1 -1
  442. package/dist/{transport-B6CHddbu.mjs → transport--Ck3RBin.mjs} +1 -1
  443. package/dist/{transport-B6CHddbu.mjs.map → transport--Ck3RBin.mjs.map} +1 -1
  444. package/dist/{transport-C2MGqtL6.d.mts → transport-OnMNbsIA.d.mts} +1 -1
  445. package/dist/{transport-C2MGqtL6.d.mts.map → transport-OnMNbsIA.d.mts.map} +1 -1
  446. package/dist/{trusted-proxy-97pajC2f.mjs → trusted-proxy-B4AfnoAp.mjs} +1 -1
  447. package/dist/{trusted-proxy-97pajC2f.mjs.map → trusted-proxy-B4AfnoAp.mjs.map} +1 -1
  448. package/dist/types-D8bhH891.mjs +125 -0
  449. package/dist/{types-DSZl1Dsv.mjs.map → types-D8bhH891.mjs.map} +1 -1
  450. package/dist/{types-DGHWRQgr.d.mts → types-DMwSpvcw.d.mts} +2 -2
  451. package/dist/{types-DGHWRQgr.d.mts.map → types-DMwSpvcw.d.mts.map} +1 -1
  452. package/dist/{types-bYmRn_Uy.d.mts → types-DWnN7weG.d.mts} +1 -1
  453. package/dist/{types-bYmRn_Uy.d.mts.map → types-DWnN7weG.d.mts.map} +1 -1
  454. package/dist/{types-Dgo6y-Ut.d.mts → types-DX6v9KzJ.d.mts} +1 -1
  455. package/dist/{types-Dgo6y-Ut.d.mts.map → types-DX6v9KzJ.d.mts.map} +1 -1
  456. package/dist/{types-DaqNzqVt.d.mts → types-DawhLFwy.d.mts} +35 -1
  457. package/dist/{types-DaqNzqVt.d.mts.map → types-DawhLFwy.d.mts.map} +1 -1
  458. package/dist/{types-CpUuGcd5.d.mts → types-DbCWhHet.d.mts} +8 -2
  459. package/dist/{types-CpUuGcd5.d.mts.map → types-DbCWhHet.d.mts.map} +1 -1
  460. package/dist/{types-Cd9UCu3t.mjs → types-DpFmlNyB.mjs} +1 -1
  461. package/dist/{types-Cd9UCu3t.mjs.map → types-DpFmlNyB.mjs.map} +1 -1
  462. package/dist/{types-D599-ruj.d.mts → types-Qa7-HJJC.d.mts} +1 -1
  463. package/dist/{types-D599-ruj.d.mts.map → types-Qa7-HJJC.d.mts.map} +1 -1
  464. package/dist/{types-B0bmgwMG.mjs → types-SF1DwGf2.mjs} +2 -2
  465. package/dist/types-SF1DwGf2.mjs.map +1 -0
  466. package/dist/{types-DaYDYW6g.d.mts → types-i8_uzhMD.d.mts} +40 -2
  467. package/dist/types-i8_uzhMD.d.mts.map +1 -0
  468. package/dist/{types-CkDSF81F.d.mts → types-kwqCOUxj.d.mts} +1 -1
  469. package/dist/{types-CkDSF81F.d.mts.map → types-kwqCOUxj.d.mts.map} +1 -1
  470. package/dist/{user-hUSOaIJy.mjs → user-X4rtyO4Y.mjs} +2 -2
  471. package/dist/{user-hUSOaIJy.mjs.map → user-X4rtyO4Y.mjs.map} +1 -1
  472. package/dist/{utils-C3wTAP-P.mjs → utils-C4Ih4DML.mjs} +1 -1
  473. package/dist/{utils-C3wTAP-P.mjs.map → utils-C4Ih4DML.mjs.map} +1 -1
  474. package/dist/{validate-IGltez8n.mjs → validate-DactmcJG.mjs} +23 -3
  475. package/dist/validate-DactmcJG.mjs.map +1 -0
  476. package/dist/{validate-DQtHw9NT.d.mts → validate-Dy6nkNls.d.mts} +25 -5
  477. package/dist/{validate-DQtHw9NT.d.mts.map → validate-Dy6nkNls.d.mts.map} +1 -1
  478. package/dist/{validation-Bmymau7y.mjs → validation-BYA4i85b.mjs} +6 -6
  479. package/dist/{validation-Bmymau7y.mjs.map → validation-BYA4i85b.mjs.map} +1 -1
  480. package/dist/version-FGcv0ooe.mjs +7 -0
  481. package/dist/{version-ITD3PlQd.mjs.map → version-FGcv0ooe.mjs.map} +1 -1
  482. package/dist/{widgets-yHQa4c6c.mjs → widgets-DG-1jxnz.mjs} +3 -3
  483. package/dist/{widgets-yHQa4c6c.mjs.map → widgets-DG-1jxnz.mjs.map} +1 -1
  484. package/dist/{zod-generator-B80aap1J.mjs → zod-generator-BNAObjSt.mjs} +3 -3
  485. package/dist/{zod-generator-B80aap1J.mjs.map → zod-generator-BNAObjSt.mjs.map} +1 -1
  486. package/package.json +7 -7
  487. package/src/api/errors.ts +7 -0
  488. package/src/api/handlers/byline-fields.ts +212 -0
  489. package/src/api/handlers/bylines.ts +126 -5
  490. package/src/api/handlers/content.ts +43 -2
  491. package/src/api/handlers/media.ts +2 -0
  492. package/src/api/openapi/document.ts +3 -0
  493. package/src/api/schemas/byline-fields.ts +188 -0
  494. package/src/api/schemas/bylines.ts +42 -0
  495. package/src/api/schemas/content.ts +2 -0
  496. package/src/api/schemas/index.ts +1 -0
  497. package/src/api/schemas/media.ts +2 -0
  498. package/src/astro/integration/routes.ts +27 -0
  499. package/src/astro/middleware/redirect.ts +5 -1
  500. package/src/astro/routes/api/admin/byline-fields/[slug]/usage.ts +36 -0
  501. package/src/astro/routes/api/admin/byline-fields/[slug].ts +92 -0
  502. package/src/astro/routes/api/admin/byline-fields/index.ts +66 -0
  503. package/src/astro/routes/api/admin/byline-fields/reorder.ts +39 -0
  504. package/src/astro/routes/api/admin/bylines/[id]/index.ts +23 -21
  505. package/src/astro/routes/api/admin/bylines/index.ts +1 -0
  506. package/src/astro/routes/api/auth/me.ts +21 -10
  507. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +15 -3
  508. package/src/astro/routes/api/content/[collection]/[id].ts +3 -1
  509. package/src/astro/routes/api/media.ts +1 -0
  510. package/src/astro/types.ts +1 -0
  511. package/src/bylines/field-defs-cache.ts +138 -0
  512. package/src/bylines/index.ts +37 -4
  513. package/src/cli/commands/content.ts +4 -2
  514. package/src/client/index.ts +4 -1
  515. package/src/components/InlinePortableTextEditor.tsx +69 -0
  516. package/src/content/converters/portable-text-to-prosemirror.ts +7 -0
  517. package/src/content/converters/prosemirror-to-portable-text.ts +16 -0
  518. package/src/content/converters/types.ts +10 -0
  519. package/src/database/migrations/041_content_locale_list_index.ts +47 -0
  520. package/src/database/migrations/042_byline_fields.ts +157 -0
  521. package/src/database/migrations/runner.ts +4 -0
  522. package/src/database/repositories/byline.ts +758 -50
  523. package/src/database/repositories/content.ts +43 -3
  524. package/src/database/repositories/media.ts +14 -0
  525. package/src/database/repositories/types.ts +38 -0
  526. package/src/database/types.ts +44 -0
  527. package/src/emdash-runtime.ts +4 -1
  528. package/src/index.ts +1 -0
  529. package/src/loader.ts +98 -10
  530. package/src/mcp/server.ts +10 -1
  531. package/src/request-cache.ts +23 -0
  532. package/src/schema/byline-registry.ts +671 -0
  533. package/src/schema/registry.ts +14 -0
  534. package/src/schema/types.ts +133 -0
  535. package/src/seed/apply.ts +101 -14
  536. package/src/seed/types.ts +21 -0
  537. package/src/seed/validate.ts +39 -0
  538. package/dist/api-BNKqxyFX.mjs.map +0 -1
  539. package/dist/apply-BOPaD-s9.mjs.map +0 -1
  540. package/dist/byline-BDylH_m4.mjs +0 -404
  541. package/dist/byline-BDylH_m4.mjs.map +0 -1
  542. package/dist/bylines-B7TFEvFf.mjs +0 -118
  543. package/dist/bylines-B7TFEvFf.mjs.map +0 -1
  544. package/dist/bylines-DWLnr6-k.d.mts.map +0 -1
  545. package/dist/content-8voQNTXX.mjs.map +0 -1
  546. package/dist/error-ChfADBuu.mjs.map +0 -1
  547. package/dist/index-D_p_jIP1.d.mts.map +0 -1
  548. package/dist/loader-D-vIJjfY.mjs.map +0 -1
  549. package/dist/media-CKQd8AYU.mjs.map +0 -1
  550. package/dist/menus-C-nWT5Tu.mjs.map +0 -1
  551. package/dist/redirects-COMLwsV5.mjs.map +0 -1
  552. package/dist/runner-Drnvs96u.mjs.map +0 -1
  553. package/dist/setup-Cf_TyOv5.mjs +0 -137
  554. package/dist/setup-Cf_TyOv5.mjs.map +0 -1
  555. package/dist/types-B0bmgwMG.mjs.map +0 -1
  556. package/dist/types-DSZl1Dsv.mjs +0 -83
  557. package/dist/types-DaYDYW6g.d.mts.map +0 -1
  558. package/dist/validate-IGltez8n.mjs.map +0 -1
  559. package/dist/version-ITD3PlQd.mjs +0 -7
  560. /package/dist/{api-tokens-iPIHAY8N.mjs → api-tokens-B6VgoE6M.mjs} +0 -0
  561. /package/dist/{ssrf-BIcd-aXW.mjs → ssrf-BvgVcfNQ.mjs} +0 -0
  562. /package/dist/{types-1NNkmTIn.mjs → types-Cj2S6FuC.mjs} +0 -0
@@ -1,6 +1,13 @@
1
1
  import { sql, type Kysely, type Selectable } from "kysely";
2
2
  import { ulid } from "ulidx";
3
3
 
4
+ import { getBylineFieldDefs } from "../../bylines/field-defs-cache.js";
5
+ import {
6
+ clearRequestCacheEntry,
7
+ peekRequestCache,
8
+ setRequestCacheEntry,
9
+ } from "../../request-cache.js";
10
+ import type { BylineFieldDefinition, CustomFieldValue } from "../../schema/types.js";
4
11
  import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
5
12
  import { listTablesLike } from "../dialect-helpers.js";
6
13
  import { withTransaction } from "../transaction.js";
@@ -8,6 +15,7 @@ import type { BylineTable, Database } from "../types.js";
8
15
  import { validateIdentifier } from "../validate.js";
9
16
  import {
10
17
  decodeCursor,
18
+ EmDashValidationError,
11
19
  encodeCursor,
12
20
  type BylineSummary,
13
21
  type ContentBylineCredit,
@@ -16,6 +24,17 @@ import {
16
24
 
17
25
  type BylineRow = Selectable<BylineTable>;
18
26
 
27
+ /**
28
+ * A byline row optionally augmented with the avatar's media columns, folded in
29
+ * by the `LEFT JOIN media` in the content-credit hydration queries. The plain
30
+ * `selectAll()` finders produce rows without these keys, so they're optional
31
+ * and `rowToByline` defaults them to null.
32
+ */
33
+ type BylineRowWithAvatar = BylineRow & {
34
+ avatar_storage_key?: string | null;
35
+ avatar_alt?: string | null;
36
+ };
37
+
19
38
  export interface CreateBylineInput {
20
39
  slug: string;
21
40
  displayName: string;
@@ -36,6 +55,16 @@ export interface CreateBylineInput {
36
55
  * throws. Mirrors `TaxonomyRepository.create`.
37
56
  */
38
57
  translationOf?: string;
58
+ /**
59
+ * Byline custom-field values to seed on the new row (Phase 6 of
60
+ * Discussion #1174). Same semantics as `UpdateBylineInput.customFields`:
61
+ * keys must match registered slugs in `_emdash_byline_fields`, values
62
+ * are validated against the field's type, and writes route to
63
+ * `_emdash_byline_field_values` (translatable) or
64
+ * `_emdash_byline_field_group_values` (group-shared). Validation runs
65
+ * before the row insert so a bad value can't leave a bare byline behind.
66
+ */
67
+ customFields?: Record<string, unknown>;
39
68
  }
40
69
 
41
70
  export interface UpdateBylineInput {
@@ -46,6 +75,24 @@ export interface UpdateBylineInput {
46
75
  websiteUrl?: string | null;
47
76
  userId?: string | null;
48
77
  isGuest?: boolean;
78
+ /**
79
+ * Byline custom-field values to write (Phase 3 of Discussion #1174).
80
+ *
81
+ * Each key must match a registered slug in `_emdash_byline_fields`;
82
+ * unknown keys throw `EmDashValidationError`. Per-field writes route
83
+ * to `_emdash_byline_field_values` (when the field's `translatable`
84
+ * flag is true) or `_emdash_byline_field_group_values` (when false).
85
+ * A value of `null` clears the row.
86
+ *
87
+ * Values are validated against the field's type:
88
+ * - `string` / `text` / `url` accept a `string`
89
+ * - `boolean` accepts a `boolean`
90
+ * - `select` accepts a `string` that appears in `validation.options`
91
+ *
92
+ * Writes are idempotent (`INSERT … ON CONFLICT DO UPDATE`), so
93
+ * retrying the same update produces the same DB state.
94
+ */
95
+ customFields?: Record<string, unknown>;
49
96
  }
50
97
 
51
98
  export interface ContentBylineInput {
@@ -53,13 +100,15 @@ export interface ContentBylineInput {
53
100
  roleLabel?: string | null;
54
101
  }
55
102
 
56
- function rowToByline(row: BylineRow): BylineSummary {
103
+ function rowToByline(row: BylineRowWithAvatar): BylineSummary {
57
104
  return {
58
105
  id: row.id,
59
106
  slug: row.slug,
60
107
  displayName: row.display_name,
61
108
  bio: row.bio,
62
109
  avatarMediaId: row.avatar_media_id,
110
+ avatarStorageKey: row.avatar_storage_key ?? null,
111
+ avatarAlt: row.avatar_alt ?? null,
63
112
  websiteUrl: row.website_url,
64
113
  userId: row.user_id,
65
114
  isGuest: row.is_guest === 1,
@@ -70,6 +119,124 @@ function rowToByline(row: BylineRow): BylineSummary {
70
119
  };
71
120
  }
72
121
 
122
+ /**
123
+ * Merge a single decoded value into a `BylineSummary.customFields` map.
124
+ * Centralised so the merge semantics (null storage, JSON.parse failure
125
+ * handling) live in one place across both translatable and group-shared
126
+ * paths.
127
+ *
128
+ * A stored row with `value = NULL` (representing an explicit null) is
129
+ * surfaced as `null` in `customFields`. A row with a malformed JSON
130
+ * payload is dropped silently with a `console.warn` — a corrupted
131
+ * payload shouldn't break the entire byline hydration; the field-defs
132
+ * cache will let admins replace the value, and the warning makes the
133
+ * issue debuggable. (Storage path uses `JSON.stringify`, so the only
134
+ * way to get malformed JSON is direct DB tampering or a future
135
+ * migration bug.)
136
+ */
137
+ function assignCustomFieldValue(
138
+ summary: BylineSummary,
139
+ field: BylineFieldDefinition,
140
+ stored: string | null,
141
+ ): void {
142
+ const target = summary.customFields ?? {};
143
+ if (stored === null) {
144
+ target[field.slug] = null;
145
+ } else {
146
+ try {
147
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion -- coerceFieldValue ran at write time, see field-defs-cache.ts
148
+ target[field.slug] = JSON.parse(stored) as CustomFieldValue;
149
+ } catch {
150
+ console.warn(
151
+ `[BylineRepository] dropping malformed JSON for byline=${summary.id} ` +
152
+ `field=${field.slug}: ${stored.slice(0, 60)}`,
153
+ );
154
+ return;
155
+ }
156
+ }
157
+ summary.customFields = target;
158
+ }
159
+
160
+ /**
161
+ * Coerce a raw write-path value to `CustomFieldValue`, throwing
162
+ * `EmDashValidationError` on type mismatch. `null` clears the field
163
+ * (DELETE in the write path).
164
+ *
165
+ * TODO: `field.required` is not enforced. The admin UI exposes the
166
+ * toggle but the backend accepts missing values; design pass needed
167
+ * on the enforcement model.
168
+ */
169
+ function coerceFieldValue(field: BylineFieldDefinition, raw: unknown): CustomFieldValue {
170
+ if (raw === null) return null;
171
+
172
+ switch (field.type) {
173
+ case "string":
174
+ case "text": {
175
+ if (typeof raw !== "string") {
176
+ throw new EmDashValidationError(
177
+ `Byline field "${field.slug}" expects a string value (received ${typeof raw})`,
178
+ { slug: field.slug, type: field.type, received: typeof raw },
179
+ );
180
+ }
181
+ return raw;
182
+ }
183
+ case "url": {
184
+ if (typeof raw !== "string") {
185
+ throw new EmDashValidationError(
186
+ `Byline field "${field.slug}" expects a string value (received ${typeof raw})`,
187
+ { slug: field.slug, type: field.type, received: typeof raw },
188
+ );
189
+ }
190
+ // Empty string round-trips as a clear from the admin UI; any
191
+ // non-empty value must be a valid http(s) URL. The scheme
192
+ // allowlist mirrors `httpUrl` in `api/schemas/common.ts` —
193
+ // `new URL` alone would accept `javascript:`/`data:` etc.
194
+ if (raw === "") return raw;
195
+ let parsed: URL;
196
+ try {
197
+ parsed = new URL(raw);
198
+ } catch {
199
+ throw new EmDashValidationError(
200
+ `Byline field "${field.slug}" expects a valid URL (received "${raw}")`,
201
+ { slug: field.slug, type: field.type, received: raw },
202
+ );
203
+ }
204
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
205
+ throw new EmDashValidationError(
206
+ `Byline field "${field.slug}" must use http or https scheme (received "${parsed.protocol}")`,
207
+ { slug: field.slug, type: field.type, received: raw, protocol: parsed.protocol },
208
+ );
209
+ }
210
+ return raw;
211
+ }
212
+ case "boolean": {
213
+ if (typeof raw !== "boolean") {
214
+ throw new EmDashValidationError(
215
+ `Byline field "${field.slug}" expects a boolean value (received ${typeof raw})`,
216
+ { slug: field.slug, type: field.type, received: typeof raw },
217
+ );
218
+ }
219
+ return raw;
220
+ }
221
+ case "select": {
222
+ if (typeof raw !== "string") {
223
+ throw new EmDashValidationError(
224
+ `Byline field "${field.slug}" expects a string value (received ${typeof raw})`,
225
+ { slug: field.slug, type: field.type, received: typeof raw },
226
+ );
227
+ }
228
+ const options = field.validation?.options ?? [];
229
+ if (!options.includes(raw)) {
230
+ throw new EmDashValidationError(
231
+ `Byline field "${field.slug}" value "${raw}" is not one of the registered choices`,
232
+ { slug: field.slug, value: raw, options },
233
+ );
234
+ }
235
+ return raw;
236
+ }
237
+ }
238
+ }
239
+
73
240
  /**
74
241
  * Byline repository for content credits.
75
242
  *
@@ -91,13 +258,248 @@ function rowToByline(row: BylineRow): BylineSummary {
91
258
  export class BylineRepository {
92
259
  constructor(private db: Kysely<Database>) {}
93
260
 
261
+ // ============================================
262
+ // Custom-field hydration (Phase 3 of #1174)
263
+ // ============================================
264
+
265
+ /**
266
+ * Merge `customFields` onto each `BylineSummary` produced from the
267
+ * given rows. Two batched queries total — one against
268
+ * `_emdash_byline_field_values` (keyed by `byline_id`), one against
269
+ * `_emdash_byline_field_group_values` (keyed by `translation_group`)
270
+ * — both chunked at `SQL_BATCH_SIZE` for D1's bound-parameter cap.
271
+ *
272
+ * When zero fields are registered, every row gets `customFields = {}`
273
+ * with no value-table reads (the field-defs cache returns `[]`).
274
+ * Group-shared values are looked up via the row's `translation_group`,
275
+ * so every locale sibling of the same byline identity sees the same
276
+ * non-translatable value without re-reading per row.
277
+ *
278
+ * **Duplicate-row handling.** Callers (notably `getContentBylinesMany`
279
+ * for list views with repeated authors) can pass the same byline row
280
+ * multiple times. We assign values by *iterating both `rows` and
281
+ * `summaries` in lockstep by index*, not by deduping into a Map keyed
282
+ * on byline id. A Map approach silently drops earlier duplicates' merge
283
+ * step (last writer wins, earlier instances keep their initial `{}`).
284
+ * Iterating by index gives every duplicate its own merged copy.
285
+ *
286
+ * Hydration is *strict per row* — values are merged onto whichever
287
+ * `BylineRow` produced them. Fallback semantics (e.g. "if no value
288
+ * for this locale, show the default-locale value") are not the
289
+ * repository's concern; consumers layer them on top if wanted, the
290
+ * same way `BylineRepository` doesn't resolve locale fallback for
291
+ * the base byline lookup.
292
+ */
293
+ private async withCustomFields(rows: BylineRow[]): Promise<BylineSummary[]> {
294
+ const summaries = rows.map(rowToByline);
295
+ // Always populate `customFields = {}` (PR plan AC #6) — even when
296
+ // no fields are registered, every BylineSummary carries the empty
297
+ // object. A fresh object per summary so duplicate rows don't share
298
+ // state.
299
+ for (const summary of summaries) {
300
+ summary.customFields = {};
301
+ }
302
+ await this.applyCustomFieldsTo(summaries);
303
+ return summaries;
304
+ }
305
+
306
+ private async withCustomFieldsOne(row: BylineRow | undefined): Promise<BylineSummary | null> {
307
+ if (!row) return null;
308
+ const [result] = await this.withCustomFields([row]);
309
+ return result ?? null;
310
+ }
311
+
312
+ /**
313
+ * Hydrate `customFields` on each `BylineSummary`, mutating in place.
314
+ *
315
+ * The public entry point for callers that fetch byline rows in
316
+ * multiple passes (e.g. `getBylinesForEntries`, which buckets by
317
+ * locale and calls `getContentBylinesMany` per bucket) and want a
318
+ * single batched hydration over the union of bylines, not one per
319
+ * pass. Use with the `skipHydration` option on the read methods to
320
+ * defer customFields work to a single call here.
321
+ *
322
+ * Two batched queries total (translatable + group-shared) regardless
323
+ * of how many bylines, locales, or translation_groups are in the
324
+ * input — meets the Phase 3 query-count envelope for mixed-locale
325
+ * list views even when sibling locales reference disjoint
326
+ * translation_groups.
327
+ *
328
+ * Replaces any existing `customFields` on each summary with a freshly
329
+ * fetched map. Callers that want to merge rather than replace should
330
+ * not use this entry point.
331
+ */
332
+ async hydrateBylineCustomFields(summaries: BylineSummary[]): Promise<void> {
333
+ for (const summary of summaries) {
334
+ summary.customFields = {};
335
+ }
336
+ await this.applyCustomFieldsTo(summaries);
337
+ }
338
+
339
+ /**
340
+ * Shared merge engine for `withCustomFields` and
341
+ * `hydrateBylineCustomFields`. Reads field defs (cached), batches the
342
+ * translatable + group-shared fetches, and walks `summaries` directly
343
+ * to apply values.
344
+ *
345
+ * Iterates `summaries` (not a `summaryById` map) so duplicate
346
+ * `BylineSummary` objects sharing the same `id` — e.g. the same
347
+ * author credited to multiple entries — each get their own merged
348
+ * values. The previous Map-based dedup silently dropped earlier
349
+ * duplicates' merge step.
350
+ */
351
+ private async applyCustomFieldsTo(summaries: BylineSummary[]): Promise<void> {
352
+ if (summaries.length === 0) return;
353
+
354
+ const defs = await getBylineFieldDefs(this.db);
355
+ if (defs.length === 0) return;
356
+
357
+ const fieldById = new Map(defs.map((d) => [d.id, d]));
358
+
359
+ // Translatable values, batched by byline_id (unique per locale, so
360
+ // IDs across different locale buckets don't collide — one batched
361
+ // query covers everything).
362
+ const translatableByByline = new Map<string, Map<string, string | null>>();
363
+ const bylineIds = [...new Set(summaries.map((s) => s.id))];
364
+ for (const chunk of chunks(bylineIds, SQL_BATCH_SIZE)) {
365
+ const trRows = await this.db
366
+ .selectFrom("_emdash_byline_field_values")
367
+ .select(["byline_id", "field_id", "value"])
368
+ .where("byline_id", "in", chunk)
369
+ .execute();
370
+ for (const trRow of trRows) {
371
+ let fieldMap = translatableByByline.get(trRow.byline_id);
372
+ if (!fieldMap) {
373
+ fieldMap = new Map();
374
+ translatableByByline.set(trRow.byline_id, fieldMap);
375
+ }
376
+ fieldMap.set(trRow.field_id, trRow.value);
377
+ }
378
+ }
379
+
380
+ // Group-shared values, batched over the union of translation_groups,
381
+ // with per-group request-cache priming so subsequent calls within
382
+ // the same request share the lookup. Together with the
383
+ // `hydrateBylineCustomFields` + `skipHydration` flow in
384
+ // `getBylinesForEntries`, this keeps mixed-locale list views to
385
+ // **one** group-shared query per request, even for disjoint
386
+ // translation_groups across locale buckets.
387
+ const groups = [
388
+ ...new Set(
389
+ summaries
390
+ .map((s) => s.translationGroup)
391
+ .filter((g): g is string => typeof g === "string" && g.length > 0),
392
+ ),
393
+ ];
394
+ const groupByGroup = await this.loadGroupValuesByIds(groups);
395
+
396
+ // Each loop gates on `field.translatable` so a row in the wrong
397
+ // owner table (e.g. left over from a translatable flip) can't
398
+ // leak into hydration.
399
+ for (const summary of summaries) {
400
+ const trValues = translatableByByline.get(summary.id);
401
+ if (trValues) {
402
+ for (const [fieldId, value] of trValues) {
403
+ const field = fieldById.get(fieldId);
404
+ if (!field || !field.translatable) continue;
405
+ assignCustomFieldValue(summary, field, value);
406
+ }
407
+ }
408
+
409
+ if (summary.translationGroup) {
410
+ const grpValues = groupByGroup.get(summary.translationGroup);
411
+ if (grpValues) {
412
+ for (const [fieldId, value] of grpValues) {
413
+ const field = fieldById.get(fieldId);
414
+ if (!field || field.translatable) continue;
415
+ assignCustomFieldValue(summary, field, value);
416
+ }
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Resolve the group-shared custom-field values for a set of
424
+ * translation_groups, sharing work across hydration calls within the
425
+ * same request via per-group `requestCached` entries.
426
+ *
427
+ * The non-translatable storage table (`_emdash_byline_field_group_values`)
428
+ * is keyed by `translation_group`, which is locale-agnostic. Combining
429
+ * this method with `skipHydration` on `getContentBylinesMany` and a
430
+ * single `hydrateBylineCustomFields` call (see
431
+ * `getBylinesForEntries`) keeps mixed-locale list hydration to **one**
432
+ * batched group-shared SQL per request — even with disjoint
433
+ * translation_groups across locale buckets. Solo callers (`findById`,
434
+ * `findMany`, etc.) still get the same per-call batching they had
435
+ * before; the cache simply means a second call in the same request
436
+ * for an overlapping group is free.
437
+ *
438
+ * Cache key: `byline-field-group-values:${groupId}` — one entry per
439
+ * group. Writes use `setRequestCacheEntry` (idempotent, doesn't
440
+ * overwrite); `BylineRepository.update` calls `clearRequestCacheEntry`
441
+ * after a group-shared write to keep the cache fresh within the same
442
+ * request.
443
+ */
444
+ private async loadGroupValuesByIds(
445
+ groups: string[],
446
+ ): Promise<Map<string, Map<string, string | null>>> {
447
+ const result = new Map<string, Map<string, string | null>>();
448
+ if (groups.length === 0) return result;
449
+
450
+ // First pass: pull any already-cached groups from the request scope.
451
+ const missing: string[] = [];
452
+ for (const g of groups) {
453
+ const cached = peekRequestCache<Map<string, string | null>>(`byline-field-group-values:${g}`);
454
+ if (cached) {
455
+ result.set(g, await cached);
456
+ } else {
457
+ missing.push(g);
458
+ }
459
+ }
460
+
461
+ if (missing.length === 0) return result;
462
+
463
+ // Second pass: one batched SQL for the union of all missing groups
464
+ // (chunked for D1's bound-parameter cap). Initialise empty maps for
465
+ // missing groups so the primed cache covers "this group has no
466
+ // values" — preventing a re-fetch on subsequent calls.
467
+ const fetched = new Map<string, Map<string, string | null>>();
468
+ for (const g of missing) fetched.set(g, new Map());
469
+ for (const chunk of chunks(missing, SQL_BATCH_SIZE)) {
470
+ const grpRows = await this.db
471
+ .selectFrom("_emdash_byline_field_group_values")
472
+ .select(["translation_group", "field_id", "value"])
473
+ .where("translation_group", "in", chunk)
474
+ .execute();
475
+ for (const grpRow of grpRows) {
476
+ const fieldMap = fetched.get(grpRow.translation_group);
477
+ if (!fieldMap) continue;
478
+ fieldMap.set(grpRow.field_id, grpRow.value);
479
+ }
480
+ }
481
+
482
+ for (const g of missing) {
483
+ const m = fetched.get(g);
484
+ if (!m) continue;
485
+ setRequestCacheEntry(`byline-field-group-values:${g}`, m);
486
+ result.set(g, m);
487
+ }
488
+
489
+ return result;
490
+ }
491
+
492
+ // ============================================
493
+ // Reads
494
+ // ============================================
495
+
94
496
  async findById(id: string): Promise<BylineSummary | null> {
95
497
  const row = await this.db
96
498
  .selectFrom("_emdash_bylines")
97
499
  .selectAll()
98
500
  .where("id", "=", id)
99
501
  .executeTakeFirst();
100
- return row ? rowToByline(row) : null;
502
+ return this.withCustomFieldsOne(row);
101
503
  }
102
504
 
103
505
  /**
@@ -109,7 +511,7 @@ export class BylineRepository {
109
511
  let query = this.db.selectFrom("_emdash_bylines").selectAll().where("slug", "=", slug);
110
512
  if (options?.locale !== undefined) query = query.where("locale", "=", options.locale);
111
513
  const row = await query.orderBy("locale", "asc").executeTakeFirst();
112
- return row ? rowToByline(row) : null;
514
+ return this.withCustomFieldsOne(row);
113
515
  }
114
516
 
115
517
  /**
@@ -122,7 +524,7 @@ export class BylineRepository {
122
524
  let query = this.db.selectFrom("_emdash_bylines").selectAll().where("user_id", "=", userId);
123
525
  if (options?.locale !== undefined) query = query.where("locale", "=", options.locale);
124
526
  const row = await query.orderBy("locale", "asc").executeTakeFirst();
125
- return row ? rowToByline(row) : null;
527
+ return this.withCustomFieldsOne(row);
126
528
  }
127
529
 
128
530
  async findMany(options?: {
@@ -176,7 +578,8 @@ export class BylineRepository {
176
578
  }
177
579
 
178
580
  const rows = await query.execute();
179
- const items = rows.slice(0, limit).map(rowToByline);
581
+ const pageRows = rows.slice(0, limit);
582
+ const items = await this.withCustomFields(pageRows);
180
583
  const result: FindManyResult<BylineSummary> = { items };
181
584
 
182
585
  if (rows.length > limit) {
@@ -211,15 +614,120 @@ export class BylineRepository {
211
614
  .where("translation_group", "=", translationGroup)
212
615
  .orderBy("locale", "asc")
213
616
  .execute();
214
- return rows.map(rowToByline);
617
+ return this.withCustomFields(rows);
618
+ }
619
+
620
+ /**
621
+ * Validate a `customFields` input map into a write list before any row
622
+ * write — throws `EmDashValidationError` on unknown slugs, type
623
+ * mismatches, or select-choice misses.
624
+ */
625
+ private async resolveCustomFieldWrites(
626
+ customFields: Record<string, unknown> | undefined,
627
+ ): Promise<Array<{ field: BylineFieldDefinition; value: CustomFieldValue }>> {
628
+ if (!customFields || Object.keys(customFields).length === 0) return [];
629
+ const defs = await getBylineFieldDefs(this.db);
630
+ const bySlug = new Map(defs.map((d) => [d.slug, d]));
631
+ const writes: Array<{ field: BylineFieldDefinition; value: CustomFieldValue }> = [];
632
+ for (const [slug, raw] of Object.entries(customFields)) {
633
+ const field = bySlug.get(slug);
634
+ if (!field) {
635
+ throw new EmDashValidationError(`Unknown byline custom field "${slug}"`, {
636
+ slug,
637
+ registered: defs.map((d) => d.slug),
638
+ });
639
+ }
640
+ writes.push({ field, value: coerceFieldValue(field, raw) });
641
+ }
642
+ return writes;
643
+ }
644
+
645
+ /**
646
+ * Write a validated custom-field list against a byline row inside the
647
+ * caller's transaction. Per-field writes route to
648
+ * `_emdash_byline_field_values` (translatable) or
649
+ * `_emdash_byline_field_group_values` (group-shared); `null` clears.
650
+ * Returns `true` when any group-shared row was touched so the caller
651
+ * can invalidate the per-request cache post-commit.
652
+ */
653
+ private async applyCustomFieldWritesInTrx(
654
+ trx: Kysely<Database>,
655
+ bylineId: string,
656
+ translationGroup: string,
657
+ writes: Array<{ field: BylineFieldDefinition; value: CustomFieldValue }>,
658
+ now: string,
659
+ ): Promise<boolean> {
660
+ if (writes.length === 0) return false;
661
+ let touchedGroupShared = false;
662
+ for (const { field, value } of writes) {
663
+ if (!field.translatable) touchedGroupShared = true;
664
+ if (field.translatable) {
665
+ if (value === null) {
666
+ await trx
667
+ .deleteFrom("_emdash_byline_field_values")
668
+ .where("byline_id", "=", bylineId)
669
+ .where("field_id", "=", field.id)
670
+ .execute();
671
+ } else {
672
+ const encoded = JSON.stringify(value);
673
+ await trx
674
+ .insertInto("_emdash_byline_field_values")
675
+ .values({
676
+ byline_id: bylineId,
677
+ field_id: field.id,
678
+ value: encoded,
679
+ created_at: now,
680
+ updated_at: now,
681
+ })
682
+ .onConflict((oc) =>
683
+ oc.columns(["byline_id", "field_id"]).doUpdateSet({
684
+ value: encoded,
685
+ updated_at: now,
686
+ }),
687
+ )
688
+ .execute();
689
+ }
690
+ } else {
691
+ if (value === null) {
692
+ await trx
693
+ .deleteFrom("_emdash_byline_field_group_values")
694
+ .where("translation_group", "=", translationGroup)
695
+ .where("field_id", "=", field.id)
696
+ .execute();
697
+ } else {
698
+ const encoded = JSON.stringify(value);
699
+ await trx
700
+ .insertInto("_emdash_byline_field_group_values")
701
+ .values({
702
+ translation_group: translationGroup,
703
+ field_id: field.id,
704
+ value: encoded,
705
+ created_at: now,
706
+ updated_at: now,
707
+ })
708
+ .onConflict((oc) =>
709
+ oc.columns(["translation_group", "field_id"]).doUpdateSet({
710
+ value: encoded,
711
+ updated_at: now,
712
+ }),
713
+ )
714
+ .execute();
715
+ }
716
+ }
717
+ }
718
+ return touchedGroupShared;
215
719
  }
216
720
 
217
721
  async create(input: CreateBylineInput): Promise<BylineSummary> {
218
722
  const id = ulid();
219
723
  const now = new Date().toISOString();
220
724
 
221
- // translationOf joins the source byline's group; otherwise we mint a
222
- // fresh group equal to id (matching migration 040's backfill pattern).
725
+ // Validate customFields before opening the transaction so a bad
726
+ // value surfaces as VALIDATION_ERROR without aborting an insert.
727
+ const customFieldWrites = await this.resolveCustomFieldWrites(input.customFields);
728
+
729
+ // translationOf joins the source's group; otherwise mint a fresh
730
+ // group = id (matches migration 040's backfill pattern).
223
731
  let translationGroup: string = id;
224
732
  if (input.translationOf) {
225
733
  const source = await this.findById(input.translationOf);
@@ -227,25 +735,44 @@ export class BylineRepository {
227
735
  translationGroup = source.translationGroup ?? source.id;
228
736
  }
229
737
 
230
- await this.db
231
- .insertInto("_emdash_bylines")
232
- .values({
738
+ // Wrap insert + custom-field writes in one transaction so a
739
+ // partial failure rolls both back on Node/PG. D1 still has its
740
+ // own no-transactions limitation — recovery for that path lives
741
+ // in `handleBylineCreate`.
742
+ let touchedGroupShared = false;
743
+ await withTransaction(this.db, async (trx) => {
744
+ await trx
745
+ .insertInto("_emdash_bylines")
746
+ .values({
747
+ id,
748
+ slug: input.slug,
749
+ display_name: input.displayName,
750
+ bio: input.bio ?? null,
751
+ avatar_media_id: input.avatarMediaId ?? null,
752
+ website_url: input.websiteUrl ?? null,
753
+ user_id: input.userId ?? null,
754
+ is_guest: input.isGuest ? 1 : 0,
755
+ created_at: now,
756
+ updated_at: now,
757
+ // Omit `locale` so the DB DEFAULT (configured defaultLocale)
758
+ // applies — matches TaxonomyRepository.create.
759
+ ...(input.locale !== undefined ? { locale: input.locale } : {}),
760
+ translation_group: translationGroup,
761
+ })
762
+ .execute();
763
+
764
+ touchedGroupShared = await this.applyCustomFieldWritesInTrx(
765
+ trx,
233
766
  id,
234
- slug: input.slug,
235
- display_name: input.displayName,
236
- bio: input.bio ?? null,
237
- avatar_media_id: input.avatarMediaId ?? null,
238
- website_url: input.websiteUrl ?? null,
239
- user_id: input.userId ?? null,
240
- is_guest: input.isGuest ? 1 : 0,
241
- created_at: now,
242
- updated_at: now,
243
- // When omitted the DB DEFAULT (configured defaultLocale) is used —
244
- // keeps behaviour consistent with TaxonomyRepository.create.
245
- ...(input.locale !== undefined ? { locale: input.locale } : {}),
246
- translation_group: translationGroup,
247
- })
248
- .execute();
767
+ translationGroup,
768
+ customFieldWrites,
769
+ now,
770
+ );
771
+ });
772
+
773
+ if (touchedGroupShared) {
774
+ clearRequestCacheEntry(`byline-field-group-values:${translationGroup}`);
775
+ }
249
776
 
250
777
  const byline = await this.findById(id);
251
778
  if (!byline) {
@@ -258,9 +785,12 @@ export class BylineRepository {
258
785
  const existing = await this.findById(id);
259
786
  if (!existing) return null;
260
787
 
261
- const updates: Record<string, unknown> = {
262
- updated_at: new Date().toISOString(),
263
- };
788
+ // Validate customFields before opening the transaction so a bad
789
+ // value surfaces as VALIDATION_ERROR without aborting an update.
790
+ const customFieldWrites = await this.resolveCustomFieldWrites(input.customFields);
791
+
792
+ const now = new Date().toISOString();
793
+ const updates: Record<string, unknown> = { updated_at: now };
264
794
 
265
795
  if (input.slug !== undefined) updates.slug = input.slug;
266
796
  if (input.displayName !== undefined) updates.display_name = input.displayName;
@@ -270,19 +800,58 @@ export class BylineRepository {
270
800
  if (input.userId !== undefined) updates.user_id = input.userId;
271
801
  if (input.isGuest !== undefined) updates.is_guest = input.isGuest ? 1 : 0;
272
802
 
273
- await this.db.updateTable("_emdash_bylines").set(updates).where("id", "=", id).execute();
803
+ const group = existing.translationGroup ?? existing.id;
804
+ // Wrap row update + custom-field writes in one transaction so a
805
+ // partial failure rolls both back on Node/PG. The post-commit
806
+ // invalidation below clears the per-request cache that the
807
+ // top-of-method `findById` populated for this group.
808
+ let touchedGroupShared = false;
809
+ await withTransaction(this.db, async (trx) => {
810
+ await trx.updateTable("_emdash_bylines").set(updates).where("id", "=", id).execute();
811
+ touchedGroupShared = await this.applyCustomFieldWritesInTrx(
812
+ trx,
813
+ id,
814
+ group,
815
+ customFieldWrites,
816
+ now,
817
+ );
818
+ });
819
+
820
+ if (touchedGroupShared) {
821
+ clearRequestCacheEntry(`byline-field-group-values:${group}`);
822
+ }
823
+
274
824
  return await this.findById(id);
275
825
  }
276
826
 
277
827
  /**
278
828
  * Delete a byline row. When this row is the last sibling in its
279
- * translation group, also drops every junction row pointing at the group
280
- * and clears `primary_byline_id` references. When other siblings remain
281
- * in the group, junctions and `primary_byline_id` pointers stay intact —
282
- * the credit lives on at other locales.
829
+ * translation group, also drops every junction row pointing at the group,
830
+ * clears `primary_byline_id` references, and removes the byline's
831
+ * non-translatable custom-field values. When other siblings remain in
832
+ * the group, junctions, `primary_byline_id` pointers, and group-shared
833
+ * custom-field values stay intact — the credit (and its shared metadata)
834
+ * lives on at other locales.
835
+ *
836
+ * **Application-level cascade.** The byline domain has standardised on
837
+ * app-level cascade rather than trusting FK ON DELETE CASCADE, partly
838
+ * because migration 040 had to strip its own FK to support the
839
+ * translation_group remap (#1021), and partly so cleanup doesn't
840
+ * depend on `PRAGMA foreign_keys = ON` (set in production via
841
+ * `connection.ts:60`, but easy to bypass in tests, scripts, and
842
+ * one-off tools). Every byline-related deletion table is cleared
843
+ * explicitly here:
283
844
  *
284
- * Migration 040 dropped the FK on `_emdash_content_bylines.byline_id`, so
285
- * this cascade is implemented here in application code.
845
+ * - `_emdash_byline_field_values` (per-byline translatable values)
846
+ * migration 041 declares FK ON DELETE CASCADE on `byline_id`; the
847
+ * explicit DELETE removes the dependency on that pragma.
848
+ * - `_emdash_content_bylines` — migration 040 dropped its FK.
849
+ * - `ec_*.primary_byline_id` — never had an FK.
850
+ * - `_emdash_byline_field_group_values` (translation-group-keyed) —
851
+ * keyed by a text column with no FK to bylines, so app-level cleanup
852
+ * is the only path.
853
+ *
854
+ * The FKs that remain (migration 041) serve as defense-in-depth.
286
855
  */
287
856
  async delete(id: string): Promise<boolean> {
288
857
  const existing = await this.findById(id);
@@ -291,6 +860,14 @@ export class BylineRepository {
291
860
  const group = existing.translationGroup ?? existing.id;
292
861
 
293
862
  await withTransaction(this.db, async (trx) => {
863
+ // Per-row translatable custom-field values. Done BEFORE the
864
+ // byline row delete so the application-level cleanup is
865
+ // observable in the transaction log even if FK enforcement is
866
+ // off; migration 041's FK ON DELETE CASCADE would catch any
867
+ // row we miss, but the explicit DELETE is what the rest of
868
+ // the byline domain expects to see.
869
+ await trx.deleteFrom("_emdash_byline_field_values").where("byline_id", "=", id).execute();
870
+
294
871
  await trx.deleteFrom("_emdash_bylines").where("id", "=", id).execute();
295
872
 
296
873
  // Count remaining siblings in the translation group. If none
@@ -307,6 +884,19 @@ export class BylineRepository {
307
884
  // Last sibling gone: cascade in application code.
308
885
  await trx.deleteFrom("_emdash_content_bylines").where("byline_id", "=", group).execute();
309
886
 
887
+ // Group-shared custom-field values are keyed by translation_group
888
+ // (no FK to bylines), so they don't cascade with the byline row.
889
+ // Clean them up explicitly so deleting the last sibling of an
890
+ // identity doesn't leave orphan group values pointing at a
891
+ // vanished translation group. Per-row translatable values
892
+ // (`_emdash_byline_field_values` keyed by byline_id) already
893
+ // cascaded when each sibling row was deleted, so no extra
894
+ // cleanup is needed for that table.
895
+ await trx
896
+ .deleteFrom("_emdash_byline_field_group_values")
897
+ .where("translation_group", "=", group)
898
+ .execute();
899
+
310
900
  const tableNames = await listTablesLike(trx, "ec_%");
311
901
  for (const tableName of tableNames) {
312
902
  validateIdentifier(tableName, "content table");
@@ -336,6 +926,7 @@ export class BylineRepository {
336
926
  let query = this.db
337
927
  .selectFrom("_emdash_content_bylines as cb")
338
928
  .innerJoin("_emdash_bylines as b", "b.translation_group", "cb.byline_id")
929
+ .leftJoin("media as m", "m.id", "b.avatar_media_id")
339
930
  .select([
340
931
  "cb.sort_order as sort_order",
341
932
  "cb.role_label as role_label",
@@ -344,6 +935,8 @@ export class BylineRepository {
344
935
  "b.display_name as display_name",
345
936
  "b.bio as bio",
346
937
  "b.avatar_media_id as avatar_media_id",
938
+ "m.storage_key as avatar_storage_key",
939
+ "m.alt as avatar_alt",
347
940
  "b.website_url as website_url",
348
941
  "b.user_id as user_id",
349
942
  "b.is_guest as is_guest",
@@ -358,11 +951,42 @@ export class BylineRepository {
358
951
  if (options?.locale !== undefined) query = query.where("b.locale", "=", options.locale);
359
952
 
360
953
  const rows = await query.execute();
361
- return rows.map((row) => ({
362
- byline: rowToByline(row),
363
- sortOrder: row.sort_order,
364
- roleLabel: row.role_label,
954
+ // Reconstruct byline rows to feed `withCustomFields`. The JOIN selects
955
+ // the `BylineRow` columns under the `b.` alias plus the avatar media
956
+ // columns from the `media` LEFT JOIN; carry both through so
957
+ // `rowToByline` can populate `avatarStorageKey`/`avatarAlt` (otherwise
958
+ // the join runs but its values are dropped here).
959
+ const bylineRows: BylineRowWithAvatar[] = rows.map((row) => ({
960
+ id: row.id,
961
+ slug: row.slug,
962
+ display_name: row.display_name,
963
+ bio: row.bio,
964
+ avatar_media_id: row.avatar_media_id,
965
+ avatar_storage_key: row.avatar_storage_key,
966
+ avatar_alt: row.avatar_alt,
967
+ website_url: row.website_url,
968
+ user_id: row.user_id,
969
+ is_guest: row.is_guest,
970
+ created_at: row.created_at,
971
+ updated_at: row.updated_at,
972
+ locale: row.locale,
973
+ translation_group: row.translation_group,
365
974
  }));
975
+ const hydrated = await this.withCustomFields(bylineRows);
976
+ return rows.map((row, i) => {
977
+ const byline = hydrated[i];
978
+ if (!byline) {
979
+ // Defensive: hydrated and rows are produced in lock-step;
980
+ // this branch is unreachable unless `withCustomFields`
981
+ // breaks its contract.
982
+ throw new Error("getContentBylines: hydration row count mismatch");
983
+ }
984
+ return {
985
+ byline,
986
+ sortOrder: row.sort_order,
987
+ roleLabel: row.role_label,
988
+ };
989
+ });
366
990
  }
367
991
 
368
992
  /**
@@ -414,11 +1038,20 @@ export class BylineRepository {
414
1038
  * When callers need per-entry-locale filtering (e.g. a list endpoint
415
1039
  * returning entries at mixed locales), they should group the input ids by
416
1040
  * the entry's locale and call this method once per group.
1041
+ *
1042
+ * When the caller will issue multiple `getContentBylinesMany` calls in
1043
+ * one request (e.g. per locale bucket) and wants a *single* batched
1044
+ * customFields hydration over the union of returned bylines, pass
1045
+ * `skipHydration: true` on each call and finish with
1046
+ * `hydrateBylineCustomFields(allBylines)`. The returned bylines carry
1047
+ * `customFields = {}` until that hydration call runs — matching the
1048
+ * "always populated" invariant from AC #6 — so callers that forget to
1049
+ * hydrate get an empty map rather than `undefined`.
417
1050
  */
418
1051
  async getContentBylinesMany(
419
1052
  collectionSlug: string,
420
1053
  contentIds: string[],
421
- options?: { locale?: string },
1054
+ options?: { locale?: string; skipHydration?: boolean },
422
1055
  ): Promise<Map<string, ContentBylineCredit[]>> {
423
1056
  const result = new Map<string, ContentBylineCredit[]>();
424
1057
  if (contentIds.length === 0) return result;
@@ -428,6 +1061,7 @@ export class BylineRepository {
428
1061
  let query = this.db
429
1062
  .selectFrom("_emdash_content_bylines as cb")
430
1063
  .innerJoin("_emdash_bylines as b", "b.translation_group", "cb.byline_id")
1064
+ .leftJoin("media as m", "m.id", "b.avatar_media_id")
431
1065
  .select([
432
1066
  "cb.content_id as content_id",
433
1067
  "cb.sort_order as sort_order",
@@ -437,6 +1071,8 @@ export class BylineRepository {
437
1071
  "b.display_name as display_name",
438
1072
  "b.bio as bio",
439
1073
  "b.avatar_media_id as avatar_media_id",
1074
+ "m.storage_key as avatar_storage_key",
1075
+ "m.alt as avatar_alt",
440
1076
  "b.website_url as website_url",
441
1077
  "b.user_id as user_id",
442
1078
  "b.is_guest as is_guest",
@@ -451,11 +1087,45 @@ export class BylineRepository {
451
1087
  if (options?.locale !== undefined) query = query.where("b.locale", "=", options.locale);
452
1088
 
453
1089
  const rows = await query.execute();
1090
+ // Carry the avatar media columns from the LEFT JOIN through the
1091
+ // reshape so `rowToByline` can populate avatarStorageKey/avatarAlt.
1092
+ const bylineRows: BylineRowWithAvatar[] = rows.map((row) => ({
1093
+ id: row.id,
1094
+ slug: row.slug,
1095
+ display_name: row.display_name,
1096
+ bio: row.bio,
1097
+ avatar_media_id: row.avatar_media_id,
1098
+ avatar_storage_key: row.avatar_storage_key,
1099
+ avatar_alt: row.avatar_alt,
1100
+ website_url: row.website_url,
1101
+ user_id: row.user_id,
1102
+ is_guest: row.is_guest,
1103
+ created_at: row.created_at,
1104
+ updated_at: row.updated_at,
1105
+ locale: row.locale,
1106
+ translation_group: row.translation_group,
1107
+ }));
454
1108
 
455
- for (const row of rows) {
1109
+ // When `skipHydration` is set, return BylineSummary objects with
1110
+ // `customFields = {}`. The caller is responsible for batching
1111
+ // `hydrateBylineCustomFields` across multiple
1112
+ // `getContentBylinesMany` calls. Otherwise hydrate per-call —
1113
+ // the historical behaviour for solo callers.
1114
+ let bylines: BylineSummary[];
1115
+ if (options?.skipHydration === true) {
1116
+ bylines = bylineRows.map(rowToByline);
1117
+ for (const b of bylines) b.customFields = {};
1118
+ } else {
1119
+ bylines = await this.withCustomFields(bylineRows);
1120
+ }
1121
+
1122
+ for (let i = 0; i < rows.length; i++) {
1123
+ const row = rows[i];
1124
+ const byline = bylines[i];
1125
+ if (!row || !byline) continue;
456
1126
  const contentId = row.content_id;
457
1127
  const credit: ContentBylineCredit = {
458
- byline: rowToByline(row),
1128
+ byline,
459
1129
  sortOrder: row.sort_order,
460
1130
  roleLabel: row.role_label,
461
1131
  };
@@ -474,24 +1144,62 @@ export class BylineRepository {
474
1144
  /**
475
1145
  * Batch-fetch byline profiles linked to user IDs in a single query.
476
1146
  * Strict-locale variant of `findByUserId`.
1147
+ *
1148
+ * `skipHydration: true` returns bylines with `customFields = {}` so
1149
+ * callers issuing multiple `findByUserIds` calls in one request (e.g.
1150
+ * the per-locale-bucket author-fallback path in `getBylinesForEntries`)
1151
+ * can defer customFields hydration to a single batched
1152
+ * `hydrateBylineCustomFields` call across the union — keeping the
1153
+ * Phase 3 query-count envelope at "+1 group-shared query per
1154
+ * hydration pass" even when buckets fetch disjoint author bylines.
477
1155
  */
478
1156
  async findByUserIds(
479
1157
  userIds: string[],
480
- options?: { locale?: string },
1158
+ options?: { locale?: string; skipHydration?: boolean },
481
1159
  ): Promise<Map<string, BylineSummary>> {
482
1160
  const result = new Map<string, BylineSummary>();
483
1161
  if (userIds.length === 0) return result;
484
1162
 
485
1163
  for (const chunk of chunks(userIds, SQL_BATCH_SIZE)) {
486
- let query = this.db.selectFrom("_emdash_bylines").selectAll().where("user_id", "in", chunk);
487
- if (options?.locale !== undefined) query = query.where("locale", "=", options.locale);
1164
+ // LEFT JOIN media so author-inferred bylines (the fallback path in
1165
+ // `getBylinesForEntries`) carry the same render-ready avatar storage
1166
+ // key as explicitly-credited bylines do.
1167
+ let query = this.db
1168
+ .selectFrom("_emdash_bylines as b")
1169
+ .leftJoin("media as m", "m.id", "b.avatar_media_id")
1170
+ .select([
1171
+ "b.id as id",
1172
+ "b.slug as slug",
1173
+ "b.display_name as display_name",
1174
+ "b.bio as bio",
1175
+ "b.avatar_media_id as avatar_media_id",
1176
+ "m.storage_key as avatar_storage_key",
1177
+ "m.alt as avatar_alt",
1178
+ "b.website_url as website_url",
1179
+ "b.user_id as user_id",
1180
+ "b.is_guest as is_guest",
1181
+ "b.created_at as created_at",
1182
+ "b.updated_at as updated_at",
1183
+ "b.locale as locale",
1184
+ "b.translation_group as translation_group",
1185
+ ])
1186
+ .where("b.user_id", "in", chunk);
1187
+ if (options?.locale !== undefined) query = query.where("b.locale", "=", options.locale);
488
1188
 
489
1189
  const rows = await query.execute();
1190
+ let bylines: BylineSummary[];
1191
+ if (options?.skipHydration === true) {
1192
+ bylines = rows.map(rowToByline);
1193
+ for (const b of bylines) b.customFields = {};
1194
+ } else {
1195
+ bylines = await this.withCustomFields(rows);
1196
+ }
490
1197
 
491
- for (const row of rows) {
492
- if (row.user_id) {
493
- result.set(row.user_id, rowToByline(row));
494
- }
1198
+ for (let i = 0; i < rows.length; i++) {
1199
+ const row = rows[i];
1200
+ const summary = bylines[i];
1201
+ if (!row || !summary || !row.user_id) continue;
1202
+ result.set(row.user_id, summary);
495
1203
  }
496
1204
  }
497
1205
  return result;