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
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared setup completion logic.
3
+ *
4
+ * Called by OAuth callbacks and the passkey verify step when the first user
5
+ * is created during setup. Persists site title/tagline from setup state
6
+ * and marks setup as complete.
7
+ */
8
+
9
+ import type { Kysely } from "kysely";
10
+
11
+ import { OptionsRepository } from "../database/repositories/options.js";
12
+ import type { Database } from "../database/types.js";
13
+
14
+ /**
15
+ * Finalize setup after the first admin user is created.
16
+ *
17
+ * Reads the setup_state option (written by the setup wizard's step 1),
18
+ * persists site_title and site_tagline, then marks setup complete.
19
+ *
20
+ * Safe to call multiple times — checks setup_complete first and no-ops
21
+ * if already done.
22
+ */
23
+ export async function finalizeSetup(db: Kysely<Database>): Promise<void> {
24
+ const options = new OptionsRepository(db);
25
+
26
+ const setupComplete = await options.get("emdash:setup_complete");
27
+ if (setupComplete === true || setupComplete === "true") return;
28
+
29
+ // Persist site title/tagline from setup state (stored in step 1)
30
+ const setupState = await options.get<Record<string, unknown>>("emdash:setup_state");
31
+ if (setupState?.title && typeof setupState.title === "string") {
32
+ await options.set("emdash:site_title", setupState.title);
33
+ }
34
+ if (setupState?.tagline && typeof setupState.tagline === "string") {
35
+ await options.set("emdash:site_tagline", setupState.tagline);
36
+ }
37
+
38
+ await options.set("emdash:setup_complete", true);
39
+ await options.delete("emdash:setup_state");
40
+ }
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,10 +12,16 @@
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";
18
- import { injectCoreRoutes, injectBuiltinAuthRoutes, injectMcpRoute } from "./routes.js";
19
+ import {
20
+ injectCoreRoutes,
21
+ injectBuiltinAuthRoutes,
22
+ injectAuthProviderRoutes,
23
+ injectMcpRoute,
24
+ } from "./routes.js";
19
25
  import type { EmDashConfig, PluginDescriptor } from "./runtime.js";
20
26
  import { createViteConfig } from "./vite-config.js";
21
27
 
@@ -112,6 +118,22 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
112
118
  }
113
119
  }
114
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
+
115
137
  // Plugin descriptors from config
116
138
  const pluginDescriptors = resolvedConfig.plugins ?? [];
117
139
  const sandboxedDescriptors = resolvedConfig.sandboxed ?? [];
@@ -157,6 +179,7 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
157
179
  database: resolvedConfig.database,
158
180
  storage: resolvedConfig.storage,
159
181
  auth: resolvedConfig.auth,
182
+ authProviders: resolvedConfig.authProviders,
160
183
  marketplace: resolvedConfig.marketplace,
161
184
  siteUrl: resolvedConfig.siteUrl,
162
185
  trustedProxyHeaders: resolvedConfig.trustedProxyHeaders,
@@ -267,7 +290,12 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
267
290
  // Inject all core routes
268
291
  injectCoreRoutes(injectRoute);
269
292
 
270
- // Only inject passkey/oauth/magic-link routes when NOT using external auth
293
+ // Inject routes from pluggable auth providers (authProviders config)
294
+ if (resolvedConfig.authProviders?.length) {
295
+ injectAuthProviderRoutes(injectRoute, resolvedConfig.authProviders);
296
+ }
297
+
298
+ // Inject passkey/oauth/magic-link routes unless transparent external auth is active
271
299
  if (!useExternalAuth) {
272
300
  injectBuiltinAuthRoutes(injectRoute);
273
301
  }
@@ -46,6 +46,12 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
46
46
  entrypoint: resolveRoute("api/manifest.ts"),
47
47
  });
48
48
 
49
+ // Auth mode endpoint (public — used by the login page to pick the right UI)
50
+ injectRoute({
51
+ pattern: "/_emdash/api/auth/mode",
52
+ entrypoint: resolveRoute("api/auth/mode.ts"),
53
+ });
54
+
49
55
  injectRoute({
50
56
  pattern: "/_emdash/api/dashboard",
51
57
  entrypoint: resolveRoute("api/dashboard.ts"),
@@ -747,6 +753,28 @@ export function injectMcpRoute(injectRoute: InjectRoute): void {
747
753
  });
748
754
  }
