emdash 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/dist/{adapters-Di31kZ28.d.mts → adapters-DoNJiveC.d.mts} +1 -1
  2. package/dist/{adapters-Di31kZ28.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
  3. package/dist/{apply-5uslYdUu.mjs → apply-BzltprvY.mjs} +90 -139
  4. package/dist/apply-BzltprvY.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +194 -17
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +6 -7
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +34 -57
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  14. package/dist/astro/middleware/redirect.mjs +17 -12
  15. package/dist/astro/middleware/redirect.mjs.map +1 -1
  16. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  17. package/dist/astro/middleware/request-context.mjs +9 -6
  18. package/dist/astro/middleware/request-context.mjs.map +1 -1
  19. package/dist/astro/middleware/setup.mjs +1 -1
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +301 -165
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +34 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
  26. package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
  27. package/dist/{byline-C4OVd8b3.mjs → byline-BSaNL1w7.mjs} +5 -5
  28. package/dist/byline-BSaNL1w7.mjs.map +1 -0
  29. package/dist/bylines-CvJ3PYz2.mjs +113 -0
  30. package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
  31. package/dist/cache-C6N_hhN7.mjs +65 -0
  32. package/dist/cache-C6N_hhN7.mjs.map +1 -0
  33. package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
  34. package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
  35. package/dist/cli/index.mjs +229 -31
  36. package/dist/cli/index.mjs.map +1 -1
  37. package/dist/client/cf-access.d.mts +1 -1
  38. package/dist/client/index.d.mts +1 -1
  39. package/dist/client/index.mjs +3 -3
  40. package/dist/client/index.mjs.map +1 -1
  41. package/dist/{config-BXwuX8Bx.mjs → config-BI0V3ICQ.mjs} +1 -1
  42. package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
  43. package/dist/{content-D7J5y73J.mjs → content-8lOYF0pr.mjs} +43 -28
  44. package/dist/content-8lOYF0pr.mjs.map +1 -0
  45. package/dist/db/index.d.mts +3 -3
  46. package/dist/db/index.mjs +2 -2
  47. package/dist/db/libsql.d.mts +1 -1
  48. package/dist/db/libsql.d.mts.map +1 -1
  49. package/dist/db/libsql.mjs +7 -2
  50. package/dist/db/libsql.mjs.map +1 -1
  51. package/dist/db/postgres.d.mts +1 -1
  52. package/dist/db/sqlite.d.mts +1 -1
  53. package/dist/db/sqlite.d.mts.map +1 -1
  54. package/dist/db/sqlite.mjs +8 -3
  55. package/dist/db/sqlite.mjs.map +1 -1
  56. package/dist/{db-errors-D0UT85nC.mjs → db-errors-WRezodiz.mjs} +1 -1
  57. package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
  58. package/dist/{default-CME5YdZ3.mjs → default-D8ksjWhO.mjs} +1 -1
  59. package/dist/{default-CME5YdZ3.mjs.map → default-D8ksjWhO.mjs.map} +1 -1
  60. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  61. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  62. package/dist/{error-CiYn9yDu.mjs → error-D_-tqP-I.mjs} +1 -1
  63. package/dist/error-D_-tqP-I.mjs.map +1 -0
  64. package/dist/{index-De6_Xv3v.d.mts → index-BFRaVcD6.d.mts} +243 -40
  65. package/dist/index-BFRaVcD6.d.mts.map +1 -0
  66. package/dist/index.d.mts +11 -11
  67. package/dist/index.mjs +29 -25
  68. package/dist/{load-CBcmDIot.mjs → load-DDqMMvZL.mjs} +2 -2
  69. package/dist/{load-CBcmDIot.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
  70. package/dist/{loader-DeiBJEMe.mjs → loader-CKLbBnhK.mjs} +32 -10
  71. package/dist/loader-CKLbBnhK.mjs.map +1 -0
  72. package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DqWNC3lM.mjs} +45 -3
  73. package/dist/manifest-schema-DqWNC3lM.mjs.map +1 -0
  74. package/dist/media/index.d.mts +1 -1
  75. package/dist/media/index.mjs +1 -1
  76. package/dist/media/local-runtime.d.mts +7 -7
  77. package/dist/media/local-runtime.mjs +3 -3
  78. package/dist/{media-DqHVh136.mjs → media-BW32b4gi.mjs} +4 -7
  79. package/dist/media-BW32b4gi.mjs.map +1 -0
  80. package/dist/{mode-CpNnGkPz.mjs → mode-ier8jbBk.mjs} +1 -1
  81. package/dist/mode-ier8jbBk.mjs.map +1 -0
  82. package/dist/options-BVp3UsTS.mjs +117 -0
  83. package/dist/options-BVp3UsTS.mjs.map +1 -0
  84. package/dist/page/index.d.mts +2 -2
  85. package/dist/{placeholder-tzpqGWII.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
  86. package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
  87. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
  88. package/dist/placeholder-CIJejMlK.mjs.map +1 -0
  89. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  90. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  91. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  92. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  93. package/dist/public-url-DByxYjUw.mjs +51 -0
  94. package/dist/public-url-DByxYjUw.mjs.map +1 -0
  95. package/dist/{query-g4Ug-9j9.mjs → query-Cg9ZKRQ0.mjs} +114 -16
  96. package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
  97. package/dist/{redirect-CN0Rt9Ob.mjs → redirect-BhUBKRc1.mjs} +13 -8
  98. package/dist/redirect-BhUBKRc1.mjs.map +1 -0
  99. package/dist/{registry-Ci3WxVAr.mjs → registry-Dw70ChxB.mjs} +69 -11
  100. package/dist/registry-Dw70ChxB.mjs.map +1 -0
  101. package/dist/{request-cache-DiR961CV.mjs → request-cache-B-bmkipQ.mjs} +1 -1
  102. package/dist/request-cache-B-bmkipQ.mjs.map +1 -0
  103. package/dist/runner-Bnoj7vjK.d.mts +44 -0
  104. package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
  105. package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
  106. package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
  107. package/dist/runtime.d.mts +6 -6
  108. package/dist/runtime.mjs +4 -4
  109. package/dist/{search-B0effn3j.mjs → search-dOGEccMa.mjs} +341 -152
  110. package/dist/search-dOGEccMa.mjs.map +1 -0
  111. package/dist/secrets-CW3reAnU.mjs +314 -0
  112. package/dist/secrets-CW3reAnU.mjs.map +1 -0
  113. package/dist/seed/index.d.mts +2 -2
  114. package/dist/seed/index.mjs +15 -14
  115. package/dist/seo/index.d.mts +1 -1
  116. package/dist/storage/local.d.mts +1 -1
  117. package/dist/storage/local.mjs +1 -1
  118. package/dist/storage/s3.d.mts +1 -1
  119. package/dist/storage/s3.d.mts.map +1 -1
  120. package/dist/storage/s3.mjs +4 -4
  121. package/dist/storage/s3.mjs.map +1 -1
  122. package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
  123. package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
  124. package/dist/{tokens-BFPFx3CA.mjs → tokens-D7zMmWi2.mjs} +2 -2
  125. package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
  126. package/dist/{transport-BykRfpyy.mjs → transport-BeMCmin1.mjs} +6 -5
  127. package/dist/{transport-BykRfpyy.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
  128. package/dist/{transport-H4Iwx7tC.d.mts → transport-DNEfeMaU.d.mts} +1 -1
  129. package/dist/{transport-H4Iwx7tC.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
  130. package/dist/types-4fVtCIm0.mjs +68 -0
  131. package/dist/types-4fVtCIm0.mjs.map +1 -0
  132. package/dist/{types-CnZYHyLW.d.mts → types-BSyXeCFW.d.mts} +24 -2
  133. package/dist/{types-CnZYHyLW.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
  134. package/dist/{types-DgrIP0tF.d.mts → types-BuBIptGk.d.mts} +80 -106
  135. package/dist/types-BuBIptGk.d.mts.map +1 -0
  136. package/dist/{types-BH2L167P.mjs → types-CDbKp7ND.mjs} +1 -1
  137. package/dist/{types-BH2L167P.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
  138. package/dist/{types-DDS4MxsT.mjs → types-CIOg5AR8.mjs} +1 -1
  139. package/dist/{types-DDS4MxsT.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
  140. package/dist/{types-6CUZRrZP.d.mts → types-CJsYGpco.d.mts} +24 -2
  141. package/dist/{types-6CUZRrZP.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
  142. package/dist/types-CRxNbK-Z.mjs +68 -0
  143. package/dist/types-CRxNbK-Z.mjs.map +1 -0
  144. package/dist/{types-C2v0c34j.d.mts → types-CrtWgIvl.d.mts} +1 -1
  145. package/dist/{types-C2v0c34j.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
  146. package/dist/{types-CFWjXmus.d.mts → types-M78DQ1lx.d.mts} +1 -1
  147. package/dist/{types-CFWjXmus.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
  148. package/dist/{validate-CqsNItbt.mjs → validate-Baqf0slj.mjs} +3 -3
  149. package/dist/{validate-CqsNItbt.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
  150. package/dist/{validate-kM8Pjuf7.d.mts → validate-BfQh_C_y.d.mts} +4 -4
  151. package/dist/{validate-kM8Pjuf7.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
  152. package/dist/validation-BfEI7tNe.mjs +144 -0
  153. package/dist/validation-BfEI7tNe.mjs.map +1 -0
  154. package/dist/version-DoxrVdYf.mjs +7 -0
  155. package/dist/{version-BnTKdfam.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
  156. package/dist/zod-generator-CC0xNe_K.mjs +132 -0
  157. package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
  158. package/locals.d.ts +1 -6
  159. package/package.json +21 -7
  160. package/src/api/auth-storage.ts +37 -0
  161. package/src/api/error.ts +6 -0
  162. package/src/api/errors.ts +8 -0
  163. package/src/api/handlers/comments.ts +19 -4
  164. package/src/api/handlers/content.ts +151 -4
  165. package/src/api/handlers/device-flow.ts +5 -0
  166. package/src/api/handlers/index.ts +2 -0
  167. package/src/api/handlers/marketplace.ts +11 -4
  168. package/src/api/handlers/media.ts +8 -1
  169. package/src/api/handlers/menus.ts +160 -21
  170. package/src/api/handlers/oauth-authorization.ts +72 -33
  171. package/src/api/handlers/redirects.ts +16 -3
  172. package/src/api/handlers/revision.ts +23 -14
  173. package/src/api/handlers/sections.ts +8 -1
  174. package/src/api/handlers/taxonomies.ts +131 -22
  175. package/src/api/handlers/validation.ts +212 -0
  176. package/src/api/openapi/document.ts +4 -1
  177. package/src/api/public-url.ts +54 -5
  178. package/src/api/route-utils.ts +14 -0
  179. package/src/api/schemas/comments.ts +2 -2
  180. package/src/api/schemas/common.ts +1 -1
  181. package/src/api/schemas/content.ts +17 -0
  182. package/src/api/schemas/sections.ts +3 -3
  183. package/src/api/schemas/setup.ts +8 -0
  184. package/src/api/schemas/users.ts +1 -1
  185. package/src/api/schemas/widgets.ts +12 -10
  186. package/src/api/setup-complete.ts +40 -0
  187. package/src/api/types.ts +5 -1
  188. package/src/astro/integration/index.ts +30 -2
  189. package/src/astro/integration/routes.ts +28 -0
  190. package/src/astro/integration/runtime.ts +49 -1
  191. package/src/astro/integration/virtual-modules.ts +73 -2
  192. package/src/astro/integration/vite-config.ts +49 -13
  193. package/src/astro/middleware/auth.ts +34 -6
  194. package/src/astro/middleware/redirect.ts +29 -16
  195. package/src/astro/middleware/request-context.ts +15 -5
  196. package/src/astro/middleware.ts +41 -10
  197. package/src/astro/routes/PluginRegistry.tsx +10 -1
  198. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  199. package/src/astro/routes/api/auth/mode.ts +57 -0
  200. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
  201. package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
  202. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  203. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  204. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  205. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  206. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  207. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  208. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  209. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  210. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  211. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  212. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
  213. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  214. package/src/astro/routes/api/content/[collection]/index.ts +1 -9
  215. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  216. package/src/astro/routes/api/import/wordpress/media.ts +2 -7
  217. package/src/astro/routes/api/import/wordpress/prepare.ts +9 -0
  218. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  219. package/src/astro/routes/api/manifest.ts +62 -45
  220. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  221. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  222. package/src/astro/routes/api/openapi.json.ts +27 -10
  223. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  224. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  225. package/src/astro/routes/api/redirects/[id].ts +10 -4
  226. package/src/astro/routes/api/redirects/index.ts +7 -3
  227. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  228. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  229. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  230. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  231. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  232. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  233. package/src/astro/routes/api/search/index.ts +10 -2
  234. package/src/astro/routes/api/sections/[slug].ts +10 -4
  235. package/src/astro/routes/api/sections/index.ts +7 -3
  236. package/src/astro/routes/api/settings/email.ts +4 -9
  237. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  238. package/src/astro/routes/api/setup/admin.ts +8 -2
  239. package/src/astro/routes/api/setup/index.ts +2 -2
  240. package/src/astro/routes/api/setup/status.ts +3 -1
  241. package/src/astro/routes/api/snapshot.ts +44 -18
  242. package/src/astro/routes/api/taxonomies/index.ts +0 -1
  243. package/src/astro/routes/api/themes/preview.ts +11 -5
  244. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
  245. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
  246. package/src/astro/routes/api/widget-areas/[name].ts +4 -1
  247. package/src/astro/routes/api/widget-areas/index.ts +4 -1
  248. package/src/astro/types.ts +32 -3
  249. package/src/auth/allowed-origins.ts +168 -0
  250. package/src/auth/mode.ts +15 -3
  251. package/src/auth/passkey-config.ts +35 -13
  252. package/src/auth/providers/github-admin.tsx +29 -0
  253. package/src/auth/providers/github.ts +31 -0
  254. package/src/auth/providers/google-admin.tsx +44 -0
  255. package/src/auth/providers/google.ts +31 -0
  256. package/src/auth/types.ts +114 -4
  257. package/src/bylines/index.ts +37 -88
  258. package/src/cli/commands/auth.ts +28 -6
  259. package/src/cli/commands/bundle-utils.ts +11 -2
  260. package/src/cli/commands/bundle.ts +31 -9
  261. package/src/cli/commands/content.ts +13 -0
  262. package/src/cli/commands/login.ts +8 -1
  263. package/src/cli/commands/publish.ts +24 -0
  264. package/src/cli/commands/secrets.ts +183 -0
  265. package/src/cli/credentials.ts +1 -1
  266. package/src/cli/index.ts +5 -1
  267. package/src/client/index.ts +4 -4
  268. package/src/client/transport.ts +17 -7
  269. package/src/components/Break.astro +2 -2
  270. package/src/components/EmDashHead.astro +18 -13
  271. package/src/components/EmDashImage.astro +7 -6
  272. package/src/components/Embed.astro +1 -1
  273. package/src/components/Gallery.astro +6 -4
  274. package/src/components/Image.astro +9 -4
  275. package/src/components/InlinePortableTextEditor.tsx +106 -19
  276. package/src/components/LiveSearch.astro +5 -14
  277. package/src/config/secrets.ts +528 -0
  278. package/src/database/dialect-helpers.ts +50 -0
  279. package/src/database/migrations/034_published_at_index.ts +1 -1
  280. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  281. package/src/database/migrations/runner.ts +156 -23
  282. package/src/database/repositories/audit.ts +6 -8
  283. package/src/database/repositories/byline.ts +6 -8
  284. package/src/database/repositories/comment.ts +12 -16
  285. package/src/database/repositories/content.ts +76 -52
  286. package/src/database/repositories/index.ts +1 -1
  287. package/src/database/repositories/media.ts +10 -13
  288. package/src/database/repositories/plugin-storage.ts +4 -6
  289. package/src/database/repositories/redirect.ts +26 -19
  290. package/src/database/repositories/taxonomy.ts +40 -3
  291. package/src/database/repositories/types.ts +57 -8
  292. package/src/database/repositories/user.ts +6 -8
  293. package/src/db/libsql.ts +1 -3
  294. package/src/db/sqlite.ts +2 -5
  295. package/src/emdash-runtime.ts +388 -247
  296. package/src/index.ts +14 -1
  297. package/src/loader.ts +30 -6
  298. package/src/mcp/server.ts +781 -141
  299. package/src/media/normalize.ts +1 -1
  300. package/src/media/url.ts +78 -0
  301. package/src/page/site-identity.ts +58 -0
  302. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  303. package/src/plugins/context.ts +13 -10
  304. package/src/plugins/define-plugin.ts +40 -12
  305. package/src/plugins/email-console.ts +10 -3
  306. package/src/plugins/hooks.ts +34 -19
  307. package/src/plugins/index.ts +9 -0
  308. package/src/plugins/manifest-schema.ts +49 -2
  309. package/src/plugins/types.ts +174 -13
  310. package/src/preview/urls.ts +23 -3
  311. package/src/query.ts +149 -6
  312. package/src/redirects/cache.ts +38 -18
  313. package/src/request-cache.ts +3 -0
  314. package/src/schema/registry.ts +97 -5
  315. package/src/schema/zod-generator.ts +27 -5
  316. package/src/search/fts-manager.ts +0 -2
  317. package/src/search/query.ts +111 -26
  318. package/src/search/types.ts +8 -1
  319. package/src/sections/index.ts +7 -9
  320. package/src/seed/apply.ts +2 -0
  321. package/src/settings/index.ts +80 -6
  322. package/src/settings/types.ts +23 -1
  323. package/src/storage/s3.ts +12 -6
  324. package/src/taxonomies/index.ts +11 -1
  325. package/src/virtual-modules.d.ts +21 -1
  326. package/src/widgets/index.ts +1 -1
  327. package/dist/apply-5uslYdUu.mjs.map +0 -1
  328. package/dist/byline-C4OVd8b3.mjs.map +0 -1
  329. package/dist/bylines-hPTW79hw.mjs +0 -157
  330. package/dist/bylines-hPTW79hw.mjs.map +0 -1
  331. package/dist/cache-BkKBuIvS.mjs +0 -56
  332. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  333. package/dist/chunk-ClPoSABd.mjs +0 -21
  334. package/dist/content-D7J5y73J.mjs.map +0 -1
  335. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  336. package/dist/error-CiYn9yDu.mjs.map +0 -1
  337. package/dist/index-De6_Xv3v.d.mts.map +0 -1
  338. package/dist/loader-DeiBJEMe.mjs.map +0 -1
  339. package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
  340. package/dist/media-DqHVh136.mjs.map +0 -1
  341. package/dist/mode-CpNnGkPz.mjs.map +0 -1
  342. package/dist/placeholder-C-fk5hYI.mjs.map +0 -1
  343. package/dist/query-g4Ug-9j9.mjs.map +0 -1
  344. package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
  345. package/dist/registry-Ci3WxVAr.mjs.map +0 -1
  346. package/dist/request-cache-DiR961CV.mjs.map +0 -1
  347. package/dist/runner-BR2xKwhn.d.mts +0 -34
  348. package/dist/runner-BR2xKwhn.d.mts.map +0 -1
  349. package/dist/search-B0effn3j.mjs.map +0 -1
  350. package/dist/taxonomies-K2z0Uhnj.mjs.map +0 -1
  351. package/dist/types-CMMN0pNg.mjs +0 -31
  352. package/dist/types-CMMN0pNg.mjs.map +0 -1
  353. package/dist/types-DgrIP0tF.d.mts.map +0 -1
  354. package/dist/version-BnTKdfam.mjs +0 -7
@@ -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
 
@@ -43,8 +43,10 @@ import {
43
43
  } from "../emdash-runtime.js";
44
44
  import { setI18nConfig } from "../i18n/config.js";
45
45
  import type { Database, Storage } from "../index.js";
46
+ import { createPublicMediaUrlResolver } from "../media/url.js";
46
47
  import type { SandboxRunner } from "../plugins/sandbox/types.js";
47
48
  import type { ResolvedPlugin } from "../plugins/types.js";
49
+ import { invalidateUrlPatternCache } from "../query.js";
48
50
  import { getRequestContext, runWithContext } from "../request-context.js";
49
51
  import type { EmDashConfig } from "./integration/runtime.js";
50
52
  import type { EmDashHandlers } from "./types.js";
@@ -232,6 +234,20 @@ export const onRequest = defineMiddleware(async (context, next) => {
232
234
  const { request, locals, cookies } = context;
233
235
  const url = context.url;
234
236
 
237
+ // Fast path: routes outside /_emdash/ that plugins inject (e.g.,
238
+ // /.well-known/atproto-client-metadata.json) skip the entire runtime
239
+ // init + middleware chain. External servers fetch these with tight
240
+ // timeouts (~1-2s) so they must respond quickly even on cold starts.
241
+ if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
242
+ const isPluginFastRoute = virtualConfig.authProviders.some(
243
+ (p: { routes?: { pattern?: string }[] }) =>
244
+ p.routes?.some((r: { pattern?: string }) => r.pattern && url.pathname === r.pattern),
245
+ );
246
+ if (isPluginFastRoute) {
247
+ return finalizeResponse(await next());
248
+ }
249
+ }
250
+
235
251
  const queryRecorder = isInstrumentationEnabled()
236
252
  ? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default")
237
253
  : undefined;
@@ -256,8 +272,15 @@ export const onRequest = defineMiddleware(async (context, next) => {
256
272
  // Read the Astro session user once up-front. Both the anonymous fast path
257
273
  // and the full doInit path need this, and the session store is network-backed
258
274
  // (KV / Durable Object) so we want to avoid re-fetching on the hot path.
259
- // Skipped entirely for prerendered requests — they have no session.
260
- 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");
261
284
 
262
285
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
263
286
  if (!sessionUser && !playgroundDb) {
@@ -301,10 +324,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
301
324
  try {
302
325
  const runtime = await getRuntime(config, initSubTimings);
303
326
  setupVerified = true;
304
- // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for these two methods
327
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for the page-contribution methods
305
328
  locals.emdash = {
306
329
  collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
307
330
  collectPageFragments: runtime.collectPageFragments.bind(runtime),
331
+ getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
308
332
  } as EmDashHandlers;
309
333
  } catch {
310
334
  // Non-fatal — EmDashHead will fall back to base SEO contributions
@@ -378,13 +402,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
378
402
  // Runtime init runs migrations, so the DB is guaranteed set up
379
403
  setupVerified = true;
380
404
 
381
- // Get manifest (cached after first call)
382
- t0 = performance.now();
383
- const manifest = await runtime.getManifest();
384
- 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.
385
410
 
386
411
  // Attach to locals for route handlers
387
- locals.emdashManifest = manifest;
388
412
  locals.emdash = {
389
413
  // Content handlers
390
414
  handleContentList: runtime.handleContentList.bind(runtime),
@@ -445,6 +469,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
445
469
  // Direct access (for advanced use cases)
446
470
  storage: runtime.storage,
447
471
  db: runtime.db,
472
+ getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
448
473
  hooks: runtime.hooks,
449
474
  email: runtime.email,
450
475
  configuredPlugins: runtime.configuredPlugins,
@@ -452,8 +477,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
452
477
  // Configuration (for checking database type, auth mode, etc.)
453
478
  config,
454
479
 
455
- // Manifest invalidation (call after schema changes)
456
- 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,
457
488
 
458
489
  // Sandbox runner (for marketplace plugin install/update)
459
490
  getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
@@ -10,6 +10,8 @@ import { AdminApp } from "@emdash-cms/admin";
10
10
  import type { Messages } from "@lingui/core";
11
11
  // @ts-ignore - virtual module generated by integration
12
12
  import { pluginAdmins } from "virtual:emdash/admin-registry";
13
+ // @ts-ignore - virtual module generated by integration
14
+ import { authProviders } from "virtual:emdash/auth-providers";
13
15
 
14
16
  interface AdminWrapperProps {
15
17
  locale: string;
@@ -17,5 +19,12 @@ interface AdminWrapperProps {
17
19
  }
18
20
 
19
21
  export default function AdminWrapper({ locale, messages }: AdminWrapperProps) {
20
- return <AdminApp pluginAdmins={pluginAdmins} locale={locale} messages={messages} />;
22
+ return (
23
+ <AdminApp
24
+ pluginAdmins={pluginAdmins}
25
+ authProviders={authProviders}
26
+ locale={locale}
27
+ messages={messages}
28
+ />
29
+ );
21
30
  }
@@ -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);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * GET /_emdash/api/auth/mode
3
+ *
4
+ * Public endpoint that returns the active authentication mode.
5
+ * Used by the login page to determine which login UI to render.
6
+ *
7
+ * Unlike the full manifest endpoint, this is intentionally public
8
+ * and returns only the auth mode — no collection schemas, plugin
9
+ * info, or other internal details.
10
+ */
11
+
12
+ import type { APIRoute } from "astro";
13
+
14
+ import { getAuthMode } from "#auth/mode.js";
15
+
16
+ export const prerender = false;
17
+
18
+ export const GET: APIRoute = async ({ locals }) => {
19
+ const { emdash } = locals;
20
+
21
+ const authMode = getAuthMode(emdash?.config);
22
+
23
+ // Only check signup for passkey auth (external providers handle their own)
24
+ let signupEnabled = false;
25
+ if (emdash?.db && authMode.type === "passkey") {
26
+ try {
27
+ const { sql } = await import("kysely");
28
+ const result = await sql<{ cnt: unknown }>`
29
+ SELECT COUNT(*) as cnt FROM allowed_domains WHERE enabled = 1
30
+ `.execute(emdash.db);
31
+ signupEnabled = Number(result.rows[0]?.cnt ?? 0) > 0;
32
+ } catch {
33
+ // Table may not exist yet
34
+ }
35
+ }
36
+
37
+ // Collect pluggable auth providers (from authProviders config)
38
+ const providers = (emdash?.config?.authProviders ?? []).map((p) => ({
39
+ id: p.id,
40
+ label: p.label,
41
+ }));
42
+
43
+ return Response.json(
44
+ {
45
+ data: {
46
+ authMode: authMode.type === "external" ? authMode.providerType : "passkey",
47
+ signupEnabled,
48
+ providers,
49
+ },
50
+ },
51
+ {
52
+ headers: {
53
+ "Cache-Control": "private, no-store",
54
+ },
55
+ },
56
+ );
57
+ };
@@ -18,7 +18,9 @@ import {
18
18
  import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
19
19
 
20
20
  import { getPublicOrigin } from "#api/public-url.js";
21
+ import { finalizeSetup } from "#api/setup-complete.js";
21
22
  import { createOAuthStateStore } from "#auth/oauth-state-store.js";
23
+ import { OptionsRepository } from "#db/repositories/options.js";
22
24
 
23
25
  type ProviderName = "github" | "google";
24
26
 
@@ -126,10 +128,22 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
126
128
  );
127
129
  }
128
130
 
131
+ const adapter = createKyselyAdapter(emdash.db);
132
+ const stateStore = createOAuthStateStore(emdash.db);
133
+
129
134
  const config: OAuthConsumerConfig = {
130
135
  baseUrl: `${getPublicOrigin(url, emdash?.config)}/_emdash`,
131
136
  providers,
132
137
  canSelfSignup: async (email: string) => {
138
+ // During setup: first user becomes admin.
139
+ // Check setup_complete flag instead of countUsers() to avoid
140
+ // a TOCTOU race where concurrent callbacks both see 0 users.
141
+ const options = new OptionsRepository(emdash.db);
142
+ const setupComplete = await options.get("emdash:setup_complete");
143
+ if (setupComplete !== true && setupComplete !== "true") {
144
+ return { allowed: true, role: Role.ADMIN };
145
+ }
146
+
133
147
  // Extract domain from email
134
148
  const domain = email.split("@")[1]?.toLowerCase();
135
149
  if (!domain) {
@@ -168,10 +182,16 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
168
182
  },
169
183
  };
170
184
 
171
- const adapter = createKyselyAdapter(emdash.db);
172
- const stateStore = createOAuthStateStore(emdash.db);
173
-
185
+ const options = new OptionsRepository(emdash.db);
186
+ const setupCompleteBefore = await options.get("emdash:setup_complete");
174
187
  const user = await handleOAuthCallback(config, adapter, provider, code, state, stateStore);
188
+ const isFirstUser = setupCompleteBefore !== true && setupCompleteBefore !== "true";
189
+
190
+ // Finalize setup outside the transaction (idempotent, safe if two callbacks race).
191
+ if (isFirstUser) {
192
+ await finalizeSetup(emdash.db);
193
+ console.log(`[oauth] Setup complete: created admin user via ${provider} (${user.email})`);
194
+ }
175
195
 
176
196
  // Create session
177
197
  if (session) {
@@ -71,16 +71,22 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
71
71
  const { emdash } = locals;
72
72
  const provider = params.provider;
73
73
 
74
+ // Determine where to redirect errors (setup wizard or login page)
75
+ const referer = request.headers.get("referer") ?? "";
76
+ const errorRedirectBase = referer.includes("/setup")
77
+ ? "/_emdash/admin/setup"
78
+ : "/_emdash/admin/login";
79
+
74
80
  // Validate provider
75
81
  if (!provider || !isValidProvider(provider)) {
76
82
  return redirect(
77
- `/_emdash/admin/login?error=invalid_provider&message=${encodeURIComponent("Invalid OAuth provider")}`,
83
+ `${errorRedirectBase}?error=invalid_provider&message=${encodeURIComponent("Invalid OAuth provider")}`,
78
84
  );
79
85
  }
80
86
 
81
87
  if (!emdash?.db) {
82
88
  return redirect(
83
- `/_emdash/admin/login?error=server_error&message=${encodeURIComponent("Database not configured")}`,
89
+ `${errorRedirectBase}?error=server_error&message=${encodeURIComponent("Database not configured")}`,
84
90
  );
85
91
  }
86
92
 
@@ -97,7 +103,7 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
97
103
 
98
104
  if (!providers[provider]) {
99
105
  return redirect(
100
- `/_emdash/admin/login?error=provider_not_configured&message=${encodeURIComponent(`OAuth provider ${provider} is not configured`)}`,
106
+ `${errorRedirectBase}?error=provider_not_configured&message=${encodeURIComponent(`OAuth provider ${provider} is not configured. Set either EMDASH_OAUTH_${provider.toUpperCase()}_CLIENT_ID and EMDASH_OAUTH_${provider.toUpperCase()}_CLIENT_SECRET, or ${provider.toUpperCase()}_CLIENT_ID and ${provider.toUpperCase()}_CLIENT_SECRET.`)}`,
101
107
  );
102
108
  }
103
109
 
@@ -114,7 +120,7 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
114
120
  } catch (error) {
115
121
  console.error("OAuth initiation error:", error);
116
122
  return redirect(
117
- `/_emdash/admin/login?error=oauth_error&message=${encodeURIComponent("Failed to start OAuth flow. Please try again.")}`,
123
+ `${errorRedirectBase}?error=oauth_error&message=${encodeURIComponent("Failed to start OAuth flow. Please try again.")}`,
118
124
  );
119
125
  }
120
126
  };
@@ -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);