emdash 0.0.0-b → 0.0.2

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 (661) hide show
  1. package/README.md +87 -43
  2. package/dist/adapters-BLMa4JGD.d.mts +106 -0
  3. package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
  4. package/dist/apply-Bjfq_b4-.mjs +1293 -0
  5. package/dist/apply-Bjfq_b4-.mjs.map +1 -0
  6. package/dist/astro/index.d.mts +51 -0
  7. package/dist/astro/index.d.mts.map +1 -0
  8. package/dist/astro/index.mjs +1336 -0
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/middleware/auth.d.mts +31 -0
  11. package/dist/astro/middleware/auth.d.mts.map +1 -0
  12. package/dist/astro/middleware/auth.mjs +654 -0
  13. package/dist/astro/middleware/auth.mjs.map +1 -0
  14. package/dist/astro/middleware/redirect.d.mts +22 -0
  15. package/dist/astro/middleware/redirect.d.mts.map +1 -0
  16. package/dist/astro/middleware/redirect.mjs +63 -0
  17. package/dist/astro/middleware/redirect.mjs.map +1 -0
  18. package/dist/astro/middleware/request-context.d.mts +18 -0
  19. package/dist/astro/middleware/request-context.d.mts.map +1 -0
  20. package/dist/astro/middleware/request-context.mjs +1310 -0
  21. package/dist/astro/middleware/request-context.mjs.map +1 -0
  22. package/dist/astro/middleware/setup.d.mts +20 -0
  23. package/dist/astro/middleware/setup.d.mts.map +1 -0
  24. package/dist/astro/middleware/setup.mjs +47 -0
  25. package/dist/astro/middleware/setup.mjs.map +1 -0
  26. package/dist/astro/middleware.d.mts +13 -0
  27. package/dist/astro/middleware.d.mts.map +1 -0
  28. package/dist/astro/middleware.mjs +1613 -0
  29. package/dist/astro/middleware.mjs.map +1 -0
  30. package/dist/astro/types.d.mts +250 -0
  31. package/dist/astro/types.d.mts.map +1 -0
  32. package/dist/astro/types.mjs +1 -0
  33. package/dist/base64-MBPo9ozB.mjs +59 -0
  34. package/dist/base64-MBPo9ozB.mjs.map +1 -0
  35. package/dist/byline-CL847F26.mjs +213 -0
  36. package/dist/byline-CL847F26.mjs.map +1 -0
  37. package/dist/bylines-C2a-2TGt.mjs +136 -0
  38. package/dist/bylines-C2a-2TGt.mjs.map +1 -0
  39. package/dist/chunk-ClPoSABd.mjs +21 -0
  40. package/dist/cli/index.d.mts +1 -0
  41. package/dist/cli/index.mjs +3909 -0
  42. package/dist/cli/index.mjs.map +1 -0
  43. package/dist/client/cf-access.d.mts +60 -0
  44. package/dist/client/cf-access.d.mts.map +1 -0
  45. package/dist/client/cf-access.mjs +179 -0
  46. package/dist/client/cf-access.mjs.map +1 -0
  47. package/dist/client/index.d.mts +398 -0
  48. package/dist/client/index.d.mts.map +1 -0
  49. package/dist/client/index.mjs +346 -0
  50. package/dist/client/index.mjs.map +1 -0
  51. package/dist/config-CKE8p9xM.mjs +55 -0
  52. package/dist/config-CKE8p9xM.mjs.map +1 -0
  53. package/dist/connection-B4zVnQIa.mjs +40 -0
  54. package/dist/connection-B4zVnQIa.mjs.map +1 -0
  55. package/dist/content-D6C2WsZC.mjs +824 -0
  56. package/dist/content-D6C2WsZC.mjs.map +1 -0
  57. package/dist/db/index.d.mts +4 -0
  58. package/dist/db/index.mjs +62 -0
  59. package/dist/db/index.mjs.map +1 -0
  60. package/dist/db/libsql.d.mts +11 -0
  61. package/dist/db/libsql.d.mts.map +1 -0
  62. package/dist/db/libsql.mjs +17 -0
  63. package/dist/db/libsql.mjs.map +1 -0
  64. package/dist/db/postgres.d.mts +11 -0
  65. package/dist/db/postgres.d.mts.map +1 -0
  66. package/dist/db/postgres.mjs +30 -0
  67. package/dist/db/postgres.mjs.map +1 -0
  68. package/dist/db/sqlite.d.mts +11 -0
  69. package/dist/db/sqlite.d.mts.map +1 -0
  70. package/dist/db/sqlite.mjs +16 -0
  71. package/dist/db/sqlite.mjs.map +1 -0
  72. package/dist/default-Cyi4aAxu.mjs +81 -0
  73. package/dist/default-Cyi4aAxu.mjs.map +1 -0
  74. package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
  75. package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
  76. package/dist/error-Cxz0tQeO.mjs +27 -0
  77. package/dist/error-Cxz0tQeO.mjs.map +1 -0
  78. package/dist/index-C1xF3OGh.d.mts +4527 -0
  79. package/dist/index-C1xF3OGh.d.mts.map +1 -0
  80. package/dist/index.d.mts +16 -0
  81. package/dist/index.mjs +30 -0
  82. package/dist/load-yOOlckBj.mjs +28 -0
  83. package/dist/load-yOOlckBj.mjs.map +1 -0
  84. package/dist/loader-fz8Q_3EO.mjs +447 -0
  85. package/dist/loader-fz8Q_3EO.mjs.map +1 -0
  86. package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
  87. package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
  88. package/dist/media/index.d.mts +26 -0
  89. package/dist/media/index.d.mts.map +1 -0
  90. package/dist/media/index.mjs +55 -0
  91. package/dist/media/index.mjs.map +1 -0
  92. package/dist/media/local-runtime.d.mts +39 -0
  93. package/dist/media/local-runtime.d.mts.map +1 -0
  94. package/dist/media/local-runtime.mjs +133 -0
  95. package/dist/media/local-runtime.mjs.map +1 -0
  96. package/dist/media-DqHVh136.mjs +200 -0
  97. package/dist/media-DqHVh136.mjs.map +1 -0
  98. package/dist/mode-C2EzN1uE.mjs +23 -0
  99. package/dist/mode-C2EzN1uE.mjs.map +1 -0
  100. package/dist/page/index.d.mts +140 -0
  101. package/dist/page/index.d.mts.map +1 -0
  102. package/dist/page/index.mjs +416 -0
  103. package/dist/page/index.mjs.map +1 -0
  104. package/dist/placeholder-CmGAmqeO.d.mts +276 -0
  105. package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
  106. package/dist/placeholder-SmpOx-_v.mjs +243 -0
  107. package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
  108. package/dist/plugin-utils.d.mts +58 -0
  109. package/dist/plugin-utils.d.mts.map +1 -0
  110. package/dist/plugin-utils.mjs +78 -0
  111. package/dist/plugin-utils.mjs.map +1 -0
  112. package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
  113. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
  114. package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
  115. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
  116. package/dist/query-CS_iSj34.mjs +460 -0
  117. package/dist/query-CS_iSj34.mjs.map +1 -0
  118. package/dist/redirect-DIfIni3r.mjs +329 -0
  119. package/dist/redirect-DIfIni3r.mjs.map +1 -0
  120. package/dist/registry-D_w5HW4G.mjs +863 -0
  121. package/dist/registry-D_w5HW4G.mjs.map +1 -0
  122. package/dist/request-context.d.mts +49 -0
  123. package/dist/request-context.d.mts.map +1 -0
  124. package/dist/request-context.mjs +43 -0
  125. package/dist/request-context.mjs.map +1 -0
  126. package/dist/runner-C0hCbYnD.mjs +1412 -0
  127. package/dist/runner-C0hCbYnD.mjs.map +1 -0
  128. package/dist/runner-EAtf0ZIe.d.mts +27 -0
  129. package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
  130. package/dist/runtime.d.mts +26 -0
  131. package/dist/runtime.d.mts.map +1 -0
  132. package/dist/runtime.mjs +42 -0
  133. package/dist/runtime.mjs.map +1 -0
  134. package/dist/search-DG603UrT.mjs +9211 -0
  135. package/dist/search-DG603UrT.mjs.map +1 -0
  136. package/dist/seed/index.d.mts +3 -0
  137. package/dist/seed/index.mjs +15 -0
  138. package/dist/seo/index.d.mts +70 -0
  139. package/dist/seo/index.d.mts.map +1 -0
  140. package/dist/seo/index.mjs +70 -0
  141. package/dist/seo/index.mjs.map +1 -0
  142. package/dist/storage/local.d.mts +39 -0
  143. package/dist/storage/local.d.mts.map +1 -0
  144. package/dist/storage/local.mjs +166 -0
  145. package/dist/storage/local.mjs.map +1 -0
  146. package/dist/storage/s3.d.mts +32 -0
  147. package/dist/storage/s3.d.mts.map +1 -0
  148. package/dist/storage/s3.mjs +175 -0
  149. package/dist/storage/s3.mjs.map +1 -0
  150. package/dist/tokens-DpgrkrXK.mjs +171 -0
  151. package/dist/tokens-DpgrkrXK.mjs.map +1 -0
  152. package/dist/transport-BFGblqwG.d.mts +42 -0
  153. package/dist/transport-BFGblqwG.d.mts.map +1 -0
  154. package/dist/transport-yxiQsi8I.mjs +418 -0
  155. package/dist/transport-yxiQsi8I.mjs.map +1 -0
  156. package/dist/types-BRuPJGdV.d.mts +102 -0
  157. package/dist/types-BRuPJGdV.d.mts.map +1 -0
  158. package/dist/types-C4-fAxN3.d.mts +182 -0
  159. package/dist/types-C4-fAxN3.d.mts.map +1 -0
  160. package/dist/types-CMMN0pNg.mjs +31 -0
  161. package/dist/types-CMMN0pNg.mjs.map +1 -0
  162. package/dist/types-CUBbjgmP.mjs +16 -0
  163. package/dist/types-CUBbjgmP.mjs.map +1 -0
  164. package/dist/types-DRjfYOEv.d.mts +426 -0
  165. package/dist/types-DRjfYOEv.d.mts.map +1 -0
  166. package/dist/types-DY5zk5HN.mjs +73 -0
  167. package/dist/types-DY5zk5HN.mjs.map +1 -0
  168. package/dist/types-DaNLHo_T.d.mts +184 -0
  169. package/dist/types-DaNLHo_T.d.mts.map +1 -0
  170. package/dist/types-DvhsUmSJ.d.mts +1111 -0
  171. package/dist/types-DvhsUmSJ.d.mts.map +1 -0
  172. package/dist/validate-CpBtVMsD.d.mts +378 -0
  173. package/dist/validate-CpBtVMsD.d.mts.map +1 -0
  174. package/dist/validate-CqRJb_xU.mjs +97 -0
  175. package/dist/validate-CqRJb_xU.mjs.map +1 -0
  176. package/dist/validate-O7PWmlnq.mjs +328 -0
  177. package/dist/validate-O7PWmlnq.mjs.map +1 -0
  178. package/locals.d.ts +46 -0
  179. package/package.json +233 -19
  180. package/src/api/authorize.ts +63 -0
  181. package/src/api/csrf.ts +48 -0
  182. package/src/api/error.ts +99 -0
  183. package/src/api/errors.ts +445 -0
  184. package/src/api/escape.ts +9 -0
  185. package/src/api/handlers/api-tokens.ts +240 -0
  186. package/src/api/handlers/comments.ts +314 -0
  187. package/src/api/handlers/content.ts +1315 -0
  188. package/src/api/handlers/dashboard.ts +205 -0
  189. package/src/api/handlers/device-flow.ts +684 -0
  190. package/src/api/handlers/index.ts +163 -0
  191. package/src/api/handlers/manifest.ts +158 -0
  192. package/src/api/handlers/marketplace.ts +930 -0
  193. package/src/api/handlers/media.ts +207 -0
  194. package/src/api/handlers/menus.ts +493 -0
  195. package/src/api/handlers/oauth-authorization.ts +429 -0
  196. package/src/api/handlers/oauth-clients.ts +349 -0
  197. package/src/api/handlers/oauth-user-lookup.ts +39 -0
  198. package/src/api/handlers/plugins.ts +254 -0
  199. package/src/api/handlers/redirects.ts +360 -0
  200. package/src/api/handlers/revision.ts +145 -0
  201. package/src/api/handlers/schema.ts +534 -0
  202. package/src/api/handlers/sections.ts +289 -0
  203. package/src/api/handlers/seo.ts +115 -0
  204. package/src/api/handlers/settings.ts +49 -0
  205. package/src/api/handlers/snapshot.ts +350 -0
  206. package/src/api/handlers/taxonomies.ts +523 -0
  207. package/src/api/index.ts +6 -0
  208. package/src/api/openapi/document.ts +2368 -0
  209. package/src/api/openapi/index.ts +1 -0
  210. package/src/api/parse.ts +139 -0
  211. package/src/api/redirect.ts +14 -0
  212. package/src/api/rev.ts +67 -0
  213. package/src/api/schemas/auth.ts +112 -0
  214. package/src/api/schemas/bylines.ts +85 -0
  215. package/src/api/schemas/comments.ts +117 -0
  216. package/src/api/schemas/common.ts +89 -0
  217. package/src/api/schemas/content.ts +191 -0
  218. package/src/api/schemas/import.ts +52 -0
  219. package/src/api/schemas/index.ts +17 -0
  220. package/src/api/schemas/media.ts +116 -0
  221. package/src/api/schemas/menus.ts +111 -0
  222. package/src/api/schemas/redirects.ts +155 -0
  223. package/src/api/schemas/schema.ts +203 -0
  224. package/src/api/schemas/search.ts +63 -0
  225. package/src/api/schemas/sections.ts +67 -0
  226. package/src/api/schemas/settings.ts +63 -0
  227. package/src/api/schemas/setup.ts +37 -0
  228. package/src/api/schemas/taxonomies.ts +113 -0
  229. package/src/api/schemas/users.ts +96 -0
  230. package/src/api/schemas/widgets.ts +80 -0
  231. package/src/api/site-url.ts +25 -0
  232. package/src/api/types.ts +82 -0
  233. package/src/astro/index.ts +27 -0
  234. package/src/astro/integration/index.ts +303 -0
  235. package/src/astro/integration/routes.ts +834 -0
  236. package/src/astro/integration/runtime.ts +338 -0
  237. package/src/astro/integration/virtual-modules.ts +469 -0
  238. package/src/astro/integration/vite-config.ts +335 -0
  239. package/src/astro/middleware/auth.ts +743 -0
  240. package/src/astro/middleware/redirect.ts +89 -0
  241. package/src/astro/middleware/request-context.ts +129 -0
  242. package/src/astro/middleware/setup.ts +89 -0
  243. package/src/astro/middleware.ts +398 -0
  244. package/src/astro/routes/PluginRegistry.tsx +15 -0
  245. package/src/astro/routes/admin.astro +81 -0
  246. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  247. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  248. package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
  249. package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
  250. package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
  251. package/src/astro/routes/api/admin/bylines/index.ts +72 -0
  252. package/src/astro/routes/api/admin/comments/[id]/status.ts +116 -0
  253. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  254. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  255. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  256. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  257. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
  258. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  259. package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
  260. package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
  261. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
  262. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
  263. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  264. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
  265. package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
  266. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  267. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
  268. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  269. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
  270. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  271. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  272. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  273. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
  274. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  275. package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
  276. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  277. package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
  278. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  279. package/src/astro/routes/api/admin/users/index.ts +66 -0
  280. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  281. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  282. package/src/astro/routes/api/auth/invite/complete.ts +84 -0
  283. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  284. package/src/astro/routes/api/auth/logout.ts +40 -0
  285. package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
  286. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  287. package/src/astro/routes/api/auth/me.ts +60 -0
  288. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
  289. package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
  290. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  291. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  292. package/src/astro/routes/api/auth/passkey/options.ts +82 -0
  293. package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
  294. package/src/astro/routes/api/auth/passkey/register/verify.ts +115 -0
  295. package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
  296. package/src/astro/routes/api/auth/signup/complete.ts +85 -0
  297. package/src/astro/routes/api/auth/signup/request.ts +77 -0
  298. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  299. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
  300. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  301. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
  302. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
  303. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
  304. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  305. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
  306. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
  307. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  308. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +101 -0
  309. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
  310. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
  311. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
  312. package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
  313. package/src/astro/routes/api/content/[collection]/index.ts +59 -0
  314. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  315. package/src/astro/routes/api/dashboard.ts +32 -0
  316. package/src/astro/routes/api/dev/emails.ts +36 -0
  317. package/src/astro/routes/api/import/probe.ts +47 -0
  318. package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
  319. package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
  320. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  321. package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
  322. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
  323. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  324. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  325. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
  326. package/src/astro/routes/api/manifest.ts +62 -0
  327. package/src/astro/routes/api/mcp.ts +124 -0
  328. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  329. package/src/astro/routes/api/media/[id].ts +145 -0
  330. package/src/astro/routes/api/media/file/[key].ts +79 -0
  331. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
  332. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  333. package/src/astro/routes/api/media/providers/index.ts +30 -0
  334. package/src/astro/routes/api/media/upload-url.ts +137 -0
  335. package/src/astro/routes/api/media.ts +190 -0
  336. package/src/astro/routes/api/menus/[name]/items.ts +87 -0
  337. package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
  338. package/src/astro/routes/api/menus/[name].ts +65 -0
  339. package/src/astro/routes/api/menus/index.ts +47 -0
  340. package/src/astro/routes/api/oauth/authorize.ts +412 -0
  341. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  342. package/src/astro/routes/api/oauth/device/code.ts +51 -0
  343. package/src/astro/routes/api/oauth/device/token.ts +69 -0
  344. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  345. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  346. package/src/astro/routes/api/oauth/token.ts +184 -0
  347. package/src/astro/routes/api/openapi.json.ts +32 -0
  348. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
  349. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  350. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  351. package/src/astro/routes/api/redirects/[id].ts +84 -0
  352. package/src/astro/routes/api/redirects/index.ts +52 -0
  353. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  354. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +58 -0
  355. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
  356. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
  357. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
  358. package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
  359. package/src/astro/routes/api/schema/collections/index.ts +47 -0
  360. package/src/astro/routes/api/schema/index.ts +109 -0
  361. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  362. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  363. package/src/astro/routes/api/search/enable.ts +64 -0
  364. package/src/astro/routes/api/search/index.ts +55 -0
  365. package/src/astro/routes/api/search/rebuild.ts +72 -0
  366. package/src/astro/routes/api/search/stats.ts +35 -0
  367. package/src/astro/routes/api/search/suggest.ts +53 -0
  368. package/src/astro/routes/api/sections/[slug].ts +84 -0
  369. package/src/astro/routes/api/sections/index.ts +52 -0
  370. package/src/astro/routes/api/settings/email.ts +150 -0
  371. package/src/astro/routes/api/settings.ts +67 -0
  372. package/src/astro/routes/api/setup/admin-verify.ts +100 -0
  373. package/src/astro/routes/api/setup/admin.ts +94 -0
  374. package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
  375. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  376. package/src/astro/routes/api/setup/index.ts +126 -0
  377. package/src/astro/routes/api/setup/status.ts +122 -0
  378. package/src/astro/routes/api/snapshot.ts +75 -0
  379. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
  380. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
  381. package/src/astro/routes/api/taxonomies/index.ts +59 -0
  382. package/src/astro/routes/api/themes/preview.ts +77 -0
  383. package/src/astro/routes/api/typegen.ts +114 -0
  384. package/src/astro/routes/api/well-known/auth.ts +68 -0
  385. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
  386. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
  387. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +68 -0
  388. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
  389. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
  390. package/src/astro/routes/api/widget-areas/[name].ts +87 -0
  391. package/src/astro/routes/api/widget-areas/index.ts +99 -0
  392. package/src/astro/routes/api/widget-components.ts +22 -0
  393. package/src/astro/routes/robots.txt.ts +77 -0
  394. package/src/astro/routes/sitemap.xml.ts +97 -0
  395. package/src/astro/storage/adapters.ts +74 -0
  396. package/src/astro/storage/index.ts +19 -0
  397. package/src/astro/storage/types.ts +60 -0
  398. package/src/astro/types.ts +346 -0
  399. package/src/auth/api-tokens.ts +25 -0
  400. package/src/auth/challenge-store.ts +80 -0
  401. package/src/auth/mode.ts +96 -0
  402. package/src/auth/oauth-state-store.ts +96 -0
  403. package/src/auth/passkey-config.ts +27 -0
  404. package/src/auth/rate-limit.ts +158 -0
  405. package/src/auth/scopes.ts +33 -0
  406. package/src/auth/types.ts +104 -0
  407. package/src/aws-sdk.d.ts +100 -0
  408. package/src/bylines/index.ts +237 -0
  409. package/src/cleanup.ts +153 -0
  410. package/src/cli/client-factory.ts +100 -0
  411. package/src/cli/commands/auth.ts +46 -0
  412. package/src/cli/commands/bundle-utils.ts +247 -0
  413. package/src/cli/commands/bundle.ts +609 -0
  414. package/src/cli/commands/content.ts +442 -0
  415. package/src/cli/commands/dev.ts +191 -0
  416. package/src/cli/commands/doctor.ts +211 -0
  417. package/src/cli/commands/export-seed.ts +630 -0
  418. package/src/cli/commands/import/wordpress.ts +1056 -0
  419. package/src/cli/commands/init.ts +192 -0
  420. package/src/cli/commands/login.ts +547 -0
  421. package/src/cli/commands/media.ts +165 -0
  422. package/src/cli/commands/menu.ts +67 -0
  423. package/src/cli/commands/plugin-init.ts +291 -0
  424. package/src/cli/commands/plugin-validate.ts +31 -0
  425. package/src/cli/commands/plugin.ts +33 -0
  426. package/src/cli/commands/publish.ts +697 -0
  427. package/src/cli/commands/schema.ts +233 -0
  428. package/src/cli/commands/search-cmd.ts +54 -0
  429. package/src/cli/commands/seed.ts +286 -0
  430. package/src/cli/commands/taxonomy.ts +128 -0
  431. package/src/cli/commands/types.ts +68 -0
  432. package/src/cli/credentials.ts +236 -0
  433. package/src/cli/index.ts +70 -0
  434. package/src/cli/output.ts +75 -0
  435. package/src/cli/wxr/parser.ts +969 -0
  436. package/src/client/cf-access.ts +193 -0
  437. package/src/client/index.ts +854 -0
  438. package/src/client/portable-text.ts +413 -0
  439. package/src/client/transport.ts +200 -0
  440. package/src/comments/moderator.ts +46 -0
  441. package/src/comments/notifications.ts +144 -0
  442. package/src/comments/query.ts +105 -0
  443. package/src/comments/service.ts +213 -0
  444. package/src/components/Break.astro +45 -0
  445. package/src/components/Button.astro +71 -0
  446. package/src/components/Buttons.astro +49 -0
  447. package/src/components/Code.astro +59 -0
  448. package/src/components/Columns.astro +59 -0
  449. package/src/components/CommentForm.astro +315 -0
  450. package/src/components/Comments.astro +232 -0
  451. package/src/components/Cover.astro +128 -0
  452. package/src/components/EmDashBodyEnd.astro +32 -0
  453. package/src/components/EmDashBodyStart.astro +32 -0
  454. package/src/components/EmDashHead.astro +53 -0
  455. package/src/components/EmDashImage.astro +178 -0
  456. package/src/components/EmDashMedia.astro +167 -0
  457. package/src/components/Embed.astro +128 -0
  458. package/src/components/File.astro +122 -0
  459. package/src/components/Gallery.astro +93 -0
  460. package/src/components/HtmlBlock.astro +33 -0
  461. package/src/components/Image.astro +178 -0
  462. package/src/components/InlineEditor.astro +27 -0
  463. package/src/components/InlinePortableTextEditor.tsx +1905 -0
  464. package/src/components/LiveSearch.astro +614 -0
  465. package/src/components/PortableText.astro +51 -0
  466. package/src/components/Pullquote.astro +51 -0
  467. package/src/components/Table.astro +108 -0
  468. package/src/components/WidgetArea.astro +22 -0
  469. package/src/components/WidgetRenderer.astro +72 -0
  470. package/src/components/index.ts +116 -0
  471. package/src/components/marks/Link.astro +31 -0
  472. package/src/components/marks/StrikeThrough.astro +7 -0
  473. package/src/components/marks/Subscript.astro +7 -0
  474. package/src/components/marks/Superscript.astro +7 -0
  475. package/src/components/marks/Underline.astro +7 -0
  476. package/src/components/widgets/Archives.astro +65 -0
  477. package/src/components/widgets/Categories.astro +35 -0
  478. package/src/components/widgets/RecentPosts.astro +51 -0
  479. package/src/components/widgets/Search.astro +18 -0
  480. package/src/components/widgets/Tags.astro +38 -0
  481. package/src/content/converters/index.ts +9 -0
  482. package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
  483. package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
  484. package/src/content/converters/types.ts +120 -0
  485. package/src/content/index.ts +5 -0
  486. package/src/database/connection.ts +67 -0
  487. package/src/database/dialect-helpers.ts +138 -0
  488. package/src/database/index.ts +5 -0
  489. package/src/database/migrations/001_initial.ts +170 -0
  490. package/src/database/migrations/002_media_status.ts +26 -0
  491. package/src/database/migrations/003_schema_registry.ts +79 -0
  492. package/src/database/migrations/004_plugins.ts +62 -0
  493. package/src/database/migrations/005_menus.ts +67 -0
  494. package/src/database/migrations/006_taxonomy_defs.ts +51 -0
  495. package/src/database/migrations/007_widgets.ts +42 -0
  496. package/src/database/migrations/008_auth.ts +194 -0
  497. package/src/database/migrations/009_user_disabled.ts +27 -0
  498. package/src/database/migrations/011_sections.ts +65 -0
  499. package/src/database/migrations/012_search.ts +25 -0
  500. package/src/database/migrations/013_scheduled_publishing.ts +51 -0
  501. package/src/database/migrations/014_draft_revisions.ts +72 -0
  502. package/src/database/migrations/015_indexes.ts +82 -0
  503. package/src/database/migrations/016_api_tokens.ts +89 -0
  504. package/src/database/migrations/017_authorization_codes.ts +45 -0
  505. package/src/database/migrations/018_seo.ts +56 -0
  506. package/src/database/migrations/019_i18n.ts +618 -0
  507. package/src/database/migrations/020_collection_url_pattern.ts +23 -0
  508. package/src/database/migrations/021_remove_section_categories.ts +43 -0
  509. package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
  510. package/src/database/migrations/023_plugin_metadata.ts +33 -0
  511. package/src/database/migrations/024_media_placeholders.ts +32 -0
  512. package/src/database/migrations/025_oauth_clients.ts +28 -0
  513. package/src/database/migrations/026_cron_tasks.ts +49 -0
  514. package/src/database/migrations/027_comments.ts +87 -0
  515. package/src/database/migrations/028_drop_author_url.ts +9 -0
  516. package/src/database/migrations/029_redirects.ts +67 -0
  517. package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
  518. package/src/database/migrations/031_bylines.ts +90 -0
  519. package/src/database/migrations/032_rate_limits.ts +39 -0
  520. package/src/database/migrations/runner.ts +170 -0
  521. package/src/database/repositories/audit.ts +294 -0
  522. package/src/database/repositories/byline.ts +387 -0
  523. package/src/database/repositories/comment.ts +458 -0
  524. package/src/database/repositories/content.ts +1144 -0
  525. package/src/database/repositories/index.ts +30 -0
  526. package/src/database/repositories/media.ts +347 -0
  527. package/src/database/repositories/options.ts +150 -0
  528. package/src/database/repositories/plugin-storage.ts +373 -0
  529. package/src/database/repositories/redirect.ts +480 -0
  530. package/src/database/repositories/revision.ts +200 -0
  531. package/src/database/repositories/seo.ts +176 -0
  532. package/src/database/repositories/taxonomy.ts +294 -0
  533. package/src/database/repositories/types.ts +132 -0
  534. package/src/database/repositories/user.ts +258 -0
  535. package/src/database/transaction.ts +54 -0
  536. package/src/database/types.ts +501 -0
  537. package/src/database/validate.ts +138 -0
  538. package/src/db/adapters.ts +125 -0
  539. package/src/db/index.ts +37 -0
  540. package/src/db/libsql.ts +23 -0
  541. package/src/db/postgres.ts +30 -0
  542. package/src/db/sqlite.ts +27 -0
  543. package/src/emdash-runtime.ts +2096 -0
  544. package/src/fields/boolean.ts +34 -0
  545. package/src/fields/datetime.ts +44 -0
  546. package/src/fields/file.ts +41 -0
  547. package/src/fields/image.ts +34 -0
  548. package/src/fields/index.ts +42 -0
  549. package/src/fields/integer.ts +50 -0
  550. package/src/fields/json.ts +37 -0
  551. package/src/fields/multiselect.ts +48 -0
  552. package/src/fields/number.ts +52 -0
  553. package/src/fields/portable-text.ts +33 -0
  554. package/src/fields/reference.ts +29 -0
  555. package/src/fields/richtext.ts +31 -0
  556. package/src/fields/select.ts +46 -0
  557. package/src/fields/slug.ts +38 -0
  558. package/src/fields/text.ts +55 -0
  559. package/src/fields/textarea.ts +52 -0
  560. package/src/fields/types.ts +64 -0
  561. package/src/i18n/config.ts +68 -0
  562. package/src/import/index.ts +90 -0
  563. package/src/import/menus.ts +436 -0
  564. package/src/import/registry.ts +111 -0
  565. package/src/import/sections.ts +103 -0
  566. package/src/import/settings.ts +281 -0
  567. package/src/import/sources/wordpress-plugin.ts +641 -0
  568. package/src/import/sources/wordpress-rest.ts +191 -0
  569. package/src/import/sources/wxr.ts +330 -0
  570. package/src/import/ssrf.ts +260 -0
  571. package/src/import/types.ts +418 -0
  572. package/src/import/utils.ts +412 -0
  573. package/src/index.ts +481 -0
  574. package/src/loader.ts +770 -0
  575. package/src/mcp/server.ts +1463 -0
  576. package/src/media/index.ts +32 -0
  577. package/src/media/local-runtime.ts +213 -0
  578. package/src/media/local.ts +46 -0
  579. package/src/media/normalize.ts +190 -0
  580. package/src/media/placeholder.ts +150 -0
  581. package/src/media/provider-loader.ts +78 -0
  582. package/src/media/types.ts +279 -0
  583. package/src/menus/index.ts +324 -0
  584. package/src/menus/types.ts +112 -0
  585. package/src/page/context.ts +93 -0
  586. package/src/page/fragments.ts +89 -0
  587. package/src/page/index.ts +58 -0
  588. package/src/page/jsonld.ts +94 -0
  589. package/src/page/metadata.ts +185 -0
  590. package/src/page/seo-contributions.ts +136 -0
  591. package/src/plugin-utils.ts +80 -0
  592. package/src/plugins/adapt-sandbox-entry.ts +207 -0
  593. package/src/plugins/context.ts +833 -0
  594. package/src/plugins/cron.ts +361 -0
  595. package/src/plugins/define-plugin.ts +259 -0
  596. package/src/plugins/email-console.ts +73 -0
  597. package/src/plugins/email.ts +209 -0
  598. package/src/plugins/hooks.ts +1273 -0
  599. package/src/plugins/index.ts +193 -0
  600. package/src/plugins/manager.ts +595 -0
  601. package/src/plugins/manifest-schema.ts +230 -0
  602. package/src/plugins/marketplace.ts +460 -0
  603. package/src/plugins/request-meta.ts +139 -0
  604. package/src/plugins/routes.ts +302 -0
  605. package/src/plugins/sandbox/index.ts +18 -0
  606. package/src/plugins/sandbox/noop.ts +76 -0
  607. package/src/plugins/sandbox/types.ts +173 -0
  608. package/src/plugins/scheduler/node.ts +122 -0
  609. package/src/plugins/scheduler/piggyback.ts +71 -0
  610. package/src/plugins/scheduler/types.ts +27 -0
  611. package/src/plugins/state.ts +208 -0
  612. package/src/plugins/storage-indexes.ts +326 -0
  613. package/src/plugins/storage-query.ts +240 -0
  614. package/src/plugins/types.ts +1284 -0
  615. package/src/preview/helpers.ts +27 -0
  616. package/src/preview/index.ts +40 -0
  617. package/src/preview/tokens.ts +279 -0
  618. package/src/preview/urls.ts +118 -0
  619. package/src/query.ts +674 -0
  620. package/src/redirects/patterns.ts +224 -0
  621. package/src/request-context.ts +67 -0
  622. package/src/runtime.ts +21 -0
  623. package/src/schema/index.ts +29 -0
  624. package/src/schema/query.ts +44 -0
  625. package/src/schema/registry.ts +965 -0
  626. package/src/schema/types.ts +276 -0
  627. package/src/schema/zod-generator.ts +413 -0
  628. package/src/search/fts-manager.ts +452 -0
  629. package/src/search/index.ts +26 -0
  630. package/src/search/query.ts +396 -0
  631. package/src/search/text-extraction.ts +162 -0
  632. package/src/search/types.ts +114 -0
  633. package/src/sections/index.ts +226 -0
  634. package/src/sections/types.ts +86 -0
  635. package/src/seed/apply.ts +1141 -0
  636. package/src/seed/default.ts +86 -0
  637. package/src/seed/index.ts +28 -0
  638. package/src/seed/load.ts +35 -0
  639. package/src/seed/types.ts +341 -0
  640. package/src/seed/validate.ts +642 -0
  641. package/src/seo/index.ts +179 -0
  642. package/src/settings/index.ts +203 -0
  643. package/src/settings/types.ts +58 -0
  644. package/src/storage/index.ts +28 -0
  645. package/src/storage/local.ts +249 -0
  646. package/src/storage/s3.ts +263 -0
  647. package/src/storage/types.ts +204 -0
  648. package/src/taxonomies/index.ts +309 -0
  649. package/src/taxonomies/types.ts +61 -0
  650. package/src/ui.ts +75 -0
  651. package/src/utils/base64.ts +73 -0
  652. package/src/utils/hash.ts +36 -0
  653. package/src/utils/sanitize.ts +20 -0
  654. package/src/utils/slugify.ts +29 -0
  655. package/src/utils/url.ts +48 -0
  656. package/src/virtual-modules.d.ts +111 -0
  657. package/src/visual-editing/editable.ts +108 -0
  658. package/src/visual-editing/toolbar.ts +1229 -0
  659. package/src/widgets/components.ts +105 -0
  660. package/src/widgets/index.ts +131 -0
  661. package/src/widgets/types.ts +81 -0
