emdash 0.8.0 → 0.10.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 (317) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
  4. package/dist/apply-UsrFuO7l.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 +118 -4
  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 +14 -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 +15 -10
  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 +8 -5
  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 +70 -121
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +25 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
  26. package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
  27. package/dist/bylines-esI7ioa9.mjs +113 -0
  28. package/dist/bylines-esI7ioa9.mjs.map +1 -0
  29. package/dist/cache-fTzxgMFJ.mjs +65 -0
  30. package/dist/cache-fTzxgMFJ.mjs.map +1 -0
  31. package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
  32. package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
  33. package/dist/cli/index.mjs +456 -90
  34. package/dist/cli/index.mjs.map +1 -1
  35. package/dist/client/cf-access.d.mts +1 -1
  36. package/dist/client/index.d.mts +1 -1
  37. package/dist/client/index.mjs +3 -3
  38. package/dist/client/index.mjs.map +1 -1
  39. package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
  40. package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
  41. package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
  42. package/dist/content-C7G4QXkK.mjs.map +1 -0
  43. package/dist/db/index.d.mts +3 -3
  44. package/dist/db/index.mjs +2 -2
  45. package/dist/db/libsql.d.mts +1 -1
  46. package/dist/db/libsql.d.mts.map +1 -1
  47. package/dist/db/libsql.mjs +7 -2
  48. package/dist/db/libsql.mjs.map +1 -1
  49. package/dist/db/postgres.d.mts +1 -1
  50. package/dist/db/sqlite.d.mts +1 -1
  51. package/dist/db/sqlite.d.mts.map +1 -1
  52. package/dist/db/sqlite.mjs +8 -3
  53. package/dist/db/sqlite.mjs.map +1 -1
  54. package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  55. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  56. package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
  57. package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  58. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  59. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  60. package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
  61. package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  62. package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
  63. package/dist/index-DjPMOfO0.d.mts.map +1 -0
  64. package/dist/index.d.mts +11 -11
  65. package/dist/index.mjs +27 -24
  66. package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
  67. package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
  68. package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
  69. package/dist/loader-Bx2_9-5e.mjs.map +1 -0
  70. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
  71. package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
  72. package/dist/media/index.d.mts +1 -1
  73. package/dist/media/index.mjs +1 -1
  74. package/dist/media/local-runtime.d.mts +7 -7
  75. package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
  76. package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  77. package/dist/options-nPxWnrya.mjs +117 -0
  78. package/dist/options-nPxWnrya.mjs.map +1 -0
  79. package/dist/page/index.d.mts +2 -2
  80. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  81. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  82. package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  83. package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  84. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  85. package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  86. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  87. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  88. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  89. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  90. package/dist/public-url-B1AxbbbQ.mjs +51 -0
  91. package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
  92. package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
  93. package/dist/query-Bo-msrmu.mjs.map +1 -0
  94. package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
  95. package/dist/redirect-C5H7VGIX.mjs.map +1 -0
  96. package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
  97. package/dist/registry-Beb7wxFc.mjs.map +1 -0
  98. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
  99. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
  100. package/dist/runner-Clwe4Mme.d.mts +44 -0
  101. package/dist/runner-Clwe4Mme.d.mts.map +1 -0
  102. package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
  103. package/dist/runner-DMnlIkh4.mjs.map +1 -0
  104. package/dist/runtime.d.mts +6 -6
  105. package/dist/runtime.mjs +2 -2
  106. package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
  107. package/dist/search-DkN-BqsS.mjs.map +1 -0
  108. package/dist/secrets-CZ8rxLX3.mjs +314 -0
  109. package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
  110. package/dist/seed/index.d.mts +2 -2
  111. package/dist/seed/index.mjs +13 -11
  112. package/dist/seo/index.d.mts +1 -1
  113. package/dist/storage/local.d.mts +1 -1
  114. package/dist/storage/local.mjs +1 -1
  115. package/dist/storage/s3.d.mts +1 -1
  116. package/dist/storage/s3.mjs +1 -1
  117. package/dist/taxonomies-CTtewrSQ.mjs +407 -0
  118. package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
  119. package/dist/taxonomy-DSxx2K2L.mjs +218 -0
  120. package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
  121. package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
  122. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  123. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  124. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  125. package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  127. package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
  128. package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  129. package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
  130. package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  131. package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
  132. package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  133. package/dist/types-CoO6mpV3.mjs +68 -0
  134. package/dist/types-CoO6mpV3.mjs.map +1 -0
  135. package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
  136. package/dist/types-D19uBYWn.d.mts.map +1 -0
  137. package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
  138. package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
  139. package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
  140. package/dist/types-Dtx1mSMX.d.mts.map +1 -0
  141. package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
  142. package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
  143. package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
  144. package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  145. package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
  146. package/dist/validate-CBIbxM3L.mjs.map +1 -0
  147. package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
  148. package/dist/validate-DHGwADqO.d.mts.map +1 -0
  149. package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
  150. package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
  151. package/dist/version-CMD42IRC.mjs +7 -0
  152. package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
  153. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
  154. package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
  155. package/locals.d.ts +1 -6
  156. package/package.json +9 -8
  157. package/src/api/handlers/comments.ts +6 -4
  158. package/src/api/handlers/content.ts +40 -1
  159. package/src/api/handlers/dashboard.ts +29 -36
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/menus.ts +256 -75
  163. package/src/api/handlers/oauth-authorization.ts +72 -33
  164. package/src/api/handlers/revision.ts +23 -14
  165. package/src/api/handlers/taxonomies.ts +273 -100
  166. package/src/api/public-url.ts +48 -2
  167. package/src/api/schemas/comments.ts +2 -2
  168. package/src/api/schemas/common.ts +7 -0
  169. package/src/api/schemas/content.ts +17 -0
  170. package/src/api/schemas/menus.ts +23 -0
  171. package/src/api/schemas/sections.ts +3 -3
  172. package/src/api/schemas/taxonomies.ts +39 -0
  173. package/src/api/schemas/users.ts +1 -1
  174. package/src/api/types.ts +5 -1
  175. package/src/astro/integration/index.ts +17 -0
  176. package/src/astro/integration/routes.ts +10 -0
  177. package/src/astro/integration/runtime.ts +30 -0
  178. package/src/astro/integration/virtual-modules.ts +32 -2
  179. package/src/astro/integration/vite-config.ts +6 -1
  180. package/src/astro/middleware/auth.ts +13 -6
  181. package/src/astro/middleware/redirect.ts +29 -16
  182. package/src/astro/middleware/request-context.ts +15 -5
  183. package/src/astro/middleware.ts +23 -9
  184. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  185. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  186. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  187. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  188. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  190. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  191. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  192. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  193. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  194. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  195. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  196. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  197. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  198. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  199. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  200. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  201. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  202. package/src/astro/routes/api/manifest.ts +62 -45
  203. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  204. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  205. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  206. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  207. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  208. package/src/astro/routes/api/menus/[name].ts +19 -10
  209. package/src/astro/routes/api/menus/index.ts +9 -6
  210. package/src/astro/routes/api/openapi.json.ts +27 -10
  211. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  212. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  213. package/src/astro/routes/api/redirects/[id].ts +10 -4
  214. package/src/astro/routes/api/redirects/index.ts +7 -3
  215. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  216. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  217. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  218. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  219. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  220. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  221. package/src/astro/routes/api/search/index.ts +10 -2
  222. package/src/astro/routes/api/sections/[slug].ts +10 -4
  223. package/src/astro/routes/api/sections/index.ts +7 -3
  224. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  225. package/src/astro/routes/api/snapshot.ts +44 -18
  226. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  227. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  228. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  229. package/src/astro/routes/api/taxonomies/index.ts +9 -7
  230. package/src/astro/routes/api/themes/preview.ts +11 -5
  231. package/src/astro/types.ts +23 -3
  232. package/src/auth/allowed-origins.ts +168 -0
  233. package/src/auth/passkey-config.ts +35 -13
  234. package/src/bylines/index.ts +37 -88
  235. package/src/cli/commands/auth.ts +28 -6
  236. package/src/cli/commands/bundle-utils.ts +11 -2
  237. package/src/cli/commands/bundle.ts +28 -8
  238. package/src/cli/commands/content.ts +13 -0
  239. package/src/cli/commands/export-seed.ts +82 -21
  240. package/src/cli/commands/login.ts +8 -1
  241. package/src/cli/commands/plugin-init.ts +216 -90
  242. package/src/cli/commands/publish.ts +24 -0
  243. package/src/cli/commands/secrets.ts +183 -0
  244. package/src/cli/credentials.ts +1 -1
  245. package/src/cli/index.ts +5 -1
  246. package/src/client/index.ts +4 -4
  247. package/src/client/transport.ts +17 -7
  248. package/src/components/Break.astro +2 -2
  249. package/src/components/EmDashHead.astro +18 -13
  250. package/src/components/Embed.astro +1 -1
  251. package/src/components/Gallery.astro +1 -1
  252. package/src/components/Image.astro +1 -1
  253. package/src/components/InlinePortableTextEditor.tsx +104 -18
  254. package/src/config/secrets.ts +528 -0
  255. package/src/database/dialect-helpers.ts +50 -0
  256. package/src/database/migrations/034_published_at_index.ts +1 -1
  257. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  258. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  259. package/src/database/migrations/runner.ts +158 -23
  260. package/src/database/repositories/content.ts +47 -12
  261. package/src/database/repositories/redirect.ts +14 -3
  262. package/src/database/repositories/taxonomy.ts +212 -82
  263. package/src/database/types.ts +10 -2
  264. package/src/db/libsql.ts +1 -3
  265. package/src/db/sqlite.ts +2 -5
  266. package/src/emdash-runtime.ts +84 -159
  267. package/src/i18n/resolve.ts +37 -0
  268. package/src/index.ts +9 -0
  269. package/src/loader.ts +73 -3
  270. package/src/mcp/server.ts +180 -54
  271. package/src/menus/index.ts +143 -124
  272. package/src/menus/types.ts +15 -1
  273. package/src/page/site-identity.ts +58 -0
  274. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  275. package/src/plugins/context.ts +13 -10
  276. package/src/plugins/define-plugin.ts +40 -12
  277. package/src/plugins/hooks.ts +23 -19
  278. package/src/plugins/index.ts +9 -0
  279. package/src/plugins/manifest-schema.ts +37 -2
  280. package/src/plugins/types.ts +151 -11
  281. package/src/preview/urls.ts +23 -3
  282. package/src/query.ts +148 -5
  283. package/src/redirects/cache.ts +38 -18
  284. package/src/schema/registry.ts +56 -0
  285. package/src/schema/zod-generator.ts +39 -7
  286. package/src/seed/apply.ts +142 -54
  287. package/src/seed/types.ts +14 -1
  288. package/src/seed/validate.ts +27 -13
  289. package/src/settings/index.ts +80 -6
  290. package/src/settings/types.ts +23 -1
  291. package/src/taxonomies/index.ts +237 -210
  292. package/src/taxonomies/types.ts +10 -0
  293. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  294. package/dist/bylines-CRNsVG88.mjs +0 -157
  295. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  296. package/dist/cache-BkKBuIvS.mjs +0 -56
  297. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  298. package/dist/chunk-ClPoSABd.mjs +0 -21
  299. package/dist/content-BcQPYxdV.mjs.map +0 -1
  300. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  301. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  302. package/dist/loader-CndGj8kM.mjs.map +0 -1
  303. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  304. package/dist/query-fqEdLFms.mjs.map +0 -1
  305. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  306. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  307. package/dist/runner-OURCaApa.d.mts +0 -34
  308. package/dist/runner-OURCaApa.d.mts.map +0 -1
  309. package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
  310. package/dist/search-BoZYFuUk.mjs.map +0 -1
  311. package/dist/taxonomies-B4IAshV8.mjs +0 -308
  312. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  313. package/dist/types-CS8FIX7L.d.mts.map +0 -1
  314. package/dist/types-i36XcA_X.d.mts.map +0 -1
  315. package/dist/validate-CxVsLehf.mjs.map +0 -1
  316. package/dist/validate-DHxmpFJt.d.mts.map +0 -1
  317. package/dist/version-Bbq8TCrz.mjs +0 -7
