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
@@ -42,26 +42,33 @@ export interface MenuWithItems extends MenuRow {
42
42
  */
43
43
  export async function handleMenuList(db: Kysely<Database>): Promise<ApiResult<MenuListItem[]>> {
44
44
  try {
45
- const menus = await db
46
- .selectFrom("_emdash_menus")
47
- .select(["id", "name", "label", "created_at", "updated_at"])
48
- .orderBy("name", "asc")
45
+ // Single query: LEFT JOIN + GROUP BY for the per-menu item count.
46
+ // Avoids the N+1 of one count query per menu.
47
+ const rows = await db
48
+ .selectFrom("_emdash_menus as m")
49
+ .leftJoin("_emdash_menu_items as i", "i.menu_id", "m.id")
50
+ .select(({ fn }) => [
51
+ "m.id",
52
+ "m.name",
53
+ "m.label",
54
+ "m.created_at",
55
+ "m.updated_at",
56
+ fn.count<number>("i.id").as("itemCount"),
57
+ ])
58
+ .groupBy(["m.id", "m.name", "m.label", "m.created_at", "m.updated_at"])
59
+ .orderBy("m.name", "asc")
49
60
  .execute();
50
61
 
51
- const menusWithCounts = await Promise.all(
52
- menus.map(async (menu) => {
53
- const { count } = await db
54
- .selectFrom("_emdash_menu_items")
55
- .select(({ fn }) => fn.countAll<number>().as("count"))
56
- .where("menu_id", "=", menu.id)
57
- .executeTakeFirstOrThrow();
58
-
59
- return {
60
- ...menu,
61
- itemCount: count,
62
- };
63
- }),
64
- );
62
+ // SQLite returns count as `number`, but some dialects (Postgres)
63
+ // return `string` from a count() aggregate. Normalize to number.
64
+ const menusWithCounts: MenuListItem[] = rows.map((row) => ({
65
+ id: row.id,
66
+ name: row.name,
67
+ label: row.label,
68
+ created_at: row.created_at,
69
+ updated_at: row.updated_at,
70
+ itemCount: typeof row.itemCount === "string" ? Number(row.itemCount) : row.itemCount,
71
+ }));
65
72
 
66
73
  return { success: true, data: menusWithCounts };
67
74
  } catch {
@@ -135,7 +142,7 @@ export async function handleMenuGet(
135
142
  if (!menu) {
136
143
  return {
137
144
  success: false,
138
- error: { code: "NOT_FOUND", message: "Menu not found" },
145
+ error: { code: "NOT_FOUND", message: `Menu '${name}' not found` },
139
146
  };
140
147
  }
141
148
 
@@ -173,7 +180,7 @@ export async function handleMenuUpdate(
173
180
  if (!menu) {
174
181
  return {
175
182
  success: false,
176
- error: { code: "NOT_FOUND", message: "Menu not found" },
183
+ error: { code: "NOT_FOUND", message: `Menu '${name}' not found` },
177
184
  };
178
185
  }
179
186
 
@@ -217,10 +224,14 @@ export async function handleMenuDelete(
217
224
  if (!menu) {
218
225
  return {
219
226
  success: false,
220
- error: { code: "NOT_FOUND", message: "Menu not found" },
227
+ error: { code: "NOT_FOUND", message: `Menu '${name}' not found` },
221
228
  };
222
229
  }
223
230
 
231
+ // D1 has FOREIGN KEYS off by default, so the migration's `ON DELETE
232
+ // CASCADE` won't fire there. Delete items explicitly first — this is
233
+ // idempotent on SQLite/Postgres where the cascade also fires.
234
+ await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menu.id).execute();
224
235
  await db.deleteFrom("_emdash_menus").where("id", "=", menu.id).execute();
225
236
 
226
237
  return { success: true, data: { deleted: true } };
@@ -443,6 +454,134 @@ export interface ReorderItem {
443
454
  sortOrder: number;
444
455
  }
445
456
 
457
+ // ---------------------------------------------------------------------------
458
+ // Atomic-replace menu items (used by the MCP `menu_set_items` tool)
459
+ // ---------------------------------------------------------------------------
460
+
461
+ export interface MenuSetItemsInput {
462
+ label: string;
463
+ type: "custom" | "page" | "post" | "taxonomy" | "collection";
464
+ customUrl?: string;
465
+ referenceCollection?: string;
466
+ referenceId?: string;
467
+ titleAttr?: string;
468
+ target?: string;
469
+ cssClasses?: string;
470
+ /**
471
+ * Index of the parent item in this same array. Must be strictly less
472
+ * than the current item's index so the insert order resolves parents
473
+ * before children. `undefined` makes the item top-level.
474
+ */
475
+ parentIndex?: number;
476
+ }
477
+
478
+ /**
479
+ * Replace the entire set of items for a menu in one atomic transaction.
480
+ *
481
+ * Existing items are deleted and the new list is inserted in the order
482
+ * provided. `parentIndex` references resolve to actual parent IDs as the
483
+ * insert proceeds.
484
+ */
485
+ export async function handleMenuSetItems(
486
+ db: Kysely<Database>,
487
+ menuName: string,
488
+ items: MenuSetItemsInput[],
489
+ ): Promise<ApiResult<{ name: string; itemCount: number }>> {
490
+ // Validate parentIndex references — must be strictly earlier so
491
+ // the array can be inserted in order with parents resolved first.
492
+ // Negative indices are out of range; only Zod's `.nonnegative()` at
493
+ // the MCP boundary catches them today, so guard explicitly here for
494
+ // any caller that bypasses Zod (REST routes, direct handler use).
495
+ for (let i = 0; i < items.length; i++) {
496
+ const item = items[i];
497
+ if (item?.parentIndex !== undefined) {
498
+ if (item.parentIndex < 0 || item.parentIndex >= i) {
499
+ return {
500
+ success: false,
501
+ error: {
502
+ code: "VALIDATION_ERROR",
503
+ message: `item[${i}].parentIndex (${item.parentIndex}) must reference an earlier item`,
504
+ },
505
+ };
506
+ }
507
+ }
508
+ }
509
+
510
+ try {
511
+ // Sentinel for "menu not found" thrown from inside the transaction
512
+ // so the rollback fires before we return the structured error.
513
+ const notFoundSentinel = Symbol("menu-not-found");
514
+
515
+ try {
516
+ await withTransaction(db, async (trx) => {
517
+ // Existence check INSIDE the transaction so a concurrent
518
+ // menu_delete between lookup and write can't leave orphan
519
+ // items on D1 (FKs disabled by default).
520
+ const menu = await trx
521
+ .selectFrom("_emdash_menus")
522
+ .select("id")
523
+ .where("name", "=", menuName)
524
+ .executeTakeFirst();
525
+
526
+ if (!menu) {
527
+ throw notFoundSentinel;
528
+ }
529
+
530
+ await trx.deleteFrom("_emdash_menu_items").where("menu_id", "=", menu.id).execute();
531
+
532
+ const insertedIds: string[] = [];
533
+ for (let i = 0; i < items.length; i++) {
534
+ const item = items[i];
535
+ if (!item) continue;
536
+ const id = ulid();
537
+ const parentId =
538
+ item.parentIndex !== undefined ? (insertedIds[item.parentIndex] ?? null) : null;
539
+ await trx
540
+ .insertInto("_emdash_menu_items")
541
+ .values({
542
+ id,
543
+ menu_id: menu.id,
544
+ parent_id: parentId,
545
+ sort_order: i,
546
+ type: item.type,
547
+ reference_collection: item.referenceCollection ?? null,
548
+ reference_id: item.referenceId ?? null,
549
+ custom_url: item.customUrl ?? null,
550
+ label: item.label,
551
+ title_attr: item.titleAttr ?? null,
552
+ target: item.target ?? null,
553
+ css_classes: item.cssClasses ?? null,
554
+ })
555
+ .execute();
556
+ insertedIds.push(id);
557
+ }
558
+
559
+ await trx
560
+ .updateTable("_emdash_menus")
561
+ .set({ updated_at: new Date().toISOString() })
562
+ .where("id", "=", menu.id)
563
+ .execute();
564
+ });
565
+ } catch (error) {
566
+ if (error === notFoundSentinel) {
567
+ return {
568
+ success: false,
569
+ error: { code: "NOT_FOUND", message: `Menu '${menuName}' not found` },
570
+ };
571
+ }
572
+ throw error;
573
+ }
574
+
575
+ return { success: true, data: { name: menuName, itemCount: items.length } };
576
+ } catch (error) {
577
+ console.error("[emdash] handleMenuSetItems failed:", error);
578
+ return {
579
+ success: false,
580
+ error: { code: "MENU_SET_ITEMS_ERROR", message: "Failed to set menu items" },
581
+ };
582
+ }
583
+ }
584
+
446
585
  /**
447
586
  * Batch reorder menu items.
448
587
  */
@@ -8,7 +8,7 @@
8
8
  * utilities. Token infrastructure is shared with the device flow.
9
9
  */
10
10
 
11
- import { clampScopes, computeS256Challenge } from "@emdash-cms/auth";
11
+ import { clampScopes, computeS256Challenge, secureCompare } from "@emdash-cms/auth";
12
12
  import type { RoleLevel } from "@emdash-cms/auth";
13
13
  import { generateCodeVerifier } from "arctic";
14
14
  import type { Kysely } from "kysely";
@@ -19,10 +19,12 @@ import {
19
19
  TOKEN_PREFIXES,
20
20
  VALID_SCOPES,
21
21
  } from "../../auth/api-tokens.js";
22
+ import { withTransaction } from "../../database/transaction.js";
22
23
  import type { Database } from "../../database/types.js";
23
24
  import { validateRedirectUri } from "../oauth/redirect-uri.js";
24
25
  import type { ApiResult } from "../types.js";
25
26
  import { lookupOAuthClient, validateClientRedirectUri } from "./oauth-clients.js";
27
+ import { lookupUserRoleAndStatus } from "./oauth-user-lookup.js";
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // Constants
@@ -296,8 +298,9 @@ export async function handleAuthorizationCodeExchange(
296
298
  }
297
299
 
298
300
  // PKCE verification: SHA256(code_verifier) must match stored code_challenge
301
+ // Use constant-time comparison to prevent timing side-channels
299
302
  const derivedChallenge = computeS256Challenge(params.code_verifier);
300
- if (derivedChallenge !== row.code_challenge) {
303
+ if (!secureCompare(derivedChallenge, row.code_challenge)) {
301
304
  return {
302
305
  success: false,
303
306
  error: { code: "invalid_grant", message: "PKCE verification failed" },
@@ -312,44 +315,80 @@ export async function handleAuthorizationCodeExchange(
312
315
  };
313
316
  }
314
317
 
315
- // Issue tokens (same as device flow)
316
- const scopes = JSON.parse(row.scopes) as string[];
318
+ // Revalidate user role before issuing tokens (same pattern as handleTokenRefresh).
319
+ // The user's role may have changed since the authorization code was issued.
320
+ const userInfo = await lookupUserRoleAndStatus(db, row.user_id);
321
+ if (!userInfo) {
322
+ return {
323
+ success: false,
324
+ error: { code: "invalid_grant", message: "User not found" },
325
+ };
326
+ }
317
327
 
328
+ if (userInfo.disabled) {
329
+ return {
330
+ success: false,
331
+ error: { code: "invalid_grant", message: "User account is disabled" },
332
+ };
333
+ }
334
+
335
+ // Re-clamp scopes against the user's current role
336
+ const storedScopes = JSON.parse(row.scopes) as string[];
337
+ let scopes = clampScopes(storedScopes, userInfo.role);
338
+
339
+ // Intersect with client's registered scopes (if restricted)
340
+ const client = await lookupOAuthClient(db, row.client_id);
341
+ if (client?.scopes?.length) {
342
+ scopes = scopes.filter((s: string) => client.scopes!.includes(s));
343
+ }
344
+
345
+ if (scopes.length === 0) {
346
+ return {
347
+ success: false,
348
+ error: {
349
+ code: "invalid_grant",
350
+ message: "User role no longer supports any of the requested scopes",
351
+ },
352
+ };
353
+ }
354
+
355
+ // Issue tokens (same as device flow)
318
356
  const accessToken = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_ACCESS);
319
357
  const accessExpires = expiresAt(ACCESS_TOKEN_TTL_SECONDS);
320
358
 
321
359
  const refreshToken = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_REFRESH);
322
360
  const refreshExpires = expiresAt(REFRESH_TOKEN_TTL_SECONDS);
323
361
 
324
- // Store access token
325
- await db
326
- .insertInto("_emdash_oauth_tokens")
327
- .values({
328
- token_hash: accessToken.hash,
329
- token_type: "access",
330
- user_id: row.user_id,
331
- scopes: JSON.stringify(scopes),
332
- client_type: "mcp",
333
- expires_at: accessExpires,
334
- refresh_token_hash: refreshToken.hash,
335
- client_id: row.client_id,
336
- })
337
- .execute();
338
-
339
- // Store refresh token
340
- await db
341
- .insertInto("_emdash_oauth_tokens")
342
- .values({
343
- token_hash: refreshToken.hash,
344
- token_type: "refresh",
345
- user_id: row.user_id,
346
- scopes: JSON.stringify(scopes),
347
- client_type: "mcp",
348
- expires_at: refreshExpires,
349
- refresh_token_hash: null,
350
- client_id: row.client_id,
351
- })
352
- .execute();
362
+ // Atomically store both tokens in a transaction
363
+ await withTransaction(db, async (trx) => {
364
+ await trx
365
+ .insertInto("_emdash_oauth_tokens")
366
+ .values({
367
+ token_hash: accessToken.hash,
368
+ token_type: "access",
369
+ user_id: row.user_id,
370
+ scopes: JSON.stringify(scopes),
371
+ client_type: "mcp",
372
+ expires_at: accessExpires,
373
+ refresh_token_hash: refreshToken.hash,
374
+ client_id: row.client_id,
375
+ })
376
+ .execute();
377
+
378
+ await trx
379
+ .insertInto("_emdash_oauth_tokens")
380
+ .values({
381
+ token_hash: refreshToken.hash,
382
+ token_type: "refresh",
383
+ user_id: row.user_id,
384
+ scopes: JSON.stringify(scopes),
385
+ client_type: "mcp",
386
+ expires_at: refreshExpires,
387
+ refresh_token_hash: null,
388
+ client_id: row.client_id,
389
+ })
390
+ .execute();
391
+ });
353
392
 
354
393
  return {
355
394
  success: true,
@@ -11,6 +11,7 @@ import {
11
11
  type NotFoundEntry,
12
12
  type NotFoundSummary,
13
13
  } from "../../database/repositories/redirect.js";
14
+ import { InvalidCursorError } from "../../database/repositories/types.js";
14
15
  import type { FindManyResult } from "../../database/repositories/types.js";
15
16
  import type { Database } from "../../database/types.js";
16
17
  import { wouldCreateLoop, detectLoops, type RedirectEdge } from "../../redirects/loops.js";
@@ -48,7 +49,13 @@ export async function handleRedirectList(
48
49
  ...(loopRedirectIds.length > 0 ? { loopRedirectIds } : {}),
49
50
  },
50
51
  };
51
- } catch {
52
+ } catch (error) {
53
+ if (error instanceof InvalidCursorError) {
54
+ return {
55
+ success: false,
56
+ error: { code: "INVALID_CURSOR", message: error.message },
57
+ };
58
+ }
52
59
  return {
53
60
  success: false,
54
61
  error: { code: "REDIRECT_LIST_ERROR", message: "Failed to fetch redirects" },
@@ -318,7 +325,7 @@ export async function handleRedirectDelete(
318
325
  function loopError(loopPath: string[]): ApiResult<never> {
319
326
  const hops = loopPath
320
327
  .slice(0, -1)
321
- .map((p, i) => `${p} \u2192 ${loopPath[i + 1]!}`)
328
+ .map((p, i) => `${p} \u2192 ${loopPath[i + 1]}`)
322
329
  .join("\n");
323
330
  return {
324
331
  success: false,
@@ -387,7 +394,13 @@ export async function handleNotFoundList(
387
394
  const repo = new RedirectRepository(db);
388
395
  const result = await repo.find404s(params);
389
396
  return { success: true, data: result };
390
- } catch {
397
+ } catch (error) {
398
+ if (error instanceof InvalidCursorError) {
399
+ return {
400
+ success: false,
401
+ error: { code: "INVALID_CURSOR", message: error.message },
402
+ };
403
+ }
391
404
  return {
392
405
  success: false,
393
406
  error: { code: "NOT_FOUND_LIST_ERROR", message: "Failed to fetch 404 log" },
@@ -6,6 +6,7 @@ import type { Kysely } from "kysely";
6
6
 
7
7
  import { ContentRepository } from "../../database/repositories/content.js";
8
8
  import { RevisionRepository, type Revision } from "../../database/repositories/revision.js";
9
+ import { withTransaction } from "../../database/transaction.js";
9
10
  import type { Database } from "../../database/types.js";
10
11
  import type { ApiResult, ContentResponse } from "../types.js";
11
12
 
@@ -95,7 +96,6 @@ export async function handleRevisionRestore(
95
96
  ): Promise<ApiResult<ContentResponse>> {
96
97
  try {
97
98
  const revisionRepo = new RevisionRepository(db);
98
- const contentRepo = new ContentRepository(db);
99
99
 
100
100
  // Get the revision
101
101
  const revision = await revisionRepo.findById(revisionId);
@@ -112,22 +112,31 @@ export async function handleRevisionRestore(
112
112
  // Extract _slug from revision data (stored as metadata, not a real column)
113
113
  const { _slug, ...fieldData } = revision.data;
114
114
 
115
- // Update the content with the revision's data
116
- const item = await contentRepo.update(revision.collection, revision.entryId, {
117
- data: fieldData,
118
- slug: typeof _slug === "string" ? _slug : undefined,
119
- });
120
-
121
- // Create a new revision to record the restore, attributed to the caller
122
- await revisionRepo.create({
123
- collection: revision.collection,
124
- entryId: revision.entryId,
125
- data: revision.data,
126
- authorId: callerUserId,
115
+ // Atomically update content and create a new revision to record the restore.
116
+ // If either operation fails, neither is committed (on engines that support
117
+ // transactions; on D1, withTransaction falls back to sequential execution).
118
+ const item = await withTransaction(db, async (trx) => {
119
+ const trxContentRepo = new ContentRepository(trx);
120
+ const trxRevisionRepo = new RevisionRepository(trx);
121
+
122
+ const updated = await trxContentRepo.update(revision.collection, revision.entryId, {
123
+ data: fieldData,
124
+ slug: typeof _slug === "string" ? _slug : undefined,
125
+ });
126
+
127
+ await trxRevisionRepo.create({
128
+ collection: revision.collection,
129
+ entryId: revision.entryId,
130
+ data: revision.data,
131
+ authorId: callerUserId,
132
+ });
133
+
134
+ return updated;
127
135
  });
128
136
 
129
137
  // Fire-and-forget: prune old revisions to prevent unbounded growth
130
- void revisionRepo.pruneOldRevisions(revision.collection, revision.entryId, 50).catch(() => {});
138
+ const pruneRepo = new RevisionRepository(db);
139
+ void pruneRepo.pruneOldRevisions(revision.collection, revision.entryId, 50).catch(() => {});
131
140
 
132
141
  return {
133
142
  success: true,
@@ -5,6 +5,7 @@
5
5
  import type { Kysely } from "kysely";
6
6
  import { ulid } from "ulidx";
7
7
 
8
+ import { InvalidCursorError } from "../../database/repositories/types.js";
8
9
  import type { FindManyResult } from "../../database/repositories/types.js";
9
10
  import type { Database } from "../../database/types.js";
10
11
  import {
@@ -36,7 +37,13 @@ export async function handleSectionList(
36
37
  });
37
38
 
38
39
  return { success: true, data: result };
39
- } catch {
40
+ } catch (error) {
41
+ if (error instanceof InvalidCursorError) {
42
+ return {
43
+ success: false,
44
+ error: { code: "INVALID_CURSOR", message: error.message },
45
+ };
46
+ }
40
47
  return {
41
48
  success: false,
42
49
  error: { code: "SECTION_LIST_ERROR", message: "Failed to fetch sections" },