emdash 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
  4. package/dist/apply-UsrFuO7l.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +118 -4
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +6 -7
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +14 -57
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  14. package/dist/astro/middleware/redirect.mjs +15 -10
  15. package/dist/astro/middleware/redirect.mjs.map +1 -1
  16. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  17. package/dist/astro/middleware/request-context.mjs +8 -5
  18. package/dist/astro/middleware/request-context.mjs.map +1 -1
  19. package/dist/astro/middleware/setup.mjs +1 -1
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +70 -121
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +25 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
  26. package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
  27. package/dist/bylines-esI7ioa9.mjs +113 -0
  28. package/dist/bylines-esI7ioa9.mjs.map +1 -0
  29. package/dist/cache-fTzxgMFJ.mjs +65 -0
  30. package/dist/cache-fTzxgMFJ.mjs.map +1 -0
  31. package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
  32. package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
  33. package/dist/cli/index.mjs +456 -90
  34. package/dist/cli/index.mjs.map +1 -1
  35. package/dist/client/cf-access.d.mts +1 -1
  36. package/dist/client/index.d.mts +1 -1
  37. package/dist/client/index.mjs +3 -3
  38. package/dist/client/index.mjs.map +1 -1
  39. package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
  40. package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
  41. package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
  42. package/dist/content-C7G4QXkK.mjs.map +1 -0
  43. package/dist/db/index.d.mts +3 -3
  44. package/dist/db/index.mjs +2 -2
  45. package/dist/db/libsql.d.mts +1 -1
  46. package/dist/db/libsql.d.mts.map +1 -1
  47. package/dist/db/libsql.mjs +7 -2
  48. package/dist/db/libsql.mjs.map +1 -1
  49. package/dist/db/postgres.d.mts +1 -1
  50. package/dist/db/sqlite.d.mts +1 -1
  51. package/dist/db/sqlite.d.mts.map +1 -1
  52. package/dist/db/sqlite.mjs +8 -3
  53. package/dist/db/sqlite.mjs.map +1 -1
  54. package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  55. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  56. package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
  57. package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  58. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  59. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  60. package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
  61. package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  62. package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
  63. package/dist/index-DjPMOfO0.d.mts.map +1 -0
  64. package/dist/index.d.mts +11 -11
  65. package/dist/index.mjs +27 -24
  66. package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
  67. package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
  68. package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
  69. package/dist/loader-Bx2_9-5e.mjs.map +1 -0
  70. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
  71. package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
  72. package/dist/media/index.d.mts +1 -1
  73. package/dist/media/index.mjs +1 -1
  74. package/dist/media/local-runtime.d.mts +7 -7
  75. package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
  76. package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  77. package/dist/options-nPxWnrya.mjs +117 -0
  78. package/dist/options-nPxWnrya.mjs.map +1 -0
  79. package/dist/page/index.d.mts +2 -2
  80. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  81. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  82. package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  83. package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  84. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  85. package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  86. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  87. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  88. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  89. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  90. package/dist/public-url-B1AxbbbQ.mjs +51 -0
  91. package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
  92. package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
  93. package/dist/query-Bo-msrmu.mjs.map +1 -0
  94. package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
  95. package/dist/redirect-C5H7VGIX.mjs.map +1 -0
  96. package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
  97. package/dist/registry-Beb7wxFc.mjs.map +1 -0
  98. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
  99. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
  100. package/dist/runner-Clwe4Mme.d.mts +44 -0
  101. package/dist/runner-Clwe4Mme.d.mts.map +1 -0
  102. package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
  103. package/dist/runner-DMnlIkh4.mjs.map +1 -0
  104. package/dist/runtime.d.mts +6 -6
  105. package/dist/runtime.mjs +2 -2
  106. package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
  107. package/dist/search-DkN-BqsS.mjs.map +1 -0
  108. package/dist/secrets-CZ8rxLX3.mjs +314 -0
  109. package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
  110. package/dist/seed/index.d.mts +2 -2
  111. package/dist/seed/index.mjs +13 -11
  112. package/dist/seo/index.d.mts +1 -1
  113. package/dist/storage/local.d.mts +1 -1
  114. package/dist/storage/local.mjs +1 -1
  115. package/dist/storage/s3.d.mts +1 -1
  116. package/dist/storage/s3.mjs +1 -1
  117. package/dist/taxonomies-CTtewrSQ.mjs +407 -0
  118. package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
  119. package/dist/taxonomy-DSxx2K2L.mjs +218 -0
  120. package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
  121. package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
  122. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  123. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  124. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  125. package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  127. package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
  128. package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  129. package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
  130. package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  131. package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
  132. package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  133. package/dist/types-CoO6mpV3.mjs +68 -0
  134. package/dist/types-CoO6mpV3.mjs.map +1 -0
  135. package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
  136. package/dist/types-D19uBYWn.d.mts.map +1 -0
  137. package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
  138. package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
  139. package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
  140. package/dist/types-Dtx1mSMX.d.mts.map +1 -0
  141. package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
  142. package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
  143. package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
  144. package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  145. package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
  146. package/dist/validate-CBIbxM3L.mjs.map +1 -0
  147. package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
  148. package/dist/validate-DHGwADqO.d.mts.map +1 -0
  149. package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
  150. package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
  151. package/dist/version-CMD42IRC.mjs +7 -0
  152. package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
  153. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
  154. package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
  155. package/locals.d.ts +1 -6
  156. package/package.json +9 -8
  157. package/src/api/handlers/comments.ts +6 -4
  158. package/src/api/handlers/content.ts +40 -1
  159. package/src/api/handlers/dashboard.ts +29 -36
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/menus.ts +256 -75
  163. package/src/api/handlers/oauth-authorization.ts +72 -33
  164. package/src/api/handlers/revision.ts +23 -14
  165. package/src/api/handlers/taxonomies.ts +273 -100
  166. package/src/api/public-url.ts +48 -2
  167. package/src/api/schemas/comments.ts +2 -2
  168. package/src/api/schemas/common.ts +7 -0
  169. package/src/api/schemas/content.ts +17 -0
  170. package/src/api/schemas/menus.ts +23 -0
  171. package/src/api/schemas/sections.ts +3 -3
  172. package/src/api/schemas/taxonomies.ts +39 -0
  173. package/src/api/schemas/users.ts +1 -1
  174. package/src/api/types.ts +5 -1
  175. package/src/astro/integration/index.ts +17 -0
  176. package/src/astro/integration/routes.ts +10 -0
  177. package/src/astro/integration/runtime.ts +30 -0
  178. package/src/astro/integration/virtual-modules.ts +32 -2
  179. package/src/astro/integration/vite-config.ts +6 -1
  180. package/src/astro/middleware/auth.ts +13 -6
  181. package/src/astro/middleware/redirect.ts +29 -16
  182. package/src/astro/middleware/request-context.ts +15 -5
  183. package/src/astro/middleware.ts +23 -9
  184. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  185. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  186. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  187. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  188. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  190. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  191. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  192. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  193. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  194. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  195. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  196. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  197. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  198. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  199. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  200. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  201. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  202. package/src/astro/routes/api/manifest.ts +62 -45
  203. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  204. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  205. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  206. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  207. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  208. package/src/astro/routes/api/menus/[name].ts +19 -10
  209. package/src/astro/routes/api/menus/index.ts +9 -6
  210. package/src/astro/routes/api/openapi.json.ts +27 -10
  211. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  212. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  213. package/src/astro/routes/api/redirects/[id].ts +10 -4
  214. package/src/astro/routes/api/redirects/index.ts +7 -3
  215. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  216. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  217. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  218. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  219. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  220. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  221. package/src/astro/routes/api/search/index.ts +10 -2
  222. package/src/astro/routes/api/sections/[slug].ts +10 -4
  223. package/src/astro/routes/api/sections/index.ts +7 -3
  224. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  225. package/src/astro/routes/api/snapshot.ts +44 -18
  226. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  227. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  228. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  229. package/src/astro/routes/api/taxonomies/index.ts +9 -7
  230. package/src/astro/routes/api/themes/preview.ts +11 -5
  231. package/src/astro/types.ts +23 -3
  232. package/src/auth/allowed-origins.ts +168 -0
  233. package/src/auth/passkey-config.ts +35 -13
  234. package/src/bylines/index.ts +37 -88
  235. package/src/cli/commands/auth.ts +28 -6
  236. package/src/cli/commands/bundle-utils.ts +11 -2
  237. package/src/cli/commands/bundle.ts +28 -8
  238. package/src/cli/commands/content.ts +13 -0
  239. package/src/cli/commands/export-seed.ts +82 -21
  240. package/src/cli/commands/login.ts +8 -1
  241. package/src/cli/commands/plugin-init.ts +216 -90
  242. package/src/cli/commands/publish.ts +24 -0
  243. package/src/cli/commands/secrets.ts +183 -0
  244. package/src/cli/credentials.ts +1 -1
  245. package/src/cli/index.ts +5 -1
  246. package/src/client/index.ts +4 -4
  247. package/src/client/transport.ts +17 -7
  248. package/src/components/Break.astro +2 -2
  249. package/src/components/EmDashHead.astro +18 -13
  250. package/src/components/Embed.astro +1 -1
  251. package/src/components/Gallery.astro +1 -1
  252. package/src/components/Image.astro +1 -1
  253. package/src/components/InlinePortableTextEditor.tsx +104 -18
  254. package/src/config/secrets.ts +528 -0
  255. package/src/database/dialect-helpers.ts +50 -0
  256. package/src/database/migrations/034_published_at_index.ts +1 -1
  257. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  258. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  259. package/src/database/migrations/runner.ts +158 -23
  260. package/src/database/repositories/content.ts +47 -12
  261. package/src/database/repositories/redirect.ts +14 -3
  262. package/src/database/repositories/taxonomy.ts +212 -82
  263. package/src/database/types.ts +10 -2
  264. package/src/db/libsql.ts +1 -3
  265. package/src/db/sqlite.ts +2 -5
  266. package/src/emdash-runtime.ts +84 -159
  267. package/src/i18n/resolve.ts +37 -0
  268. package/src/index.ts +9 -0
  269. package/src/loader.ts +73 -3
  270. package/src/mcp/server.ts +180 -54
  271. package/src/menus/index.ts +143 -124
  272. package/src/menus/types.ts +15 -1
  273. package/src/page/site-identity.ts +58 -0
  274. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  275. package/src/plugins/context.ts +13 -10
  276. package/src/plugins/define-plugin.ts +40 -12
  277. package/src/plugins/hooks.ts +23 -19
  278. package/src/plugins/index.ts +9 -0
  279. package/src/plugins/manifest-schema.ts +37 -2
  280. package/src/plugins/types.ts +151 -11
  281. package/src/preview/urls.ts +23 -3
  282. package/src/query.ts +148 -5
  283. package/src/redirects/cache.ts +38 -18
  284. package/src/schema/registry.ts +56 -0
  285. package/src/schema/zod-generator.ts +39 -7
  286. package/src/seed/apply.ts +142 -54
  287. package/src/seed/types.ts +14 -1
  288. package/src/seed/validate.ts +27 -13
  289. package/src/settings/index.ts +80 -6
  290. package/src/settings/types.ts +23 -1
  291. package/src/taxonomies/index.ts +237 -210
  292. package/src/taxonomies/types.ts +10 -0
  293. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  294. package/dist/bylines-CRNsVG88.mjs +0 -157
  295. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  296. package/dist/cache-BkKBuIvS.mjs +0 -56
  297. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  298. package/dist/chunk-ClPoSABd.mjs +0 -21
  299. package/dist/content-BcQPYxdV.mjs.map +0 -1
  300. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  301. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  302. package/dist/loader-CndGj8kM.mjs.map +0 -1
  303. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  304. package/dist/query-fqEdLFms.mjs.map +0 -1
  305. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  306. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  307. package/dist/runner-OURCaApa.d.mts +0 -34
  308. package/dist/runner-OURCaApa.d.mts.map +0 -1
  309. package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
  310. package/dist/search-BoZYFuUk.mjs.map +0 -1
  311. package/dist/taxonomies-B4IAshV8.mjs +0 -308
  312. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  313. package/dist/types-CS8FIX7L.d.mts.map +0 -1
  314. package/dist/types-i36XcA_X.d.mts.map +0 -1
  315. package/dist/validate-CxVsLehf.mjs.map +0 -1
  316. package/dist/validate-DHxmpFJt.d.mts.map +0 -1
  317. package/dist/version-Bbq8TCrz.mjs +0 -7