package/src/mcp/server.ts CHANGED
@@ -14,6 +14,8 @@ import { canActOnOwn, hasPermission, Role } from "@emdash-cms/auth";
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
15
  import { z } from "zod";
16
16
 
17
+ import { contentBylineInputSchema, contentSeoInput } from "#api/schemas.js";
18
+
17
19
  import type { EmDashHandlers } from "../astro/types.js";
18
20
  import { hasScope } from "../auth/api-tokens.js";
19
21
 
@@ -623,7 +625,10 @@ export function createMcpServer(): McpServer {
623
625
  "Update an existing content item. Only include fields you want to change " +
624
626
  "in the 'data' object — unspecified fields are left unchanged. Pass the " +
625
627
  "_rev token from content_get to enable optimistic concurrency checking " +
626
- "(the update fails if the item was modified since you read it).",
628
+ "(the update fails if the item was modified since you read it). " +
629
+ "`seo` and `bylines` are persisted alongside the field updates in a " +
630
+ "single transaction. `publishedAt` requires the content:publish_any " +
631
+ "permission and is useful for migrations or correcting historical dates.",
627
632
  inputSchema: z.object({
628
633
  collection: z.string().describe("Collection slug"),
629
634
  id: z.string().describe("Content item ID or slug"),
@@ -638,6 +643,28 @@ export function createMcpServer(): McpServer {
638
643
  .describe(
639
644
  "New status. Setting to 'published' requires publish permission. Setting to 'draft' unpublishes the item and also requires publish permission.",
640
645
  ),
646
+ // Reuse the REST schema rather than redefining inline. The REST schema's
647
+ // `canonical` field is gated through `httpUrl` (validates the URL parses
648
+ // AND has an http(s) scheme) which rejects javascript:/data: URIs that
649
+ // would otherwise become stored XSS in the rendered <link rel="canonical">.
650
+ // Inlining a looser shape here would let MCP callers bypass that.
651
+ seo: contentSeoInput
652
+ .optional()
653
+ .describe(
654
+ "Per-content SEO metadata. Only valid for collections with SEO enabled (see schema_get_collection.hasSeo). Fields not included are left unchanged; pass null to clear.",
655
+ ),
656
+ bylines: z
657
+ .array(contentBylineInputSchema)
658
+ .optional()
659
+ .describe(
660
+ "Replace the byline list for this item. The first entry becomes the primary byline. Pass an empty array to clear all bylines.",
661
+ ),
662
+ publishedAt: z.iso
663
+ .datetime({ offset: true, message: "must be an ISO 8601 datetime" })
664
+ .nullish()
665
+ .describe(
666
+ "Override the publication timestamp (ISO 8601). Requires content:publish_any permission. Pass null to clear. Useful for content migrations.",
667
+ ),
641
668
  _rev: z
642
669
  .string()
643
670
  .optional()
@@ -647,35 +674,48 @@ export function createMcpServer(): McpServer {
647
674
  async (args, extra) => {
648
675
  requireScope(extra, "content:write");
649
676
  requireRole(extra, Role.AUTHOR);
650
- const { emdash, userId } = getExtra(extra);
677
+ const { emdash, userId, userRole } = getExtra(extra);
651
678
 
652
679
  // Fetch item to check ownership
653
680
  const existing = await emdash.handleContentGet(args.collection, args.id);
654
681
  if (!existing.success) {
655
682
  return unwrap(existing);
656
683
  }
657
- requireOwnership(
658
- extra,
659
- extractContentAuthorId(existing.data),
660
- "content:edit_own",
661
- "content:edit_any",
662
- );
684
+ const ownerId = extractContentAuthorId(existing.data);
685
+ requireOwnership(extra, ownerId, "content:edit_own", "content:edit_any");
686
+
687
+ // Writing publishedAt directly (incl. clearing to null) overwrites
688
+ // historical record — gate behind publish_any, mirroring the REST PUT
689
+ // route. Status-driven publishes are gated separately below.
690
+ if (args.publishedAt !== undefined) {
691
+ const user = { id: userId, role: userRole };
692
+ if (!hasPermission(user, "content:publish_any" as Permission)) {
693
+ throw new EmDashAuthError(
694
+ "Setting publishedAt requires content:publish_any permission",
695
+ "INSUFFICIENT_PERMISSIONS",
696
+ );
697
+ }
698
+ }
663
699
 
664
700
  const resolvedId = extractContentId(existing.data) ?? args.id;
665
701
 
666
702
  // Status transitions route through dedicated handlers for proper revision management
667
703
  if (args.status === "published") {
668
- requireOwnership(
669
- extra,
670
- extractContentAuthorId(existing.data),
671
- "content:publish_own",
672
- "content:publish_any",
673
- );
674
- if (args.data || args.slug) {
704
+ requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
705
+ if (
706
+ args.data ||
707
+ args.slug ||
708
+ args.seo !== undefined ||
709
+ args.bylines !== undefined ||
710
+ args.publishedAt !== undefined
711
+ ) {
675
712
  const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
676
713
  data: args.data,
677
714
  slug: args.slug,
678
715
  authorId: userId,
716
+ seo: args.seo,
717
+ bylines: args.bylines,
718
+ publishedAt: args.publishedAt,
679
719
  _rev: args._rev,
680
720
  });
681
721
  if (!updateResult.success) return unwrap(updateResult);
@@ -684,17 +724,21 @@ export function createMcpServer(): McpServer {
684
724
  }
685
725
 
686
726
  if (args.status === "draft") {
687
- requireOwnership(
688
- extra,
689
- extractContentAuthorId(existing.data),
690
- "content:publish_own",
691
- "content:publish_any",
692
- );
693
- if (args.data || args.slug) {
727
+ requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
728
+ if (
729
+ args.data ||
730
+ args.slug ||
731
+ args.seo !== undefined ||
732
+ args.bylines !== undefined ||
733
+ args.publishedAt !== undefined
734
+ ) {
694
735
  const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
695
736
  data: args.data,
696
737
  slug: args.slug,
697
738
  authorId: userId,
739
+ seo: args.seo,
740
+ bylines: args.bylines,
741
+ publishedAt: args.publishedAt,
698
742
  _rev: args._rev,
699
743
  });
700
744
  if (!updateResult.success) return unwrap(updateResult);
@@ -707,6 +751,9 @@ export function createMcpServer(): McpServer {
707
751
  data: args.data,
708
752
  slug: args.slug,
709
753
  authorId: userId,
754
+ seo: args.seo,
755
+ bylines: args.bylines,
756
+ publishedAt: args.publishedAt,
710
757
  _rev: args._rev,
711
758
  }),
712
759
  );
@@ -809,31 +856,53 @@ export function createMcpServer(): McpServer {
809
856
  description:
810
857
  "Publish a content item, making it live on the site. Creates a published " +
811
858
  "revision from the current draft. Further edits create a new draft without " +
812
- "affecting the live version until re-published.",
859
+ "affecting the live version until re-published. Pass `publishedAt` to " +
860
+ "backdate (e.g. when migrating content from another CMS) — this requires " +
861
+ "the content:publish_any permission. Without `publishedAt`, the existing " +
862
+ "`published_at` is preserved on re-publish (idempotent) and falls back to " +
863
+ "the current time on first publish.",
813
864
  inputSchema: z.object({
814
865
  collection: z.string().describe("Collection slug"),
815
866
  id: z.string().describe("Content item ID or slug"),
867
+ publishedAt: z.iso
868
+ .datetime({ offset: true, message: "must be an ISO 8601 datetime" })
869
+ .optional()
870
+ .describe(
871
+ "Override publication timestamp (ISO 8601). Requires content:publish_any permission. Useful when importing content with original publish dates.",
872
+ ),
816
873
  }),
817
874
  },
818
875
  async (args, extra) => {
819
876
  requireScope(extra, "content:write");
820
877
  requireRole(extra, Role.AUTHOR);
821
- const ec = getEmDash(extra);
878
+ const { emdash, userId, userRole } = getExtra(extra);
822
879
 
823
880
  // Fetch item to check ownership
824
- const existing = await ec.handleContentGet(args.collection, args.id);
881
+ const existing = await emdash.handleContentGet(args.collection, args.id);
825
882
  if (!existing.success) {
826
883
  return unwrap(existing);
827
884
  }
828
- requireOwnership(
829
- extra,
830
- extractContentAuthorId(existing.data),
831
- "content:publish_own",
832
- "content:publish_any",
833
- );
885
+ const ownerId = extractContentAuthorId(existing.data);
886
+ requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
887
+
888
+ // Backdating overwrites historical record — gate behind publish_any
889
+ // regardless of ownership (mirrors the REST PUT route's publishedAt gate).
890
+ if (args.publishedAt !== undefined) {
891
+ const user = { id: userId, role: userRole };
892
+ if (!hasPermission(user, "content:publish_any" as Permission)) {
893
+ throw new EmDashAuthError(
894
+ "Setting publishedAt requires content:publish_any permission",
895
+ "INSUFFICIENT_PERMISSIONS",
896
+ );
897
+ }
898
+ }
834
899
 
835
900
  const resolvedId = extractContentId(existing.data) ?? args.id;
836
- return unwrap(await ec.handleContentPublish(args.collection, resolvedId));
901
+ return unwrap(
902
+ await emdash.handleContentPublish(args.collection, resolvedId, {
903
+ publishedAt: args.publishedAt,
904
+ }),
905
+ );
837
906
  },
838
907
  );
839
908
 
@@ -1195,7 +1264,7 @@ export function createMcpServer(): McpServer {
1195
1264
  // ['drafts', 'revisions'] when undefined; pass through verbatim.
1196
1265
  supports: args.supports,
1197
1266
  });
1198
- ec.invalidateManifest();
1267
+ ec.invalidateUrlPatternCache();
1199
1268
  return jsonResult(collection);
1200
1269
  } catch (error) {
1201
1270
  return respondHandlerError(error, "SCHEMA_CREATE_ERROR");
@@ -1227,7 +1296,7 @@ export function createMcpServer(): McpServer {
1227
1296
  const { SchemaRegistry } = await import("../schema/index.js");
1228
1297
  const registry = new SchemaRegistry(ec.db);
1229
1298
  await registry.deleteCollection(args.slug, { force: args.force });
1230
- ec.invalidateManifest();
1299
+ ec.invalidateUrlPatternCache();
1231
1300
  return jsonResult({ deleted: args.slug });
1232
1301
  } catch (error) {
1233
1302
  return respondHandlerError(error, "SCHEMA_DELETE_ERROR");
@@ -1331,7 +1400,6 @@ export function createMcpServer(): McpServer {
1331
1400
  searchable: args.searchable,
1332
1401
  translatable: args.translatable,
1333
1402
  });
1334
- ec.invalidateManifest();
1335
1403
  return jsonResult(field);
1336
1404
  } catch (error) {
1337
1405
  return respondHandlerError(error, "FIELD_CREATE_ERROR");
@@ -1360,7 +1428,6 @@ export function createMcpServer(): McpServer {
1360
1428
  const { SchemaRegistry } = await import("../schema/index.js");
1361
1429
  const registry = new SchemaRegistry(ec.db);
1362
1430
  await registry.deleteField(args.collection, args.fieldSlug);
1363
- ec.invalidateManifest();
1364
1431
  return jsonResult({ deleted: args.fieldSlug, collection: args.collection });
1365
1432
  } catch (error) {
1366
1433
  return respondHandlerError(error, "FIELD_DELETE_ERROR");
@@ -1600,16 +1667,19 @@ export function createMcpServer(): McpServer {
1600
1667
  description:
1601
1668
  "List all taxonomy definitions (e.g. categories, tags). Taxonomies are " +
1602
1669
  "classification systems applied to content. Each has a name, label, and " +
1603
- "can be hierarchical (categories) or flat (tags).",
1604
- inputSchema: z.object({}),
1670
+ "can be hierarchical (categories) or flat (tags). Optionally filter by " +
1671
+ "locale.",
1672
+ inputSchema: z.object({
1673
+ locale: z.string().optional().describe("Filter by locale (omit for all)"),
1674
+ }),
1605
1675
  annotations: { readOnlyHint: true },
1606
1676
  },
1607
- async (_args, extra) => {
1677
+ async (args, extra) => {
1608
1678
  requireScope(extra, "content:read");
1609
1679
  const ec = getEmDash(extra);
1610
1680
  try {
1611
1681
  const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
1612
- return unwrap(await handleTaxonomyList(ec.db));
1682
+ return unwrap(await handleTaxonomyList(ec.db, { locale: args.locale }));
1613
1683
  } catch (error) {
1614
1684
  return respondHandlerError(error, "TAXONOMY_LIST_ERROR");
1615
1685
  }
@@ -1628,6 +1698,7 @@ export function createMcpServer(): McpServer {
1628
1698
  taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
1629
1699
  limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
1630
1700
  cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor"),
1701
+ locale: z.string().optional().describe("Filter by locale (omit for all)"),
1631
1702
  }),
1632
1703
  annotations: { readOnlyHint: true },
1633
1704
  },
@@ -1635,9 +1706,8 @@ export function createMcpServer(): McpServer {
1635
1706
  requireScope(extra, "content:read");
1636
1707
  const ec = getEmDash(extra);
1637
1708
  try {
1638
- // Verify taxonomy exists via handler layer
1639
1709
  const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
1640
- const listResult = await handleTaxonomyList(ec.db);
1710
+ const listResult = await handleTaxonomyList(ec.db, { locale: args.locale });
1641
1711
  if (!listResult.success) return unwrap(listResult);
1642
1712
 
1643
1713
  const taxonomies = (listResult.data as { taxonomies: Array<{ name: string; id?: string }> })
@@ -1645,13 +1715,12 @@ export function createMcpServer(): McpServer {
1645
1715
  const taxonomy = taxonomies.find((t: { name: string }) => t.name === args.taxonomy);
1646
1716
  if (!taxonomy) return respondError("NOT_FOUND", `Taxonomy '${args.taxonomy}' not found`);
1647
1717
 
1648
- // Paginated term query via repository (avoids N+1 of handleTermList)
1649
1718
  const { TaxonomyRepository } = await import("../database/repositories/taxonomy.js");
1650
1719
  const { decodeCursor, encodeCursor, InvalidCursorError } =
1651
1720
  await import("../database/repositories/types.js");
1652
1721
  const repo = new TaxonomyRepository(ec.db);
1653
1722
  const limit = Math.min(args.limit ?? 50, 100);
1654
- const terms = await repo.findByName(args.taxonomy);
1723
+ const terms = await repo.findByName(args.taxonomy, { locale: args.locale });
1655
1724
 
1656
1725
  // Manual keyset pagination over the sorted-by-label results.
1657
1726
  // Using a base64-encoded `(label, id)` cursor matches the
@@ -1693,6 +1762,8 @@ export function createMcpServer(): McpServer {
1693
1762
  label: t.label,
1694
1763
  parentId: t.parentId,
1695
1764
  description: typeof t.data?.description === "string" ? t.data.description : undefined,
1765
+ locale: t.locale,
1766
+ translationGroup: t.translationGroup,
1696
1767
  })),
1697
1768
  nextCursor,
1698
1769
  });
@@ -1718,6 +1789,11 @@ export function createMcpServer(): McpServer {
1718
1789
  label: z.string().describe("Display name"),
1719
1790
  parentId: z.string().optional().describe("Parent term ID for hierarchical taxonomies"),
1720
1791
  description: z.string().optional().describe("Description of the term"),
1792
+ locale: z.string().optional().describe("Locale for the new term (e.g. 'es')"),
1793
+ translationOf: z
1794
+ .string()
1795
+ .optional()
1796
+ .describe("Term id to join as a translation (same translation_group)"),
1721
1797
  }),
1722
1798
  },
1723
1799
  async (args, extra) => {
@@ -1732,6 +1808,8 @@ export function createMcpServer(): McpServer {
1732
1808
  label: args.label,
1733
1809
  parentId: args.parentId,
1734
1810
  description: args.description,
1811
+ locale: args.locale,
1812
+ translationOf: args.translationOf,
1735
1813
  }),
1736
1814
  );
1737
1815
  } catch (error) {
@@ -1808,6 +1886,29 @@ export function createMcpServer(): McpServer {
1808
1886
  },
1809
1887
  );