749
755
 
756
+ /**
757
+ * Injects routes from pluggable auth providers.
758
+ *
759
+ * Each provider declares the routes it needs in its `AuthProviderDescriptor.routes` array.
760
+ * Routes are injected at build time so Vite can bundle them.
761
+ */
762
+ export function injectAuthProviderRoutes(
763
+ injectRoute: InjectRoute,
764
+ providers: Array<{ routes?: Array<{ pattern: string; entrypoint: string }> }>,
765
+ ): void {
766
+ for (const provider of providers) {
767
+ if (provider.routes) {
768
+ for (const route of provider.routes) {
769
+ injectRoute({
770
+ pattern: route.pattern,
771
+ entrypoint: route.entrypoint,
772
+ });
773
+ }
774
+ }
775
+ }
776
+ }
777
+
750
778
  /**
751
779
  * Injects passkey/oauth/magic-link auth routes.
752
780
  * Only used when NOT using external auth.
@@ -7,7 +7,7 @@
7
7
  * DO NOT import Node.js-only modules here (fs, path, module, etc.)
8
8
  */
9
9
 
10
- import type { AuthDescriptor } from "../../auth/types.js";
10
+ import type { AuthDescriptor, AuthProviderDescriptor } from "../../auth/types.js";
11
11
  import type { DatabaseDescriptor } from "../../db/adapters.js";
12
12
  import type { MediaProviderDescriptor } from "../../media/types.js";
13
13
  import type { ResolvedPlugin } from "../../plugins/types.js";