@@ -29,9 +29,10 @@ interface SiteUrlConfig {
29
29
  */
30
30
  let _envSiteUrl: string | undefined | null = null;
31
31
 
32
- /** @internal Reset cached env value — test-only. */
33
- export function _resetEnvSiteUrlCache(): void {
32
+ /** @internal Reset cached env values — test-only. */
33
+ export function _resetEnvCache(): void {
34
34
  _envSiteUrl = null;
35
+ _envAllowedOrigins = null;
35
36
  }
36
37
 
37
38
  function getEnvSiteUrl(): string | undefined {
@@ -74,6 +75,51 @@ export function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {
74
75
  return config?.siteUrl || getEnvSiteUrl() || url.origin;
75
76
  }
76
77
 
78
+ /**
79
+ * Resolve additional accepted passkey origins from runtime environment.
80
+ *
81
+ * Reads `EMDASH_ALLOWED_ORIGINS` (comma-separated list of origins) for
82
+ * multi-origin deployments where the same RP is reachable under several
83
+ * hostnames sharing the registrable parent domain (e.g. apex + preview).
84
+ *
85
+ * Each entry is parsed via `new URL()` and reduced to its `origin`. Unlike
86
+ * `getEnvSiteUrl` (which silently falls back to `url.origin` on bad input),
87
+ * this throws on any unparseable or non-http(s) entry — `EMDASH_ALLOWED_ORIGINS`
88
+ * is an allowlist for passkey verification, so silently dropping a typo would
89
+ * surface as "I can't authenticate on this origin" with no diagnostic. Fail
90
+ * loud at first read.
91
+ *
92
+ * Uses `process.env` (Vite leaves it untouched at runtime). Result is cached
93
+ * on success.
94
+ */
95
+ let _envAllowedOrigins: string[] | null = null;
96
+
97
+ export function getEnvAllowedOrigins(): string[] {
98
+ if (_envAllowedOrigins !== null) return _envAllowedOrigins;
99
+ const raw = typeof process !== "undefined" ? process.env?.EMDASH_ALLOWED_ORIGINS || "" : "";
100
+ const parsed: string[] = [];
101
+ for (const entry of raw.split(",")) {
102
+ const trimmed = entry.trim();
103
+ if (!trimmed) continue;
104
+ let u: URL;
105
+ try {
106
+ u = new URL(trimmed);
107
+ } catch (e) {
108
+ throw new Error(`EmDash config error in EMDASH_ALLOWED_ORIGINS: invalid URL: "${trimmed}"`, {
109
+ cause: e,
110
+ });
111
+ }
112
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
113
+ throw new Error(
114
+ `EmDash config error in EMDASH_ALLOWED_ORIGINS: origin must be http or https: "${trimmed}" (got ${u.protocol})`,
115
+ );
116
+ }
117
+ parsed.push(u.origin);
118
+ }
119
+ _envAllowedOrigins = parsed;
120
+ return parsed;
121
+ }
122
+
77
123
  /**
78
124
  * Build a full public URL by appending a path to the public origin.
79
125
  *
@@ -33,8 +33,8 @@ export const commentListQuery = z
33
33
  status: z.enum(["pending", "approved", "spam", "trash"]).optional(),
34
34
  collection: z.string().optional(),
35
35
  search: z.string().optional(),
36
- limit: z.coerce.number().int().min(1).max(100).optional(),
37
- cursor: z.string().optional(),
36
+ limit: z.coerce.number().int().min(1).max(100).optional().default(50),
37
+ cursor: z.string().max(2048).optional(),
38
38
  })
39
39
  .meta({ id: "CommentListQuery" });
40
40
 
@@ -59,6 +59,13 @@ export const localeCode = z
59
59
  .regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "Invalid locale code")
60
60
  .transform((v) => v.toLowerCase());
61
61
 
62
+ /** Shared `?locale=xx` query shape for endpoints that filter by locale. */
63
+ export const localeFilterQuery = z
64
+ .object({
65
+ locale: z.string().min(1).optional(),
66
+ })
67
+ .meta({ id: "LocaleFilterQuery" });
68
+
62
69
  // ---------------------------------------------------------------------------
63
70
  // OpenAPI: Shared response schemas
64
71
  // ---------------------------------------------------------------------------
@@ -72,6 +72,23 @@ export const contentScheduleBody = z
72
72
  })
