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
@@ -46,6 +46,7 @@ import type { Database, Storage } from "../index.js";
46
46
  import { createPublicMediaUrlResolver } from "../media/url.js";
47
47
  import type { SandboxRunner } from "../plugins/sandbox/types.js";
48
48
  import type { ResolvedPlugin } from "../plugins/types.js";
49
+ import { invalidateUrlPatternCache } from "../query.js";
49
50
  import { getRequestContext, runWithContext } from "../request-context.js";
50
51
  import type { EmDashConfig } from "./integration/runtime.js";
51
52
  import type { EmDashHandlers } from "./types.js";
@@ -271,8 +272,15 @@ export const onRequest = defineMiddleware(async (context, next) => {
271
272
  // Read the Astro session user once up-front. Both the anonymous fast path
272
273
  // and the full doInit path need this, and the session store is network-backed
273
274
  // (KV / Durable Object) so we want to avoid re-fetching on the hot path.
274
- // Skipped entirely for prerendered requests — they have no session.
275
- const sessionUser = context.isPrerendered ? null : await context.session?.get("user");
275
+ // Skipped entirely for:
276
+ // - prerendered requests (no session at build time)
277
+ // - requests without an `astro-session` cookie (no session to look up)
278
+ // The cookie check matters on Cloudflare Workers, where Astro's session
279
+ // backend is KV: calling session.get() on every anonymous public request
280
+ // turns normal traffic into a flood of KV read misses. See #733.
281
+ const hasSessionCookie = cookies.get("astro-session") !== undefined;
282
+ const sessionUser =
283
+ context.isPrerendered || !hasSessionCookie ? null : await context.session?.get("user");
276
284
 
277
285
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
278
286
  if (!sessionUser && !playgroundDb) {
@@ -394,13 +402,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
394
402
  // Runtime init runs migrations, so the DB is guaranteed set up
395
403
  setupVerified = true;
396
404
 
397
- // Get manifest (cached after first call)
398
- t0 = performance.now();
399
- const manifest = await runtime.getManifest();
400
- timings.push({ name: "manifest", dur: performance.now() - t0, desc: "Manifest" });
405
+ // The manifest is no longer pre-loaded here. It's admin-only
406
+ // content that public/anonymous requests never read, and
407
+ // loading it on every request put logged-out hot paths on
408
+ // the same staleness budget as admin operations. Admin
409
+ // routes call `emdash.getManifest()` directly.
401
410
 
402
411
  // Attach to locals for route handlers
403
- locals.emdashManifest = manifest;
404
412
  locals.emdash = {
405
413
  // Content handlers
406
414
  handleContentList: runtime.handleContentList.bind(runtime),
@@ -469,8 +477,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
469
477
  // Configuration (for checking database type, auth mode, etc.)
470
478
  config,
471
479
 
472
- // Manifest invalidation (call after schema changes)
473
- invalidateManifest: runtime.invalidateManifest.bind(runtime),
480
+ // Lazy manifest accessor — admin-only consumers call this on
481
+ // demand. `requestCached` inside `getManifest` dedupes within
482
+ // a single request.
483
+ getManifest: runtime.getManifest.bind(runtime),
484
+
485
+ // Clear the URL pattern cache after schema mutations that
486
+ // affect collection URL patterns.
487
+ invalidateUrlPatternCache,
474
488
 
475
489
  // Sandbox runner (for marketplace plugin install/update)
476
490
  getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
@@ -17,6 +17,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
17
17
  import { isParseError, parseBody } from "#api/parse.js";
18
18
  import { getPublicOrigin } from "#api/public-url.js";
19
19
  import { inviteCompleteBody } from "#api/schemas.js";
20
+ import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
20
21
  import { createChallengeStore } from "#auth/challenge-store.js";
21
22
  import { getPasskeyConfig } from "#auth/passkey-config.js";
22
23
  import { OptionsRepository } from "#db/repositories/options.js";
@@ -39,7 +40,11 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
39
40
  const options = new OptionsRepository(emdash.db);
40
41
  const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
41
42
  const siteUrl = getPublicOrigin(url, emdash?.config);
42
- const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
43
+ const allowedOrigins = validateAllowedOrigins(
44
+ siteUrl,
45
+ getConfiguredAllowedOrigins(emdash?.config),
46
+ );
47
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
43
48
 
44
49
  // Verify the passkey registration response
45
50
  const challengeStore = createChallengeStore(emdash.db);
@@ -15,6 +15,7 @@ import { apiError, apiSuccess } from "#api/error.js";
15
15
  import { isParseError, parseBody } from "#api/parse.js";
16
16
  import { getPublicOrigin } from "#api/public-url.js";
17
17
  import { passkeyRegisterVerifyBody } from "#api/schemas.js";
18
+ import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
18
19
  import { createChallengeStore } from "#auth/challenge-store.js";
19
20
  import { getPasskeyConfig } from "#auth/passkey-config.js";
20
21
  import { OptionsRepository } from "#db/repositories/options.js";
@@ -60,7 +61,11 @@ export const POST: APIRoute = async ({ request, locals }) => {
60
61
  const optionsRepo = new OptionsRepository(emdash.db);
61
62
  const siteName = (await optionsRepo.get<string>("emdash:site_title")) ?? undefined;
62
63
  const siteUrl = getPublicOrigin(url, emdash?.config);
63
- const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
64
+ const allowedOrigins = validateAllowedOrigins(
65
+ siteUrl,
66
+ getConfiguredAllowedOrigins(emdash?.config),
67
+ );
68
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
64
69
 
65
70
  // Verify the registration response
66
71
  const challengeStore = createChallengeStore(emdash.db);
@@ -15,6 +15,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
15
15
  import { isParseError, parseBody } from "#api/parse.js";
16
16
  import { getPublicOrigin } from "#api/public-url.js";
17
17
  import { passkeyVerifyBody } from "#api/schemas.js";
18
+ import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
18
19
  import { createChallengeStore } from "#auth/challenge-store.js";
19
20
  import { getPasskeyConfig } from "#auth/passkey-config.js";
20
21
  import { OptionsRepository } from "#db/repositories/options.js";
@@ -35,7 +36,11 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
35
36
  const options = new OptionsRepository(emdash.db);
36
37
  const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
37
38
  const siteUrl = getPublicOrigin(url, emdash?.config);
38
- const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
39
+ const allowedOrigins = validateAllowedOrigins(
40
+ siteUrl,
41
+ getConfiguredAllowedOrigins(emdash?.config),
42
+ );
43
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
39
44
 
40
45
  // Authenticate with passkey
41
46
  const adapter = createKyselyAdapter(emdash.db);
@@ -17,6 +17,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
17
17
  import { isParseError, parseBody } from "#api/parse.js";
18
18
  import { getPublicOrigin } from "#api/public-url.js";
19
19
  import { signupCompleteBody } from "#api/schemas.js";
20
+ import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
20
21
  import { createChallengeStore } from "#auth/challenge-store.js";
21
22
  import { getPasskeyConfig } from "#auth/passkey-config.js";
22
23
  import { OptionsRepository } from "#db/repositories/options.js";
@@ -39,7 +40,11 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
39
40
  const options = new OptionsRepository(emdash.db);
40
41
  const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
41
42
  const siteUrl = getPublicOrigin(url, emdash?.config);
42
- const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
43
+ const allowedOrigins = validateAllowedOrigins(
44
+ siteUrl,
45
+ getConfiguredAllowedOrigins(emdash?.config),
46
+ );
47
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
43
48
 
44
49
  // Verify the passkey registration response
45
50
  const challengeStore = createChallengeStore(emdash.db);
@@ -14,6 +14,7 @@ import { createCommentBody } from "#api/schemas.js";
14
14
  import { getSiteBaseUrl } from "#api/site-url.js";
15
15
  import { sendCommentNotification } from "#comments/notifications.js";
16
16
  import { createComment, type CommentHookRunner } from "#comments/service.js";
17
+ import { resolveSecretsCached } from "#config/secrets.js";
17
18
  import { CommentRepository } from "#db/repositories/comment.js";
18
19
  import { validateIdentifier } from "#db/validate.js";
19
20
  import { extractRequestMeta } from "#plugins/request-meta.js";
@@ -140,8 +141,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
140
141
 
141
142
  // Anti-spam: Rate limiting
142
143
  const meta = extractRequestMeta(request, emdash.config);
143
- const ipSalt =
144
- import.meta.env.EMDASH_AUTH_SECRET || import.meta.env.AUTH_SECRET || "emdash-ip-salt";
144
+ const { ipSalt } = await resolveSecretsCached(emdash.db);
145
145
  let ipHash: string;
146
146
  if (meta.ip) {
147
147
  ipHash = await hashIp(meta.ip, ipSalt);
@@ -44,11 +44,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
44
44
  const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
45
45
  if (denied) return denied;
46
46
 
47
- const result = await emdash.handleContentDiscardDraft(collection, id);
47
+ const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
48
+
49
+ const result = await emdash.handleContentDiscardDraft(collection, resolvedId);
48
50
 
49
51
  if (!result.success) return unwrapResult(result);
50
52
 
51
- if (cache.enabled) await cache.invalidate({ tags: [collection, id] });
53
+ if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
52
54
 
53
55
  return unwrapResult(result);
54
56
  };
@@ -16,7 +16,7 @@ export const DELETE: APIRoute = async ({ params, locals, cache }) => {
16
16
  const collection = params.collection!;
17
17
  const id = params.id!;
18
18
 
19
- const denied = requirePerm(user, "import:execute");
19
+ const denied = requirePerm(user, "content:delete_permanent");
20
20
  if (denied) return denied;
21
21
 
22
22
  if (!emdash?.handleContentPermanentDelete) {
@@ -6,7 +6,7 @@
6
6
  * Request body:
7
7
  * {
8
8
  * expiresIn?: string | number; // Default: "1h"
9
- * pathPattern?: string; // Default: "/{collection}/{id}"
9
+ * pathPattern?: string; // Default: "/{collection}/{id}" (or EMDASH_PREVIEW_PATH_PATTERN)
10
10
  * }
11
11
  *
12
12
  * Response:
@@ -22,8 +22,11 @@ import { requirePerm } from "#api/authorize.js";
22
22
  import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
23
23
  import { parseOptionalBody, isParseError } from "#api/parse.js";
24
24
  import { contentPreviewUrlBody } from "#api/schemas.js";
25
+ import { resolveSecretsCached } from "#config/secrets.js";
25
26
  import { getPreviewUrl } from "#preview/index.js";
26
27
 
28
+ import { getI18nConfig } from "../../../../../../i18n/config.js";
29
+
27
30
  export const prerender = false;
28
31
 
29
32
  const DURATION_PATTERN = /^(\d+)([smhdw])$/;
@@ -35,21 +38,23 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
35
38
  const collection = params.collection!;
36
39
  const id = params.id!;
37
40
 
38
- // Get the preview secret from environment
39
- const previewSecret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET;
40
-
41
- if (!previewSecret) {
42
- return apiError(
43
- "NOT_CONFIGURED",
44
- "Preview not configured. Set EMDASH_PREVIEW_SECRET environment variable.",
45
- 500,
46
- );
41
+ if (!emdash?.db) {
42
+ return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
47
43
  }
48
44
 
49
- // Verify the content exists (optional, but good for UX)
45
+ // Resolve the preview secret. Env override wins; otherwise a stable
46
+ // site-specific value is read from (or generated into) the options table.
47
+ // The resolver always returns a usable secret, so this path can no
48
+ // longer be silently disabled by a missing env var.
49
+ const { previewSecret } = await resolveSecretsCached(emdash.db);
50
+
51
+ // Verify the content exists. The fetched item also yields the entry's
52
+ // locale, used below to resolve the `{locale}` placeholder.
53
+ let entryLocale: string | null = null;
50
54
  if (emdash?.handleContentGet) {
51
55
  const result = await emdash.handleContentGet(collection, id);
52
56
  if (!result.success) return unwrapResult(result);
57
+ entryLocale = result.data?.item?.locale ?? null;
53
58
  }
54
59
 
55
60
  // Parse request body
@@ -57,7 +62,23 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
57
62
  if (isParseError(body)) return body;
58
63
 
59
64
  const expiresIn = body.expiresIn || "1h";
60
- const pathPattern = body.pathPattern;
65
+ // Allow a project-wide default `pathPattern` so the admin's "View on site"
66
+ // link can match the site's actual route shape without each call having
67
+ // to override the default `/{collection}/{id}`.
68
+ const defaultPathPattern = import.meta.env.EMDASH_PREVIEW_PATH_PATTERN || "/{collection}/{id}";
69
+ const pathPattern = body.pathPattern || defaultPathPattern;
70
+
71
+ // Resolve the locale segment substituted for `{locale}`: empty when the
72
+ // entry is in the default locale and `prefixDefaultLocale` is `false`,
73
+ // the entry's own locale otherwise.
74
+ const i18n = getI18nConfig();
75
+ let localeSegment = "";
76
+ if (entryLocale && i18n) {
77
+ const isDefault = entryLocale === i18n.defaultLocale;
78
+ localeSegment = isDefault && !i18n.prefixDefaultLocale ? "" : entryLocale;
79
+ } else if (entryLocale) {
80
+ localeSegment = entryLocale;
81
+ }
61
82
 
62
83
  // Calculate expiry timestamp
63
84
  const expiresInSeconds = typeof expiresIn === "number" ? expiresIn : parseExpiresIn(expiresIn);
@@ -70,6 +91,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
70
91
  secret: previewSecret,
71
92
  expiresIn,
72
93
  pathPattern,
94
+ locale: localeSegment,
73
95
  });
74
96
 
75
97
  return apiSuccess({ url, expiresAt });
@@ -2,16 +2,25 @@
2
2
  * Publish content - promotes draft to live
3
3
  *
4
4
  * POST /_emdash/api/content/{collection}/{id}/publish
5
+ *
6
+ * Optional JSON body: { publishedAt?: string }
7
+ * publishedAt — ISO 8601 datetime to backdate the publish (e.g. when
8
+ * migrating content). Writing publishedAt requires content:publish_any.
9
+ * Without it, the existing published_at is preserved on re-publish and
10
+ * falls back to the current time on first publish.
5
11
  */
6
12
 
13
+ import { hasPermission } from "@emdash-cms/auth";
7
14
  import type { APIRoute } from "astro";
8
15
 
9
16
  import { requireOwnerPerm } from "#api/authorize.js";
10
17
  import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
18
+ import { isParseError, parseOptionalBody } from "#api/parse.js";
19
+ import { contentPublishBody } from "#api/schemas.js";
11
20
 
12
21
  export const prerender = false;
13
22
 
14
- export const POST: APIRoute = async ({ params, locals, cache }) => {
23
+ export const POST: APIRoute = async ({ params, request, locals, cache }) => {
15
24
  const { emdash, user } = locals;
16
25
  const collection = params.collection!;
17
26
  const id = params.id!;
@@ -20,6 +29,11 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
20
29
  return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
21
30
  }
22
31
 
32
+ // Body is optional — empty body means use the legacy behavior (preserve
33
+ // or default published_at). Pass `publishedAt` to backdate.
34
+ const body = await parseOptionalBody(request, contentPublishBody, {});
35
+ if (isParseError(body)) return body;
36
+
23
37
  // Fetch item to check ownership
24
38
  const existing = await emdash.handleContentGet(collection, id);
25
39
  if (!existing.success) {
@@ -44,9 +58,25 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
44
58
  const denied = requireOwnerPerm(user, authorId, "content:publish_own", "content:publish_any");
45
59
  if (denied) return denied;
46
60
 
61
+ // Schema narrows `publishedAt` to `string | undefined`; null is rejected
62
+ // at the schema layer (publish has no semantic meaning for "clear").
63
+ const publishedAt = body?.publishedAt;
64
+
65
+ // Backdating overwrites historical record — gate behind publish_any
66
+ // regardless of ownership.
67
+ if (publishedAt !== undefined && !hasPermission(user, "content:publish_any")) {
68
+ return apiError(
69
+ "FORBIDDEN",
70
+ "Setting publishedAt requires content:publish_any permission",
71
+ 403,
72
+ );
73
+ }
74
+
47
75
  const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
48
76
 
49
- const result = await emdash.handleContentPublish(collection, resolvedId);
77
+ const result = await emdash.handleContentPublish(collection, resolvedId, {
78
+ publishedAt,
79
+ });
50
80
 
51
81
  if (!result.success) return unwrapResult(result);
52
82
 
@@ -44,11 +44,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
44
44
  const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
45
45
  if (denied) return denied;
46
46
 
47
- const result = await emdash.handleContentRestore(collection, id);
47
+ const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
48
+
49
+ const result = await emdash.handleContentRestore(collection, resolvedId);
48
50
 
49
51
  if (!result.success) return unwrapResult(result);
50
52
 
51
- if (cache.enabled) await cache.invalidate({ tags: [collection, id] });
53
+ if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
52
54
 
53
55
  return unwrapResult(result);
54
56
  };
@@ -22,9 +22,10 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
22
22
  return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
23
23
  }
24
24
 
25
- const limit = url.searchParams.get("limit");
25
+ const limitParam = url.searchParams.get("limit");
26
+ const parsedLimit = limitParam ? parseInt(limitParam, 10) : undefined;
26
27
  const result = await emdash.handleRevisionList(collection, id, {
27
- limit: limit ? parseInt(limit, 10) : undefined,
28
+ limit: parsedLimit ? Math.max(1, Math.min(parsedLimit, 100)) : undefined,
28
29
  });
29
30
 
30
31
  return unwrapResult(result);
@@ -98,6 +98,10 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
98
98
  const editDenied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
99
99
  if (editDenied) return editDenied;
100
100
 
101
+ // Resolve the canonical content ID from the handler result.
102
+ // The URL `id` param may be a slug; we must use the real ID for term storage.
103
+ const canonicalId = typeof existingItem?.id === "string" ? existingItem.id : id;
104
+
101
105
  try {
102
106
  const body = await parseBody(request, contentTermsBody);
103
107
  if (isParseError(body)) return body;
@@ -120,15 +124,15 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
120
124
  }
121
125
  }
122
126
 
123
- // Set the terms (replaces existing)
124
- await repo.setTermsForEntry(collection, id, taxonomy, termIds);
127
+ // Set the terms (replaces existing) using the canonical ID
128
+ await repo.setTermsForEntry(collection, canonicalId, taxonomy, termIds);
125
129
 
126
130
  // Term assignments changed — invalidate the hasAnyTermAssignments cache
127
131
  // so hydration on subsequent reads issues a fresh query.
128
132
  invalidateTermCache();
129
133
 
130
- // Get the updated terms
131
- const terms = await repo.getTermsForEntry(collection, id, taxonomy);
134
+ // Get the updated terms using the canonical ID
135
+ const terms = await repo.getTermsForEntry(collection, canonicalId, taxonomy);
132
136
 
133
137
  return apiSuccess({
134
138
  terms: terms.map((t) => ({
@@ -47,6 +47,18 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
47
47
  if (status !== "published") {
48
48
  return apiError("NOT_FOUND", `Content item not found: ${id}`, 404);
49
49
  }
50
+
51
+ // Strip draft hydration data from response for users without read_drafts.
52
+ // handleContentGet overlays draft revision data onto item.data and exposes
53
+ // the published values in item.liveData. Without this, subscribers see
54
+ // unpublished edits in the data field.
55
+ if (item) {
56
+ if (item.liveData && typeof item.liveData === "object") {
57
+ item.data = item.liveData;
58
+ }
59
+ delete item.liveData;
60
+ delete item.draftRevisionId;
61
+ }
50
62
  }
51
63
 
52
64
  return unwrapResult(result);
@@ -60,7 +60,7 @@ export interface ImportResult {
60
60
  }
61
61
 
62
62
  export const POST: APIRoute = async ({ request, locals }) => {
63
- const { emdash, emdashManifest, user } = locals;
63
+ const { emdash, user } = locals;
64
64
 
65
65
  const denied = requirePerm(user, "import:execute");
66
66
  if (denied) return denied;
@@ -70,6 +70,8 @@ export const POST: APIRoute = async ({ request, locals }) => {
70
70
  }
71
71
 
72
72
  try {
73
+ const emdashManifest = await emdash.getManifest();
74
+
73
75
  const formData = await request.formData();
74
76
  const fileEntry = formData.get("file");
75
77
  const file = fileEntry instanceof File ? fileEntry : null;
@@ -58,14 +58,13 @@ export const POST: APIRoute = async ({ request, locals }) => {
58
58
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to PrepareRequest
59
59
  const result = await prepareImport(emdash.db, body as PrepareRequest);
60
60
 
61
- // If prepare created any new collections or fields, invalidate the
62
- // persisted manifest cache (`emdash:manifest_cache` in the options
63
- // table) so that the execute endpoint -- a separate request -- sees
64
- // the new schema. Without this the execute step reads a stale
65
- // manifest and reports `Collection "<slug>" does not exist` for
66
- // every item destined for a freshly-created collection. See #747.
67
- if (result.collectionsCreated.length > 0 || result.fieldsCreated.length > 0) {
68
- emdash.invalidateManifest();
61
+ // Invalidate the URL pattern cache when prepare adds new collections so
62
+ // public routing picks up their patterns immediately. The manifest
63
+ // itself is built fresh per admin request, so cross-request
64
+ // staleness (the original failure mode in #747) is no longer
65
+ // possible the execute step always reads live schema.
66
+ if (result.collectionsCreated.length > 0) {
67
+ emdash.invalidateUrlPatternCache();
69
68
  }
70
69
 
71
70
  return apiSuccess(result, result.success ? 200 : 400);