1810
1888
 
1889
+ server.registerTool(
1890
+ "taxonomy_term_translations",
1891
+ {
1892
+ title: "List Term Translations",
1893
+ description:
1894
+ "Return every locale variant of a taxonomy term, identified via its shared translation_group.",
1895
+ inputSchema: z.object({
1896
+ id: z.string().describe("Term id (or translation_group)"),
1897
+ }),
1898
+ annotations: { readOnlyHint: true },
1899
+ },
1900
+ async (args, extra) => {
1901
+ requireScope(extra, "content:read");
1902
+ const ec = getEmDash(extra);
1903
+ try {
1904
+ const { handleTermTranslations } = await import("../api/handlers/taxonomies.js");
1905
+ return unwrap(await handleTermTranslations(ec.db, args.id));
1906
+ } catch (error) {
1907
+ return respondHandlerError(error, "TERM_TRANSLATIONS_ERROR");
1908
+ }
1909
+ },
1910
+ );
1911
+
1811
1912
  // =====================================================================
1812
1913
  // Menu tools
1813
1914
  // =====================================================================
@@ -1817,18 +1918,20 @@ export function createMcpServer(): McpServer {
1817
1918
  {
1818
1919
  title: "List Menus",
1819
1920
  description:
1820
- "List all navigation menus defined in the CMS. Menus are named " +
1821
- "navigation structures (e.g. 'main', 'footer') containing ordered " +
1822
- "items with labels, URLs, and optional nesting.",
1823
- inputSchema: z.object({}),
1921
+ "List navigation menus. Menus are per-locale: filter by `locale` to " +
1922
+ "get just one locale's worth, or omit to list every row (one per " +
1923
+ "locale per menu name).",
1924
+ inputSchema: z.object({
1925
+ locale: z.string().optional().describe("Filter by locale (omit for all)"),
1926
+ }),
1824
1927
  annotations: { readOnlyHint: true },
1825
1928
  },
1826
- async (_args, extra) => {
1929
+ async (args, extra) => {
1827
1930
  requireScope(extra, "content:read");
1828
1931
  const ec = getEmDash(extra);
1829
1932
  try {
1830
1933
  const { handleMenuList } = await import("../api/handlers/menus.js");
1831
- return unwrap(await handleMenuList(ec.db));
1934
+ return unwrap(await handleMenuList(ec.db, { locale: args.locale }));
1832
1935
  } catch (error) {
1833
1936
  return respondHandlerError(error, "MENU_LIST_ERROR");
1834
1937
  }
@@ -1840,11 +1943,11 @@ export function createMcpServer(): McpServer {
1840
1943
  {
1841
1944
  title: "Get Menu with Items",
1842
1945
  description:
1843
- "Get a menu by name including all its items in order. Items have a " +
1844
- "label, URL, type (custom/content/collection), and optional parent " +
1845
- "for nesting.",
1946
+ "Get a menu by name, including its items. When multiple locales exist, " +
1947
+ "pass `locale` to pick the right one.",
1846
1948
  inputSchema: z.object({
1847
1949
  name: z.string().describe("Menu name (e.g. 'main', 'footer')"),
1950
+ locale: z.string().optional().describe("Locale to resolve the menu for"),
1848
1951
  }),
1849
1952
  annotations: { readOnlyHint: true },
1850
1953
  },
@@ -1853,13 +1956,36 @@ export function createMcpServer(): McpServer {
1853
1956
  const ec = getEmDash(extra);
1854
1957
  try {
1855
1958
  const { handleMenuGet } = await import("../api/handlers/menus.js");
1856
- return unwrap(await handleMenuGet(ec.db, args.name));
1959
+ return unwrap(await handleMenuGet(ec.db, args.name, { locale: args.locale }));
1857
1960
  } catch (error) {
1858
1961
  return respondHandlerError(error, "MENU_GET_ERROR");
1859
1962
  }
1860
1963
  },
1861
1964
  );
1862
1965
 
1966
+ server.registerTool(
1967
+ "menu_translations",
1968
+ {
1969
+ title: "List Menu Translations",
1970
+ description:
1971
+ "Return every locale variant of a menu, identified via the shared translation_group.",
1972
+ inputSchema: z.object({
1973
+ id: z.string().describe("Menu id (or translation_group)"),
1974
+ }),
1975
+ annotations: { readOnlyHint: true },
1976
+ },
1977
+ async (args, extra) => {
1978
+ requireScope(extra, "content:read");
1979
+ const ec = getEmDash(extra);
1980
+ try {
1981
+ const { handleMenuTranslations } = await import("../api/handlers/menus.js");
1982
+ return unwrap(await handleMenuTranslations(ec.db, args.id));
1983
+ } catch (error) {
1984
+ return respondHandlerError(error, "MENU_TRANSLATIONS_ERROR");
1985
+ }
1986
+ },
1987
+ );
1988
+
1863
1989
  server.registerTool(
1864
1990
  "menu_create",
1865
1991
  {