73
73
  .meta({ id: "ContentScheduleBody" });
74
74
 
75
+ export const contentPublishBody = z
76
+ .object({
77
+ // .optional() rather than .nullish(): publishing has no semantic
78
+ // meaning for `null` (you can't "clear" a publish timestamp by
79
+ // publishing). Tightening the schema here means callers either
80
+ // pass a valid datetime or omit the field, and the route doesn't
81
+ // have to silently drop a null that snuck through.
82
+ publishedAt: z.iso
83
+ .datetime({ offset: true, message: "must be an ISO 8601 datetime" })
84
+ .optional()
85
+ .meta({
86
+ description:
87
+ "Optional ISO 8601 datetime to backdate the publish (e.g. when migrating content). Requires content:publish_any permission. Without this, existing published_at is preserved on re-publish.",
88
+ }),
89
+ })
90
+ .meta({ id: "ContentPublishBody" });
91
+
75
92
  export const contentPreviewUrlBody = z
76
93
  .object({
77
94
  expiresIn: z.union([z.string(), z.number()]).optional(),
@@ -20,6 +20,10 @@ export const createMenuBody = z
20
20
  .object({
21
21
  name: z.string().min(1),
22
22
  label: z.string().min(1),
23
+ locale: z.string().min(1).optional(),
24
+ /** When set, clones the items from the source menu. The new menu joins
25
+ * the source's translation_group. */
26
+ translationOf: z.string().min(1).optional(),
23
27
  })
24
28
  .meta({ id: "CreateMenuBody" });
25
29
 
@@ -87,6 +91,8 @@ export const menuSchema = z
87
91
  label: z.string(),
88
92
  created_at: z.string(),
89
93
  updated_at: z.string(),
94
+ locale: z.string(),
95
+ translation_group: z.string().nullable(),
90
96
  })
