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
@@ -10,7 +10,7 @@
10
10
  import type { APIRoute } from "astro";
11
11
  import { MediaRepository } from "emdash";
12
12
 
13
- import { requirePerm } from "#api/authorize.js";
13
+ import { requireOwnerPerm, requirePerm } from "#api/authorize.js";
14
14
  import { apiError, apiSuccess, handleError } from "#api/error.js";
15
15
  import { isParseError, parseOptionalBody } from "#api/parse.js";
16
16
  import { mediaConfirmBody } from "#api/schemas.js";
@@ -62,6 +62,15 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
62
62
  return apiError("INVALID_STATE", `Media item is not pending: ${existing.status}`, 400);
63
63
  }
64
64
 
65
+ // Only the uploader or a user with media:edit_any can confirm/fail a pending upload
66
+ const ownerDenied = requireOwnerPerm(
67
+ user,
68
+ existing.authorId ?? "",
69
+ "media:edit_own",
70
+ "media:edit_any",
71
+ );
72
+ if (ownerDenied) return ownerDenied;
73
+
65
74
  // Optionally verify the file exists in storage
66
75
  if (emdash.storage) {
67
76
  const exists = await emdash.storage.exists(existing.storageKey);
@@ -37,9 +37,8 @@ export const GET: APIRoute = async ({ params, request, locals }) => {
37
37
 
38
38
  const url = new URL(request.url);
39
39
  const cursor = url.searchParams.get("cursor") || undefined;
40
- const limit = url.searchParams.get("limit")
41
- ? parseInt(url.searchParams.get("limit")!, 10)
42
- : undefined;
40
+ const rawLimit = url.searchParams.get("limit");
41
+ const limit = rawLimit ? Math.max(1, Math.min(parseInt(rawLimit, 10) || 50, 100)) : undefined;
43
42
  const query = url.searchParams.get("query") || undefined;
44
43
  const mimeType = url.searchParams.get("mimeType") || undefined;
45
44
 
@@ -98,6 +97,16 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
98
97
  return apiError("NO_FILE", "No file provided", 400);
99
98
  }
100
99
 
100
+ // Basic size validation (default 50MB, configurable via maxUploadSize)
101
+ const maxSize = emdash.config?.maxUploadSize ?? 50 * 1024 * 1024;
102
+ if (file.size > maxSize) {
103
+ return apiError(
104
+ "FILE_TOO_LARGE",
105
+ `File exceeds maximum size of ${Math.round(maxSize / 1024 / 1024)}MB`,
106
+ 413,
107
+ );
108
+ }
109
+
101
110
  const item = await provider.upload({
102
111
  file,
103
112
  filename: file.name,
@@ -9,33 +9,50 @@
9
9
 
10
10
  import type { APIRoute } from "astro";
11
11
 
12
+ import { handleError } from "../../../api/error.js";
12
13
  import { generateOpenApiDocument } from "../../../api/openapi/index.js";
13
14
 
14
15
  export const prerender = false;
15
16
 
16
- let cachedSpec: string | null = null;
17
+ // Use globalThis with Symbol.for to survive Vite's SSR module duplication
18
+ const OPENAPI_CACHE_KEY = Symbol.for("emdash.openapi.cachedSpec");
19
+
20
+ function getCachedSpec(): string | null {
21
+ const val = (globalThis as Record<symbol, unknown>)[OPENAPI_CACHE_KEY];
22
+ return typeof val === "string" ? val : null;
23
+ }
24
+
25
+ function setCachedSpec(spec: string): void {
26
+ (globalThis as Record<symbol, unknown>)[OPENAPI_CACHE_KEY] = spec;
27
+ }
17
28
 
18
29
  export const GET: APIRoute = async ({ locals }) => {
19
30
  const { emdash } = locals;
20
- if (!cachedSpec && emdash) {
31
+
32
+ let spec = getCachedSpec();
33
+ if (!spec && emdash) {
21
34
  try {
22
35
  const doc = generateOpenApiDocument({ maxUploadSize: emdash.config.maxUploadSize });
23
- cachedSpec = JSON.stringify(doc);
24
- } catch {
25
- return new Response(
26
- JSON.stringify({ error: "Failed to generate OpenAPI document: invalid configuration" }),
27
- { status: 500, headers: { "Content-Type": "application/json" } },
28
- );
36
+ spec = JSON.stringify(doc);
37
+ setCachedSpec(spec);
38
+ } catch (error) {
39
+ return handleError(error, "Failed to generate OpenAPI document", "OPENAPI_ERROR");
29
40
  }
30
41
  }
31
42
 
32
- const spec = cachedSpec ?? JSON.stringify(generateOpenApiDocument());
43
+ if (!spec) {
44
+ try {
45
+ spec = JSON.stringify(generateOpenApiDocument());
46
+ } catch (error) {
47
+ return handleError(error, "Failed to generate OpenAPI document", "OPENAPI_ERROR");
48
+ }
49
+ }
33
50
 
34
51
  return new Response(spec, {
35
52
  status: 200,
36
53
  headers: {
37
54
  "Content-Type": "application/json",
38
- "Cache-Control": "public, max-age=3600",
55
+ "Cache-Control": "private, no-store",
39
56
  "Access-Control-Allow-Origin": "*",
40
57
  },
41
58
  });
@@ -9,7 +9,7 @@
9
9
  import type { APIRoute } from "astro";
10
10
 
11
11
  import { requirePerm } from "#api/authorize.js";
12
- import { handleError, unwrapResult } from "#api/error.js";
12
+ import { handleError, requireDb, unwrapResult } from "#api/error.js";
13
13
  import {
14
14
  handleNotFoundClear,
15
15
  handleNotFoundList,
@@ -22,7 +22,9 @@ export const prerender = false;
22
22
 
23
23
  export const GET: APIRoute = async ({ url, locals }) => {
24
24
  const { emdash, user } = locals;
25
- const db = emdash.db;
25
+ const dbErr = requireDb(emdash?.db);
26
+ if (dbErr) return dbErr;
27
+ const db = emdash!.db;
26
28
 
27
29
  const denied = requirePerm(user, "redirects:read");
28
30
  if (denied) return denied;
@@ -40,7 +42,9 @@ export const GET: APIRoute = async ({ url, locals }) => {
40
42
 
41
43
  export const DELETE: APIRoute = async ({ locals }) => {
42
44
  const { emdash, user } = locals;
43
- const db = emdash.db;
45
+ const dbErr = requireDb(emdash?.db);
46
+ if (dbErr) return dbErr;
47
+ const db = emdash!.db;
44
48
 
45
49
  const denied = requirePerm(user, "redirects:manage");
46
50
  if (denied) return denied;
@@ -55,7 +59,9 @@ export const DELETE: APIRoute = async ({ locals }) => {
55
59
 
56
60
  export const POST: APIRoute = async ({ request, locals }) => {
57
61
  const { emdash, user } = locals;
58
- const db = emdash.db;
62
+ const dbErr = requireDb(emdash?.db);
63
+ if (dbErr) return dbErr;
64
+ const db = emdash!.db;
59
65
 
60
66
  const denied = requirePerm(user, "redirects:manage");
61
67
  if (denied) return denied;
@@ -7,7 +7,7 @@
7
7
  import type { APIRoute } from "astro";
8
8
 
9
9
  import { requirePerm } from "#api/authorize.js";
10
- import { handleError, unwrapResult } from "#api/error.js";
10
+ import { handleError, requireDb, unwrapResult } from "#api/error.js";
11
11
  import { handleNotFoundSummary } from "#api/handlers/redirects.js";
12
12
  import { isParseError, parseQuery } from "#api/parse.js";
13
13
  import { notFoundSummaryQuery } from "#api/schemas.js";
@@ -16,7 +16,9 @@ export const prerender = false;
16
16
 
17
17
  export const GET: APIRoute = async ({ url, locals }) => {
18
18
  const { emdash, user } = locals;
19
- const db = emdash.db;
19
+ const dbErr = requireDb(emdash?.db);
20
+ if (dbErr) return dbErr;
21
+ const db = emdash!.db;
20
22
 
21
23
  const denied = requirePerm(user, "redirects:read");
22
24
  if (denied) return denied;
@@ -9,7 +9,7 @@
9
9
  import type { APIRoute } from "astro";
10
10
 
11
11
  import { requirePerm } from "#api/authorize.js";
12
- import { apiError, handleError, unwrapResult } from "#api/error.js";
12
+ import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
13
13
  import {
14
14
  handleRedirectDelete,
15
15
  handleRedirectGet,
@@ -23,7 +23,9 @@ export const prerender = false;
23
23
 
24
24
  export const GET: APIRoute = async ({ params, locals }) => {
25
25
  const { emdash, user } = locals;
26
- const db = emdash.db;
26
+ const dbErr = requireDb(emdash?.db);
27
+ if (dbErr) return dbErr;
28
+ const db = emdash!.db;
27
29
  const { id } = params;
28
30
 
29
31
  const denied = requirePerm(user, "redirects:read");
@@ -43,7 +45,9 @@ export const GET: APIRoute = async ({ params, locals }) => {
43
45
 
44
46
  export const PUT: APIRoute = async ({ params, request, locals }) => {
45
47
  const { emdash, user } = locals;
46
- const db = emdash.db;
48
+ const dbErr = requireDb(emdash?.db);
49
+ if (dbErr) return dbErr;
50
+ const db = emdash!.db;
47
51
  const { id } = params;
48
52
 
49
53
  const denied = requirePerm(user, "redirects:manage");
@@ -67,7 +71,9 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
67
71
 
68
72
  export const DELETE: APIRoute = async ({ params, locals }) => {
69
73
  const { emdash, user } = locals;
70
- const db = emdash.db;
74
+ const dbErr = requireDb(emdash?.db);
75
+ if (dbErr) return dbErr;
76
+ const db = emdash!.db;
71
77
  const { id } = params;
72
78
 
73
79
  const denied = requirePerm(user, "redirects:manage");
@@ -8,7 +8,7 @@
8
8
  import type { APIRoute } from "astro";
9
9
 
10
10
  import { requirePerm } from "#api/authorize.js";
11
- import { handleError, unwrapResult } from "#api/error.js";
11
+ import { handleError, requireDb, unwrapResult } from "#api/error.js";
12
12
  import { handleRedirectCreate, handleRedirectList } from "#api/handlers/redirects.js";
13
13
  import { isParseError, parseBody, parseQuery } from "#api/parse.js";
14
14
  import { createRedirectBody, redirectsListQuery } from "#api/schemas.js";
@@ -18,7 +18,9 @@ export const prerender = false;
18
18
 
19
19
  export const GET: APIRoute = async ({ url, locals }) => {
20
20
  const { emdash, user } = locals;
21
- const db = emdash.db;
21
+ const dbErr = requireDb(emdash?.db);
22
+ if (dbErr) return dbErr;
23
+ const db = emdash!.db;
22
24
 
23
25
  const denied = requirePerm(user, "redirects:read");
24
26
  if (denied) return denied;
@@ -36,7 +38,9 @@ export const GET: APIRoute = async ({ url, locals }) => {
36
38
 
37
39
  export const POST: APIRoute = async ({ request, locals }) => {
38
40
  const { emdash, user } = locals;
39
- const db = emdash.db;
41
+ const dbErr = requireDb(emdash?.db);
42
+ if (dbErr) return dbErr;
43
+ const db = emdash!.db;
40
44
 
41
45
  const denied = requirePerm(user, "redirects:manage");
42
46
  if (denied) return denied;
@@ -16,7 +16,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
16
16
  const { emdash, user } = locals;
17
17
  const revisionId = params.revisionId!;
18
18
 
19
- const denied = requirePerm(user, "content:read");
19
+ const denied = requirePerm(user, "content:read_drafts");
20
20
  if (denied) return denied;
21
21
 
22
22
  if (!emdash?.handleRevisionGet) {
@@ -57,7 +57,6 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
57
57
  fieldSlug,
58
58
  body as UpdateFieldInput,
59
59
  );
60
- if (result.success) emdash!.invalidateManifest();
61
60
  return unwrapResult(result);
62
61
  };
63
62
 
@@ -73,6 +72,5 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
73
72
  if (denied) return denied;
74
73
 
75
74
  const result = await handleSchemaFieldDelete(emdash!.db, collectionSlug, fieldSlug);
76
- if (result.success) emdash!.invalidateManifest();
77
75
  return unwrapResult(result);
78
76
  };
@@ -48,6 +48,5 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
48
48
  collectionSlug,
49
49
  body as CreateFieldInput,
50
50
  );
51
- if (result.success) emdash!.invalidateManifest();
52
51
  return unwrapResult(result, 201);
53
52
  };
@@ -28,6 +28,5 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
28
28
  if (isParseError(body)) return body;
29
29
 
30
30
  const result = await handleSchemaFieldReorder(emdash!.db, collectionSlug, body.fieldSlugs);
31
- if (result.success) emdash!.invalidateManifest();
32
31
  return unwrapResult(result);
33
32
  };
@@ -59,7 +59,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
59
59
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- parseBody validates via Zod
60
60
  body as UpdateCollectionInput,
61
61
  );
62
- emdash!.invalidateManifest();
62
+ emdash!.invalidateUrlPatternCache();
63
63
  return unwrapResult(result);
64
64
  };
65
65
 
@@ -77,6 +77,6 @@ export const DELETE: APIRoute = async ({ params, url, locals }) => {
77
77
  const result = await handleSchemaCollectionDelete(emdash!.db, slug, {
78
78
  force,
79
79
  });
80
- emdash!.invalidateManifest();
80
+ emdash!.invalidateUrlPatternCache();
81
81
  return unwrapResult(result);
82
82
  };
@@ -43,6 +43,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
43
43
 
44
44
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to CreateCollectionInput
45
45
  const result = await handleSchemaCollectionCreate(emdash!.db, body as CreateCollectionInput);
46
- emdash!.invalidateManifest();
46
+ emdash!.invalidateUrlPatternCache();
47
47
  return unwrapResult(result, 201);
48
48
  };
@@ -4,6 +4,7 @@
4
4
  * GET /_emdash/api/search?q=query&collections=posts,pages&limit=20
5
5
  */
6
6
 
7
+ import { hasPermission } from "@emdash-cms/auth";
7
8
  import type { APIRoute } from "astro";
8
9
 
9
10
  import { apiError, apiSuccess, handleError } from "#api/error.js";
@@ -23,7 +24,7 @@ export const prerender = false;
23
24
  * - limit: Maximum results (optional, defaults to 20)
24
25
  */
25
26
  export const GET: APIRoute = async ({ url, locals }) => {
26
- const { emdash } = locals;
27
+ const { emdash, user } = locals;
27
28
 
28
29
  if (!emdash?.db) {
29
30
  return apiError("NOT_CONFIGURED", "EmDash not configured", 500);
@@ -36,6 +37,13 @@ export const GET: APIRoute = async ({ url, locals }) => {
36
37
  ? query.collections.split(",").map((c: string) => c.trim())
37
38
  : undefined;
38
39
 
40
+ // Only users with content:read_drafts may search non-published statuses.
41
+ // Anonymous and subscriber requests are forced to "published".
42
+ const status =
43
+ query.status && query.status !== "published" && hasPermission(user, "content:read_drafts")
44
+ ? query.status
45
+ : "published";
46
+
39
47
  try {
40
48
  // Verify FTS indexes are healthy on first use. At most once per worker
41
49
  // lifetime; no-op after that. Moved off the cold-start hot path to
@@ -44,7 +52,7 @@ export const GET: APIRoute = async ({ url, locals }) => {
44
52
 
45
53
  const result = await searchWithDb(emdash.db, query.q, {
46
54
  collections,
47
- status: query.status,
55
+ status,
48
56
  locale: query.locale,
49
57
  limit: query.limit,
50
58
  });
@@ -9,7 +9,7 @@
9
9
  import type { APIRoute } from "astro";
10
10
 
11
11
  import { requirePerm } from "#api/authorize.js";
12
- import { apiError, handleError, unwrapResult } from "#api/error.js";
12
+ import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
13
13
  import {
14
14
  handleSectionDelete,
15
15
  handleSectionGet,
@@ -22,7 +22,9 @@ export const prerender = false;
22
22
 
23
23
  export const GET: APIRoute = async ({ params, locals }) => {
24
24
  const { emdash, user } = locals;
25
- const db = emdash.db;
25
+ const dbErr = requireDb(emdash?.db);
26
+ if (dbErr) return dbErr;
27
+ const db = emdash!.db;
26
28
  const { slug } = params;
27
29
 
28
30
  const denied = requirePerm(user, "sections:read");
@@ -42,7 +44,9 @@ export const GET: APIRoute = async ({ params, locals }) => {
42
44
 
43
45
  export const PUT: APIRoute = async ({ params, request, locals }) => {
44
46
  const { emdash, user } = locals;
45
- const db = emdash.db;
47
+ const dbErr = requireDb(emdash?.db);
48
+ if (dbErr) return dbErr;
49
+ const db = emdash!.db;
46
50
  const { slug } = params;
47
51
 
48
52
  const denied = requirePerm(user, "sections:manage");
@@ -65,7 +69,9 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
65
69
 
66
70
  export const DELETE: APIRoute = async ({ params, locals }) => {
67
71
  const { emdash, user } = locals;
68
- const db = emdash.db;
72
+ const dbErr = requireDb(emdash?.db);
73
+ if (dbErr) return dbErr;
74
+ const db = emdash!.db;
69
75
  const { slug } = params;
70
76
 
71
77
  const denied = requirePerm(user, "sections:manage");
@@ -8,7 +8,7 @@
8
8
  import type { APIRoute } from "astro";
9
9
 
10
10
  import { requirePerm } from "#api/authorize.js";
11
- import { handleError, unwrapResult } from "#api/error.js";
11
+ import { handleError, requireDb, unwrapResult } from "#api/error.js";
12
12
  import { handleSectionCreate, handleSectionList } from "#api/handlers/sections.js";
13
13
  import { isParseError, parseBody, parseQuery } from "#api/parse.js";
14
14
  import { createSectionBody, sectionsListQuery } from "#api/schemas.js";
@@ -17,7 +17,9 @@ export const prerender = false;
17
17
 
18
18
  export const GET: APIRoute = async ({ url, locals }) => {
19
19
  const { emdash, user } = locals;
20
- const db = emdash.db;
20
+ const dbErr = requireDb(emdash?.db);
21
+ if (dbErr) return dbErr;
22
+ const db = emdash!.db;
21
23
 
22
24
  const denied = requirePerm(user, "sections:read");
23
25
  if (denied) return denied;
@@ -35,7 +37,9 @@ export const GET: APIRoute = async ({ url, locals }) => {
35
37
 
36
38
  export const POST: APIRoute = async ({ request, locals }) => {
37
39
  const { emdash, user } = locals;
38
- const db = emdash.db;
40
+ const dbErr = requireDb(emdash?.db);
41
+ if (dbErr) return dbErr;
42
+ const db = emdash!.db;
39
43
 
40
44
  const denied = requirePerm(user, "sections:manage");
41
45
  if (denied) return denied;
@@ -16,6 +16,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
16
16
  import { isParseError, parseBody } from "#api/parse.js";
17
17
  import { getPublicOrigin } from "#api/public-url.js";
18
18
  import { setupAdminVerifyBody } from "#api/schemas.js";
19
+ import { getConfiguredAllowedOrigins, validateAllowedOrigins } from "#auth/allowed-origins.js";
19
20
  import { createChallengeStore } from "#auth/challenge-store.js";
20
21
  import { getPasskeyConfig } from "#auth/passkey-config.js";
21
22
  import { SETUP_NONCE_COOKIE } from "#auth/setup-nonce.js";
@@ -83,7 +84,11 @@ export const POST: APIRoute = async ({ cookies, request, locals }) => {
83
84
  const url = new URL(request.url);
84
85
  const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
85
86
  const siteUrl = getPublicOrigin(url, emdash?.config);
86
- const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
87
+ const allowedOrigins = validateAllowedOrigins(
88
+ siteUrl,
89
+ getConfiguredAllowedOrigins(emdash?.config),
90
+ );
91
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl, allowedOrigins);
87
92
 
88
93
  // Verify the registration response
89
94
  const challengeStore = createChallengeStore(emdash.db);
@@ -7,6 +7,7 @@
7
7
  * - Excludes auth/user/session/token tables
8
8
  */
9
9
 
10
+ import type { User } from "@emdash-cms/auth";
10
11
  import type { APIRoute } from "astro";
11
12
 
12
13
  import { requirePerm } from "#api/authorize.js";
@@ -17,11 +18,31 @@ import {
17
18
  verifyPreviewSignature,
18
19
  } from "#api/handlers/snapshot.js";
19
20
  import { getPublicOrigin } from "#api/public-url.js";
21
+ import { resolveSecretsCached } from "#config/secrets.js";
20
22
 
21
23
  export const prerender = false;
22
24
 
23
- export const GET: APIRoute = async ({ request, locals, url }) => {
24
- const { emdash, user } = locals;
25
+ export const GET: APIRoute = async ({ request, locals, url, session }) => {
26
+ const { emdash } = locals;
27
+ // This route is in PUBLIC_API_EXACT (for preview-signature callers with no session),
28
+ // so auth middleware skips user resolution. Manually resolve the session user here
29
+ // to support session-authenticated admin users alongside preview-signature auth.
30
+ let user: User | undefined = (locals as { user?: User }).user;
31
+ if (!user && session && emdash?.db) {
32
+ try {
33
+ const { createKyselyAdapter } = await import("@emdash-cms/auth/adapters/kysely");
34
+ const sessionUser = await session.get("user");
35
+ if (sessionUser?.id) {
36
+ const adapter = createKyselyAdapter(emdash.db);
37
+ const resolved = await adapter.getUserById(sessionUser.id);
38
+ if (resolved && !resolved.disabled) {
39
+ user = resolved;
40
+ }
41
+ }
42
+ } catch {
43
+ // Session resolution failed, continue to preview-signature check
44
+ }
45
+ }
25
46
 
26
47
  if (!emdash?.db) {
27
48
  return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
@@ -32,24 +53,29 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
32
53
  let authorized = false;
33
54
 
34
55
  if (previewSig) {
35
- const secret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || "";
36
- if (!secret) {
37
- console.warn(
38
- "[snapshot] X-Preview-Signature header present but no PREVIEW_SECRET configured",
39
- );
56
+ // Resolves env override or DB-stored value. Always non-empty after
57
+ // resolution, so the signature path is never silently disabled.
58
+ // Note: a signing process without access to this database (e.g. a
59
+ // remote preview Worker) must set the same `EMDASH_PREVIEW_SECRET`
60
+ // env var on both sides.
61
+ const { previewSecret: secret, previewSecretSource } = await resolveSecretsCached(emdash.db);
62
+ const parsed = parsePreviewSignatureHeader(previewSig);
63
+ if (!parsed) {
64
+ console.warn("[snapshot] Failed to parse X-Preview-Signature header");
40
65
  } else {
41
- const parsed = parsePreviewSignatureHeader(previewSig);
42
- if (!parsed) {
43
- console.warn("[snapshot] Failed to parse X-Preview-Signature header");
44
- } else {
45
- authorized = await verifyPreviewSignature(parsed.source, parsed.exp, parsed.sig, secret);
46
- if (!authorized) {
47
- console.warn("[snapshot] Preview signature verification failed", {
48
- source: parsed.source,
49
- exp: parsed.exp,
50
- expired: parsed.exp < Date.now() / 1000,
51
- });
66
+ authorized = await verifyPreviewSignature(parsed.source, parsed.exp, parsed.sig, secret);
67
+ if (!authorized) {
68
+ const fields: Record<string, unknown> = {
69
+ source: parsed.source,
70
+ exp: parsed.exp,
71
+ expired: parsed.exp < Date.now() / 1000,
72
+ secretSource: previewSecretSource,
73
+ };
74
+ if (previewSecretSource === "db") {
75
+ fields.hint =
76
+ "Set EMDASH_PREVIEW_SECRET in both this process and the signing process to share secrets across deployments";
52
77
  }
78
+ console.warn("[snapshot] Preview signature verification failed", fields);
53
79
  }
54
80
  }
55
81
  }
@@ -52,7 +52,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
52
52
  if (isParseError(body)) return body;
53
53
 
54
54
  const result = await handleTaxonomyCreate(emdash.db, body);
55
- if (result.success) emdash.invalidateManifest();
56
55
  return unwrapResult(result, 201);
57
56
  } catch (error) {
58
57
  return handleError(error, "Failed to create taxonomy", "TAXONOMY_CREATE_ERROR");
@@ -4,7 +4,13 @@
4
4
  * POST /_emdash/api/themes/preview
5
5
  *
6
6
  * Generates a signed preview URL for the "Try with my data" feature.
7
- * The PREVIEW_SECRET must be set in the environment (shared with preview Workers).
7
+ *
8
+ * Uses the resolved preview secret: env override (`EMDASH_PREVIEW_SECRET`)
9
+ * wins, otherwise an auto-generated stable per-site value persisted in the
10
+ * options table is used. Processes that share the same database converge on
11
+ * the same auto-generated value; only set `EMDASH_PREVIEW_SECRET` in both
12
+ * processes when the verifying side runs without access to the EmDash DB
13
+ * (e.g. a remote preview Worker).
8
14
  */
9
15
 
10
16
  import type { APIRoute } from "astro";
@@ -12,6 +18,7 @@ import type { APIRoute } from "astro";
12
18
  import { requirePerm } from "#api/authorize.js";
13
19
  import { apiError, apiSuccess } from "#api/error.js";
14
20
  import { getPublicOrigin } from "#api/public-url.js";
21
+ import { resolveSecretsCached } from "#config/secrets.js";
15
22
 
16
23
  export const prerender = false;
17
24
 
@@ -25,10 +32,9 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
25
32
  const denied = requirePerm(user, "plugins:read");
26
33
  if (denied) return denied;
27
34
 
28
- const secret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || "";
29
- if (!secret) {
30
- return apiError("NOT_CONFIGURED", "PREVIEW_SECRET is not configured", 500);
31
- }
35
+ // Always non-empty after resolution; env override wins, otherwise a
36
+ // stable DB-stored value is used.
37
+ const { previewSecret: secret } = await resolveSecretsCached(emdash.db);
32
38
 
33
39
  let body: { previewUrl: string };
34
40
  try {
@@ -228,6 +228,15 @@ export interface EmDashHandlers {
228
228
  slug?: string;
229
229
  status?: string;
230
230
  authorId?: string | null;
231
+ bylines?: Array<{ bylineId: string; roleLabel?: string | null }>;
232
+ seo?: {
233
+ title?: string | null;
234
+ description?: string | null;
235
+ image?: string | null;
236
+ canonical?: string | null;
237
+ noIndex?: boolean;
238
+ };
239
+ publishedAt?: string | null;
231
240
  _rev?: string;
232
241
  },
233
242
  ) => Promise<HandlerResponse>;
@@ -255,7 +264,11 @@ export interface EmDashHandlers {
255
264
  ) => Promise<HandlerResponse>;
256
265
 
257
266
  // Publishing & Scheduling handlers
258
- handleContentPublish: (collection: string, id: string) => Promise<HandlerResponse>;
267
+ handleContentPublish: (
268
+ collection: string,
269
+ id: string,
270
+ options?: { publishedAt?: string },
271
+ ) => Promise<HandlerResponse>;
259
272
 
260
273
  handleContentUnpublish: (collection: string, id: string) => Promise<HandlerResponse>;
261
274
 
@@ -362,8 +375,15 @@ export interface EmDashHandlers {
362
375
  // Configuration (for checking database type, auth mode, etc.)
363
376
  config: import("./integration/runtime.js").EmDashConfig;
364
377
 
365
- // Manifest invalidation (call after schema changes)
366
- invalidateManifest: () => void;
378
+ // Build the admin manifest from the live database. Only used by admin
379
+ // routes; logged-out requests don't need it. Per-request, deduplicated
380
+ // by `requestCached`.
381
+ getManifest: () => Promise<EmDashManifest>;
382
+
383
+ // Clear the cached URL patterns used by `resolveEmDashPath`. Call after
384
+ // any schema mutation that creates/updates/deletes a collection's
385
+ // `urlPattern` so public routing picks up the change immediately.
386
+ invalidateUrlPatternCache: () => void;
367
387
 
368
388
  // Sandbox runner (for marketplace plugin install/update)
369
389
  getSandboxRunner: () => import("../plugins/sandbox/types.js").SandboxRunner | null;