@@ -222,6 +222,24 @@ export interface EmDashConfig {
222
222
  */
223
223
  auth?: AuthDescriptor;
224
224
 
225
+ /**
226
+ * Pluggable auth providers (login methods on the login page).
227
+ *
228
+ * Auth providers appear as options alongside passkey on the login page
229
+ * and setup wizard. Any provider can be used to create the initial
230
+ * admin account. Passkey is built-in; providers listed here are additive.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * import { atproto } from "@emdash-cms/auth-atproto";
235
+ *
236
+ * emdash({
237
+ * authProviders: [atproto()],
238
+ * })
239
+ * ```
240
+ */
241
+ authProviders?: AuthProviderDescriptor[];
242
+
225
243
  /**
226
244
  * MCP (Model Context Protocol) server endpoint.
227
245
  *
@@ -285,6 +303,36 @@ export interface EmDashConfig {
285
303
  siteUrl?: string;
286
304
 
287
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
+ /*
288
336
  * Headers to trust for client IP resolution when running behind a reverse
289
337
  * proxy. The first header in this list that is present on the request
290
338
  * wins. Applies to rate limiting for auth endpoints and comment
@@ -10,6 +10,7 @@ import { readFileSync } from "node:fs";
10
10
  import { createRequire } from "node:module";
11
11
  import { resolve } from "node:path";
12
12
 
13
+ import type { AuthProviderDescriptor } from "../../auth/types.js";
13
14
  import type { MediaProviderDescriptor } from "../../media/types.js";
14
15
  import { defaultSeed } from "../../seed/default.js";
15
16
  import type { PluginDescriptor } from "./runtime.js";
@@ -47,6 +48,9 @@ export const RESOLVED_VIRTUAL_SANDBOXED_PLUGINS_ID = "\0" + VIRTUAL_SANDBOXED_PL
47
48
  export const VIRTUAL_AUTH_ID = "virtual:emdash/auth";
48
49
  export const RESOLVED_VIRTUAL_AUTH_ID = "\0" + VIRTUAL_AUTH_ID;
49
50
 
51
+ export const VIRTUAL_AUTH_PROVIDERS_ID = "virtual:emdash/auth-providers";
52
+ export const RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID = "\0" + VIRTUAL_AUTH_PROVIDERS_ID;
53
+
50
54
  export const VIRTUAL_MEDIA_PROVIDERS_ID = "virtual:emdash/media-providers";
51
55
  export const RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID = "\0" + VIRTUAL_MEDIA_PROVIDERS_ID;
52
56
 
@@ -135,6 +139,43 @@ export const authenticate = _authenticate;
135
139
  `;
136
140
  }
137
141
 
142
+ /**
143
+ * Generates the auth providers module.
144
+ *
145
+ * Statically imports each auth provider's `adminEntry` module and exports
146
+ * a registry keyed by provider ID. The admin UI uses this to render
147
+ * provider-specific login buttons/forms and setup steps.
148
+ *
149
+ * Follows the same pattern as `generateAdminRegistryModule()` for plugins.
150
+ */
151
+ export function generateAuthProvidersModule(descriptors: AuthProviderDescriptor[]): string {
152
+ const withAdmin = descriptors.filter((d) => d.adminEntry);
153
+
154
+ if (withAdmin.length === 0) {
155
+ return `export const authProviders = {};`;
156
+ }
157
+
158
+ const imports: string[] = [];
159
+ const entries: string[] = [];
160
+
161
+ withAdmin.forEach((descriptor, index) => {
162
+ const varName = `authProvider${index}`;
163
+ imports.push(`import * as ${varName} from ${JSON.stringify(descriptor.adminEntry)};`);
164
+ entries.push(
165
+ ` ${JSON.stringify(descriptor.id)}: { ...${varName}, id: ${JSON.stringify(descriptor.id)}, label: ${JSON.stringify(descriptor.label)} },`,
166
+ );
167
+ });
168
+
169
+ return `
170
+ // Auto-generated auth provider registry
171
+ ${imports.join("\n")}
172
+
173
+ export const authProviders = {
174
+ ${entries.join("\n")}
175
+ };
176
+ `;
177
+ }
178
+
138
179
  /**
139
180
  * Generates the plugins module.
140
181
  * Imports and instantiates all plugins at runtime.
@@ -360,9 +401,20 @@ export function generateWaitUntilModule(adapterName: string | undefined): string
360
401
  * Reads the user's seed file at build time (in Node context) and embeds it,
361
402
  * so the runtime doesn't need filesystem access (required for workerd).
362
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
+ *
363
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.
364
416
  */
365
- export function generateSeedModule(projectRoot: string): string {
417
+ export function generateSeedModule(projectRoot: string, warnOnFallback = false): string {
366
418
  let userSeedJson: string | null = null;
367
419
 
368
420
  // Try .emdash/seed.json
@@ -393,11 +445,30 @@ export function generateSeedModule(projectRoot: string): string {
393
445
  }
394
446
  }
395
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
+
396
460
  if (userSeedJson) {
397
461
  return [`export const userSeed = ${userSeedJson};`, `export const seed = userSeed;`].join("\n");
398
462
  }
399
463
 
400
- // 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
+ }
401
472
  return [
402
473
  `export const userSeed = null;`,
403
474
  `export const seed = ${JSON.stringify(defaultSeed)};`,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { existsSync } from "node:fs";
9
9
  import { createRequire } from "node:module";
10
- import { dirname, resolve } from "node:path";
10
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
13
13
  import type { AstroConfig } from "astro";
@@ -32,6 +32,8 @@ import {
32
32
  RESOLVED_VIRTUAL_SANDBOXED_PLUGINS_ID,
33
33
  VIRTUAL_AUTH_ID,
34
34
  RESOLVED_VIRTUAL_AUTH_ID,
35
+ VIRTUAL_AUTH_PROVIDERS_ID,
36
+ RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID,
35
37
  VIRTUAL_MEDIA_PROVIDERS_ID,
36
38
  RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID,
37
39
  VIRTUAL_BLOCK_COMPONENTS_ID,
@@ -46,6 +48,7 @@ import {
46
48
  generateDialectModule,
47
49
  generateStorageModule,
48
50
  generateAuthModule,
51
+ generateAuthProvidersModule,
49
52
  generatePluginsModule,
50
53
  generateAdminRegistryModule,
51
54
  generateSandboxRunnerModule,
@@ -104,24 +107,34 @@ function resolveAdminDist(): string {
104
107
  return dirname(adminPath);
105
108
  }
106
109
 
110
+ /**
111
+ * Check whether child is inside parent without relying on simple prefix checks.
112
+ */
113
+ function isInside(parent: string, child: string): boolean {
114
+ const relativePath = relative(parent, child);
115
+ return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
116
+ }
117
+
107
118
  /**
108
119
  * Resolve path to the admin package source directory.
109
- * In dev mode, we alias @emdash-cms/admin to the source so Vite processes it
110
- * directly — giving instant HMR instead of requiring a rebuild + restart.
120
+ * In dev mode inside this repo, we alias @emdash-cms/admin to the source so
121
+ * Vite processes it directly — giving instant HMR instead of requiring a
122
+ * rebuild + restart. External apps should use the built package surface.
111
123
  */
112
- function resolveAdminSource(): string | undefined {
124
+ function resolveAdminSource(projectRoot: string): string | undefined {
113
125
  const require = createRequire(import.meta.url);
114
126
  const adminPath = require.resolve("@emdash-cms/admin");
115
127
  // dist/index.js -> go up to package root, then into src/
116
128
  const packageRoot = resolve(dirname(adminPath), "..");
129
+ const repoRoot = resolve(packageRoot, "..", "..");
117
130
  const srcEntry = resolve(packageRoot, "src", "index.ts");
118
131
 
119
132
  try {
120
- if (existsSync(srcEntry)) {
133
+ if (existsSync(srcEntry) && isInside(repoRoot, projectRoot)) {
121
134
  return resolve(packageRoot, "src");
122
135
  }
123
136
  } catch {
124
- // Not in monorepo — fall back to dist
137
+ // Not in local repo — fall back to dist
125
138
  }
126
139
  return undefined;
127
140
  }
@@ -143,8 +156,13 @@ export interface VitePluginOptions {
143
156
  export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
144
157
  const { serializableConfig, resolvedConfig, pluginDescriptors, astroConfig } = options;
145
158
 
159
+ let viteCommand: "build" | "serve" | undefined;
160
+
146
161
  return {
147
162
  name: "emdash-virtual-modules",
163
+ configResolved(config) {
164
+ viteCommand = config.command;
165
+ },
148
166
  resolveId(id: string) {
149
167
  if (id === VIRTUAL_CONFIG_ID) {
150
168
  return RESOLVED_VIRTUAL_CONFIG_ID;
@@ -170,6 +188,9 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
170
188
  if (id === VIRTUAL_AUTH_ID) {
171
189
  return RESOLVED_VIRTUAL_AUTH_ID;
172
190
  }
191
+ if (id === VIRTUAL_AUTH_PROVIDERS_ID) {
192
+ return RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID;
193
+ }
173
194
  if (id === VIRTUAL_MEDIA_PROVIDERS_ID) {
174
195
  return RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID;
175
196
  }
@@ -228,6 +249,10 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
228
249
  }
229
250
  return generateAuthModule(authDescriptor.entrypoint);
230
251
  }
252
+ // Generate auth providers module (pluggable login methods)
253
+ if (id === RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID) {
254
+ return generateAuthProvidersModule(resolvedConfig.authProviders ?? []);
255
+ }
231
256
  // Generate media providers module
232
257
  if (id === RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID) {
233
258
  return generateMediaProvidersModule(resolvedConfig.mediaProviders ?? []);
@@ -239,7 +264,7 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
239
264
  // Generate seed module — embeds user seed or default at build time
240
265
  if (id === RESOLVED_VIRTUAL_SEED_ID) {
241
266
  const projectRoot = fileURLToPath(astroConfig.root);
242
- return generateSeedModule(projectRoot);
267
+ return generateSeedModule(projectRoot, viteCommand === "serve");
243
268
  }
244
269
  // Generate wait-until module — re-exports cloudflare:workers'
245
270
  // waitUntil under the Cloudflare adapter, undefined otherwise.
@@ -281,12 +306,9 @@ export function createViteConfig(
281
306
  const adminDistPath = resolveAdminDist();
282
307
  const cloudflare = isCloudflareAdapter(options.astroConfig);
283
308
  const isDev = command === "dev";
309
+ const projectRoot = fileURLToPath(options.astroConfig.root);
284
310
 
285
- // In dev mode within the monorepo, alias JS imports to source for instant HMR.
286
- // CSS always comes from dist/ (pre-compiled by @tailwindcss/cli) since Tailwind's
287
- // Vite plugin has native deps that don't bundle well. Run `pnpm dev` in packages/admin
288
- // alongside the demo server to get CSS watch-rebuilds too.
289
- const adminSourcePath = isDev ? resolveAdminSource() : undefined;
311
+ const adminSourcePath = isDev ? resolveAdminSource(projectRoot) : undefined;
290
312
  const useSource = adminSourcePath !== undefined;
291
313
 
292
314
  return {
@@ -308,6 +330,20 @@ export function createViteConfig(
308
330
  alias: [
309
331
  { find: "@emdash-cms/admin/styles.css", replacement: resolve(adminDistPath, "styles.css") },
310
332
  { find: "@emdash-cms/admin", replacement: useSource ? adminSourcePath : adminDistPath },
333
+ // `use-sync-external-store/shim` is a React <18 polyfill that ships
334
+ // only as CJS. It's pulled in transitively by `@tiptap/react`. With
335
+ // pnpm's virtual store the file lives under .pnpm/, where Vite's
336
+ // dep scanner can't reach it for pre-bundling — so the browser is
337
+ // served raw `module.exports` and hydration fails with
338
+ // `SyntaxError: ... does not provide an export named
339
+ // 'useSyncExternalStore'`. Redirect both shim entry points to the
340
+ // main `use-sync-external-store` package, which on React >=18
341
+ // (our peer-dep floor) delegates to React's built-in hook.
342
+ {
343
+ find: "use-sync-external-store/shim/index.js",
344
+ replacement: "use-sync-external-store",
345
+ },
346
+ { find: "use-sync-external-store/shim", replacement: "use-sync-external-store" },
311
347
  ],
312
348
  },
313
349
  // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly.
@@ -316,7 +352,7 @@ export function createViteConfig(
316
352
  // In dev mode with source alias, compile Lingui macros on the fly
317
353
  // and redirect locale .mjs imports to dist/.
318
354
  // In production, macros are pre-compiled by tsdown in the admin package.
319
- ...(useSource ? [linguiMacroPlugin(adminSourcePath!, adminDistPath)] : []),
355
+ ...(useSource ? [linguiMacroPlugin(adminSourcePath, adminDistPath)] : []),
320
356
  ] as NonNullable<AstroConfig["vite"]>["plugins"],
321
357
  // Handle native modules for SSR.
322
358
  // On Node: external keeps native addons out of the SSR bundle.
@@ -17,6 +17,8 @@ import { ulid } from "ulidx";
17
17
  // Import auth provider via virtual module (statically bundled)
18
18
  // This avoids dynamic import issues in Cloudflare Workers
19
19
  import { authenticate as virtualAuthenticate } from "virtual:emdash/auth";
20
+ // @ts-ignore - virtual module
21
+ import virtualConfig from "virtual:emdash/config";
20
22
 
21
23
  import { checkPublicCsrf } from "../../api/csrf.js";
22
24
  import { apiError } from "../../api/error.js";
@@ -30,7 +32,7 @@ import { resolveApiToken, resolveOAuthToken } from "../../api/handlers/api-token
30
32
  import { hasScope } from "../../auth/api-tokens.js";
31
33
  import { getAuthMode, type ExternalAuthMode } from "../../auth/mode.js";
32
34
  import type { ExternalAuthConfig } from "../../auth/types.js";
33
- import type { EmDashHandlers, EmDashManifest } from "../types.js";
35
+ import type { EmDashHandlers } from "../types.js";
34
36
  import { buildEmDashCsp } from "./csp.js";
35
37
 
36
38
  declare global {
@@ -40,7 +42,6 @@ declare global {
40
42
  /** Token scopes when authenticated via API token or OAuth token. Undefined for session auth. */
41
43
  tokenScopes?: string[];
42
44
  emdash?: EmDashHandlers;
43
- emdashManifest?: EmDashManifest;
44
45
  }
45
46
  interface SessionData {
46
47
  user: { id: string };
@@ -111,6 +112,7 @@ const PUBLIC_API_PREFIXES = [
111
112
  const PUBLIC_API_EXACT = new Set([
112
113
  "/_emdash/api/auth/passkey/options",
113
114
  "/_emdash/api/auth/passkey/verify",
115
+ "/_emdash/api/auth/mode",
114
116
  "/_emdash/api/oauth/token",
115
117
  "/_emdash/api/snapshot",
116
118
  // Public site search — read-only. The query layer hardcodes status='published'
@@ -119,6 +121,22 @@ const PUBLIC_API_EXACT = new Set([
119
121
  "/_emdash/api/search",
120
122
  ]);
121
123
 
124
+ // Build merged public routes at module load from auth provider descriptors.
125
+ // Routes ending with "/" are treated as prefixes; all others are exact matches.
126
+ const { exact: _providerExactRoutes, prefixes: _providerPrefixRoutes } = (() => {
127
+ const exact = new Set<string>();
128
+ const prefixes: string[] = [];
129
+ if (!virtualConfig?.authProviders) return { exact, prefixes };
130
+ for (const route of virtualConfig.authProviders.flatMap((p) => p.publicRoutes ?? [])) {
131
+ if (route.endsWith("/")) {
132
+ prefixes.push(route);
133
+ } else {
134
+ exact.add(route);
135
+ }
136
+ }
137
+ return { exact, prefixes };
138
+ })();
139
+
122
140
  /**
123
141
  * OAuth protocol endpoints that are CSRF-exempt by design.
124
142
  *
@@ -146,6 +164,8 @@ const CSRF_EXEMPT_PUBLIC_ROUTES = new Set([
146
164
  function isPublicEmDashRoute(pathname: string): boolean {
147
165
  if (PUBLIC_API_EXACT.has(pathname)) return true;
148
166
  if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) return true;
167
+ if (_providerExactRoutes.has(pathname)) return true;
168
+ if (_providerPrefixRoutes.some((p) => pathname.startsWith(p))) return true;
149
169
  if (import.meta.env.DEV && pathname === "/_emdash/api/typegen") return true;
150
170
  return false;
151
171
  }
@@ -702,10 +722,14 @@ const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
702
722
  ["/_emdash/api/schema", "WRITE", "schema:write"],
703
723
 
704
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.
705
729
  ["/_emdash/api/taxonomies", "GET", "content:read"],
706
- ["/_emdash/api/taxonomies", "WRITE", "content:write"],
730
+ ["/_emdash/api/taxonomies", "WRITE", "taxonomies:manage"],
707
731
  ["/_emdash/api/menus", "GET", "content:read"],
708
- ["/_emdash/api/menus", "WRITE", "content:write"],
732
+ ["/_emdash/api/menus", "WRITE", "menus:manage"],
709
733
  ["/_emdash/api/sections", "GET", "content:read"],
710
734
  ["/_emdash/api/sections", "WRITE", "content:write"],
711
735
  ["/_emdash/api/widget-areas", "GET", "content:read"],
@@ -717,12 +741,16 @@ const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
717
741
  ["/_emdash/api/search", "GET", "content:read"],
718
742
  ["/_emdash/api/search", "WRITE", "admin"],
719
743
 
720
- // Import, admin, settings, plugins — all require admin scope
744
+ // Import, admin, plugins — all require admin scope
721
745
  ["/_emdash/api/import", "*", "admin"],
722
746
  ["/_emdash/api/admin", "*", "admin"],
723
- ["/_emdash/api/settings", "*", "admin"],
724
747
  ["/_emdash/api/plugins", "*", "admin"],
725
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
+
726
754
  // MCP endpoint — scopes enforced per-tool inside mcp/server.ts
727
755
  ["/_emdash/api/mcp", "*", "content:read"],
728
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();