91
97
  .meta({ id: "Menu" });
92
98
 
@@ -105,9 +111,26 @@ export const menuItemSchema = z
105
111
  target: z.string().nullable(),
106
112
  css_classes: z.string().nullable(),
107
113
  created_at: z.string(),
114
+ locale: z.string(),
115
+ translation_group: z.string().nullable(),
108
116
  })
109
117
  .meta({ id: "MenuItem" });
110
118
 
119
+ export const menuTranslationsSchema = z
120
+ .object({
121
+ translationGroup: z.string().nullable(),
122
+ translations: z.array(
123
+ z.object({
124
+ id: z.string(),
125
+ name: z.string(),
126
+ label: z.string(),
127
+ locale: z.string(),
128
+ updatedAt: z.string(),
129
+ }),
130
+ ),
131
+ })
132
+ .meta({ id: "MenuTranslations" });
133
+
111
134
  export const menuListItemSchema = menuSchema
112
135
  .extend({
113
136
  itemCount: z.number().int(),
@@ -10,8 +10,8 @@ export const sectionsListQuery = z
10
10
  .object({
11
11
  source: sectionSource.optional(),
12
12
  search: z.string().optional(),
13
- limit: z.coerce.number().int().min(1).max(100).optional(),
14
- cursor: z.string().optional(),
13
+ limit: z.coerce.number().int().min(1).max(100).optional().default(50),
14
+ cursor: z.string().max(2048).optional(),
15
15
  })
16
16
  .meta({ id: "SectionsListQuery" });
17
17
 
@@ -23,7 +23,7 @@ export const createSectionBody = z
23
23
  keywords: z.array(z.string()).optional(),
24
24
  content: z.array(z.record(z.string(), z.unknown())),
25
25
  previewMediaId: z.string().optional(),
26
- source: sectionSource.optional(),
26
+ source: z.enum(["user", "import"]).optional(),
27
27
  themeId: z.string().optional(),
28
28
  })
