emdash 0.8.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 (286) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-DoNJiveC.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-BzltprvY.mjs} +85 -135
  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 +110 -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 +16 -59
  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 +72 -124
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +26 -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-Chbr2GoP.mjs → byline-BSaNL1w7.mjs} +4 -4
  28. package/dist/{byline-Chbr2GoP.mjs.map → byline-BSaNL1w7.mjs.map} +1 -1
  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 +224 -30
  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-BcQPYxdV.mjs → content-8lOYF0pr.mjs} +32 -15
  44. package/dist/{content-BcQPYxdV.mjs.map → content-8lOYF0pr.mjs.map} +1 -1
  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-l1Qh2RPR.mjs → db-errors-WRezodiz.mjs} +1 -1
  57. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
  58. package/dist/{default-DCVqE5ib.mjs → default-D8ksjWhO.mjs} +1 -1
  59. package/dist/{default-DCVqE5ib.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-zG5T1UGA.mjs → error-D_-tqP-I.mjs} +1 -1
  63. package/dist/{error-zG5T1UGA.mjs.map → error-D_-tqP-I.mjs.map} +1 -1
  64. package/dist/{index-DIb-CzNx.d.mts → index-BFRaVcD6.d.mts} +94 -34
  65. package/dist/index-BFRaVcD6.d.mts.map +1 -0
  66. package/dist/index.d.mts +11 -11
  67. package/dist/index.mjs +29 -27
  68. package/dist/{load-CyEoextb.mjs → load-DDqMMvZL.mjs} +2 -2
  69. package/dist/{load-CyEoextb.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
  70. package/dist/{loader-CndGj8kM.mjs → loader-CKLbBnhK.mjs} +27 -7
  71. package/dist/loader-CKLbBnhK.mjs.map +1 -0
  72. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-DqWNC3lM.mjs} +33 -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-D8FbNsl0.mjs → media-BW32b4gi.mjs} +2 -2
  79. package/dist/{media-D8FbNsl0.mjs.map → media-BW32b4gi.mjs.map} +1 -1
  80. package/dist/{mode-BnAOqItE.mjs → mode-ier8jbBk.mjs} +1 -1
  81. package/dist/{mode-BnAOqItE.mjs.map → mode-ier8jbBk.mjs.map} +1 -1
  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-D29tWZ7o.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
  86. package/dist/{placeholder-D29tWZ7o.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-C-fk5hYI.mjs.map → placeholder-CIJejMlK.mjs.map} +1 -1
  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-fqEdLFms.mjs → query-Cg9ZKRQ0.mjs} +114 -16
  96. package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
  97. package/dist/{redirect-D_pshWdf.mjs → redirect-BhUBKRc1.mjs} +11 -6
  98. package/dist/redirect-BhUBKRc1.mjs.map +1 -0
  99. package/dist/{registry-C3Mr0ODu.mjs → registry-Dw70ChxB.mjs} +38 -4
  100. package/dist/registry-Dw70ChxB.mjs.map +1 -0
  101. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-B-bmkipQ.mjs} +1 -1
  102. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-B-bmkipQ.mjs.map} +1 -1
  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-BoZYFuUk.mjs → search-dOGEccMa.mjs} +129 -83
  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.mjs +1 -1
  120. package/dist/{taxonomies-B4IAshV8.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
  121. package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
  122. package/dist/{tokens-D9vnZqYS.mjs → tokens-D7zMmWi2.mjs} +2 -2
  123. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
  124. package/dist/{transport-C9ugt2Nr.mjs → transport-BeMCmin1.mjs} +6 -5
  125. package/dist/{transport-C9ugt2Nr.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts → transport-DNEfeMaU.d.mts} +1 -1
  127. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
  128. package/dist/types-4fVtCIm0.mjs +68 -0
  129. package/dist/types-4fVtCIm0.mjs.map +1 -0
  130. package/dist/{types-BmPPSUEx.d.mts → types-BSyXeCFW.d.mts} +24 -2
  131. package/dist/{types-BmPPSUEx.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
  132. package/dist/{types-i36XcA_X.d.mts → types-BuBIptGk.d.mts} +65 -134
  133. package/dist/types-BuBIptGk.d.mts.map +1 -0
  134. package/dist/{types-CgqmmMJB.mjs → types-CDbKp7ND.mjs} +1 -1
  135. package/dist/{types-CgqmmMJB.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
  136. package/dist/{types-Bm1dn-q3.mjs → types-CIOg5AR8.mjs} +1 -1
  137. package/dist/{types-Bm1dn-q3.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
  138. package/dist/{types-BrA0xf5I.d.mts → types-CJsYGpco.d.mts} +1 -1
  139. package/dist/{types-BrA0xf5I.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
  140. package/dist/{types-BIgulNsW.mjs → types-CRxNbK-Z.mjs} +2 -2
  141. package/dist/{types-BIgulNsW.mjs.map → types-CRxNbK-Z.mjs.map} +1 -1
  142. package/dist/{types-CS8FIX7L.d.mts → types-CrtWgIvl.d.mts} +1 -1
  143. package/dist/{types-CS8FIX7L.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
  144. package/dist/{types-DIMwPFub.d.mts → types-M78DQ1lx.d.mts} +1 -1
  145. package/dist/{types-DIMwPFub.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
  146. package/dist/{validate-CxVsLehf.mjs → validate-Baqf0slj.mjs} +3 -3
  147. package/dist/{validate-CxVsLehf.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
  148. package/dist/{validate-DHxmpFJt.d.mts → validate-BfQh_C_y.d.mts} +4 -4
  149. package/dist/{validate-DHxmpFJt.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
  150. package/dist/{validation-C-ZpN2GI.mjs → validation-BfEI7tNe.mjs} +6 -6
  151. package/dist/{validation-C-ZpN2GI.mjs.map → validation-BfEI7tNe.mjs.map} +1 -1
  152. package/dist/version-DoxrVdYf.mjs +7 -0
  153. package/dist/{version-Bbq8TCrz.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
  154. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-CC0xNe_K.mjs} +4 -4
  155. package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
  156. package/locals.d.ts +1 -6
  157. package/package.json +9 -8
  158. package/src/api/handlers/comments.ts +6 -4
  159. package/src/api/handlers/content.ts +29 -1
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/oauth-authorization.ts +72 -33
  163. package/src/api/handlers/revision.ts +23 -14
  164. package/src/api/handlers/taxonomies.ts +3 -6
  165. package/src/api/public-url.ts +48 -2
  166. package/src/api/schemas/comments.ts +2 -2
  167. package/src/api/schemas/content.ts +17 -0
  168. package/src/api/schemas/sections.ts +3 -3
  169. package/src/api/schemas/users.ts +1 -1
  170. package/src/api/types.ts +5 -1
  171. package/src/astro/integration/index.ts +17 -0
  172. package/src/astro/integration/runtime.ts +30 -0
  173. package/src/astro/integration/virtual-modules.ts +32 -2
  174. package/src/astro/integration/vite-config.ts +6 -1
  175. package/src/astro/middleware/auth.ts +13 -6
  176. package/src/astro/middleware/redirect.ts +29 -16
  177. package/src/astro/middleware/request-context.ts +15 -5
  178. package/src/astro/middleware.ts +23 -9
  179. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  180. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  181. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  182. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  183. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  184. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  185. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  186. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  187. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  188. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  190. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  191. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  192. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  193. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  194. package/src/astro/routes/api/manifest.ts +62 -45
  195. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  196. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  197. package/src/astro/routes/api/openapi.json.ts +27 -10
  198. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  199. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  200. package/src/astro/routes/api/redirects/[id].ts +10 -4
  201. package/src/astro/routes/api/redirects/index.ts +7 -3
  202. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  203. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  204. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  205. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  206. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  207. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  208. package/src/astro/routes/api/search/index.ts +10 -2
  209. package/src/astro/routes/api/sections/[slug].ts +10 -4
  210. package/src/astro/routes/api/sections/index.ts +7 -3
  211. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  212. package/src/astro/routes/api/snapshot.ts +44 -18
  213. package/src/astro/routes/api/taxonomies/index.ts +0 -1
  214. package/src/astro/routes/api/themes/preview.ts +11 -5
  215. package/src/astro/types.ts +23 -3
  216. package/src/auth/allowed-origins.ts +168 -0
  217. package/src/auth/passkey-config.ts +35 -13
  218. package/src/bylines/index.ts +37 -88
  219. package/src/cli/commands/auth.ts +28 -6
  220. package/src/cli/commands/bundle-utils.ts +11 -2
  221. package/src/cli/commands/bundle.ts +28 -8
  222. package/src/cli/commands/content.ts +13 -0
  223. package/src/cli/commands/login.ts +8 -1
  224. package/src/cli/commands/publish.ts +24 -0
  225. package/src/cli/commands/secrets.ts +183 -0
  226. package/src/cli/credentials.ts +1 -1
  227. package/src/cli/index.ts +5 -1
  228. package/src/client/index.ts +4 -4
  229. package/src/client/transport.ts +17 -7
  230. package/src/components/Break.astro +2 -2
  231. package/src/components/EmDashHead.astro +18 -13
  232. package/src/components/Embed.astro +1 -1
  233. package/src/components/Gallery.astro +1 -1
  234. package/src/components/Image.astro +1 -1
  235. package/src/components/InlinePortableTextEditor.tsx +104 -18
  236. package/src/config/secrets.ts +528 -0
  237. package/src/database/dialect-helpers.ts +50 -0
  238. package/src/database/migrations/034_published_at_index.ts +1 -1
  239. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  240. package/src/database/migrations/runner.ts +156 -23
  241. package/src/database/repositories/content.ts +36 -12
  242. package/src/database/repositories/redirect.ts +14 -3
  243. package/src/database/repositories/taxonomy.ts +26 -0
  244. package/src/db/libsql.ts +1 -3
  245. package/src/db/sqlite.ts +2 -5
  246. package/src/emdash-runtime.ts +84 -159
  247. package/src/index.ts +9 -0
  248. package/src/loader.ts +24 -1
  249. package/src/mcp/server.ts +103 -36
  250. package/src/page/site-identity.ts +58 -0
  251. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  252. package/src/plugins/context.ts +13 -10
  253. package/src/plugins/define-plugin.ts +40 -12
  254. package/src/plugins/hooks.ts +23 -19
  255. package/src/plugins/index.ts +9 -0
  256. package/src/plugins/manifest-schema.ts +37 -2
  257. package/src/plugins/types.ts +151 -11
  258. package/src/preview/urls.ts +23 -3
  259. package/src/query.ts +148 -5
  260. package/src/redirects/cache.ts +38 -18
  261. package/src/schema/registry.ts +56 -0
  262. package/src/schema/zod-generator.ts +27 -5
  263. package/src/seed/apply.ts +2 -0
  264. package/src/settings/index.ts +80 -6
  265. package/src/settings/types.ts +23 -1
  266. package/src/taxonomies/index.ts +11 -1
  267. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  268. package/dist/bylines-CRNsVG88.mjs +0 -157
  269. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  270. package/dist/cache-BkKBuIvS.mjs +0 -56
  271. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  272. package/dist/chunk-ClPoSABd.mjs +0 -21
  273. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  274. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  275. package/dist/loader-CndGj8kM.mjs.map +0 -1
  276. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  277. package/dist/query-fqEdLFms.mjs.map +0 -1
  278. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  279. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  280. package/dist/runner-OURCaApa.d.mts +0 -34
  281. package/dist/runner-OURCaApa.d.mts.map +0 -1
  282. package/dist/search-BoZYFuUk.mjs.map +0 -1
  283. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  284. package/dist/types-i36XcA_X.d.mts.map +0 -1
  285. package/dist/version-Bbq8TCrz.mjs +0 -7
  286. package/dist/zod-generator-CpwccCIv.mjs.map +0 -1
@@ -12,6 +12,8 @@
12
12
 
13
13
  import { defineMiddleware } from "astro:middleware";
14
14
 
15
+ import { resolveSecretsCached } from "#config/secrets.js";
16
+
15
17
  import { verifyPreviewToken, parseContentId } from "../../preview/tokens.js";
16
18
  import { getRequestContext, runWithContext } from "../../request-context.js";
17
19
  import { renderToolbar } from "../../visual-editing/toolbar.js";
@@ -79,17 +81,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
79
81
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured
80
82
  const locale = (context as { currentLocale?: string }).currentLocale;
81
83
 
82
- // Verify preview token if present
84
+ // Verify preview token if present.
85
+ // The preview secret is resolved via `resolveSecretsCached`: env wins,
86
+ // otherwise a DB-stored value is read (or generated on first need).
87
+ // `emdash.db` is set by the runtime middleware which runs first; the
88
+ // only path where it's missing is a runtime-init failure.
83
89
  let preview: { collection: string; id: string } | undefined;
84
90
  if (hasPreviewToken) {
85
- const secret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || "";
86
-
87
- if (secret) {
88
- const result = await verifyPreviewToken({ url, secret });
91
+ const db = context.locals.emdash?.db;
92
+ if (db) {
93
+ const { previewSecret } = await resolveSecretsCached(db);
94
+ const result = await verifyPreviewToken({ url, secret: previewSecret });
89
95
  if (result.valid) {
90
96
  const { collection, id } = parseContentId(result.payload.cid);
91
97
  preview = { collection, id };
92
98
  }
99
+ } else {
100
+ console.warn(
101
+ "[emdash] Preview token present but EmDash runtime not initialized; preview disabled.",
102
+ );
93
103
  }
94
104
  }
95
105
 
@@ -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
  };
@@ -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);
@@ -36,7 +36,7 @@ export interface WpPluginImportResponse {
36
36
  }
37
37
 
38
38
  export const POST: APIRoute = async ({ request, locals }) => {
39
- const { emdash, emdashManifest, user } = locals;
39
+ const { emdash, user } = locals;
40
40
 
41
41
  const denied = requirePerm(user, "import:execute");
42
42
  if (denied) return denied;
@@ -46,6 +46,8 @@ export const POST: APIRoute = async ({ request, locals }) => {
46
46
  }
47
47
 
48
48
  try {
49
+ const emdashManifest = await emdash.getManifest();
50
+
49
51
  const body = await parseBody(request, wpPluginExecuteBody);
50
52
  if (isParseError(body)) return body;
51
53
 
@@ -9,64 +9,81 @@
9
9
 
10
10
  import type { APIRoute } from "astro";
11
11
 
12
+ import { handleError } from "#api/error.js";
12
13
  import { getAuthMode } from "#auth/mode.js";
13
14
 
14
15
  import { COMMIT, VERSION } from "../../../version.js";
15
- import { getStoredConfig } from "../../integration/runtime.js";
16
16
  import type { EmDashManifest } from "../../types.js";
17
17
 
18
18
  export const prerender = false;
19
19
 
20
20
  export const GET: APIRoute = async ({ locals }) => {
21
- const { emdashManifest, emdash } = locals;
21
+ const { emdash } = locals;
22
22
 
23
- // Determine auth mode from config
24
- const authMode = getAuthMode(emdash?.config);
23
+ try {
24
+ // Manifest is built fresh from the live database per admin request.
25
+ // `requestCached` inside `getManifest` dedupes if multiple consumers
26
+ // share the request. Wrapped in try/catch so any future DB-touching
27
+ // additions to `getManifest()` (plugin manifest loading, marketplace
28
+ // lookup, etc.) return the standard error envelope rather than an
29
+ // unstructured 500 — matches the pattern used by the WP execute
30
+ // routes.
31
+ const emdashManifest = emdash ? await emdash.getManifest() : null;
25
32
 
26
- // Read admin branding from build-time config
27
- const storedConfig = getStoredConfig();
28
- const adminBranding = storedConfig?.admin;
33
+ // Determine auth mode from config
34
+ const authMode = getAuthMode(emdash?.config);
29
35
 
30
- // Check if self-signup is enabled (any allowed domain with enabled = 1)
31
- // Only relevant for passkey auth external auth providers handle their own signup
32
- let signupEnabled = false;
33
- if (emdash?.db && authMode.type === "passkey") {
34
- try {
35
- const { sql } = await import("kysely");
36
- const result = await sql<{ cnt: unknown }>`
37
- SELECT COUNT(*) as cnt FROM allowed_domains WHERE enabled = 1
38
- `.execute(emdash.db);
39
- signupEnabled = Number(result.rows[0]?.cnt ?? 0) > 0;
40
- } catch {
41
- // Table may not exist yet, that's fine
42
- }
43
- }
36
+ // Read admin branding from the per-request config plumbed through middleware
37
+ // (same source admin.astro reads from). Reading from a build-time global
38
+ // here was unreliable -- the virtual config module exports the config but
39
+ // doesn't assign it to globalThis, so getStoredConfig() always returned
40
+ // null and the React SPA never received custom logo/siteName/favicon.
41
+ // See issue #835.
42
+ const adminBranding = emdash?.config?.admin;
44
43
 
45
- const manifest: EmDashManifest = emdashManifest
46
- ? {
47
- ...emdashManifest,
48
- authMode: authMode.type === "external" ? authMode.providerType : "passkey",
49
- signupEnabled,
50
- admin: adminBranding,
44
+ // Check if self-signup is enabled (any allowed domain with enabled = 1)
45
+ // Only relevant for passkey auth — external auth providers handle their own signup
46
+ let signupEnabled = false;
47
+ if (emdash?.db && authMode.type === "passkey") {
48
+ try {
49
+ const { sql } = await import("kysely");
50
+ const result = await sql<{ cnt: unknown }>`
51
+ SELECT COUNT(*) as cnt FROM allowed_domains WHERE enabled = 1
52
+ `.execute(emdash.db);
53
+ signupEnabled = Number(result.rows[0]?.cnt ?? 0) > 0;
54
+ } catch {
55
+ // Table may not exist yet, that's fine
51
56
  }
52
- : {
53
- version: VERSION,
54
- commit: COMMIT,
55
- hash: "default",
56
- collections: {},
57
- plugins: {},
58
- taxonomies: [],
59
- authMode: "passkey",
60
- signupEnabled,
61
- admin: adminBranding,
62
- };
57
+ }
63
58
 
64
- return Response.json(
65
- { data: manifest },
66
- {
67
- headers: {
68
- "Cache-Control": "private, no-store",
59
+ const manifest: EmDashManifest = emdashManifest
60
+ ? {
61
+ ...emdashManifest,
62
+ authMode: authMode.type === "external" ? authMode.providerType : "passkey",
63
+ signupEnabled,
64
+ admin: adminBranding,
65
+ }
66
+ : {
67
+ version: VERSION,
68
+ commit: COMMIT,
69
+ hash: "default",
70
+ collections: {},
71
+ plugins: {},
72
+ taxonomies: [],
73
+ authMode: "passkey",
74
+ signupEnabled,
75
+ admin: adminBranding,
76
+ };
77
+
78
+ return Response.json(
79
+ { data: manifest },
80
+ {
81
+ headers: {
82
+ "Cache-Control": "private, no-store",
83
+ },
69
84
  },
70
- },
71
- );
85
+ );
86
+ } catch (error) {
87
+ return handleError(error, "Failed to build manifest", "MANIFEST_BUILD_ERROR");
88
+ }
72
89
  };