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
@@ -20,20 +20,151 @@ import type { FieldType } from "../schema/types.js";
20
20
  // =============================================================================
21
21
 
22
22
  /**
23
- * Plugin capabilities determine what APIs are available in context
23
+ * Plugin capabilities determine what APIs are available in context.
24
+ *
25
+ * Capabilities follow the formula `<resource>[.<sub-resource>]:<verb>[:<qualifier>]`
26
+ * — resource first, verb second, matching RBAC. The `unrestricted` qualifier
27
+ * (used by `network:request:unrestricted`) is intentionally verbose so that
28
+ * granting it stands out in manifest review.
29
+ *
30
+ * Hook-registration capabilities (`hooks.<family>:register`) are a distinct
31
+ * audit category from data-access capabilities — they gate which hooks a
32
+ * plugin is allowed to register, not which context APIs it gets.
33
+ *
34
+ * @see CAPABILITY_RENAMES for the legacy → current mapping, and
35
+ * `normalizeCapability()` for the runtime alias layer.
24
36
  */
25
37
  export type PluginCapability =
26
- | "network:fetch" // ctx.http is available (host-restricted via allowedHosts)
27
- | "network:fetch:any" // ctx.http is available (unrestricted outbound — use for user-configured URLs)
28
- | "read:content" // ctx.content.get/list available
29
- | "write:content" // ctx.content.create/update/delete available
30
- | "read:media" // ctx.media.get/list available
31
- | "write:media" // ctx.media.getUploadUrl/delete available
32
- | "read:users" // ctx.users is available
38
+ // ── Network ─────────────────────────────────────────────────
39
+ | "network:request" // ctx.http is available (host-restricted via allowedHosts)
40
+ | "network:request:unrestricted" // ctx.http is available (unrestricted outbound — use for user-configured URLs)
41
+ // ── Content ─────────────────────────────────────────────────
42
+ | "content:read" // ctx.content.get/list available
43
+ | "content:write" // ctx.content.create/update/delete available
44
+ // ── Media ───────────────────────────────────────────────────
45
+ | "media:read" // ctx.media.get/list available
46
+ | "media:write" // ctx.media.getUploadUrl/delete available
47
+ // ── Users ───────────────────────────────────────────────────
48
+ | "users:read" // ctx.users is available
49
+ // ── Email ───────────────────────────────────────────────────
33
50
  | "email:send" // ctx.email is available (when a provider is configured)