29
29
  .meta({ id: "CreateSectionBody" });
@@ -15,6 +15,7 @@ export const createTaxonomyDefBody = z
15
15
  .max(63)
16
16
  .regex(/^[a-z][a-z0-9_]*$/, "Name must be lowercase alphanumeric with underscores"),
17
17
  label: z.string().min(1).max(200),
18
+ labelSingular: z.string().min(1).max(200).optional(),
18
19
  hierarchical: z.boolean().optional().default(false),
19
20
  collections: z
20
21
  .array(
@@ -23,6 +24,8 @@ export const createTaxonomyDefBody = z
23
24
  .max(100)
24
25
  .optional()
25
26
  .default([]),
27
+ locale: z.string().min(1).optional(),
28
+ translationOf: z.string().min(1).optional(),
26
29
  })
27
30
  .meta({ id: "CreateTaxonomyDefBody" });
28
31
 
@@ -36,6 +39,8 @@ export const createTermBody = z
36
39
  label: z.string().min(1),
37
40
  parentId: z.string().nullish(),
38
41
  description: z.string().optional(),
42
+ locale: z.string().min(1).optional(),
43
+ translationOf: z.string().min(1).optional(),
39
44
  })
40
45
  .meta({ id: "CreateTermBody" });
41
46
 
@@ -60,9 +65,25 @@ export const taxonomyDefSchema = z
60
65
  labelSingular: z.string().optional(),
61
66
  hierarchical: z.boolean(),
62
67
  collections: z.array(z.string()),
68
+ locale: z.string(),
69
+ translationGroup: z.string().nullable(),
63
70
  })
64
71
  .meta({ id: "TaxonomyDef" });
65
72
 
73
+ export const taxonomyDefTranslationsSchema = z
74
+ .object({
75
+ translationGroup: z.string().nullable(),
76
+ translations: z.array(
77
+ z.object({
78
+ id: z.string(),
79
+ name: z.string(),
80
+ label: z.string(),
81
+ locale: z.string(),
82
+ }),
83
+ ),
84
+ })
85
+ .meta({ id: "TaxonomyDefTranslations" });
86
+
66
87
  export const taxonomyListResponseSchema = z
67
88
  .object({ taxonomies: z.array(taxonomyDefSchema) })
68
89
  .meta({ id: "TaxonomyListResponse" });
@@ -75,9 +96,25 @@ export const termSchema = z
75
96
  label: z.string(),
76
97
  parentId: z.string().nullable(),
77
98
  description: z.string().optional(),
99
+ locale: z.string(),
100
+ translationGroup: z.string().nullable(),
78
101
  })
79
102
  .meta({ id: "Term" });
80
103
 
104
+ export const termTranslationsSchema = z
105
+ .object({
106
+ translationGroup: z.string().nullable(),
107
+ translations: z.array(
108
+ z.object({
109
+ id: z.string(),
110
+ slug: z.string(),
111
+ label: z.string(),
112
+ locale: z.string(),
113
+ }),
114
+ ),
115
+ })
116
+ .meta({ id: "TermTranslations" });
117
+
81
118
  export const termWithCountSchema: z.ZodType = z
82
119
  .object({
83
120
  id: z.string(),
@@ -88,6 +125,8 @@ export const termWithCountSchema: z.ZodType = z
88
125
  description: z.string().optional(),
89
126
  count: z.number().int(),
90
127
  children: z.array(z.lazy(() => termWithCountSchema)),
128
+ locale: z.string(),
129
+ translationGroup: z.string().nullable(),
91
130
  })
92
131
  .meta({ id: "TermWithCount" });
93
132
 
@@ -10,7 +10,7 @@ export const usersListQuery = z
10
10
  .object({
11
11
  search: z.string().optional(),
12
12
  role: z.string().optional(),
13
- cursor: z.string().optional(),
13
+ cursor: z.string().max(2048).optional(),
14
14
  limit: z.coerce.number().int().min(1).max(100).optional().default(50),
15
15
  })
16
16
  .meta({ id: "UsersListQuery" });
package/src/api/types.ts CHANGED
@@ -51,7 +51,11 @@ export interface FieldDescriptor {
51
51
  kind: string;
52
52
  label?: string;
53
53
  required?: boolean;
54
- options?: Array<{ value: string; label: string }>;
54
+ /**
55
+ * For `select` / `multiSelect`: the list of enum choices.
56
+ * For `json` fields driven by a plugin `widget`: arbitrary widget config.
57
+ */
58
+ options?: Array<{ value: string; label: string }> | Record<string, unknown>;
55
59
  }