@@ -0,0 +1,1273 @@
1
+ /**
2
+ * Plugin Hooks System v2
3
+ *
4
+ * Uses the unified PluginContext for all hooks.
5
+ * Manages lifecycle hooks with:
6
+ * - Deterministic ordering via priority + dependencies
7
+ * - Timeout enforcement
8
+ * - Error isolation
9
+ * - Observability
10
+ *
11
+ */
12
+
13
+ import { PluginContextFactory, type PluginContextFactoryOptions } from "./context.js";
14
+ import type {
15
+ ResolvedPlugin,
16
+ ResolvedHook,
17
+ PluginContext,
18
+ ContentHookEvent,
19
+ ContentDeleteEvent,
20
+ MediaUploadEvent,
21
+ MediaAfterUploadEvent,
22
+ LifecycleEvent,
23
+ UninstallEvent,
24
+ CronEvent,
25
+ EmailBeforeSendEvent,
26
+ EmailBeforeSendHandler,
27
+ EmailDeliverHandler,
28
+ EmailAfterSendHandler,
29
+ ContentBeforeSaveHandler,
30
+ ContentAfterSaveHandler,
31
+ ContentBeforeDeleteHandler,
32
+ ContentAfterDeleteHandler,
33
+ MediaBeforeUploadHandler,
34
+ MediaAfterUploadHandler,
35
+ LifecycleHandler,
36
+ UninstallHandler,
37
+ CronHandler,
38
+ EmailMessage,
39
+ CommentBeforeCreateEvent,
40
+ CommentBeforeCreateHandler,
41
+ CommentModerateHandler,
42
+ CommentAfterCreateEvent,
43
+ CommentAfterCreateHandler,
44
+ CommentAfterModerateEvent,
45
+ CommentAfterModerateHandler,
46
+ PageMetadataEvent,
47
+ PageMetadataHandler,
48
+ PageMetadataContribution,
49
+ PageFragmentEvent,
50
+ PageFragmentHandler,
51
+ PageFragmentContribution,
52
+ } from "./types.js";
53
+
54
+ // Hook name type for v2
55
+ type HookNameV2 =
56
+ | "plugin:install"
57
+ | "plugin:activate"
58
+ | "plugin:deactivate"
59
+ | "plugin:uninstall"
60
+ | "content:beforeSave"
61
+ | "content:afterSave"
62
+ | "content:beforeDelete"
63
+ | "content:afterDelete"
64
+ | "media:beforeUpload"
65
+ | "media:afterUpload"
66
+ | "cron"
67
+ | "email:beforeSend"
68
+ | "email:deliver"
69
+ | "email:afterSend"
70
+ | "comment:beforeCreate"
71
+ | "comment:moderate"
72
+ | "comment:afterCreate"
73
+ | "comment:afterModerate"
74
+ | "page:metadata"
75
+ | "page:fragments";
76
+
77
+ /**
78
+ * Map from hook name to handler type — used for type-safe hook retrieval
79
+ */
80
+ interface HookHandlerMap {
81
+ "plugin:install": LifecycleHandler;
82
+ "plugin:activate": LifecycleHandler;
83
+ "plugin:deactivate": LifecycleHandler;
84
+ "plugin:uninstall": UninstallHandler;
85
+ "content:beforeSave": ContentBeforeSaveHandler;
86
+ "content:afterSave": ContentAfterSaveHandler;
87
+ "content:beforeDelete": ContentBeforeDeleteHandler;
88
+ "content:afterDelete": ContentAfterDeleteHandler;
89
+ "media:beforeUpload": MediaBeforeUploadHandler;
90
+ "media:afterUpload": MediaAfterUploadHandler;
91
+ cron: CronHandler;
92
+ "email:beforeSend": EmailBeforeSendHandler;
93
+ "email:deliver": EmailDeliverHandler;
94
+ "email:afterSend": EmailAfterSendHandler;
95
+ "comment:beforeCreate": CommentBeforeCreateHandler;
96
+ "comment:moderate": CommentModerateHandler;
97
+ "comment:afterCreate": CommentAfterCreateHandler;
98
+ "comment:afterModerate": CommentAfterModerateHandler;
99
+ "page:metadata": PageMetadataHandler;
100
+ "page:fragments": PageFragmentHandler;
101
+ }
102
+
103
+ /**
104
+ * Hook execution result
105
+ */
106
+ export interface HookResult<T> {
107
+ success: boolean;
108
+ value?: T;
109
+ error?: Error;
110
+ pluginId: string;
111
+ duration: number;
112
+ }
113
+
114
+ /**
115
+ * Hook pipeline for executing hooks in order
116
+ */
117
+ export class HookPipeline {
118
+ private hooks: Map<HookNameV2, Array<ResolvedHook<unknown>>> = new Map();
119
+ private pluginMap: Map<string, ResolvedPlugin> = new Map();
120
+ private contextFactory: PluginContextFactory | null = null;
121
+ /** Stored so setContextFactory can merge incrementally. */
122
+ private contextFactoryOptions: Partial<PluginContextFactoryOptions> = {};
123
+
124
+ /** Hook names where at least one handler declared exclusive: true */
125
+ private exclusiveHookNames: Set<string> = new Set();
126
+
127
+ /**
128
+ * Selected provider plugin ID for each exclusive hook.
129
+ * Set by the PluginManager after resolution.
130
+ */
131
+ private exclusiveSelections: Map<string, string> = new Map();
132
+
133
+ constructor(plugins: ResolvedPlugin[], factoryOptions?: PluginContextFactoryOptions) {
134
+ if (factoryOptions) {
135
+ this.contextFactory = new PluginContextFactory(factoryOptions);
136
+ this.contextFactoryOptions = { ...factoryOptions };
137
+ }
138
+
139
+ for (const plugin of plugins) {
140
+ this.pluginMap.set(plugin.id, plugin);
141
+ }
142
+ this.registerPlugins(plugins);
143
+ }
144
+
145
+ /**
146
+ * Set or update the context factory options.
147
+ *
148
+ * When called on a pipeline that already has a factory, the new options
149
+ * are merged on top of the existing ones so that callers don't need to
150
+ * repeat every field (e.g. adding `cronReschedule` without losing
151
+ * `storage` / `getUploadUrl`).
152
+ */
153
+ setContextFactory(options: Partial<PluginContextFactoryOptions>): void {
154
+ const merged = { ...this.contextFactoryOptions, ...options };
155
+ // The first call must include `db`; subsequent calls merge incrementally.
156
+ this.contextFactory = new PluginContextFactory(merged as PluginContextFactoryOptions);
157
+ this.contextFactoryOptions = merged;
158
+ }
159
+
160
+ /**
161
+ * Get context for a plugin
162
+ */
163
+ private getContext(pluginId: string): PluginContext {
164
+ const plugin = this.pluginMap.get(pluginId);
165
+ if (!plugin) {
166
+ throw new Error(`Plugin "${pluginId}" not found`);
167
+ }
168
+ if (!this.contextFactory) {
169
+ throw new Error("Context factory not initialized - call setContextFactory first");
170
+ }
171
+ return this.contextFactory.createContext(plugin);
172
+ }
173
+
174
+ /**
175
+ * Get typed hooks for a specific hook name.
176
+ * The internal map stores ResolvedHook<unknown>, but we know each name
177
+ * maps to a specific handler type via HookHandlerMap.
178
+ *
179
+ * Exclusive hooks that have a selected provider are filtered out — they
180
+ * should only run via invokeExclusiveHook(), not in the regular pipeline.
181
+ */
182
+ private getTypedHooks<N extends HookNameV2>(name: N): Array<ResolvedHook<HookHandlerMap[N]>> {
183
+ // The map stores hooks as ResolvedHook<unknown>. Each hook name corresponds
184
+ // to a specific handler type. The cast here is the single point where we
185
+ // bridge the untyped map to the typed API — callers never need to cast.
186
+ const all = (this.hooks.get(name) ?? []) as Array<ResolvedHook<HookHandlerMap[N]>>;
187
+
188
+ // If this hook has an exclusive selection, filter out all exclusive handlers
189
+ // so they don't run in the regular pipeline
190
+ if (this.exclusiveSelections.has(name)) {
191
+ return all.filter((h) => !h.exclusive);
192
+ }
193
+
194
+ return all;
195
+ }
196
+
197
+ /**
198
+ * Register all hooks from plugins.
199
+ *
200
+ * Registers each hook name individually to preserve type safety. The
201
+ * internal map stores ResolvedHook<unknown> since it's keyed by string,
202
+ * but getTypedHooks() restores the correct handler type on retrieval.
203
+ */
204
+ private registerPlugins(plugins: ResolvedPlugin[]): void {
205
+ for (const plugin of plugins) {
206
+ this.registerPluginHook(plugin, "plugin:install");
207
+ this.registerPluginHook(plugin, "plugin:activate");
208
+ this.registerPluginHook(plugin, "plugin:deactivate");
209
+ this.registerPluginHook(plugin, "plugin:uninstall");
210
+ this.registerPluginHook(plugin, "content:beforeSave");
211
+ this.registerPluginHook(plugin, "content:afterSave");
212
+ this.registerPluginHook(plugin, "content:beforeDelete");
213
+ this.registerPluginHook(plugin, "content:afterDelete");
214
+ this.registerPluginHook(plugin, "media:beforeUpload");
215
+ this.registerPluginHook(plugin, "media:afterUpload");
216
+ this.registerPluginHook(plugin, "cron");
217
+ this.registerPluginHook(plugin, "email:beforeSend");
218
+ this.registerPluginHook(plugin, "email:deliver");
219
+ this.registerPluginHook(plugin, "email:afterSend");
220
+ this.registerPluginHook(plugin, "comment:beforeCreate");
221
+ this.registerPluginHook(plugin, "comment:moderate");
222
+ this.registerPluginHook(plugin, "comment:afterCreate");
223
+ this.registerPluginHook(plugin, "comment:afterModerate");
224
+ this.registerPluginHook(plugin, "page:metadata");
225
+ this.registerPluginHook(plugin, "page:fragments");
226
+ }
227
+
228
+ // Sort hooks by priority and dependencies
229
+ for (const [hookName, hooks] of this.hooks) {
230
+ this.hooks.set(hookName, this.sortHooks(hooks));
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Maps hook names to the capability required to register them.
236
+ *
237
+ * Hooks not listed here have no capability requirement (e.g. lifecycle
238
+ * hooks, cron). Any plugin declaring a listed hook without the required
239
+ * capability will have that hook silently skipped at registration time.
240
+ */
241
+ private static readonly HOOK_REQUIRED_CAPABILITY: ReadonlyMap<string, string> = new Map([
242
+ // Email
243
+ ["email:beforeSend", "email:intercept"],
244
+ ["email:afterSend", "email:intercept"],
245
+ ["email:deliver", "email:provide"],
246
+ // Content — beforeSave can mutate content, so requires write:content.
247
+ // afterSave is read-only notification, so read:content suffices.
248
+ ["content:beforeSave", "write:content"],
249
+ ["content:afterSave", "read:content"],
250
+ ["content:beforeDelete", "read:content"],
251
+ ["content:afterDelete", "read:content"],
252
+ // Media
253
+ ["media:beforeUpload", "write:media"],
254
+ ["media:afterUpload", "read:media"],
255
+ // Comments — hooks expose author email, IP hash, user agent
256
+ ["comment:beforeCreate", "read:users"],
257
+ ["comment:moderate", "read:users"],
258
+ ["comment:afterCreate", "read:users"],
259
+ ["comment:afterModerate", "read:users"],
260
+ // Page fragments — can inject arbitrary scripts into every public page
261
+ ["page:fragments", "page:inject"],
262
+ ]);
263
+
264
+ /**
265
+ * Register a single plugin's hook by name
266
+ */
267
+ private registerPluginHook(plugin: ResolvedPlugin, name: HookNameV2): void {
268
+ const hook = plugin.hooks[name];
269
+ if (!hook) return;
270
+
271
+ // Hooks that expose sensitive data or inject into pages require specific
272
+ // capabilities. Plugins without the required capability have the hook
273
+ // silently skipped to prevent unauthorized data access or page injection.
274
+ const requiredCapability = HookPipeline.HOOK_REQUIRED_CAPABILITY.get(name);
275
+ if (requiredCapability && !plugin.capabilities.includes(requiredCapability as never)) {
276
+ console.warn(
277
+ `[hooks] Plugin "${plugin.id}" declares ${name} hook without ${requiredCapability} capability — skipping`,
278
+ );
279
+ return;
280
+ }
281
+
282
+ // Track exclusive hooks
283
+ if (hook.exclusive) {
284
+ this.exclusiveHookNames.add(name);
285
+ }
286
+
287
+ // ResolvedHook<SpecificHandler> is assignable to ResolvedHook<unknown>
288
+ // because the handler property is covariant
289
+ this.registerHook(name, hook);
290
+ }
291
+
292
+ /**
293
+ * Register a single hook
294
+ */
295
+ private registerHook(name: HookNameV2, hook: ResolvedHook<unknown>): void {
296
+ const existing = this.hooks.get(name) || [];
297
+ existing.push(hook);
298
+ this.hooks.set(name, existing);
299
+ }
300
+
301
+ /**
302
+ * Sort hooks by priority and dependencies
303
+ */
304
+ private sortHooks(hooks: Array<ResolvedHook<unknown>>): Array<ResolvedHook<unknown>> {
305
+ const sorted: Array<ResolvedHook<unknown>> = [];
306
+ const remaining = [...hooks];
307
+
308
+ // Simple topological sort with priority as tiebreaker
309
+ while (remaining.length > 0) {
310
+ // Find hooks whose dependencies are satisfied
311
+ const ready = remaining.filter((hook) =>
312
+ hook.dependencies.every((dep) => sorted.some((s) => s.pluginId === dep)),
313
+ );
314
+
315
+ if (ready.length === 0) {
316
+ // Circular dependency or missing dependency - just add by priority
317
+ remaining.sort((a, b) => a.priority - b.priority);
318
+ sorted.push(...remaining);
319
+ break;
320
+ }
321
+
322
+ // Sort ready hooks by priority and add the first one
323
+ ready.sort((a, b) => a.priority - b.priority);
324
+ const next = ready[0];
325
+ sorted.push(next);
326
+ remaining.splice(remaining.indexOf(next), 1);
327
+ }
328
+
329
+ return sorted;
330
+ }
331
+
332
+ /**
333
+ * Execute a hook with timeout
334
+ */
335
+ private async executeWithTimeout<T>(fn: () => Promise<T>, timeout: number): Promise<T> {
336
+ return Promise.race([
337
+ fn(),
338
+ new Promise<T>((_, reject) =>
339
+ setTimeout(() => reject(new Error(`Hook timeout after ${timeout}ms`)), timeout),
340
+ ),
341
+ ]);
342
+ }
343
+
344
+ // =========================================================================
345
+ // Lifecycle Hooks
346
+ // =========================================================================
347
+
348
+ /**
349
+ * Run plugin:install hooks
350
+ */
351
+ async runPluginInstall(pluginId: string): Promise<HookResult<void>[]> {
352
+ return this.runLifecycleHook("plugin:install", pluginId);
353
+ }
354
+
355
+ /**
356
+ * Run plugin:activate hooks
357
+ */
358
+ async runPluginActivate(pluginId: string): Promise<HookResult<void>[]> {
359
+ return this.runLifecycleHook("plugin:activate", pluginId);
360
+ }
361
+
362
+ /**
363
+ * Run plugin:deactivate hooks
364
+ */
365
+ async runPluginDeactivate(pluginId: string): Promise<HookResult<void>[]> {
366
+ return this.runLifecycleHook("plugin:deactivate", pluginId);
367
+ }
368
+
369
+ /**
370
+ * Run plugin:uninstall hooks
371
+ */
372
+ async runPluginUninstall(pluginId: string, deleteData: boolean): Promise<HookResult<void>[]> {
373
+ const hooks = this.getTypedHooks("plugin:uninstall");
374
+ const results: HookResult<void>[] = [];
375
+
376
+ // Only run the hook for the specific plugin being uninstalled
377
+ const hook = hooks.find((h) => h.pluginId === pluginId);
378
+ if (!hook) return results;
379
+
380
+ const { handler } = hook;
381
+ const event: UninstallEvent = { deleteData };
382
+ const ctx = this.getContext(pluginId);
383
+ const start = Date.now();
384
+
385
+ try {
386
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
387
+ results.push({
388
+ success: true,
389
+ pluginId: hook.pluginId,
390
+ duration: Date.now() - start,
391
+ });
392
+ } catch (error) {
393
+ results.push({
394
+ success: false,
395
+ error: error instanceof Error ? error : new Error(String(error)),
396
+ pluginId: hook.pluginId,
397
+ duration: Date.now() - start,
398
+ });
399
+ }
400
+
401
+ return results;
402
+ }
403
+
404
+ private async runLifecycleHook(
405
+ hookName: "plugin:install" | "plugin:activate" | "plugin:deactivate",
406
+ pluginId: string,
407
+ ): Promise<HookResult<void>[]> {
408
+ const hooks = this.getTypedHooks(hookName);
409
+ const results: HookResult<void>[] = [];
410
+
411
+ // Only run the hook for the specific plugin
412
+ const hook = hooks.find((h) => h.pluginId === pluginId);
413
+ if (!hook) return results;
414
+
415
+ const { handler } = hook;
416
+ const event: LifecycleEvent = {};
417
+ const ctx = this.getContext(pluginId);
418
+ const start = Date.now();
419
+
420
+ try {
421
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
422
+ results.push({
423
+ success: true,
424
+ pluginId: hook.pluginId,
425
+ duration: Date.now() - start,
426
+ });
427
+ } catch (error) {
428
+ results.push({
429
+ success: false,
430
+ error: error instanceof Error ? error : new Error(String(error)),
431
+ pluginId: hook.pluginId,
432
+ duration: Date.now() - start,
433
+ });
434
+ }
435
+
436
+ return results;
437
+ }
438
+
439
+ // =========================================================================
440
+ // Content Hooks
441
+ // =========================================================================
442
+
443
+ /**
444
+ * Run content:beforeSave hooks
445
+ * Returns modified content from the pipeline
446
+ */
447
+ async runContentBeforeSave(
448
+ content: Record<string, unknown>,
449
+ collection: string,
450
+ isNew: boolean,
451
+ ): Promise<{
452
+ content: Record<string, unknown>;
453
+ results: HookResult<Record<string, unknown>>[];
454
+ }> {
455
+ const hooks = this.getTypedHooks("content:beforeSave");
456
+ const results: HookResult<Record<string, unknown>>[] = [];
457
+ let currentContent = content;
458
+
459
+ for (const hook of hooks) {
460
+ const { handler } = hook;
461
+ const event: ContentHookEvent = {
462
+ content: currentContent,
463
+ collection,
464
+ isNew,
465
+ };
466
+ const ctx = this.getContext(hook.pluginId);
467
+ const start = Date.now();
468
+
469
+ try {
470
+ const result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
471
+ // Handler can return modified content or void (keep current)
472
+ if (result !== undefined) {
473
+ currentContent = result;
474
+ }
475
+ results.push({
476
+ success: true,
477
+ value: currentContent,
478
+ pluginId: hook.pluginId,
479
+ duration: Date.now() - start,
480
+ });
481
+ } catch (error) {
482
+ results.push({
483
+ success: false,
484
+ error: error instanceof Error ? error : new Error(String(error)),
485
+ pluginId: hook.pluginId,
486
+ duration: Date.now() - start,
487
+ });
488
+
489
+ if (hook.errorPolicy === "abort") {
490
+ throw error;
491
+ }
492
+ }
493
+ }
494
+
495
+ return { content: currentContent, results };
496
+ }
497
+
498
+ /**
499
+ * Run content:afterSave hooks
500
+ */
501
+ async runContentAfterSave(
502
+ content: Record<string, unknown>,
503
+ collection: string,
504
+ isNew: boolean,
505
+ ): Promise<HookResult<void>[]> {
506
+ const hooks = this.getTypedHooks("content:afterSave");
507
+ const results: HookResult<void>[] = [];
508
+
509
+ for (const hook of hooks) {
510
+ const { handler } = hook;
511
+ const event: ContentHookEvent = { content, collection, isNew };
512
+ const ctx = this.getContext(hook.pluginId);
513
+ const start = Date.now();
514
+
515
+ try {
516
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
517
+ results.push({
518
+ success: true,
519
+ pluginId: hook.pluginId,
520
+ duration: Date.now() - start,
521
+ });
522
+ } catch (error) {
523
+ results.push({
524
+ success: false,
525
+ error: error instanceof Error ? error : new Error(String(error)),
526
+ pluginId: hook.pluginId,
527
+ duration: Date.now() - start,
528
+ });
529
+
530
+ if (hook.errorPolicy === "abort") {
531
+ throw error;
532
+ }
533
+ }
534
+ }
535
+
536
+ return results;
537
+ }
538
+
539
+ /**
540
+ * Run content:beforeDelete hooks
541
+ * Returns whether deletion is allowed
542
+ */
543
+ async runContentBeforeDelete(
544
+ id: string,
545
+ collection: string,
546
+ ): Promise<{ allowed: boolean; results: HookResult<boolean>[] }> {
547
+ const hooks = this.getTypedHooks("content:beforeDelete");
548
+ const results: HookResult<boolean>[] = [];
549
+ let allowed = true;
550
+
551
+ for (const hook of hooks) {
552
+ const { handler } = hook;
553
+ const event: ContentDeleteEvent = { id, collection };
554
+ const ctx = this.getContext(hook.pluginId);
555
+ const start = Date.now();
556
+
557
+ try {
558
+ const result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
559
+ // Handler returns false to block, true or void to allow
560
+ if (result === false) {
561
+ allowed = false;
562
+ }
563
+ results.push({
564
+ success: true,
565
+ value: result !== false,
566
+ pluginId: hook.pluginId,
567
+ duration: Date.now() - start,
568
+ });
569
+ } catch (error) {
570
+ results.push({
571
+ success: false,
572
+ error: error instanceof Error ? error : new Error(String(error)),
573
+ pluginId: hook.pluginId,
574
+ duration: Date.now() - start,
575
+ });
576
+
577
+ if (hook.errorPolicy === "abort") {
578
+ throw error;
579
+ }
580
+ }
581
+ }
582
+
583
+ return { allowed, results };
584
+ }
585
+
586
+ /**
587
+ * Run content:afterDelete hooks
588
+ */
589
+ async runContentAfterDelete(id: string, collection: string): Promise<HookResult<void>[]> {
590
+ const hooks = this.getTypedHooks("content:afterDelete");
591
+ const results: HookResult<void>[] = [];
592
+
593
+ for (const hook of hooks) {
594
+ const { handler } = hook;
595
+ const event: ContentDeleteEvent = { id, collection };
596
+ const ctx = this.getContext(hook.pluginId);
597
+ const start = Date.now();
598
+
599
+ try {
600
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
601
+ results.push({
602
+ success: true,
603
+ pluginId: hook.pluginId,
604
+ duration: Date.now() - start,
605
+ });
606
+ } catch (error) {
607
+ results.push({
608
+ success: false,
609
+ error: error instanceof Error ? error : new Error(String(error)),
610
+ pluginId: hook.pluginId,
611
+ duration: Date.now() - start,
612
+ });
613
+
614
+ if (hook.errorPolicy === "abort") {
615
+ throw error;
616
+ }
617
+ }
618
+ }
619
+
620
+ return results;
621
+ }
622
+
623
+ // =========================================================================
624
+ // Media Hooks
625
+ // =========================================================================
626
+
627
+ /**
628
+ * Run media:beforeUpload hooks
629
+ */
630
+ async runMediaBeforeUpload(file: { name: string; type: string; size: number }): Promise<{
631
+ file: { name: string; type: string; size: number };
632
+ results: HookResult<{ name: string; type: string; size: number }>[];
633
+ }> {
634
+ const hooks = this.getTypedHooks("media:beforeUpload");
635
+ const results: HookResult<{
636
+ name: string;
637
+ type: string;
638
+ size: number;
639
+ }>[] = [];
640
+ let currentFile = file;
641
+
642
+ for (const hook of hooks) {
643
+ const { handler } = hook;
644
+ const event: MediaUploadEvent = { file: currentFile };
645
+ const ctx = this.getContext(hook.pluginId);
646
+ const start = Date.now();
647
+
648
+ try {
649
+ const result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
650
+ // Handler can return modified file info or void
651
+ if (result !== undefined) {
652
+ currentFile = result;
653
+ }
654
+ results.push({
655
+ success: true,
656
+ value: currentFile,
657
+ pluginId: hook.pluginId,
658
+ duration: Date.now() - start,
659
+ });
660
+ } catch (error) {
661
+ results.push({
662
+ success: false,
663
+ error: error instanceof Error ? error : new Error(String(error)),
664
+ pluginId: hook.pluginId,
665
+ duration: Date.now() - start,
666
+ });
667
+
668
+ if (hook.errorPolicy === "abort") {
669
+ throw error;
670
+ }
671
+ }
672
+ }
673
+
674
+ return { file: currentFile, results };
675
+ }
676
+
677
+ /**
678
+ * Run media:afterUpload hooks
679
+ */
680
+ async runMediaAfterUpload(media: {
681
+ id: string;
682
+ filename: string;
683
+ mimeType: string;
684
+ size: number | null;
685
+ url: string;
686
+ createdAt: string;
687
+ }): Promise<HookResult<void>[]> {
688
+ const hooks = this.getTypedHooks("media:afterUpload");
689
+ const results: HookResult<void>[] = [];
690
+
691
+ for (const hook of hooks) {
692
+ const { handler } = hook;
693
+ const event: MediaAfterUploadEvent = { media };
694
+ const ctx = this.getContext(hook.pluginId);
695
+ const start = Date.now();
696
+
697
+ try {
698
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
699
+ results.push({
700
+ success: true,
701
+ pluginId: hook.pluginId,
702
+ duration: Date.now() - start,
703
+ });
704
+ } catch (error) {
705
+ results.push({
706
+ success: false,
707
+ error: error instanceof Error ? error : new Error(String(error)),
708
+ pluginId: hook.pluginId,
709
+ duration: Date.now() - start,
710
+ });
711
+
712
+ if (hook.errorPolicy === "abort") {
713
+ throw error;
714
+ }
715
+ }
716
+ }
717
+
718
+ return results;
719
+ }
720
+
721
+ // =========================================================================
722
+ // Cron Hook (per-plugin dispatch)
723
+ // =========================================================================
724
+
725
+ /**
726
+ * Invoke the cron hook for a specific plugin.
727
+ *
728
+ * Unlike other hooks which broadcast to all plugins, the cron hook is
729
+ * dispatched only to the target plugin — the one that owns the task.
730
+ */
731
+ async invokeCronHook(pluginId: string, event: CronEvent): Promise<HookResult<void>> {
732
+ const hooks = this.getTypedHooks("cron");
733
+ const hook = hooks.find((h) => h.pluginId === pluginId);
734
+
735
+ if (!hook) {
736
+ return {
737
+ success: false,
738
+ error: new Error(`Plugin "${pluginId}" has no cron hook registered`),
739
+ pluginId,
740
+ duration: 0,
741
+ };
742
+ }
743
+
744
+ const { handler } = hook;
745
+ const ctx = this.getContext(pluginId);
746
+ const start = Date.now();
747
+
748
+ try {
749
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
750
+ return {
751
+ success: true,
752
+ pluginId,
753
+ duration: Date.now() - start,
754
+ };
755
+ } catch (error) {
756
+ return {
757
+ success: false,
758
+ error: error instanceof Error ? error : new Error(String(error)),
759
+ pluginId,
760
+ duration: Date.now() - start,
761
+ };
762
+ }
763
+ }
764
+
765
+ // =========================================================================
766
+ // Email Hooks
767
+ // =========================================================================
768
+
769
+ /**
770
+ * Run email:beforeSend hooks (middleware pipeline).
771
+ *
772
+ * Each handler receives the message and returns a modified message or
773
+ * `false` to cancel delivery. The pipeline chains message transformations —
774
+ * each handler receives the output of the previous one.
775
+ */
776
+ async runEmailBeforeSend(
777
+ message: EmailMessage,
778
+ source: string,
779
+ ): Promise<{ message: EmailMessage | false; results: HookResult<EmailMessage | false>[] }> {
780
+ const hooks = this.getTypedHooks("email:beforeSend");
781
+ const results: HookResult<EmailMessage | false>[] = [];
782
+ let currentMessage: EmailMessage = message;
783
+
784
+ for (const hook of hooks) {
785
+ const { handler } = hook;
786
+ // Shallow-clone message to prevent handlers from mutating
787
+ // the shared reference and leaking changes to subsequent stages
788
+ const event: EmailBeforeSendEvent = { message: { ...currentMessage }, source };
789
+ const ctx = this.getContext(hook.pluginId);
790
+ const start = Date.now();
791
+
792
+ try {
793
+ const result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
794
+
795
+ if (result === false) {
796
+ // Cancelled
797
+ results.push({
798
+ success: true,
799
+ value: false,
800
+ pluginId: hook.pluginId,
801
+ duration: Date.now() - start,
802
+ });
803
+ return { message: false, results };
804
+ }
805
+
806
+ // Handler returned a modified message
807
+ if (result && typeof result === "object") {
808
+ currentMessage = result;
809
+ }
810
+
811
+ results.push({
812
+ success: true,
813
+ value: currentMessage,
814
+ pluginId: hook.pluginId,
815
+ duration: Date.now() - start,
816
+ });
817
+ } catch (error) {
818
+ results.push({
819
+ success: false,
820
+ error: error instanceof Error ? error : new Error(String(error)),
821
+ pluginId: hook.pluginId,
822
+ duration: Date.now() - start,
823
+ });
824
+
825
+ if (hook.errorPolicy === "abort") {
826
+ throw error;
827
+ }
828
+ }
829
+ }
830
+
831
+ return { message: currentMessage, results };
832
+ }
833
+
834
+ /**
835
+ * Run email:afterSend hooks (fire-and-forget).
836
+ *
837
+ * Errors are logged but don't propagate — they don't affect the caller.
838
+ */
839
+ async runEmailAfterSend(message: EmailMessage, source: string): Promise<HookResult<void>[]> {
840
+ const hooks = this.getTypedHooks("email:afterSend");
841
+ const results: HookResult<void>[] = [];
842
+
843
+ for (const hook of hooks) {
844
+ const { handler } = hook;
845
+ const event = { message, source };
846
+ const ctx = this.getContext(hook.pluginId);
847
+ const start = Date.now();
848
+
849
+ try {
850
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
851
+ results.push({
852
+ success: true,
853
+ pluginId: hook.pluginId,
854
+ duration: Date.now() - start,
855
+ });
856
+ } catch (error) {
857
+ // Fire-and-forget: log but don't propagate
858
+ console.error(
859
+ `[email:afterSend] Plugin "${hook.pluginId}" error:`,
860
+ error instanceof Error ? error.message : error,
861
+ );
862
+ results.push({
863
+ success: false,
864
+ error: error instanceof Error ? error : new Error(String(error)),
865
+ pluginId: hook.pluginId,
866
+ duration: Date.now() - start,
867
+ });
868
+ }
869
+ }
870
+
871
+ return results;
872
+ }
873
+
874
+ // =========================================================================
875
+ // Comment Hooks
876
+ // =========================================================================
877
+
878
+ /**
879
+ * Run comment:beforeCreate hooks (middleware pipeline).
880
+ *
881
+ * Each handler receives the event and returns a modified event or
882
+ * `false` to reject the comment. The pipeline chains transformations —
883
+ * each handler receives the output of the previous one.
884
+ */
885
+ async runCommentBeforeCreate(
886
+ event: CommentBeforeCreateEvent,
887
+ ): Promise<CommentBeforeCreateEvent | false> {
888
+ const hooks = this.getTypedHooks("comment:beforeCreate");
889
+ let currentEvent = event;
890
+
891
+ for (const hook of hooks) {
892
+ const { handler } = hook;
893
+ const ctx = this.getContext(hook.pluginId);
894
+ const start = Date.now();
895
+
896
+ try {
897
+ const result = await this.executeWithTimeout(
898
+ () => handler({ ...currentEvent }, ctx),
899
+ hook.timeout,
900
+ );
901
+
902
+ if (result === false) {
903
+ return false;
904
+ }
905
+
906
+ if (result && typeof result === "object") {
907
+ currentEvent = result;
908
+ }
909
+ } catch (error) {
910
+ console.error(
911
+ `[comment:beforeCreate] Plugin "${hook.pluginId}" error (${Date.now() - start}ms):`,
912
+ error instanceof Error ? error.message : error,
913
+ );
914
+
915
+ if (hook.errorPolicy === "abort") {
916
+ throw error;
917
+ }
918
+ }
919
+ }
920
+
921
+ return currentEvent;
922
+ }
923
+
924
+ /**
925
+ * Run comment:afterCreate hooks (fire-and-forget).
926
+ *
927
+ * Errors are logged but don't propagate — they don't affect the caller.
928
+ */
929
+ async runCommentAfterCreate(event: CommentAfterCreateEvent): Promise<void> {
930
+ const hooks = this.getTypedHooks("comment:afterCreate");
931
+
932
+ for (const hook of hooks) {
933
+ const { handler } = hook;
934
+ const ctx = this.getContext(hook.pluginId);
935
+
936
+ try {
937
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
938
+ } catch (error) {
939
+ console.error(
940
+ `[comment:afterCreate] Plugin "${hook.pluginId}" error:`,
941
+ error instanceof Error ? error.message : error,
942
+ );
943
+ }
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Run comment:afterModerate hooks (fire-and-forget).
949
+ *
950
+ * Errors are logged but don't propagate — they don't affect the caller.
951
+ */
952
+ async runCommentAfterModerate(event: CommentAfterModerateEvent): Promise<void> {
953
+ const hooks = this.getTypedHooks("comment:afterModerate");
954
+
955
+ for (const hook of hooks) {
956
+ const { handler } = hook;
957
+ const ctx = this.getContext(hook.pluginId);
958
+
959
+ try {
960
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
961
+ } catch (error) {
962
+ console.error(
963
+ `[comment:afterModerate] Plugin "${hook.pluginId}" error:`,
964
+ error instanceof Error ? error.message : error,
965
+ );
966
+ }
967
+ }
968
+ }
969
+
970
+ // =========================================================================
971
+ // Public Page Hooks
972
+ // =========================================================================
973
+
974
+ /**
975
+ * Run page:metadata hooks. Each handler returns contributions that are
976
+ * merged by the metadata collector. Errors are logged but don't propagate.
977
+ */
978
+ async runPageMetadata(
979
+ event: PageMetadataEvent,
980
+ ): Promise<Array<{ pluginId: string; contributions: PageMetadataContribution[] }>> {
981
+ const hooks = this.getTypedHooks("page:metadata");
982
+ const results: Array<{ pluginId: string; contributions: PageMetadataContribution[] }> = [];
983
+
984
+ for (const hook of hooks) {
985
+ const { handler } = hook;
986
+ const ctx = this.getContext(hook.pluginId);
987
+
988
+ try {
989
+ const result = await this.executeWithTimeout(
990
+ () => Promise.resolve(handler(event, ctx)),
991
+ hook.timeout,
992
+ );
993
+
994
+ if (result != null) {
995
+ const contributions = Array.isArray(result) ? result : [result];
996
+ results.push({ pluginId: hook.pluginId, contributions });
997
+ }
998
+ } catch (error) {
999
+ console.error(
1000
+ `[page:metadata] Plugin "${hook.pluginId}" error:`,
1001
+ error instanceof Error ? error.message : error,
1002
+ );
1003
+ }
1004
+ }
1005
+
1006
+ return results;
1007
+ }
1008
+
1009
+ /**
1010
+ * Run page:fragments hooks. Only trusted plugins should be registered
1011
+ * for this hook. Errors are logged but don't propagate.
1012
+ */
1013
+ async runPageFragments(
1014
+ event: PageFragmentEvent,
1015
+ ): Promise<Array<{ pluginId: string; contributions: PageFragmentContribution[] }>> {
1016
+ const hooks = this.getTypedHooks("page:fragments");
1017
+ const results: Array<{ pluginId: string; contributions: PageFragmentContribution[] }> = [];
1018
+
1019
+ for (const hook of hooks) {
1020
+ const { handler } = hook;
1021
+ const ctx = this.getContext(hook.pluginId);
1022
+
1023
+ try {
1024
+ const result = await this.executeWithTimeout(
1025
+ () => Promise.resolve(handler(event, ctx)),
1026
+ hook.timeout,
1027
+ );
1028
+
1029
+ if (result != null) {
1030
+ const contributions = Array.isArray(result) ? result : [result];
1031
+ results.push({ pluginId: hook.pluginId, contributions });
1032
+ }
1033
+ } catch (error) {
1034
+ console.error(
1035
+ `[page:fragments] Plugin "${hook.pluginId}" error:`,
1036
+ error instanceof Error ? error.message : error,
1037
+ );
1038
+ }
1039
+ }
1040
+
1041
+ return results;
1042
+ }
1043
+
1044
+ // =========================================================================
1045
+ // Utilities
1046
+ // =========================================================================
1047
+
1048
+ /**
1049
+ * Check if any hooks are registered for a given name
1050
+ */
1051
+ hasHooks(name: HookNameV2): boolean {
1052
+ const hooks = this.hooks.get(name);
1053
+ return hooks !== undefined && hooks.length > 0;
1054
+ }
1055
+
1056
+ /**
1057
+ * Get hook count for debugging
1058
+ */
1059
+ getHookCount(name: HookNameV2): number {
1060
+ return this.hooks.get(name)?.length || 0;
1061
+ }
1062
+
1063
+ /**
1064
+ * Get all registered hook names
1065
+ */
1066
+ getRegisteredHooks(): HookNameV2[] {
1067
+ return [...this.hooks.keys()];
1068
+ }
1069
+
1070
+ // =========================================================================
1071
+ // Exclusive Hook Support
1072
+ // =========================================================================
1073
+
1074
+ /**
1075
+ * Returns hook names where at least one handler declared exclusive: true
1076
+ */
1077
+ getRegisteredExclusiveHooks(): string[] {
1078
+ return [...this.exclusiveHookNames];
1079
+ }
1080
+
1081
+ /**
1082
+ * Check if a hook is exclusive
1083
+ */
1084
+ isExclusiveHook(name: string): boolean {
1085
+ return this.exclusiveHookNames.has(name);
1086
+ }
1087
+
1088
+ /**
1089
+ * Set the selected provider for an exclusive hook.
1090
+ * Called by PluginManager after resolution.
1091
+ */
1092
+ setExclusiveSelection(hookName: string, pluginId: string): void {
1093
+ this.exclusiveSelections.set(hookName, pluginId);
1094
+ }
1095
+
1096
+ /**
1097
+ * Clear the selected provider for an exclusive hook.
1098
+ */
1099
+ clearExclusiveSelection(hookName: string): void {
1100
+ this.exclusiveSelections.delete(hookName);
1101
+ }
1102
+
1103
+ /**
1104
+ * Get the selected provider for an exclusive hook (if any).
1105
+ */
1106
+ getExclusiveSelection(hookName: string): string | undefined {
1107
+ return this.exclusiveSelections.get(hookName);
1108
+ }
1109
+
1110
+ /**
1111
+ * Get all plugins that registered a handler for a given exclusive hook.
1112
+ */
1113
+ getExclusiveHookProviders(hookName: string): Array<{ pluginId: string }> {
1114
+ const hooks = this.hooks.get(hookName as HookNameV2) ?? [];
1115
+ return hooks.filter((h) => h.exclusive).map((h) => ({ pluginId: h.pluginId }));
1116
+ }
1117
+
1118
+ /**
1119
+ * Invoke an exclusive hook — dispatch only to the selected provider.
1120
+ * Returns null if no provider is selected or if the selected hook
1121
+ * is not found in the pipeline.
1122
+ *
1123
+ * This is a generic dispatch used by the email pipeline and other
1124
+ * exclusive hook consumers. The handler type is unknown — callers
1125
+ * must know the expected signature.
1126
+ *
1127
+ * Errors are isolated: a failing handler returns an error result
1128
+ * instead of propagating the exception to the caller.
1129
+ */
1130
+ async invokeExclusiveHook(
1131
+ hookName: string,
1132
+ event: unknown,
1133
+ ): Promise<{ result: unknown; pluginId: string; error?: Error; duration: number } | null> {
1134
+ const selectedPluginId = this.exclusiveSelections.get(hookName);
1135
+ if (!selectedPluginId) return null;
1136
+
1137
+ const hooks = this.hooks.get(hookName as HookNameV2) ?? [];
1138
+ const hook = hooks.find((h) => h.pluginId === selectedPluginId && h.exclusive);
1139
+ if (!hook) return null;
1140
+
1141
+ const start = Date.now();
1142
+ try {
1143
+ const ctx = this.getContext(selectedPluginId);
1144
+ const handler = hook.handler as (event: unknown, ctx: PluginContext) => Promise<unknown>;
1145
+ const result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
1146
+ return { result, pluginId: selectedPluginId, duration: Date.now() - start };
1147
+ } catch (error) {
1148
+ return {
1149
+ result: undefined,
1150
+ pluginId: selectedPluginId,
1151
+ error: error instanceof Error ? error : new Error(String(error)),
1152
+ duration: Date.now() - start,
1153
+ };
1154
+ }
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Create a hook pipeline from plugins
1160
+ */
1161
+ export function createHookPipeline(
1162
+ plugins: ResolvedPlugin[],
1163
+ factoryOptions?: PluginContextFactoryOptions,
1164
+ ): HookPipeline {
1165
+ return new HookPipeline(plugins, factoryOptions);
1166
+ }
1167
+
1168
+ // ── Shared exclusive hook resolution ─────────────────────────────────────────
1169
+
1170
+ /**
1171
+ * Options for exclusive hook resolution.
1172
+ */
1173
+ export interface ExclusiveHookResolutionOptions {
1174
+ pipeline: HookPipeline;
1175
+ /**
1176
+ * Check whether a plugin ID is currently active.
1177
+ * Used to filter providers — only active providers participate in selection.
1178
+ */
1179
+ isActive: (pluginId: string) => boolean;
1180
+ /** Read an option value from persistent storage. */
1181
+ getOption: (key: string) => Promise<string | null>;
1182
+ /** Write an option value to persistent storage. */
1183
+ setOption: (key: string, value: string) => Promise<void>;
1184
+ /** Delete an option from persistent storage. */
1185
+ deleteOption: (key: string) => Promise<void>;
1186
+ /**
1187
+ * Map of pluginId → hook names the plugin prefers to handle.
1188
+ * Used as a tiebreaker when no DB selection exists and multiple providers are active.
1189
+ */
1190
+ preferredHints?: Map<string, string[]>;
1191
+ }
1192
+
1193
+ /** Options table key prefix for exclusive hook selections */
1194
+ const EXCLUSIVE_HOOK_KEY_PREFIX = "emdash:exclusive_hook:";
1195
+
1196
+ /**
1197
+ * Resolve exclusive hook selections.
1198
+ *
1199
+ * Shared algorithm used by both PluginManager and EmDashRuntime:
1200
+ * 1. If a DB selection exists and that plugin is active → keep it.
1201
+ * 2. If DB selection is stale (plugin inactive/gone) → clear it.
1202
+ * 3. If no selection and only one active provider → auto-select it.
1203
+ * 4. If preferred hints match an active provider → first match wins.
1204
+ * 5. If multiple providers and no hint → leave unselected (admin must choose).
1205
+ */
1206
+ export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions): Promise<void> {
1207
+ const { pipeline, isActive, getOption, setOption, deleteOption, preferredHints } = opts;
1208
+ const exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();
1209
+
1210
+ for (const hookName of exclusiveHookNames) {
1211
+ const providers = pipeline.getExclusiveHookProviders(hookName);
1212
+ const activeProviderIds = new Set(
1213
+ providers.map((p) => p.pluginId).filter((id) => isActive(id)),
1214
+ );
1215
+
1216
+ const key = `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`;
1217
+ let currentSelection: string | null = null;
1218
+ try {
1219
+ currentSelection = await getOption(key);
1220
+ } catch {
1221
+ // Options table may not be ready
1222
+ continue;
1223
+ }
1224
+
1225
+ // If selection exists and the plugin is still active → keep it
1226
+ if (currentSelection && activeProviderIds.has(currentSelection)) {
1227
+ pipeline.setExclusiveSelection(hookName, currentSelection);
1228
+ continue;
1229
+ }
1230
+
1231
+ // Selection is stale or missing — clear it
1232
+ if (currentSelection) {
1233
+ try {
1234
+ await deleteOption(key);
1235
+ } catch {
1236
+ // Non-fatal
1237
+ }
1238
+ }
1239
+
1240
+ // Auto-select if only one active provider
1241
+ if (activeProviderIds.size === 1) {
1242
+ const [onlyProvider] = activeProviderIds;
1243
+ try {
1244
+ await setOption(key, onlyProvider);
1245
+ } catch {
1246
+ // Non-fatal
1247
+ }
1248
+ pipeline.setExclusiveSelection(hookName, onlyProvider);
1249
+ continue;
1250
+ }
1251
+
1252
+ // Check preferred hints
1253
+ if (preferredHints) {
1254
+ let found = false;
1255
+ for (const [pluginId, hooks] of preferredHints) {
1256
+ if (hooks.includes(hookName) && activeProviderIds.has(pluginId)) {
1257
+ try {
1258
+ await setOption(key, pluginId);
1259
+ } catch {
1260
+ // Non-fatal
1261
+ }
1262
+ pipeline.setExclusiveSelection(hookName, pluginId);
1263
+ found = true;
1264
+ break;
1265
+ }
1266
+ }
1267
+ if (found) continue;
1268
+ }
1269
+
1270
+ // Multiple providers, no hint — leave unselected
1271
+ pipeline.clearExclusiveSelection(hookName);
1272
+ }
1273
+ }