34
- | "email:provide" // can register email:deliver exclusive hook (transport provider)
35
- | "email:intercept" // can register email:beforeSend / email:afterSend hooks
36
- | "page:inject"; // can register page:fragments hook (inject scripts/styles into pages)
51
+ // ── Hook registration ───────────────────────────────────────
52
+ | "hooks.email-transport:register" // can register email:deliver exclusive hook (transport provider)
53
+ | "hooks.email-events:register" // can register email:beforeSend / email:afterSend hooks
54
+ | "hooks.page-fragments:register" // can register page:fragments hook (inject scripts/styles into pages)
55
+ // ── Deprecated (legacy aliases) ─────────────────────────────
56
+ // Kept in the union for one minor with @deprecated tags so existing
57
+ // plugins typecheck during migration. Normalized to current names at
58
+ // definition time via normalizeCapability(). Will be removed in the
59
+ // following minor.
60
+ /** @deprecated Use `network:request` instead. */
61
+ | "network:fetch"
62
+ /** @deprecated Use `network:request:unrestricted` instead. */
63
+ | "network:fetch:any"
64
+ /** @deprecated Use `content:read` instead. */
65
+ | "read:content"
66
+ /** @deprecated Use `content:write` instead. */
67
+ | "write:content"
68
+ /** @deprecated Use `media:read` instead. */
69
+ | "read:media"
70
+ /** @deprecated Use `media:write` instead. */
71
+ | "write:media"
72
+ /** @deprecated Use `users:read` instead. */
73
+ | "read:users"
74
+ /** @deprecated Use `hooks.email-transport:register` instead. */
75
+ | "email:provide"
76
+ /** @deprecated Use `hooks.email-events:register` instead. */
77
+ | "email:intercept"
78
+ /** @deprecated Use `hooks.page-fragments:register` instead. */
79
+ | "page:inject";
80
+
81
+ /**
82
+ * Deprecated capability names that map to current names.
83
+ *
84
+ * These are accepted at every external boundary (manifest parse, definePlugin,
85
+ * adaptSandboxEntry) and silently normalized to the new names before reaching
86
+ * the runtime. The runtime never sees deprecated names.
87
+ *
88
+ * Authors are warned at `bundle` / `validate`, and hard-failed at `publish`.
89
+ */
90
+ export type DeprecatedPluginCapability =
91
+ | "network:fetch"
92
+ | "network:fetch:any"
93
+ | "read:content"
94
+ | "write:content"
95
+ | "read:media"
96
+ | "write:media"
97
+ | "read:users"
98
+ | "email:provide"
99
+ | "email:intercept"
100
+ | "page:inject";
101
+
102
+ /**
103
+ * Current (non-deprecated) capability names.
104
+ */
105
+ export type CurrentPluginCapability = Exclude<PluginCapability, DeprecatedPluginCapability>;
106
+
107
+ /**
108
+ * Mapping from deprecated capability names to their current replacements.
109
+ *
110
+ * Used by `normalizeCapability()` and the marketplace `diffCapabilities`
111
+ * helper to compare manifests across the rename without flagging spurious
112
+ * "capability changed" prompts on upgrade.
113
+ */
114
+ export const CAPABILITY_RENAMES: Readonly<
115
+ Record<DeprecatedPluginCapability, CurrentPluginCapability>
116
+ > = Object.freeze({
117
+ "network:fetch": "network:request",
118
+ "network:fetch:any": "network:request:unrestricted",
119
+ "read:content": "content:read",
120
+ "write:content": "content:write",
121
+ "read:media": "media:read",
122
+ "write:media": "media:write",
123
+ "read:users": "users:read",
124
+ "email:provide": "hooks.email-transport:register",
125
+ "email:intercept": "hooks.email-events:register",
126
+ "page:inject": "hooks.page-fragments:register",
127
+ });
128
+
129
+ /**
130
+ * Type guard: is this capability one of the deprecated legacy names?
131
+ *
132
+ * Uses an own-property check so that prototype keys like "toString" or
133
+ * "constructor" don't accidentally pass.
134
+ */
135
+ export function isDeprecatedCapability(cap: string): cap is DeprecatedPluginCapability {
136
+ return Object.hasOwn(CAPABILITY_RENAMES, cap);
137
+ }
138
+
139
+ /**
140
+ * Normalize a capability string — deprecated names map to current names,
141
+ * current names pass through unchanged. Unknown strings are returned as-is
142
+ * so that downstream validators can produce a precise error.
143
+ */
144
+ export function normalizeCapability(cap: string): string {
145
+ if (isDeprecatedCapability(cap)) {
146
+ return CAPABILITY_RENAMES[cap];
147
+ }
148
+ return cap;
149
+ }
150
+
151
+ /**
152
+ * Normalize an array of capabilities. Deduplicates by normalized name so
153
+ * that a plugin declaring both `read:content` and `content:read` ends up
154
+ * with a single `content:read` entry.
155
+ */
156
+ export function normalizeCapabilities(caps: readonly string[]): string[] {
157
+ const seen = new Set<string>();
158
+ const out: string[] = [];
159
+ for (const cap of caps) {
160
+ const normalized = normalizeCapability(cap);
161
+ if (!seen.has(normalized)) {
162
+ seen.add(normalized);
163
+ out.push(normalized);
164
+ }
165
+ }
166
+ return out;
167
+ }
37
168
 
38
169
  // =============================================================================
39
170
  // Storage Types