56
60
 
57
61
  /**
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { AstroIntegration, AstroIntegrationLogger } from "astro";
14
14
 
15
+ import { validateAllowedOrigins, validateOriginShape } from "../../auth/allowed-origins.js";
15
16
  import type { ResolvedPlugin } from "../../plugins/types.js";
16
17
  import { local } from "../storage/adapters.js";
17
18
  import { notoSans } from "./font-provider.js";
@@ -117,6 +118,22 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
117
118
  }
118
119
  }
119
120
 
121
+ // Validate config.allowedOrigins shape at startup (per-entry rules: parseable,
122
+ // http(s), no trailing dots, no empty labels). The siteUrl-dependent rules
123
+ // (Rule A: requires siteUrl; Rule B: must be a subdomain of siteUrl) are
124
+ // deferred to runtime when config.siteUrl is absent — EMDASH_SITE_URL may
125
+ // supply it post-build, just like the env-var fallback for siteUrl above.
126
+ // When config.siteUrl IS present, run the full validator here for fail-fast.
127
+ if (resolvedConfig.allowedOrigins?.length) {
128
+ const tagged = resolvedConfig.allowedOrigins.map((origin) => ({
129
+ origin,
130
+ source: "config.allowedOrigins" as const,
131
+ }));
132
+ resolvedConfig.allowedOrigins = resolvedConfig.siteUrl
133
+ ? validateAllowedOrigins(resolvedConfig.siteUrl, tagged)
134
+ : validateOriginShape(tagged);
135
+ }
136
+
120
137
  // Plugin descriptors from config
121
138
  const pluginDescriptors = resolvedConfig.plugins ?? [];
122
139
  const sandboxedDescriptors = resolvedConfig.sandboxed ?? [];
@@ -313,6 +313,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
313
313
  entrypoint: resolveRoute("api/taxonomies/[name]/terms/[slug].ts"),
314
314
  });
315
315
 
316
+ injectRoute({
317
+ pattern: "/_emdash/api/taxonomies/[name]/terms/[slug]/translations",
318
+ entrypoint: resolveRoute("api/taxonomies/[name]/terms/[slug]/translations.ts"),
319
+ });
320
+
316
321
  injectRoute({
317
322
  pattern: "/_emdash/api/content/[collection]/[id]/terms/[taxonomy]",
318
323
  entrypoint: resolveRoute("api/content/[collection]/[id]/terms/[taxonomy].ts"),
@@ -555,6 +560,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
555
560
  entrypoint: resolveRoute("api/menus/[name]/reorder.ts"),
556
561
  });
557
562
 
563
+ injectRoute({
564
+ pattern: "/_emdash/api/menus/[name]/translations",
565
+ entrypoint: resolveRoute("api/menus/[name]/translations.ts"),
566
+ });
567
+
558
568
  // Widget area routes
559
569
  injectRoute({
560
570
  pattern: "/_emdash/api/widget-areas",
@@ -303,6 +303,36 @@ export interface EmDashConfig {
303
303
  siteUrl?: string;
304
304
 
305
305
  /**
306
+ * Additional origins accepted by passkey verification.
307
+ *
308
+ * When the same EmDash deployment is reachable under several hostnames sharing
309
+ * a registrable parent (e.g. `https://example.com` plus
310
+ * `https://preview.example.com`), the canonical `siteUrl` defines the `rpId`
311
+ * and the entries here are the *additional* origins from which assertions
312
+ * are accepted. Each entry must be the same hostname as `siteUrl` or a
313
+ * subdomain of it — WebAuthn requires `rpId` to be a registrable suffix of
314
+ * every origin.
315
+ *
316
+ * Merged at runtime with the `EMDASH_ALLOWED_ORIGINS` env var (comma-separated).
317
+ * Validation:
318
+ * - Config-declared entries are shape-checked at Astro startup.
319
+ * - Subdomain relationship to `siteUrl` is checked at startup when
320
+ * `siteUrl` is also config-declared, otherwise at first passkey
321
+ * verification (since `siteUrl` may come from `EMDASH_SITE_URL`).
322
+ *
323
+ * Mismatches throw with a source-attributed message naming
324
+ * `config.allowedOrigins` or `EMDASH_ALLOWED_ORIGINS`.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * emdash({
329
+ * siteUrl: "https://example.com",
330
+ * allowedOrigins: ["https://preview.example.com"],
331
+ * })
332
+ * ```
333
+ */
334
+ allowedOrigins?: string[];
335
+ /*
306
336
  * Headers to trust for client IP resolution when running behind a reverse
307
337
  * proxy. The first header in this list that is present on the request
308
338
  * wins. Applies to rate limiting for auth endpoints and comment
@@ -401,9 +401,20 @@ export function generateWaitUntilModule(adapterName: string | undefined): string
401
401
  * Reads the user's seed file at build time (in Node context) and embeds it,
402
402
  * so the runtime doesn't need filesystem access (required for workerd).
403
403
  *
404
+ * Search order:
405
+ * 1. `.emdash/seed.json`
406
+ * 2. `package.json` → `emdash.seed` reference
407
+ * 3. `seed/seed.json` (conventional template path)
408
+ *
404
409
  * Exports `userSeed` (user's seed or null) and `seed` (user's seed or default).
410
+ *
411
+ * When no user seed is found, falls back to the built-in default seed and
412
+ * (if `warnOnFallback` is true) logs a warning so misconfiguration is visible
413
+ * during `astro dev`. Build/preview/sync stay silent so sites that
414
+ * intentionally use the default seed (e.g. the blank template) don't
415
+ * generate noisy logs.
405
416
  */
