emdash 0.7.0 → 0.9.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 (354) hide show
  1. package/dist/{adapters-Di31kZ28.d.mts → adapters-DoNJiveC.d.mts} +1 -1
  2. package/dist/{adapters-Di31kZ28.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
  3. package/dist/{apply-5uslYdUu.mjs → apply-BzltprvY.mjs} +90 -139
  4. package/dist/apply-BzltprvY.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +194 -17
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +6 -7
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +34 -57
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  14. package/dist/astro/middleware/redirect.mjs +17 -12
  15. package/dist/astro/middleware/redirect.mjs.map +1 -1
  16. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  17. package/dist/astro/middleware/request-context.mjs +9 -6
  18. package/dist/astro/middleware/request-context.mjs.map +1 -1
  19. package/dist/astro/middleware/setup.mjs +1 -1
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +301 -165
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +34 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
  26. package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
  27. package/dist/{byline-C4OVd8b3.mjs → byline-BSaNL1w7.mjs} +5 -5
  28. package/dist/byline-BSaNL1w7.mjs.map +1 -0
  29. package/dist/bylines-CvJ3PYz2.mjs +113 -0
  30. package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
  31. package/dist/cache-C6N_hhN7.mjs +65 -0
  32. package/dist/cache-C6N_hhN7.mjs.map +1 -0
  33. package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
  34. package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
  35. package/dist/cli/index.mjs +229 -31
  36. package/dist/cli/index.mjs.map +1 -1
  37. package/dist/client/cf-access.d.mts +1 -1
  38. package/dist/client/index.d.mts +1 -1
  39. package/dist/client/index.mjs +3 -3
  40. package/dist/client/index.mjs.map +1 -1
  41. package/dist/{config-BXwuX8Bx.mjs → config-BI0V3ICQ.mjs} +1 -1
  42. package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
  43. package/dist/{content-D7J5y73J.mjs → content-8lOYF0pr.mjs} +43 -28
  44. package/dist/content-8lOYF0pr.mjs.map +1 -0
  45. package/dist/db/index.d.mts +3 -3
  46. package/dist/db/index.mjs +2 -2
  47. package/dist/db/libsql.d.mts +1 -1
  48. package/dist/db/libsql.d.mts.map +1 -1
  49. package/dist/db/libsql.mjs +7 -2
  50. package/dist/db/libsql.mjs.map +1 -1
  51. package/dist/db/postgres.d.mts +1 -1
  52. package/dist/db/sqlite.d.mts +1 -1
  53. package/dist/db/sqlite.d.mts.map +1 -1
  54. package/dist/db/sqlite.mjs +8 -3
  55. package/dist/db/sqlite.mjs.map +1 -1
  56. package/dist/{db-errors-D0UT85nC.mjs → db-errors-WRezodiz.mjs} +1 -1
  57. package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
  58. package/dist/{default-CME5YdZ3.mjs → default-D8ksjWhO.mjs} +1 -1
  59. package/dist/{default-CME5YdZ3.mjs.map → default-D8ksjWhO.mjs.map} +1 -1
  60. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  61. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  62. package/dist/{error-CiYn9yDu.mjs → error-D_-tqP-I.mjs} +1 -1
  63. package/dist/error-D_-tqP-I.mjs.map +1 -0
  64. package/dist/{index-De6_Xv3v.d.mts → index-BFRaVcD6.d.mts} +243 -40
  65. package/dist/index-BFRaVcD6.d.mts.map +1 -0
  66. package/dist/index.d.mts +11 -11
  67. package/dist/index.mjs +29 -25
  68. package/dist/{load-CBcmDIot.mjs → load-DDqMMvZL.mjs} +2 -2
  69. package/dist/{load-CBcmDIot.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
  70. package/dist/{loader-DeiBJEMe.mjs → loader-CKLbBnhK.mjs} +32 -10
  71. package/dist/loader-CKLbBnhK.mjs.map +1 -0
  72. package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DqWNC3lM.mjs} +45 -3
  73. package/dist/manifest-schema-DqWNC3lM.mjs.map +1 -0
  74. package/dist/media/index.d.mts +1 -1
  75. package/dist/media/index.mjs +1 -1
  76. package/dist/media/local-runtime.d.mts +7 -7
  77. package/dist/media/local-runtime.mjs +3 -3
  78. package/dist/{media-DqHVh136.mjs → media-BW32b4gi.mjs} +4 -7
  79. package/dist/media-BW32b4gi.mjs.map +1 -0
  80. package/dist/{mode-CpNnGkPz.mjs → mode-ier8jbBk.mjs} +1 -1
  81. package/dist/mode-ier8jbBk.mjs.map +1 -0
  82. package/dist/options-BVp3UsTS.mjs +117 -0
  83. package/dist/options-BVp3UsTS.mjs.map +1 -0
  84. package/dist/page/index.d.mts +2 -2
  85. package/dist/{placeholder-tzpqGWII.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
  86. package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
  87. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
  88. package/dist/placeholder-CIJejMlK.mjs.map +1 -0
  89. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  90. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  91. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  92. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  93. package/dist/public-url-DByxYjUw.mjs +51 -0
  94. package/dist/public-url-DByxYjUw.mjs.map +1 -0
  95. package/dist/{query-g4Ug-9j9.mjs → query-Cg9ZKRQ0.mjs} +114 -16
  96. package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
  97. package/dist/{redirect-CN0Rt9Ob.mjs → redirect-BhUBKRc1.mjs} +13 -8
  98. package/dist/redirect-BhUBKRc1.mjs.map +1 -0
  99. package/dist/{registry-Ci3WxVAr.mjs → registry-Dw70ChxB.mjs} +69 -11
  100. package/dist/registry-Dw70ChxB.mjs.map +1 -0
  101. package/dist/{request-cache-DiR961CV.mjs → request-cache-B-bmkipQ.mjs} +1 -1
  102. package/dist/request-cache-B-bmkipQ.mjs.map +1 -0
  103. package/dist/runner-Bnoj7vjK.d.mts +44 -0
  104. package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
  105. package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
  106. package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
  107. package/dist/runtime.d.mts +6 -6
  108. package/dist/runtime.mjs +4 -4
  109. package/dist/{search-B0effn3j.mjs → search-dOGEccMa.mjs} +341 -152
  110. package/dist/search-dOGEccMa.mjs.map +1 -0
  111. package/dist/secrets-CW3reAnU.mjs +314 -0
  112. package/dist/secrets-CW3reAnU.mjs.map +1 -0
  113. package/dist/seed/index.d.mts +2 -2
  114. package/dist/seed/index.mjs +15 -14
  115. package/dist/seo/index.d.mts +1 -1
  116. package/dist/storage/local.d.mts +1 -1
  117. package/dist/storage/local.mjs +1 -1
  118. package/dist/storage/s3.d.mts +1 -1
  119. package/dist/storage/s3.d.mts.map +1 -1
  120. package/dist/storage/s3.mjs +4 -4
  121. package/dist/storage/s3.mjs.map +1 -1
  122. package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
  123. package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
  124. package/dist/{tokens-BFPFx3CA.mjs → tokens-D7zMmWi2.mjs} +2 -2
  125. package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
  126. package/dist/{transport-BykRfpyy.mjs → transport-BeMCmin1.mjs} +6 -5
  127. package/dist/{transport-BykRfpyy.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
  128. package/dist/{transport-H4Iwx7tC.d.mts → transport-DNEfeMaU.d.mts} +1 -1
  129. package/dist/{transport-H4Iwx7tC.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
  130. package/dist/types-4fVtCIm0.mjs +68 -0
  131. package/dist/types-4fVtCIm0.mjs.map +1 -0
  132. package/dist/{types-CnZYHyLW.d.mts → types-BSyXeCFW.d.mts} +24 -2
  133. package/dist/{types-CnZYHyLW.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
  134. package/dist/{types-DgrIP0tF.d.mts → types-BuBIptGk.d.mts} +80 -106
  135. package/dist/types-BuBIptGk.d.mts.map +1 -0
  136. package/dist/{types-BH2L167P.mjs → types-CDbKp7ND.mjs} +1 -1
  137. package/dist/{types-BH2L167P.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
  138. package/dist/{types-DDS4MxsT.mjs → types-CIOg5AR8.mjs} +1 -1
  139. package/dist/{types-DDS4MxsT.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
  140. package/dist/{types-6CUZRrZP.d.mts → types-CJsYGpco.d.mts} +24 -2
  141. package/dist/{types-6CUZRrZP.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
  142. package/dist/types-CRxNbK-Z.mjs +68 -0
  143. package/dist/types-CRxNbK-Z.mjs.map +1 -0
  144. package/dist/{types-C2v0c34j.d.mts → types-CrtWgIvl.d.mts} +1 -1
  145. package/dist/{types-C2v0c34j.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
  146. package/dist/{types-CFWjXmus.d.mts → types-M78DQ1lx.d.mts} +1 -1
  147. package/dist/{types-CFWjXmus.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
  148. package/dist/{validate-CqsNItbt.mjs → validate-Baqf0slj.mjs} +3 -3
  149. package/dist/{validate-CqsNItbt.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
  150. package/dist/{validate-kM8Pjuf7.d.mts → validate-BfQh_C_y.d.mts} +4 -4
  151. package/dist/{validate-kM8Pjuf7.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
  152. package/dist/validation-BfEI7tNe.mjs +144 -0
  153. package/dist/validation-BfEI7tNe.mjs.map +1 -0
  154. package/dist/version-DoxrVdYf.mjs +7 -0
  155. package/dist/{version-BnTKdfam.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
  156. package/dist/zod-generator-CC0xNe_K.mjs +132 -0
  157. package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
  158. package/locals.d.ts +1 -6
  159. package/package.json +21 -7
  160. package/src/api/auth-storage.ts +37 -0
  161. package/src/api/error.ts +6 -0
  162. package/src/api/errors.ts +8 -0
  163. package/src/api/handlers/comments.ts +19 -4
  164. package/src/api/handlers/content.ts +151 -4
  165. package/src/api/handlers/device-flow.ts +5 -0
  166. package/src/api/handlers/index.ts +2 -0
  167. package/src/api/handlers/marketplace.ts +11 -4
  168. package/src/api/handlers/media.ts +8 -1
  169. package/src/api/handlers/menus.ts +160 -21
  170. package/src/api/handlers/oauth-authorization.ts +72 -33
  171. package/src/api/handlers/redirects.ts +16 -3
  172. package/src/api/handlers/revision.ts +23 -14
  173. package/src/api/handlers/sections.ts +8 -1
  174. package/src/api/handlers/taxonomies.ts +131 -22
  175. package/src/api/handlers/validation.ts +212 -0
  176. package/src/api/openapi/document.ts +4 -1
  177. package/src/api/public-url.ts +54 -5
  178. package/src/api/route-utils.ts +14 -0
  179. package/src/api/schemas/comments.ts +2 -2
  180. package/src/api/schemas/common.ts +1 -1
  181. package/src/api/schemas/content.ts +17 -0
  182. package/src/api/schemas/sections.ts +3 -3
  183. package/src/api/schemas/setup.ts +8 -0
  184. package/src/api/schemas/users.ts +1 -1
  185. package/src/api/schemas/widgets.ts +12 -10
  186. package/src/api/setup-complete.ts +40 -0
  187. package/src/api/types.ts +5 -1
  188. package/src/astro/integration/index.ts +30 -2
  189. package/src/astro/integration/routes.ts +28 -0
  190. package/src/astro/integration/runtime.ts +49 -1
  191. package/src/astro/integration/virtual-modules.ts +73 -2
  192. package/src/astro/integration/vite-config.ts +49 -13
  193. package/src/astro/middleware/auth.ts +34 -6
  194. package/src/astro/middleware/redirect.ts +29 -16
  195. package/src/astro/middleware/request-context.ts +15 -5
  196. package/src/astro/middleware.ts +41 -10
  197. package/src/astro/routes/PluginRegistry.tsx +10 -1
  198. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  199. package/src/astro/routes/api/auth/mode.ts +57 -0
  200. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
  201. package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
  202. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  203. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  204. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  205. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  206. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  207. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  208. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  209. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  210. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  211. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  212. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
  213. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  214. package/src/astro/routes/api/content/[collection]/index.ts +1 -9
  215. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  216. package/src/astro/routes/api/import/wordpress/media.ts +2 -7
  217. package/src/astro/routes/api/import/wordpress/prepare.ts +9 -0
  218. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  219. package/src/astro/routes/api/manifest.ts +62 -45
  220. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  221. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  222. package/src/astro/routes/api/openapi.json.ts +27 -10
  223. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  224. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  225. package/src/astro/routes/api/redirects/[id].ts +10 -4
  226. package/src/astro/routes/api/redirects/index.ts +7 -3
  227. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  228. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  229. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  230. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  231. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  232. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  233. package/src/astro/routes/api/search/index.ts +10 -2
  234. package/src/astro/routes/api/sections/[slug].ts +10 -4
  235. package/src/astro/routes/api/sections/index.ts +7 -3
  236. package/src/astro/routes/api/settings/email.ts +4 -9
  237. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  238. package/src/astro/routes/api/setup/admin.ts +8 -2
  239. package/src/astro/routes/api/setup/index.ts +2 -2
  240. package/src/astro/routes/api/setup/status.ts +3 -1
  241. package/src/astro/routes/api/snapshot.ts +44 -18
  242. package/src/astro/routes/api/taxonomies/index.ts +0 -1
  243. package/src/astro/routes/api/themes/preview.ts +11 -5
  244. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
  245. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
  246. package/src/astro/routes/api/widget-areas/[name].ts +4 -1
  247. package/src/astro/routes/api/widget-areas/index.ts +4 -1
  248. package/src/astro/types.ts +32 -3
  249. package/src/auth/allowed-origins.ts +168 -0
  250. package/src/auth/mode.ts +15 -3
  251. package/src/auth/passkey-config.ts +35 -13
  252. package/src/auth/providers/github-admin.tsx +29 -0
  253. package/src/auth/providers/github.ts +31 -0
  254. package/src/auth/providers/google-admin.tsx +44 -0
  255. package/src/auth/providers/google.ts +31 -0
  256. package/src/auth/types.ts +114 -4
  257. package/src/bylines/index.ts +37 -88
  258. package/src/cli/commands/auth.ts +28 -6
  259. package/src/cli/commands/bundle-utils.ts +11 -2
  260. package/src/cli/commands/bundle.ts +31 -9
  261. package/src/cli/commands/content.ts +13 -0
  262. package/src/cli/commands/login.ts +8 -1
  263. package/src/cli/commands/publish.ts +24 -0
  264. package/src/cli/commands/secrets.ts +183 -0
  265. package/src/cli/credentials.ts +1 -1
  266. package/src/cli/index.ts +5 -1
  267. package/src/client/index.ts +4 -4
  268. package/src/client/transport.ts +17 -7
  269. package/src/components/Break.astro +2 -2
  270. package/src/components/EmDashHead.astro +18 -13
  271. package/src/components/EmDashImage.astro +7 -6
  272. package/src/components/Embed.astro +1 -1
  273. package/src/components/Gallery.astro +6 -4
  274. package/src/components/Image.astro +9 -4
  275. package/src/components/InlinePortableTextEditor.tsx +106 -19
  276. package/src/components/LiveSearch.astro +5 -14
  277. package/src/config/secrets.ts +528 -0
  278. package/src/database/dialect-helpers.ts +50 -0
  279. package/src/database/migrations/034_published_at_index.ts +1 -1
  280. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  281. package/src/database/migrations/runner.ts +156 -23
  282. package/src/database/repositories/audit.ts +6 -8
  283. package/src/database/repositories/byline.ts +6 -8
  284. package/src/database/repositories/comment.ts +12 -16
  285. package/src/database/repositories/content.ts +76 -52
  286. package/src/database/repositories/index.ts +1 -1
  287. package/src/database/repositories/media.ts +10 -13
  288. package/src/database/repositories/plugin-storage.ts +4 -6
  289. package/src/database/repositories/redirect.ts +26 -19
  290. package/src/database/repositories/taxonomy.ts +40 -3
  291. package/src/database/repositories/types.ts +57 -8
  292. package/src/database/repositories/user.ts +6 -8
  293. package/src/db/libsql.ts +1 -3
  294. package/src/db/sqlite.ts +2 -5
  295. package/src/emdash-runtime.ts +388 -247
  296. package/src/index.ts +14 -1
  297. package/src/loader.ts +30 -6
  298. package/src/mcp/server.ts +781 -141
  299. package/src/media/normalize.ts +1 -1
  300. package/src/media/url.ts +78 -0
  301. package/src/page/site-identity.ts +58 -0
  302. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  303. package/src/plugins/context.ts +13 -10
  304. package/src/plugins/define-plugin.ts +40 -12
  305. package/src/plugins/email-console.ts +10 -3
  306. package/src/plugins/hooks.ts +34 -19
  307. package/src/plugins/index.ts +9 -0
  308. package/src/plugins/manifest-schema.ts +49 -2
  309. package/src/plugins/types.ts +174 -13
  310. package/src/preview/urls.ts +23 -3
  311. package/src/query.ts +149 -6
  312. package/src/redirects/cache.ts +38 -18
  313. package/src/request-cache.ts +3 -0
  314. package/src/schema/registry.ts +97 -5
  315. package/src/schema/zod-generator.ts +27 -5
  316. package/src/search/fts-manager.ts +0 -2
  317. package/src/search/query.ts +111 -26
  318. package/src/search/types.ts +8 -1
  319. package/src/sections/index.ts +7 -9
  320. package/src/seed/apply.ts +2 -0
  321. package/src/settings/index.ts +80 -6
  322. package/src/settings/types.ts +23 -1
  323. package/src/storage/s3.ts +12 -6
  324. package/src/taxonomies/index.ts +11 -1
  325. package/src/virtual-modules.d.ts +21 -1
  326. package/src/widgets/index.ts +1 -1
  327. package/dist/apply-5uslYdUu.mjs.map +0 -1
  328. package/dist/byline-C4OVd8b3.mjs.map +0 -1
  329. package/dist/bylines-hPTW79hw.mjs +0 -157
  330. package/dist/bylines-hPTW79hw.mjs.map +0 -1
  331. package/dist/cache-BkKBuIvS.mjs +0 -56
  332. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  333. package/dist/chunk-ClPoSABd.mjs +0 -21
  334. package/dist/content-D7J5y73J.mjs.map +0 -1
  335. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  336. package/dist/error-CiYn9yDu.mjs.map +0 -1
  337. package/dist/index-De6_Xv3v.d.mts.map +0 -1
  338. package/dist/loader-DeiBJEMe.mjs.map +0 -1
  339. package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
  340. package/dist/media-DqHVh136.mjs.map +0 -1
  341. package/dist/mode-CpNnGkPz.mjs.map +0 -1
  342. package/dist/placeholder-C-fk5hYI.mjs.map +0 -1
  343. package/dist/query-g4Ug-9j9.mjs.map +0 -1
  344. package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
  345. package/dist/registry-Ci3WxVAr.mjs.map +0 -1
  346. package/dist/request-cache-DiR961CV.mjs.map +0 -1
  347. package/dist/runner-BR2xKwhn.d.mts +0 -34
  348. package/dist/runner-BR2xKwhn.d.mts.map +0 -1
  349. package/dist/search-B0effn3j.mjs.map +0 -1
  350. package/dist/taxonomies-K2z0Uhnj.mjs.map +0 -1
  351. package/dist/types-CMMN0pNg.mjs +0 -31
  352. package/dist/types-CMMN0pNg.mjs.map +0 -1
  353. package/dist/types-DgrIP0tF.d.mts.map +0 -1
  354. package/dist/version-BnTKdfam.mjs +0 -7
@@ -122,16 +122,27 @@ export async function handleTaxonomyList(
122
122
  db: Kysely<Database>,
123
123
  ): Promise<ApiResult<TaxonomyListResponse>> {
124
124
  try {
125
- const rows = await db.selectFrom("_emdash_taxonomy_defs").selectAll().execute();
126
-
127
- const taxonomies: TaxonomyDef[] = rows.map((row) => ({
128
- id: row.id,
129
- name: row.name,
130
- label: row.label,
131
- labelSingular: row.label_singular ?? undefined,
132
- hierarchical: row.hierarchical === 1,
133
- collections: row.collections ? JSON.parse(row.collections) : [],
134
- }));
125
+ const [rows, collectionRows] = await Promise.all([
126
+ db.selectFrom("_emdash_taxonomy_defs").selectAll().execute(),
127
+ db.selectFrom("_emdash_collections").select("slug").execute(),
128
+ ]);
129
+
130
+ // Filter orphan collection references on read so the response stays
131
+ // consistent with `schema_list_collections`. Storage is untouched —
132
+ // re-creating the collection re-links automatically.
133
+ const realCollections = new Set(collectionRows.map((r) => r.slug));
134
+
135
+ const taxonomies: TaxonomyDef[] = rows.map((row) => {
136
+ const stored: string[] = row.collections ? JSON.parse(row.collections) : [];
137
+ return {
138
+ id: row.id,
139
+ name: row.name,
140
+ label: row.label,
141
+ labelSingular: row.label_singular ?? undefined,
142
+ hierarchical: row.hierarchical === 1,
143
+ collections: stored.filter((slug) => realCollections.has(slug)),
144
+ };
145
+ });
135
146
 
136
147
  return { success: true, data: { taxonomies } };
137
148
  } catch {
@@ -260,12 +271,9 @@ export async function handleTermList(
260
271
  const repo = new TaxonomyRepository(db);
261
272
  const terms = await repo.findByName(taxonomyName);
262
273
 
263
- // Get counts for each term
264
- const counts = new Map<string, number>();
265
- for (const term of terms) {
266
- const count = await repo.countEntriesWithTerm(term.id);
267
- counts.set(term.id, count);
268
- }
274
+ // Batch count entries per term in a single query (replaces N+1 pattern)
275
+ const termIds = terms.map((t) => t.id);
276
+ const counts = await repo.countEntriesForTerms(termIds);
269
277
 
270
278
  const termData: TermWithCount[] = terms.map((term) => ({
271
279
  id: term.id,
@@ -290,6 +298,84 @@ export async function handleTermList(
290
298
  }
291
299
  }
292
300
 
301
+ /**
302
+ * Validate a parent term reference for create/update.
303
+ *
304
+ * Returns `null` on success or a structured error message that callers
305
+ * wrap in their own ApiResult.
306
+ *
307
+ * - `parentId === undefined` -> no-op (no parent change requested).
308
+ * - `parentId === null` -> caller intends to detach; no-op here.
309
+ * - parent must exist (FK exists -> term row not soft-deleted).
310
+ * - parent must live in the same taxonomy.
311
+ * - if `termId` is provided (update path), reject `parentId === termId`
312
+ * (self-parent) and walk up the parent chain to detect cycles.
313
+ */
314
+ async function validateParentTerm(
315
+ repo: TaxonomyRepository,
316
+ taxonomyName: string,
317
+ termId: string | undefined,
318
+ parentId: string | null | undefined,
319
+ ): Promise<{ code: "VALIDATION_ERROR"; message: string } | null> {
320
+ if (parentId === undefined || parentId === null) return null;
321
+
322
+ if (termId !== undefined && parentId === termId) {
323
+ return {
324
+ code: "VALIDATION_ERROR",
325
+ message: "A term cannot be its own parent",
326
+ };
327
+ }
328
+
329
+ const parent = await repo.findById(parentId);
330
+ if (!parent) {
331
+ return {
332
+ code: "VALIDATION_ERROR",
333
+ message: `Parent term '${parentId}' not found`,
334
+ };
335
+ }
336
+ if (parent.name !== taxonomyName) {
337
+ return {
338
+ code: "VALIDATION_ERROR",
339
+ message: `Parent term '${parentId}' belongs to taxonomy '${parent.name}', not '${taxonomyName}'`,
340
+ };
341
+ }
342
+
343
+ // Walk up the parent chain. Two checks fold into one walk:
344
+ // - Cycle detection (only on update — a non-existent term-being-
345
+ // created can't be its own ancestor): if the walk revisits termId
346
+ // the proposed parent makes the term a descendant of itself.
347
+ // - Depth bound: refuse to extend a chain past MAX_DEPTH ancestors.
348
+ // Runs on both create and update so a malicious or buggy caller
349
+ // can't grow the tree without limit.
350
+ //
351
+ // The depth-exceeded error fires only when we hit the limit AND there
352
+ // was still chain to walk — a legitimate chain of exactly MAX_DEPTH
353
+ // ancestors exits with `cursor === null` and is accepted.
354
+ const MAX_DEPTH = 100;
355
+ let cursor: string | null = parent.parentId;
356
+ let steps = 0;
357
+ while (cursor !== null && steps < MAX_DEPTH) {
358
+ if (termId !== undefined && cursor === termId) {
359
+ return {
360
+ code: "VALIDATION_ERROR",
361
+ message: "Cycle detected: cannot make a descendant the parent",
362
+ };
363
+ }
364
+ const next = await repo.findById(cursor);
365
+ if (!next) break;
366
+ cursor = next.parentId;
367
+ steps++;
368
+ }
369
+ if (cursor !== null && steps >= MAX_DEPTH) {
370
+ return {
371
+ code: "VALIDATION_ERROR",
372
+ message: "Parent chain exceeds maximum depth",
373
+ };
374
+ }
375
+
376
+ return null;
377
+ }
378
+
293
379
  /**
294
380
  * Create a new term in a taxonomy
295
381
  */
@@ -304,6 +390,10 @@ export async function handleTermCreate(
304
390
 
305
391
  const repo = new TaxonomyRepository(db);
306
392
 
393
+ // Coerce empty-string parentId to undefined (treat as "no parent").
394
+ const parentId =
395
+ input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
396
+
307
397
  // Check for slug conflict
308
398
  const existing = await repo.findBySlug(taxonomyName, input.slug);
309
399
  if (existing) {
@@ -316,11 +406,18 @@ export async function handleTermCreate(
316
406
  };
317
407
  }
318
408
 
409
+ // Validate parentId: must exist AND belong to the same taxonomy.
410
+ // (Cycle check is N/A on create — the term doesn't exist yet.)
411
+ const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
412
+ if (parentError) {
413
+ return { success: false, error: parentError };
414
+ }
415
+
319
416
  const term = await repo.create({
320
417
  name: taxonomyName,
321
418
  slug: input.slug,
322
419
  label: input.label,
323
- parentId: input.parentId ?? undefined,
420
+ parentId: parentId ?? undefined,
324
421
  data: input.description ? { description: input.description } : undefined,
325
422
  });
326
423
 
@@ -426,24 +523,36 @@ export async function handleTermUpdate(
426
523
  };
427
524
  }
428
525
 
526
+ // Coerce empty-string slug/parentId to undefined (treat as "no change").
527
+ // `null` parentId is a valid request meaning "detach from parent".
528
+ const newSlug = input.slug === "" || input.slug === undefined ? undefined : input.slug;
529
+ const newParentId =
530
+ input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
531
+
429
532
  // Check if new slug conflicts
430
- if (input.slug && input.slug !== termSlug) {
431
- const existing = await repo.findBySlug(taxonomyName, input.slug);
533
+ if (newSlug !== undefined && newSlug !== termSlug) {
534
+ const existing = await repo.findBySlug(taxonomyName, newSlug);
432
535
  if (existing && existing.id !== term.id) {
433
536
  return {
434
537
  success: false,
435
538
  error: {
436
539
  code: "CONFLICT",
437
- message: `Term with slug '${input.slug}' already exists in taxonomy '${taxonomyName}'`,
540
+ message: `Term with slug '${newSlug}' already exists in taxonomy '${taxonomyName}'`,
438
541
  },
439
542
  };
440
543
  }
441
544
  }
442
545
 
546
+ // Validate parentId: existence, same-taxonomy, no self-parent, no cycle.
547
+ const parentError = await validateParentTerm(repo, taxonomyName, term.id, newParentId);
548
+ if (parentError) {
549
+ return { success: false, error: parentError };
550
+ }
551
+
443
552
  const updated = await repo.update(term.id, {
444
- slug: input.slug,
553
+ slug: newSlug,
445
554
  label: input.label,
446
- parentId: input.parentId,
555
+ parentId: newParentId,
447
556
  data: input.description !== undefined ? { description: input.description } : undefined,
448
557
  });
449
558
 
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Field-level validation for content create / update.
3
+ *
4
+ * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)
5
+ * into the handler boundary so REST and MCP both get the same enforcement:
6
+ *
7
+ * - required fields must be present and non-empty
8
+ * - select / multiSelect values must match the configured options
9
+ * - reference fields must resolve to a real, non-trashed target
10
+ *
11
+ * Errors surface as `{ code: "VALIDATION_ERROR", message }` with all
12
+ * offending fields listed in one message so callers can fix everything in
13
+ * a single round trip.
14
+ */
15
+
16
+ import { sql, type Kysely } from "kysely";
17
+
18
+ import type { Database } from "../../database/types.js";
19
+ import { validateIdentifier } from "../../database/validate.js";
20
+ import { SchemaRegistry } from "../../schema/registry.js";
21
+ import type { Field } from "../../schema/types.js";
22
+ import { generateZodSchema } from "../../schema/zod-generator.js";
23
+ import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
24
+ import { isMissingTableError } from "../../utils/db-errors.js";
25
+
26
+ type ValidationResult =
27
+ | { ok: true }
28
+ | { ok: false; error: { code: "VALIDATION_ERROR" | "COLLECTION_NOT_FOUND"; message: string } };
29
+
30
+ /** Treat `undefined`, `null`, and `""` as "not set". */
31
+ function isMissing(value: unknown): boolean {
32
+ return value === undefined || value === null || value === "";
33
+ }
34
+
35
+ /**
36
+ * Resolve the target collection slug for a reference field.
37
+ *
38
+ * Schema-defined reference fields (the static `reference()` factory in
39
+ * `fields/reference.ts`) put the target in `options.collection`. The MCP
40
+ * `schema_create_field` tool also puts it there. Tests and some admin paths
41
+ * stash it inside `validation.collection` directly; we accept both.
42
+ */
43
+ function getReferenceTargetCollection(field: Field): string | undefined {
44
+ const fromOptions = field.options?.collection;
45
+ if (typeof fromOptions === "string" && fromOptions.length > 0) return fromOptions;
46
+ const validation = field.validation;
47
+ if (validation && "collection" in validation) {
48
+ const fromValidation: unknown = (validation as { collection?: unknown }).collection;
49
+ if (typeof fromValidation === "string" && fromValidation.length > 0) return fromValidation;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ /**
55
+ * Format a Zod issue path into a human-readable field reference, e.g.
56
+ * `tags`, `tags.1`, `image.alt`.
57
+ */
58
+ function formatIssuePath(path: ReadonlyArray<PropertyKey>): string {
59
+ if (path.length === 0) return "(root)";
60
+ return path.map((seg) => String(seg)).join(".");
61
+ }
62
+
63
+ /**
64
+ * Validate `data` against the collection's field definitions.
65
+ *
66
+ * `partial: true` switches Zod into partial mode so updates can include
67
+ * only the fields being changed without tripping required-field errors on
68
+ * fields the caller didn't touch. Required fields that ARE present in
69
+ * partial-mode data still get the empty-string check below.
70
+ */
71
+ export async function validateContentData(
72
+ db: Kysely<Database>,
73
+ collection: string,
74
+ data: Record<string, unknown>,
75
+ options: { partial?: boolean } = {},
76
+ ): Promise<ValidationResult> {
77
+ const registry = new SchemaRegistry(db);
78
+ const collectionWithFields = await registry.getCollectionWithFields(collection);
79
+ if (!collectionWithFields) {
80
+ return {
81
+ ok: false,
82
+ error: {
83
+ code: "COLLECTION_NOT_FOUND",
84
+ message: `Collection '${collection}' not found`,
85
+ },
86
+ };
87
+ }
88
+
89
+ const issues: string[] = [];
90
+
91
+ // Detect unknown keys explicitly so callers get a useful error rather
92
+ // than silently dropped data. Leading-underscore keys (e.g. `_slug`,
93
+ // `_rev`) are reserved for internal handler/runtime use and aren't real
94
+ // fields; skip them.
95
+ const knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));
96
+ for (const key of Object.keys(data)) {
97
+ if (key.startsWith("_")) continue;
98
+ if (!knownFields.has(key)) {
99
+ issues.push(`${key}: unknown field on collection '${collection}'`);
100
+ }
101
+ }
102
+
103
+ // Zod handles type, enum, length and missing-required (in non-partial
104
+ // mode) checks. Empty-string handling for required string fields is
105
+ // done as a separate pass below since Zod's `z.string()` accepts "".
106
+ const baseSchema = generateZodSchema(collectionWithFields);
107
+ const schema = options.partial ? baseSchema.partial() : baseSchema;
108
+ const parsed = schema.safeParse(data);
109
+ if (!parsed.success) {
110
+ for (const issue of parsed.error.issues) {
111
+ issues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);
112
+ }
113
+ }
114
+
115
+ // Empty-string-on-required check. In create mode (partial=false) Zod
116
+ // already catches missing/null for required fields, but `z.string()`
117
+ // happily accepts "". In update mode (partial=true) the field is only
118
+ // checked if it's present in `data`.
119
+ for (const field of collectionWithFields.fields) {
120
+ if (!field.required) continue;
121
+ const present = Object.hasOwn(data, field.slug);
122
+ if (options.partial && !present) continue;
123
+ if (data[field.slug] === "") {
124
+ issues.push(`${field.slug}: required (empty value not allowed)`);
125
+ }
126
+ }
127
+
128
+ // Reference target existence. Only check fields that:
129
+ // - have a value (non-missing) in `data`
130
+ // - have a resolvable target collection
131
+ // - in partial mode: are present in `data`
132
+ // Batch one IN-query per target collection to keep round-trips low.
133
+ const refsByTarget = new Map<string, { field: string; id: string }[]>();
134
+ for (const field of collectionWithFields.fields) {
135
+ if (field.type !== "reference") continue;
136
+ if (options.partial && !Object.hasOwn(data, field.slug)) continue;
137
+ const value = data[field.slug];
138
+ if (isMissing(value)) continue;
139
+ if (typeof value !== "string") continue; // Zod will have flagged this already
140
+ const target = getReferenceTargetCollection(field);
141
+ if (!target) continue;
142
+ const list = refsByTarget.get(target) ?? [];
143
+ list.push({ field: field.slug, id: value });
144
+ refsByTarget.set(target, list);
145
+ }
146
+
147
+ for (const [target, refs] of refsByTarget) {
148
+ // Validate the target collection slug before interpolating into raw
149
+ // SQL — defense-in-depth even though slugs are already validated at
150
+ // schema-create time.
151
+ try {
152
+ validateIdentifier(target, "reference target collection");
153
+ } catch {
154
+ for (const ref of refs) {
155
+ issues.push(`${ref.field}: invalid reference target collection '${target}'`);
156
+ }
157
+ continue;
158
+ }
159
+
160
+ const ids = [...new Set(refs.map((r) => r.id))];
161
+ const tableName = `ec_${target}`;
162
+
163
+ // Chunk the IN clause to stay below D1's bind-parameter limit. One
164
+ // reference per request is the common case today; chunking makes the
165
+ // helper safe if a future multiSelect-of-references is added.
166
+ const found = new Set<string>();
167
+ let targetTableMissing = false;
168
+ for (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {
169
+ try {
170
+ const rows = await sql<{ id: string }>`
171
+ SELECT id FROM ${sql.ref(tableName)}
172
+ WHERE id IN (${sql.join(idChunk)})
173
+ AND deleted_at IS NULL
174
+ `.execute(db);
175
+ for (const row of rows.rows) {
176
+ found.add(row.id);
177
+ }
178
+ } catch (error) {
179
+ // Missing table = the target collection table doesn't exist
180
+ // (orphan reference). Treat all those references as missing.
181
+ // Any other DB error (permissions, connection, syntax) must
182
+ // propagate — silently dropping data integrity errors as
183
+ // "not found" is exactly the bug F5 fixes.
184
+ if (isMissingTableError(error)) {
185
+ targetTableMissing = true;
186
+ break;
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ if (targetTableMissing) {
192
+ for (const ref of refs) {
193
+ issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
194
+ }
195
+ continue;
196
+ }
197
+ for (const ref of refs) {
198
+ if (!found.has(ref.id)) {
199
+ issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
200
+ }
201
+ }
202
+ }
203
+
204
+ if (issues.length === 0) return { ok: true };
205
+ return {
206
+ ok: false,
207
+ error: {
208
+ code: "VALIDATION_ERROR",
209
+ message: issues.join("; "),
210
+ },
211
+ };
212
+ }
@@ -122,6 +122,7 @@ import {
122
122
  reorderWidgetsBody,
123
123
  updateWidgetBody,
124
124
  widgetAreaSchema,
125
+ widgetAreaWithWidgetsAndCountSchema,
125
126
  widgetAreaWithWidgetsSchema,
126
127
  widgetSchema,
127
128
  } from "../schemas/widgets.js";
@@ -1581,7 +1582,9 @@ const widgetPaths = {
1581
1582
  description: "Widget area list",
1582
1583
  content: {
1583
1584
  [JSON_CONTENT]: {
1584
- schema: successEnvelope(z.object({ items: z.array(widgetAreaSchema) })),
1585
+ schema: successEnvelope(
1586
+ z.object({ items: z.array(widgetAreaWithWidgetsAndCountSchema) }),
1587
+ ),
1585
1588
  },
1586
1589
  },
1587
1590
  },
@@ -9,7 +9,10 @@
9
9
  * Workers-safe: no Node.js imports.
10
10
  */
11
11
 
12
- import type { EmDashConfig } from "../astro/integration/runtime.js";
12
+ /** Minimal config shape — avoids importing the full EmDashConfig type tree. */
13
+ interface SiteUrlConfig {
14
+ siteUrl?: string;
15
+ }
13
16
 
14
17
  /**
15
18
  * Resolve siteUrl from runtime environment variables.
@@ -26,9 +29,10 @@ import type { EmDashConfig } from "../astro/integration/runtime.js";
26
29
  */
27
30
  let _envSiteUrl: string | undefined | null = null;
28
31
 
29
- /** @internal Reset cached env value — test-only. */
30
- export function _resetEnvSiteUrlCache(): void {
32
+ /** @internal Reset cached env values — test-only. */
33
+ export function _resetEnvCache(): void {
31
34
  _envSiteUrl = null;
35
+ _envAllowedOrigins = null;
32
36
  }
33
37
 
34
38
  function getEnvSiteUrl(): string | undefined {
@@ -67,10 +71,55 @@ function getEnvSiteUrl(): string | undefined {
67
71
  * @param config The EmDash config (from `locals.emdash?.config`)
68
72
  * @returns Origin string, e.g. `"https://mysite.example.com"`
69
73
  */
70
- export function getPublicOrigin(url: URL, config?: EmDashConfig): string {
74
+ export function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {
71
75
  return config?.siteUrl || getEnvSiteUrl() || url.origin;
72
76
  }
73
77
 
78
+ /**
79
+ * Resolve additional accepted passkey origins from runtime environment.
80
+ *
81
+ * Reads `EMDASH_ALLOWED_ORIGINS` (comma-separated list of origins) for
82
+ * multi-origin deployments where the same RP is reachable under several
83
+ * hostnames sharing the registrable parent domain (e.g. apex + preview).
84
+ *
85
+ * Each entry is parsed via `new URL()` and reduced to its `origin`. Unlike
86
+ * `getEnvSiteUrl` (which silently falls back to `url.origin` on bad input),
87
+ * this throws on any unparseable or non-http(s) entry — `EMDASH_ALLOWED_ORIGINS`
88
+ * is an allowlist for passkey verification, so silently dropping a typo would
89
+ * surface as "I can't authenticate on this origin" with no diagnostic. Fail
90
+ * loud at first read.
91
+ *
92
+ * Uses `process.env` (Vite leaves it untouched at runtime). Result is cached
93
+ * on success.
94
+ */
95
+ let _envAllowedOrigins: string[] | null = null;
96
+
97
+ export function getEnvAllowedOrigins(): string[] {
98
+ if (_envAllowedOrigins !== null) return _envAllowedOrigins;
99
+ const raw = typeof process !== "undefined" ? process.env?.EMDASH_ALLOWED_ORIGINS || "" : "";
100
+ const parsed: string[] = [];
101
+ for (const entry of raw.split(",")) {
102
+ const trimmed = entry.trim();
103
+ if (!trimmed) continue;
104
+ let u: URL;
105
+ try {
106
+ u = new URL(trimmed);
107
+ } catch (e) {
108
+ throw new Error(`EmDash config error in EMDASH_ALLOWED_ORIGINS: invalid URL: "${trimmed}"`, {
109
+ cause: e,
110
+ });
111
+ }
112
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
113
+ throw new Error(
114
+ `EmDash config error in EMDASH_ALLOWED_ORIGINS: origin must be http or https: "${trimmed}" (got ${u.protocol})`,
115
+ );
116
+ }
117
+ parsed.push(u.origin);
118
+ }
119
+ _envAllowedOrigins = parsed;
120
+ return parsed;
121
+ }
122
+
74
123
  /**
75
124
  * Build a full public URL by appending a path to the public origin.
76
125
  *
@@ -79,6 +128,6 @@ export function getPublicOrigin(url: URL, config?: EmDashConfig): string {
79
128
  * @param path Path to append (must start with `/`)
80
129
  * @returns Full URL string, e.g. `"https://mysite.example.com/_emdash/admin/login"`
81
130
  */
82
- export function getPublicUrl(url: URL, config: EmDashConfig | undefined, path: string): string {
131
+ export function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {
83
132
  return `${getPublicOrigin(url, config)}${path}`;
84
133
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Public API route utilities for auth provider routes.
3
+ *
4
+ * This module re-exports the utilities that auth provider route handlers
5
+ * need from core. Auth providers (plugins) import these via `emdash/api/route-utils`.
6
+ */
7
+
8
+ export { apiError, apiSuccess, handleError } from "./error.js";
9
+ export { parseBody, parseQuery, isParseError } from "./parse.js";
10
+ export type { ParseResult } from "./parse.js";
11
+ export { finalizeSetup } from "./setup-complete.js";
12
+ export { OptionsRepository } from "../database/repositories/options.js";
13
+ export { getAuthProviderStorage } from "./auth-storage.js";
14
+ export { getPublicOrigin } from "./public-url.js";
@@ -33,8 +33,8 @@ export const commentListQuery = z
33
33
  status: z.enum(["pending", "approved", "spam", "trash"]).optional(),
34
34
  collection: z.string().optional(),
35
35
  search: z.string().optional(),
36
- limit: z.coerce.number().int().min(1).max(100).optional(),
37
- cursor: z.string().optional(),
36
+ limit: z.coerce.number().int().min(1).max(100).optional().default(50),
37
+ cursor: z.string().max(2048).optional(),
38
38
  })
39
39
  .meta({ id: "CommentListQuery" });
40
40
 
@@ -22,7 +22,7 @@ export const roleLevel = z.coerce
22
22
  /** Pagination query params — cursor-based */
23
23
  export const cursorPaginationQuery = z
24
24
  .object({
25
- cursor: z.string().optional().meta({ description: "Opaque cursor for pagination" }),
25
+ cursor: z.string().max(2048).optional().meta({ description: "Opaque cursor for pagination" }),
26
26
  limit: z.coerce.number().int().min(1).max(100).optional().default(50).meta({
27
27
  description: "Maximum number of items to return (1-100, default 50)",
28
28
  }),
@@ -72,6 +72,23 @@ export const contentScheduleBody = z
72
72
  })
73
73
  .meta({ id: "ContentScheduleBody" });
74
74
 
75
+ export const contentPublishBody = z
76
+ .object({
77
+ // .optional() rather than .nullish(): publishing has no semantic
78
+ // meaning for `null` (you can't "clear" a publish timestamp by
79
+ // publishing). Tightening the schema here means callers either
80
+ // pass a valid datetime or omit the field, and the route doesn't
81
+ // have to silently drop a null that snuck through.
82
+ publishedAt: z.iso
83
+ .datetime({ offset: true, message: "must be an ISO 8601 datetime" })
84
+ .optional()
85
+ .meta({
86
+ description:
87
+ "Optional ISO 8601 datetime to backdate the publish (e.g. when migrating content). Requires content:publish_any permission. Without this, existing published_at is preserved on re-publish.",
88
+ }),
89
+ })
90
+ .meta({ id: "ContentPublishBody" });
91
+
75
92
  export const contentPreviewUrlBody = z
76
93
  .object({
77
94
  expiresIn: z.union([z.string(), z.number()]).optional(),
@@ -10,8 +10,8 @@ export const sectionsListQuery = z
10
10
  .object({
11
11
  source: sectionSource.optional(),
12
12
  search: z.string().optional(),
13
- limit: z.coerce.number().int().min(1).max(100).optional(),
14
- cursor: z.string().optional(),
13
+ limit: z.coerce.number().int().min(1).max(100).optional().default(50),
14
+ cursor: z.string().max(2048).optional(),
15
15
  })
16
16
  .meta({ id: "SectionsListQuery" });
17
17
 
@@ -23,7 +23,7 @@ export const createSectionBody = z
23
23
  keywords: z.array(z.string()).optional(),
24
24
  content: z.array(z.record(z.string(), z.unknown())),
25
25
  previewMediaId: z.string().optional(),
26
- source: sectionSource.optional(),
26
+ source: z.enum(["user", "import"]).optional(),
27
27
  themeId: z.string().optional(),
28
28
  })
29
29
  .meta({ id: "CreateSectionBody" });
@@ -35,3 +35,11 @@ export const setupAdminBody = z.object({
35
35
  export const setupAdminVerifyBody = z.object({
36
36
  credential: registrationCredential,
37
37
  });
38
+
39
+ export const atprotoLoginBody = z.object({
40
+ handle: z.string().trim().min(1),
41
+ });
42
+
43
+ export const setupAtprotoAdminBody = z.object({
44
+ handle: z.string().trim().min(1),
45
+ });
@@ -10,7 +10,7 @@ export const usersListQuery = z
10
10
  .object({
11
11
  search: z.string().optional(),
12
12
  role: z.string().optional(),
13
- cursor: z.string().optional(),
13
+ cursor: z.string().max(2048).optional(),
14
14
  limit: z.coerce.number().int().min(1).max(100).optional().default(50),
15
15
  })
16
16
  .meta({ id: "UsersListQuery" });
@@ -60,16 +60,12 @@ export const widgetAreaSchema = z
60
60
  export const widgetSchema = z
61
61
  .object({
62
62
  id: z.string(),
63
- area_id: z.string(),
64
- type: z.string(),
65
- title: z.string().nullable(),
66
- content: z.string().nullable(),
67
- menu_name: z.string().nullable(),
68
- component_id: z.string().nullable(),
69
- component_props: z.string().nullable(),
70
- sort_order: z.number().int(),
71
- created_at: z.string(),
72
- updated_at: z.string(),
63
+ type: widgetType,
64
+ title: z.string().optional(),
65
+ content: z.array(z.record(z.string(), z.unknown())).optional(),
66
+ menuName: z.string().optional(),
67
+ componentId: z.string().optional(),
68
+ componentProps: z.record(z.string(), z.unknown()).optional(),
73
69
  })
74
70
  .meta({ id: "Widget" });
75
71
 
@@ -78,3 +74,9 @@ export const widgetAreaWithWidgetsSchema = widgetAreaSchema
78
74
  widgets: z.array(widgetSchema),
79
75
  })
80
76
  .meta({ id: "WidgetAreaWithWidgets" });
77
+
78
+ export const widgetAreaWithWidgetsAndCountSchema = widgetAreaWithWidgetsSchema
79
+ .extend({
80
+ widgetCount: z.number().int(),
81
+ })
82
+ .meta({ id: "WidgetAreaWithWidgetsAndCount" });