@@ -1114,7 +1245,14 @@ export interface PluginDashboardWidget {
1114
1245
  /**
1115
1246
  * Settings field types (for admin UI generation)
1116
1247
  */
1117
- export type SettingFieldType = "string" | "number" | "boolean" | "select" | "secret";
1248
+ export type SettingFieldType =
1249
+ | "string"
1250
+ | "number"
1251
+ | "boolean"
1252
+ | "select"
1253
+ | "secret"
1254
+ | "url"
1255
+ | "email";
1118
1256
 
1119
1257
  export interface BaseSettingField {
1120
1258
  type: SettingFieldType;
@@ -1150,12 +1288,26 @@ export interface SecretSettingField extends BaseSettingField {
1150
1288
  type: "secret";
1151
1289
  }
1152
1290
 
1291
+ export interface UrlSettingField extends BaseSettingField {
1292
+ type: "url";
1293
+ default?: string;
1294
+ placeholder?: string;
1295
+ }
1296
+
1297
+ export interface EmailSettingField extends BaseSettingField {
1298
+ type: "email";
1299
+ default?: string;
1300
+ placeholder?: string;
1301
+ }
1302
+
1153
1303
  export type SettingField =
1154
1304
  | StringSettingField
1155
1305
  | NumberSettingField
1156
1306
  | BooleanSettingField
1157
1307
  | SelectSettingField
1158
- | SecretSettingField;
1308
+ | SecretSettingField
1309
+ | UrlSettingField
1310
+ | EmailSettingField;
1159
1311
 
1160
1312
  /**
1161
1313
  * Block Kit element for block editing fields.
@@ -1180,6 +1332,15 @@ export interface PortableTextBlockConfig {
1180
1332
  placeholder?: string;
1181
1333
  /** Block Kit form fields for the editing UI. If declared, replaces the simple URL input. */
1182
1334
  fields?: PortableTextBlockField[];
1335
+ /**
1336
+ * Optional. Display category in the slash menu. Defaults to "Embeds".
1337
+ *
1338
+ * Plugin authors should pick a meaningful category that reflects what the
1339
+ * block actually is — e.g. "Sections", "Marketing", "Media", "Embeds",
1340
+ * "Layout". Blocks with the same category are grouped together in the
1341
+ * editor's slash menu.
1342
+ */
1343
+ category?: string;
1183
1344
  }
1184
1345
 
1185
1346
  /**
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { generatePreviewToken } from "./tokens.js";
8
8
 
9
+ const REPEATED_SLASHES = /\/{2,}/g;
10
+
9
11
  /**
10
12
  * Options for generating a preview URL
11
13
  */
@@ -20,8 +22,18 @@ export interface GetPreviewUrlOptions {
20
22
  expiresIn?: string | number;
21
23
  /** Base URL of the site. If not provided, returns a relative URL. */
22
24
  baseUrl?: string;
23
- /** Custom path pattern. Use {collection} and {id} as placeholders. Default: "/{collection}/{id}" */
25
+ /**
26
+ * Custom path pattern. Supports `{collection}`, `{id}` and `{locale}`
27
+ * placeholders. Default: `"/{collection}/{id}"`.
28
+ */
24
29
  pathPattern?: string;
30
+ /**
31
+ * Locale segment substituted for the `{locale}` placeholder in `pathPattern`.
32
+ * Pass an empty string to omit the locale prefix (e.g. for the default locale
33
+ * when `prefixDefaultLocale` is `false`); adjacent slashes left by an empty
34
+ * value are collapsed and any trailing slash is trimmed.
35
+ */
36
+ locale?: string;
25
37
  }
26
38
 
27
39
  /**
@@ -65,6 +77,7 @@ export async function getPreviewUrl(options: GetPreviewUrlOptions): Promise<stri
65
77
  expiresIn = "1h",
66
78
  baseUrl,
67
79
  pathPattern = "/{collection}/{id}",
80
+ locale = "",
68
81
  } = options;
69
82
 
70
83
  // Generate the signed token
@@ -74,8 +87,15 @@ export async function getPreviewUrl(options: GetPreviewUrlOptions): Promise<stri
74
87
  secret,
75
88
  });
76
89
 
77
- // Build the path
78
- const path = pathPattern.replace("{collection}", collection).replace("{id}", id);
90
+ // Build the path. `{locale}` may resolve to an empty string (default locale
91
+ // without a prefix); collapse the resulting double slashes and trim a
92
+ // trailing slash so the URL stays clean.
93
+ let path = pathPattern
94
+ .replace("{collection}", collection)
95
+ .replace("{id}", id)
96
+ .replace("{locale}", locale);
97
+ path = path.replace(REPEATED_SLASHES, "/");
98
+ if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
79
99
 
80
100
  // Add token as query parameter
81
101
  const url = new URL(path, baseUrl || "http://placeholder");
package/src/query.ts CHANGED
@@ -12,7 +12,9 @@
12
12
  * and sets the context; query functions read it automatically.
13
13
  */
14
14
 
15
+ import { encodeCursor } from "./database/repositories/types.js";
15
16
  import { getFallbackChain, getI18nConfig, isI18nEnabled } from "./i18n/config.js";
17
+ import { CURSOR_RAW_VALUES } from "./loader.js";
16
18
  import { requestCached } from "./request-cache.js";
17
19
  import { getRequestContext } from "./request-context.js";
18
20
  import { isMissingTableError } from "./utils/db-errors.js";
@@ -279,9 +281,144 @@ export async function getEmDashCollection<T extends string, D = InferCollectionD
279
281
  // appears on the home page AND in the sidebar) — caching collapses
280
282
  // those duplicate queries, along with the bylines and taxonomy-term
281
283
  // hydration each call would otherwise re-do.
282
- return requestCached(collectionCacheKey(type, filter), () =>
283
- getEmDashCollectionUncached<T, D>(type, filter),
284
+ //
285
+ // Bucket small limits to a shared minimum so a page with several
286
+ // "recent N posts" widgets at slightly different limits (e.g. a
287
+ // post-detail page asking for 4 in the body and 5 in the sidebar)
288
+ // shares one fetch + hydration round-trip rather than running two.
289
+ // Cursor-paginated calls are exempt: their limit is part of the
290
+ // pagination contract.
291
+ const bucketed = bucketFilter(filter);
292
+ const cached = await requestCached(collectionCacheKey(type, bucketed.fetchFilter), () =>
293
+ getEmDashCollectionUncached<T, D>(type, bucketed.fetchFilter),
284
294
  );
295
+ return bucketed.requestedLimit === undefined
296
+ ? cached
297
+ : sliceCollectionResult(cached, bucketed.requestedLimit, filter?.orderBy);
298
+ }
299
+
300
+ /**
301
+ * Threshold for limit bucketing. Page templates routinely render small
302
+ * "recent posts" widgets at limits 3-8; rounding those up to a single
303
+ * shared bucket lets one fetch satisfy several widgets within a request.
304
+ * Above this, the requested limit is honoured exactly — bucketing limit:50
305
+ * to limit:64 would waste hydration work for callers fetching real pages.
306
+ */
307
+ const BUCKET_LIMIT_THRESHOLD = 10;
308
+
309
+ interface BucketedFilter {
310
+ /** Filter to pass to the loader (with limit possibly raised). */
311
+ fetchFilter: CollectionFilter | undefined;
312
+ /** Original limit; defined only when bucketing was applied. */
313
+ requestedLimit: number | undefined;
314
+ }
315
+
316
+ /** @internal exported for unit tests; not part of the public API. */
317
+ export function bucketFilter(filter: CollectionFilter | undefined): BucketedFilter {
318
+ const limit = filter?.limit;
319
+ if (
320
+ limit === undefined ||
321
+ limit >= BUCKET_LIMIT_THRESHOLD ||
322
+ limit <= 0 ||
323
+ filter?.cursor !== undefined
324
+ ) {
325
+ return { fetchFilter: filter, requestedLimit: undefined };
326
+ }
327
+ return {
328
+ fetchFilter: { ...filter, limit: BUCKET_LIMIT_THRESHOLD },
329
+ requestedLimit: limit,
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Slice a cached bucketed result down to the originally-requested limit
335
+ * and recompute `nextCursor` from the row that would have been the
336
+ * over-fetch detector for that limit. When truncation is needed, returns
337
+ * a shallow-copied result with a new `entries` array; otherwise returns
338
+ * the cached result unchanged (including error results and results
339
+ * already within the requested limit).
340
+ */
341
+ /** @internal exported for unit tests; not part of the public API. */
342
+ export function sliceCollectionResult<D>(
343
+ cached: CollectionResult<D>,
344
+ limit: number,
345
+ orderBy: OrderBySpec | undefined,
346
+ ): CollectionResult<D> {
347
+ if (cached.error) return cached;
348
+ if (cached.entries.length <= limit) return cached;
349
+ const sliced = cached.entries.slice(0, limit);
350
+ // Mirror the loader's encoding: cursor points at the last returned row,
351
+ // so "next page" picks up at the row immediately after it. See
352
+ // buildCursorCondition in loader.ts — it filters strictly past this row.
353
+ const lastEntry = sliced.at(-1);
354
+ const nextCursor = lastEntry ? encodeEntryCursor(lastEntry, orderBy) : undefined;
355
+ return { ...cached, entries: sliced, nextCursor };
356
+ }
357
+
358
+ /** Map of database column names to camelCase keys present on entry.data. */
359
+ const ENTRY_DATA_KEY_MAP: Record<string, string> = {
360
+ created_at: "createdAt",
361
+ updated_at: "updatedAt",
362
+ published_at: "publishedAt",
363
+ scheduled_at: "scheduledAt",
364
+ author_id: "authorId",
365
+ primary_byline_id: "primaryBylineId",
366
+ };
367
+
368
+ // Mirror loader.ts FIELD_NAME_PATTERN. Kept in sync intentionally — diverging
369
+ // would let the encoder accept a field name the loader's getPrimarySort then
370
+ // rejected, producing a cursor that paginates against a different column.
371
+ const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
372
+
373
+ /**
374
+ * Encode a `nextCursor` from a content entry, mirroring the loader's
375
+ * encoding scheme: `(orderValue, id)` where `orderValue` is the primary
376
+ * sort field's stringified value. For date columns, reads the raw DB
377
+ * string the loader stashed via CURSOR_RAW_VALUES — round-tripping the
378
+ * parsed Date through `toISOString()` would lose precision for stored
379
+ * values that aren't already ISO-with-milliseconds.
380
+ */
381
+ function encodeEntryCursor<D>(
382
+ entry: ContentEntry<D>,
383
+ orderBy: OrderBySpec | undefined,
384
+ ): string | undefined {
385
+ const data = entryData(entry);
386
+ const id = dataStr(data, "id");
387
+ if (!id) return undefined;
388
+
389
+ // Match loader.ts getPrimarySort: take the first valid field, default to created_at.
390
+ let dbField = "created_at";
391
+ if (orderBy) {
392
+ for (const field of Object.keys(orderBy)) {
393
+ if (FIELD_NAME_PATTERN.test(field)) {
394
+ dbField = field;
395
+ break;
396
+ }
397
+ }
398
+ }
399
+
400
+ // Date columns: prefer the raw stored string captured by the loader so
401
+ // the cursor matches what a direct loader fetch would emit, regardless
402
+ // of how the DB stored the timestamp.
403
+ const rawDateValuesRaw = Reflect.get(data, CURSOR_RAW_VALUES);
404
+ if (rawDateValuesRaw !== null && typeof rawDateValuesRaw === "object") {
405
+ const raw = Reflect.get(rawDateValuesRaw, dbField);
406
+ if (typeof raw === "string") return encodeCursor(raw, id);
407
+ }
408
+
409
+ const dataKey = ENTRY_DATA_KEY_MAP[dbField] ?? dbField;
410
+ const value = data[dataKey];
411
+ let orderValue: string;
412
+ if (value instanceof Date) {
413
+ orderValue = value.toISOString();
414
+ } else if (typeof value === "string" || typeof value === "number") {
415
+ orderValue = String(value);
416
+ } else {
417
+ // Match the loader's empty-string fallback for null/undefined order
418
+ // values so cursor decoding stays valid even at the boundary.
419
+ orderValue = "";
420
+ }
421
+ return encodeCursor(orderValue, id);
285
422
  }
286
423
 
287
424
  /**
@@ -478,7 +615,7 @@ export async function getEmDashEntry<T extends string, D = InferCollectionData<T
478
615
  // Edit mode (authenticated editors) has collection-wide draft access.
479
616
  if (isPreviewMode && !isEditMode) {
480
617
  const dbId = entryDatabaseId(baseEntry);
481
- if (preview!.id !== dbId && preview!.id !== id) {
618
+ if (preview.id !== dbId && preview.id !== id) {
482
619
  // Token doesn't match — serve only if publicly visible, without draft access
483
620
  if (isVisible(baseEntry)) {
484
621
  return successResult(wrapEntry(baseEntry), {
@@ -562,10 +699,16 @@ async function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]):
562
699
  try {
563
700
  const { getBylinesForEntries } = await import("./bylines/index.js");
564
701
 
565
- const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
566
- if (ids.length === 0) return;
702
+ const refs = entries
703
+ .map((e) => {
704
+ const data = entryData(e);
705
+ const id = dataStr(data, "id");
706
+ return id ? { id, authorId: dataStr(data, "authorId") || null } : null;
707
+ })
708
+ .filter((r): r is { id: string; authorId: string | null } => r !== null);
709
+ if (refs.length === 0) return;
567
710
 
568
- const bylinesMap = await getBylinesForEntries(type, ids);
711
+ const bylinesMap = await getBylinesForEntries(type, refs);
569
712
 
570
713
  for (const entry of entries) {
571
714
  const data = entryData(entry);
@@ -1,8 +1,13 @@
1
1
  /**
2
- * Redirect pattern cache.
2
+ * Redirect rule cache.
3
3
  *
4
- * Module-level cache for compiled redirect pattern rules. The middleware
5
- * populates this on first request; route handlers invalidate it on writes.
4
+ * Module-level cache for enabled redirect rules. The middleware populates this
5
+ * on first request; route handlers invalidate it on writes.
6
+ *
7
+ * Both exact-match and pattern rules are loaded from one query and cached
8
+ * together: exact rules indexed by source path in a Map, pattern rules
9
+ * pre-compiled into an array. A single warm request issues zero database
10
+ * queries; a cold isolate issues one.
6
11
  *
7
12
  * This module deliberately has NO Astro imports so it can be safely imported
8
13
  * from handlers, seed, CLI, and tests without dragging in `astro:middleware`.
@@ -17,36 +22,51 @@ export interface CachedRedirectRule {
17
22
  compiled: CompiledPattern;
18
23
  }
19
24
 
25
+ export interface CachedRedirects {
26
+ /** Exact-match rules indexed by source path (`source` -> `Redirect`). */
27
+ exact: Map<string, Redirect>;
28
+ /** Pattern rules with their compiled regexes, preserving insertion order. */
29
+ patterns: CachedRedirectRule[];
30
+ }
31
+
20
32
  /**
21
- * Cached pattern rules with compiled regexes.
22
- * null = not yet populated, array = cached.
33
+ * Cached enabled redirects.
34
+ * null = not yet populated, object = cached.
23
35
  */
24
- let cachedPatternRules: CachedRedirectRule[] | null = null;
36
+ let cachedRedirects: CachedRedirects | null = null;
25
37
 
26
38
  /**
27
- * Invalidate the cached redirect pattern rules.
39
+ * Invalidate the cached redirects (both exact and pattern).
28
40
  * Call when redirects are created, updated, or deleted.
29
41
  */
30
42
  export function invalidateRedirectCache(): void {
31
- cachedPatternRules = null;
43
+ cachedRedirects = null;
32
44
  }
33
45
 
34
46
  /**
35
- * Get the cached compiled pattern rules, or null if the cache is cold.
47
+ * Get the cached redirects, or null if the cache is cold.
36
48
  */
37
- export function getCachedPatternRules(): CachedRedirectRule[] | null {
38
- return cachedPatternRules;
49
+ export function getCachedRedirects(): CachedRedirects | null {
50
+ return cachedRedirects;
39
51
  }
40
52
 
41
53
  /**
42
- * Populate the pattern rules cache from a list of enabled pattern redirects.
54
+ * Populate the cache from a list of enabled redirects (both exact and
55
+ * pattern). The caller is responsible for passing only enabled rows — the
56
+ * cache stores them as-is.
43
57
  */
44
- export function setCachedPatternRules(redirects: Redirect[]): CachedRedirectRule[] {
45
- cachedPatternRules = redirects.map((r) => ({
46
- redirect: r,
47
- compiled: compilePattern(r.source),
48
- }));
49
- return cachedPatternRules;
58
+ export function setCachedRedirects(redirects: Redirect[]): CachedRedirects {
59
+ const exact = new Map<string, Redirect>();
60
+ const patterns: CachedRedirectRule[] = [];
61
+ for (const r of redirects) {
62
+ if (r.isPattern) {
63
+ patterns.push({ redirect: r, compiled: compilePattern(r.source) });
64
+ } else {
65
+ exact.set(r.source, r);
66
+ }
67
+ }
68
+ cachedRedirects = { exact, patterns };
69
+ return cachedRedirects;
50
70
  }
51
71
 
52
72
  /**
@@ -22,6 +22,7 @@ type CacheStore = WeakMap<EmDashRequestContext, Map<string, Promise<unknown>>>;
22
22
  const STORE_KEY = Symbol.for("emdash:request-cache");
23
23
  const g = globalThis as Record<symbol, unknown>;
24
24
  const store: CacheStore =
25
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts)
25
26
  (g[STORE_KEY] as CacheStore | undefined) ??
26
27
  (() => {
27
28
  const wm: CacheStore = new WeakMap();
@@ -47,6 +48,7 @@ export function requestCached<T>(key: string, fn: () => Promise<T>): Promise<T>
47
48
  }
48
49
 
49
50
  const existing = cache.get(key);
51
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; key namespacing guarantees the stored promise resolves to T
50
52
  if (existing) return existing as Promise<T>;
51
53
 
52
54
  const promise = Promise.resolve()
@@ -74,6 +76,7 @@ export function peekRequestCache<T>(key: string): Promise<T> | undefined {
74
76
  const ctx = getRequestContext();
75
77
  if (!ctx) return undefined;
76
78
  const cache = store.get(ctx);
79
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; caller is responsible for using a T-compatible key
77
80
  return cache?.get(key) as Promise<T> | undefined;
78
81
  }
79
82