406
- export function generateSeedModule(projectRoot: string): string {
417
+ export function generateSeedModule(projectRoot: string, warnOnFallback = false): string {
407
418
  let userSeedJson: string | null = null;
408
419
 
409
420
  // Try .emdash/seed.json
@@ -434,11 +445,30 @@ export function generateSeedModule(projectRoot: string): string {
434
445
  }
435
446
  }
436
447
 
448
+ // Try conventional seed/seed.json fallback
449
+ if (!userSeedJson) {
450
+ try {
451
+ const seedPath = resolve(projectRoot, "seed", "seed.json");
452
+ const content = readFileSync(seedPath, "utf-8");
453
+ JSON.parse(content); // validate
454
+ userSeedJson = content;
455
+ } catch {
456
+ // Not found
457
+ }
458
+ }
459
+
437
460
  if (userSeedJson) {
438
461
  return [`export const userSeed = ${userSeedJson};`, `export const seed = userSeed;`].join("\n");
439
462
  }
440
463
 
441
- // No user seed — inline the default
464
+ // No user seed — inline the default. Caller (the Vite plugin) gates this
465
+ // to dev-only so production builds stay quiet for sites that intentionally
466
+ // rely on the default seed.
467
+ if (warnOnFallback) {
468
+ console.warn(
469
+ "[emdash] No user seed found at .emdash/seed.json, package.json#emdash.seed, or seed/seed.json. Falling back to the built-in default seed; the setup wizard will not offer demo content for this site.",
470
+ );
471
+ }
442
472
  return [
443
473
  `export const userSeed = null;`,
444
474
  `export const seed = ${JSON.stringify(defaultSeed)};`,
@@ -156,8 +156,13 @@ export interface VitePluginOptions {
156
156
  export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
157
157
  const { serializableConfig, resolvedConfig, pluginDescriptors, astroConfig } = options;
158
158
 
159
+ let viteCommand: "build" | "serve" | undefined;
160
+
159
161
  return {
160
162
  name: "emdash-virtual-modules",
163
+ configResolved(config) {
164
+ viteCommand = config.command;
165
+ },
161
166
  resolveId(id: string) {
162
167
  if (id === VIRTUAL_CONFIG_ID) {
163
168
  return RESOLVED_VIRTUAL_CONFIG_ID;
@@ -259,7 +264,7 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
259
264
  // Generate seed module — embeds user seed or default at build time
260
265
  if (id === RESOLVED_VIRTUAL_SEED_ID) {
261
266
  const projectRoot = fileURLToPath(astroConfig.root);
262
- return generateSeedModule(projectRoot);
267
+ return generateSeedModule(projectRoot, viteCommand === "serve");
263
268
  }
264
269
  // Generate wait-until module — re-exports cloudflare:workers'
265
270
  // waitUntil under the Cloudflare adapter, undefined otherwise.
@@ -32,7 +32,7 @@ import { resolveApiToken, resolveOAuthToken } from "../../api/handlers/api-token
32
32
  import { hasScope } from "../../auth/api-tokens.js";
33
33
  import { getAuthMode, type ExternalAuthMode } from "../../auth/mode.js";
34
34
  import type { ExternalAuthConfig } from "../../auth/types.js";
35
- import type { EmDashHandlers, EmDashManifest } from "../types.js";
35
+ import type { EmDashHandlers } from "../types.js";
36
36
  import { buildEmDashCsp } from "./csp.js";
37
37
 
38
38
  declare global {
@@ -42,7 +42,6 @@ declare global {
42
42
  /** Token scopes when authenticated via API token or OAuth token. Undefined for session auth. */
43
43
  tokenScopes?: string[];
44
44
  emdash?: EmDashHandlers;
45
- emdashManifest?: EmDashManifest;
46
45
  }
47
46
  interface SessionData {
48
47
  user: { id: string };
@@ -723,10 +722,14 @@ const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
723
722
  ["/_emdash/api/schema", "WRITE", "schema:write"],
724
723
 
725
724
  // Taxonomy, menu, section, widget, revision — all content domain
725
+ // GET uses content:read (implicit from taxonomies:read / menus:read via role).
726
+ // WRITE uses the granular scope so tokens with only taxonomies:manage or
727
+ // menus:manage are not rejected. content:write implicitly grants these via
728
+ // IMPLICIT_SCOPE_GRANTS in @emdash-cms/auth.
726
729
  ["/_emdash/api/taxonomies", "GET", "content:read"],
727
- ["/_emdash/api/taxonomies", "WRITE", "content:write"],
730
+ ["/_emdash/api/taxonomies", "WRITE", "taxonomies:manage"],
728
731
  ["/_emdash/api/menus", "GET", "content:read"],
729
- ["/_emdash/api/menus", "WRITE", "content:write"],
732
+ ["/_emdash/api/menus", "WRITE", "menus:manage"],
730
733
  ["/_emdash/api/sections", "GET", "content:read"],
731
734
  ["/_emdash/api/sections", "WRITE", "content:write"],
732
735
  ["/_emdash/api/widget-areas", "GET", "content:read"],
@@ -738,12 +741,16 @@ const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
738
741
  ["/_emdash/api/search", "GET", "content:read"],
739
742
  ["/_emdash/api/search", "WRITE", "admin"],
740
743
 
741
- // Import, admin, settings, plugins — all require admin scope
744
+ // Import, admin, plugins — all require admin scope
742
745
  ["/_emdash/api/import", "*", "admin"],
743
746
  ["/_emdash/api/admin", "*", "admin"],
744
- ["/_emdash/api/settings", "*", "admin"],
745
747
  ["/_emdash/api/plugins", "*", "admin"],
746
748
 
749
+ // Settings — use granular scopes so tokens with settings:read or
750
+ // settings:manage are not rejected at the middleware level.
751
+ ["/_emdash/api/settings", "GET", "settings:read"],
752
+ ["/_emdash/api/settings", "WRITE", "settings:manage"],
753
+
747
754
  // MCP endpoint — scopes enforced per-tool inside mcp/server.ts
748
755
  ["/_emdash/api/mcp", "*", "content:read"],
749
756
  ];
@@ -17,10 +17,11 @@
17
17
  import { defineMiddleware } from "astro:middleware";
18
18
 
19
19
  import { RedirectRepository } from "../../database/repositories/redirect.js";
20
+ import { getDb } from "../../loader.js";
20
21
  import {
21
- getCachedPatternRules,
22
+ getCachedRedirects,
22
23
  matchCachedPatterns,
23
- setCachedPatternRules,
24
+ setCachedRedirects,
24
25
  } from "../../redirects/cache.js";
25
26
 
26
27
  /** Paths that should never be intercepted by redirects */
@@ -46,16 +47,34 @@ export const onRequest = defineMiddleware(async (context, next) => {
46
47
  return next();
47
48
  }
48
49
 
49
- const { emdash } = context.locals;
50
- if (!emdash?.db) {
51
- return next();
50
+ // Public visitors hit the runtime's anonymous fast path, which intentionally
51
+ // omits `db` from `locals.emdash` to keep the public render boundary minimal
52
+ // (issue #808). Fall back to `getDb()`, which transparently returns the
53
+ // per-request scoped db (set in ALS by the runtime middleware) or the
54
+ // singleton — same path the loader and template helpers use.
55
+ let db = context.locals.emdash?.db;
56
+ if (!db) {
57
+ try {
58
+ db = await getDb();
59
+ } catch {
60
+ return next();
61
+ }
52
62
  }
53
63
 
54
64
  try {
55
- const repo = new RedirectRepository(emdash.db);
65
+ const repo = new RedirectRepository(db);
66
+
67
+ // One query loads both exact and pattern rules into the cache; warm
68
+ // requests issue zero queries. Empty-redirect sites cache an empty
69
+ // Map + array, so the next request returns immediately without probing.
70
+ let cached = getCachedRedirects();
71
+ if (!cached) {
72
+ const all = await repo.findAllEnabled();
73
+ cached = setCachedRedirects(all);
74
+ }
56
75
 
57
- // 1. Exact match (fast, indexed)
58
- const exact = await repo.findExactMatch(pathname);
76
+ // 1. Exact match (O(1) Map lookup)
77
+ const exact = cached.exact.get(pathname);
59
78
  if (exact) {
60
79
  const dest = exact.destination;
61
80
  if (dest.startsWith("//") || dest.startsWith("/\\")) return next();
@@ -64,14 +83,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
64
83
  return context.redirect(dest, code);
65
84
  }
66
85
 
67
- // 2. Pattern match (cached: compile once, match every request)
68
- let rules = getCachedPatternRules();
69
- if (!rules) {
70
- const patterns = await repo.findEnabledPatternRules();
71
- rules = setCachedPatternRules(patterns);
72
- }
73
-
74
- const patternMatch = matchCachedPatterns(rules, pathname);
86
+ // 2. Pattern match (compile once, match every request)
87
+ const patternMatch = matchCachedPatterns(cached.patterns, pathname);
75
88
  if (patternMatch) {
76
89
  const { redirect, destination } = patternMatch;
77
90
  if (destination.startsWith("//") || destination.startsWith("/\\")) return next();
@@ -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