emdash 0.40.0 → 0.40.1

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 (200) hide show
  1. package/dist/api/route-utils.mjs +6 -6
  2. package/dist/api/schemas/index.d.mts +1 -1
  3. package/dist/{api-cFFZpyS0.mjs → api-CuyNEHZG.mjs} +3 -3
  4. package/dist/{api-cFFZpyS0.mjs.map → api-CuyNEHZG.mjs.map} +1 -1
  5. package/dist/{apply-CoT603H3.mjs → apply-npY1TAhG.mjs} +237 -34
  6. package/dist/apply-npY1TAhG.mjs.map +1 -0
  7. package/dist/astro/index.d.mts +3 -3
  8. package/dist/astro/index.mjs +9 -9
  9. package/dist/astro/index.mjs.map +1 -1
  10. package/dist/astro/middleware/auth.d.mts +3 -3
  11. package/dist/astro/middleware/redirect.mjs +1 -1
  12. package/dist/astro/middleware.d.mts +3 -3
  13. package/dist/astro/middleware.d.mts.map +1 -1
  14. package/dist/astro/middleware.mjs +19 -18
  15. package/dist/astro/middleware.mjs.map +1 -1
  16. package/dist/astro/routes/api/admin/media-usage/progress.mjs +2 -2
  17. package/dist/astro/routes/api/admin/media-usage/repair.mjs +2 -2
  18. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +8 -8
  19. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +8 -8
  20. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +8 -8
  21. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +11 -11
  22. package/dist/astro/routes/api/admin/plugins/index.mjs +8 -8
  23. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +8 -8
  24. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +11 -11
  25. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +8 -8
  26. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +8 -8
  27. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +12 -12
  28. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +9 -9
  29. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +9 -9
  30. package/dist/astro/routes/api/admin/plugins/updates.mjs +8 -8
  31. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +8 -8
  32. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +8 -8
  33. package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +3 -3
  34. package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +3 -3
  35. package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +3 -3
  36. package/dist/astro/routes/api/admin/transfer/capabilities.mjs +3 -3
  37. package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +3 -3
  38. package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +3 -3
  39. package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +3 -3
  40. package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +3 -3
  41. package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +3 -3
  42. package/dist/astro/routes/api/admin/transfer/exports/index.mjs +3 -3
  43. package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +3 -3
  44. package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +3 -3
  45. package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +3 -3
  46. package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +3 -3
  47. package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +3 -3
  48. package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +3 -3
  49. package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +3 -3
  50. package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +3 -3
  51. package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +3 -3
  52. package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +3 -3
  53. package/dist/astro/routes/api/admin/transfer/imports/index.mjs +3 -3
  54. package/dist/astro/routes/api/dashboard.mjs +2 -2
  55. package/dist/astro/routes/api/health.mjs +1 -1
  56. package/dist/astro/routes/api/import/probe.mjs +3 -3
  57. package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
  58. package/dist/astro/routes/api/import/wordpress/media.d.mts +3 -3
  59. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +3 -3
  60. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
  61. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +3 -3
  62. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
  63. package/dist/astro/routes/api/manifest.mjs +1 -1
  64. package/dist/astro/routes/api/mcp.mjs +13 -33
  65. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  66. package/dist/astro/routes/api/media/_id_/confirm.mjs +1 -1
  67. package/dist/astro/routes/api/media/_id_/upload.mjs +1 -1
  68. package/dist/astro/routes/api/media/_id_/usage.mjs +2 -2
  69. package/dist/astro/routes/api/media/_id_.mjs +2 -2
  70. package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +1 -1
  71. package/dist/astro/routes/api/media/upload-url.mjs +1 -1
  72. package/dist/astro/routes/api/media.mjs +2 -2
  73. package/dist/astro/routes/api/redirects/404s/index.mjs +2 -2
  74. package/dist/astro/routes/api/redirects/404s/summary.mjs +2 -2
  75. package/dist/astro/routes/api/redirects/_id_.mjs +2 -2
  76. package/dist/astro/routes/api/redirects/index.mjs +2 -2
  77. package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +8 -8
  78. package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +8 -8
  79. package/dist/astro/routes/api/schema/block-types/index.mjs +8 -8
  80. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +8 -8
  81. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +8 -8
  82. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +8 -8
  83. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +8 -8
  84. package/dist/astro/routes/api/schema/collections/index.mjs +8 -8
  85. package/dist/astro/routes/api/schema/collections/reorder.mjs +8 -8
  86. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +8 -8
  87. package/dist/astro/routes/api/schema/orphans/index.mjs +8 -8
  88. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +2 -2
  89. package/dist/astro/routes/api/settings/backups/archives/index.mjs +2 -2
  90. package/dist/astro/routes/api/settings/backups/export.mjs +2 -2
  91. package/dist/astro/routes/api/settings/backups/index.mjs +2 -2
  92. package/dist/astro/routes/api/settings.mjs +3 -3
  93. package/dist/astro/routes/api/setup/dev-bypass.mjs +4 -4
  94. package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
  95. package/dist/astro/routes/api/setup/index.mjs +35 -10
  96. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  97. package/dist/astro/routes/api/snapshot.mjs +2 -2
  98. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  99. package/dist/astro/routes/robots.txt.mjs +2 -2
  100. package/dist/astro/routes/sitemap-_collection_.xml.mjs +2 -2
  101. package/dist/astro/routes/sitemap.xml.mjs +2 -2
  102. package/dist/astro/types.d.mts +7 -3
  103. package/dist/astro/types.d.mts.map +1 -1
  104. package/dist/{backup-CBu0LrXf.mjs → backup-BQRj3afm.mjs} +3 -3
  105. package/dist/{backup-CBu0LrXf.mjs.map → backup-BQRj3afm.mjs.map} +1 -1
  106. package/dist/cli/index.mjs +6 -6
  107. package/dist/client/index.d.mts +14 -14
  108. package/dist/{config-5ii2haYu.d.mts → config-BjEm_3xL.d.mts} +25 -16
  109. package/dist/config-BjEm_3xL.d.mts.map +1 -0
  110. package/dist/{context-BeGccdUX.mjs → context-BV_7t-Ua.mjs} +6 -6
  111. package/dist/{context-BeGccdUX.mjs.map → context-BV_7t-Ua.mjs.map} +1 -1
  112. package/dist/{dashboard-DjrB-xxZ.mjs → dashboard-BP-iJqpd.mjs} +2 -2
  113. package/dist/{dashboard-DjrB-xxZ.mjs.map → dashboard-BP-iJqpd.mjs.map} +1 -1
  114. package/dist/db/libsql-migrations.mjs +2 -2
  115. package/dist/db/postgres-migrations.mjs +2 -2
  116. package/dist/db/sqlite-migrations.mjs +2 -2
  117. package/dist/{direct-executor-BmuFJJZV.mjs → direct-executor-D2lNadwo.mjs} +2 -2
  118. package/dist/{direct-executor-BmuFJJZV.mjs.map → direct-executor-D2lNadwo.mjs.map} +1 -1
  119. package/dist/{emdash-runtime-DAjghyEG.mjs → emdash-runtime-CtfnZmPi.mjs} +56 -56
  120. package/dist/emdash-runtime-CtfnZmPi.mjs.map +1 -0
  121. package/dist/{entry-lock-DVhCt49A.d.mts → entry-lock-Cf5OoIgN.d.mts} +18 -18
  122. package/dist/{entry-lock-DVhCt49A.d.mts.map → entry-lock-Cf5OoIgN.d.mts.map} +1 -1
  123. package/dist/{hreflang-DMFBL2eb.mjs → hreflang-CbVI4paR.mjs} +2 -2
  124. package/dist/{hreflang-DMFBL2eb.mjs.map → hreflang-CbVI4paR.mjs.map} +1 -1
  125. package/dist/{identity-IyNWwXr6.mjs → identity-Be5kzutJ.mjs} +2 -2
  126. package/dist/{identity-IyNWwXr6.mjs.map → identity-Be5kzutJ.mjs.map} +1 -1
  127. package/dist/{import-C3JZplWq.mjs → import-YzfKCaco.mjs} +2 -2
  128. package/dist/{import-C3JZplWq.mjs.map → import-YzfKCaco.mjs.map} +1 -1
  129. package/dist/index.d.mts +4 -4
  130. package/dist/index.mjs +16 -16
  131. package/dist/{manifest-builder-B8zR-73J.mjs → manifest-builder-BmSAOOEF.mjs} +2 -2
  132. package/dist/{manifest-builder-B8zR-73J.mjs.map → manifest-builder-BmSAOOEF.mjs.map} +1 -1
  133. package/dist/media/local-runtime.d.mts +3 -3
  134. package/dist/media/local-runtime.mjs +3 -3
  135. package/dist/{media-Qp5S3eH3.mjs → media-CPXTbb1R.mjs} +20 -2
  136. package/dist/media-CPXTbb1R.mjs.map +1 -0
  137. package/dist/{media-h7tOiDLj.mjs → media-JXRWDhw3.mjs} +77 -4
  138. package/dist/media-JXRWDhw3.mjs.map +1 -0
  139. package/dist/{media-usage-My-Vi-ex.mjs → media-usage-De-zeR9E.mjs} +2 -2
  140. package/dist/{media-usage-My-Vi-ex.mjs.map → media-usage-De-zeR9E.mjs.map} +1 -1
  141. package/dist/{menus-TsSGmSR4.mjs → menus-Cm68rsAq.mjs} +4 -4
  142. package/dist/{menus-TsSGmSR4.mjs.map → menus-Cm68rsAq.mjs.map} +1 -1
  143. package/dist/migrations/index.mjs +2 -2
  144. package/dist/page/index.mjs +2 -2
  145. package/dist/plugin-test-runtime.d.mts +3 -3
  146. package/dist/plugin-test-runtime.mjs +18 -18
  147. package/dist/plugin-utils.d.mts +3 -3
  148. package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
  149. package/dist/plugins/host.d.mts +1 -1
  150. package/dist/plugins/host.mjs +6 -6
  151. package/dist/{redirect-iJf5fltD.mjs → redirect-B29UjdUn.mjs} +10 -14
  152. package/dist/redirect-B29UjdUn.mjs.map +1 -0
  153. package/dist/{redirects-CYu1u1aK.mjs → redirects-Ca2MPfEG.mjs} +2 -2
  154. package/dist/{redirects-CYu1u1aK.mjs.map → redirects-Ca2MPfEG.mjs.map} +1 -1
  155. package/dist/{routes-CUsM40Cc.mjs → routes-vrR95WR6.mjs} +2 -2
  156. package/dist/{routes-CUsM40Cc.mjs.map → routes-vrR95WR6.mjs.map} +1 -1
  157. package/dist/runtime.d.mts +3 -3
  158. package/dist/seed/index.mjs +4 -4
  159. package/dist/seo/index.mjs +3 -3
  160. package/dist/{settings-CgjRWGW9.mjs → settings-B4vDl1fK.mjs} +2 -2
  161. package/dist/{settings-CgjRWGW9.mjs.map → settings-B4vDl1fK.mjs.map} +1 -1
  162. package/dist/{settings-2xNYr90o.mjs → settings-Dyj6v8C4.mjs} +2 -2
  163. package/dist/{settings-2xNYr90o.mjs.map → settings-Dyj6v8C4.mjs.map} +1 -1
  164. package/dist/{snapshot-CugwUqKQ.mjs → snapshot-PawoX-WG.mjs} +180 -3
  165. package/dist/snapshot-PawoX-WG.mjs.map +1 -0
  166. package/dist/{storage-query-Bdv7A62k.d.mts → storage-query-FXJH5pI8.d.mts} +6 -1
  167. package/dist/storage-query-FXJH5pI8.d.mts.map +1 -0
  168. package/dist/{transfer-BhUCrgDc.mjs → transfer-DgeCvHnj.mjs} +4 -4
  169. package/dist/{transfer-BhUCrgDc.mjs.map → transfer-DgeCvHnj.mjs.map} +1 -1
  170. package/dist/validate-CM79QXYE.d.mts.map +1 -1
  171. package/dist/version-DXsxRrob.mjs +7 -0
  172. package/dist/{version-DUQDHgrf.mjs.map → version-DXsxRrob.mjs.map} +1 -1
  173. package/package.json +5 -5
  174. package/src/api/handlers/index.ts +1 -0
  175. package/src/api/handlers/media-upload.ts +17 -71
  176. package/src/api/handlers/media.ts +97 -3
  177. package/src/api/handlers/snapshot.ts +257 -3
  178. package/src/astro/integration/vite-config.ts +10 -4
  179. package/src/astro/middleware.ts +1 -0
  180. package/src/astro/routes/api/setup/index.ts +66 -23
  181. package/src/astro/routes/api/snapshot.ts +2 -1
  182. package/src/astro/types.ts +5 -0
  183. package/src/cleanup.ts +10 -0
  184. package/src/components/Columns.astro +3 -1
  185. package/src/components/Cover.astro +3 -1
  186. package/src/database/repositories/media.ts +40 -1
  187. package/src/database/repositories/redirect.ts +17 -34
  188. package/src/emdash-runtime.ts +28 -0
  189. package/src/index.ts +1 -0
  190. package/src/mcp/server.ts +13 -48
  191. package/src/seed/apply.ts +322 -41
  192. package/dist/apply-CoT603H3.mjs.map +0 -1
  193. package/dist/config-5ii2haYu.d.mts.map +0 -1
  194. package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
  195. package/dist/media-Qp5S3eH3.mjs.map +0 -1
  196. package/dist/media-h7tOiDLj.mjs.map +0 -1
  197. package/dist/redirect-iJf5fltD.mjs.map +0 -1
  198. package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
  199. package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
  200. package/dist/version-DUQDHgrf.mjs +0 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emdash-runtime-CtfnZmPi.mjs","names":["normalizeManifestRoute","resolveExclusiveHooksShared","uploadMedia"],"sources":["../src/api/handlers/media-upload.ts","../src/i18n/repair-locale-casing.ts","../src/i18n/taxonomy-locale-diagnostic.ts","../src/plugins/sandbox/proxy.ts","../src/plugins/sandbox/runner-options.ts","../src/media/usage/cleanup.ts","../src/cleanup.ts","../src/comments/moderator.ts","../src/scheduled-publish.ts","../src/emdash-runtime.ts"],"sourcesContent":["/**\n * Programmatic media upload handler (MCP `media_upload` tool).\n *\n * Accepts file bytes as base64, then runs the same pipeline as the multipart\n * REST upload route: allowlist + size validation, content-hash deduplication,\n * storage upload, image metadata enrichment, and record creation.\n */\n\nimport * as path from \"node:path\";\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { MediaRepository, type MediaItem } from \"../../database/repositories/media.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { enrichImageMetadata } from \"../../media/enrich.js\";\nimport { matchesMimeAllowlist, normalizeMime } from \"../../media/mime.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport { decodeBase64Bytes } from \"../../utils/base64.js\";\nimport { computeContentHash } from \"../../utils/hash.js\";\nimport { CONTENT_TYPE_RE, DEFAULT_MAX_UPLOAD_SIZE, formatFileSize } from \"../schemas/media.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { GLOBAL_UPLOAD_ALLOWLIST } from \"./media-allowlist.js\";\n\nexport interface MediaUploadInput {\n\t/** Original filename (e.g. 'logo.png'); the extension is kept on the storage key. */\n\tfilename: string;\n\t/** Base64-encoded file contents. */\n\tbase64: string;\n\t/** MIME type of the decoded bytes. */\n\tcontentType: string;\n\t/** Alt text stored on the media record. */\n\talt?: string;\n\tauthorId?: string;\n\t/** Upload size limit in bytes (defaults to DEFAULT_MAX_UPLOAD_SIZE). */\n\tmaxUploadSize?: number;\n}\n\nexport type MediaUploadResult = ApiResult<{\n\titem: MediaItem & { url: string };\n\tdeduplicated?: boolean;\n}>;\n\nexport interface MediaUploadHooks {\n\tbeforeUpload?(file: {\n\t\tname: string;\n\t\ttype: string;\n\t\tsize: number;\n\t}): Promise<{ name: string; type: string; size: number }>;\n}\n\nfunction fail(code: string, message: string): MediaUploadResult {\n\treturn { success: false, error: { code, message } };\n}\n\n/** Same relative-URL shape the REST media routes return. */\nfunction withUrl(item: MediaItem): MediaItem & { url: string } {\n\treturn { ...item, url: `/_emdash/api/media/file/${item.storageKey}` };\n}\n\n/** Decode the file bytes after rejecting an oversized encoded payload. */\nasync function acquireBytes(\n\tinput: MediaUploadInput,\n\tmaxUploadSize: number,\n): Promise<{ bytes: Uint8Array; mimeType: string } | MediaUploadResult> {\n\t// Cheap size precheck on the encoded string (decoded size is ~3/4 of\n\t// the base64 length) before allocating the decoded buffer.\n\tif ((input.base64.length * 3) / 4 > maxUploadSize) {\n\t\treturn fail(\n\t\t\t\"PAYLOAD_TOO_LARGE\",\n\t\t\t`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,\n\t\t);\n\t}\n\ttry {\n\t\treturn { bytes: decodeBase64Bytes(input.base64), mimeType: input.contentType };\n\t} catch {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid base64 data\");\n\t}\n}\n\n/**\n * Upload a media file from base64 data.\n *\n * Mirrors the REST `POST /_emdash/api/media` route: global MIME allowlist,\n * size limit, content-hash dedupe (returns the existing item with\n * `deduplicated: true`), storage upload with cleanup on failure, and\n * image metadata enrichment (dimensions, blurhash, dominant color).\n */\nexport async function handleMediaUpload(\n\tdb: Kysely<Database>,\n\tstorage: Storage,\n\tinput: MediaUploadInput,\n\thooks: MediaUploadHooks = {},\n): Promise<MediaUploadResult> {\n\tif (!input.base64 || !input.contentType) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"base64 and contentType are required\");\n\t}\n\n\tconst rawMax = input.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;\n\tif (!Number.isFinite(rawMax) || rawMax <= 0) {\n\t\treturn fail(\"CONFIGURATION_ERROR\", \"Invalid maxUploadSize configuration\");\n\t}\n\n\tconst acquired = await acquireBytes(input, rawMax);\n\tif (\"success\" in acquired) return acquired;\n\tconst { bytes } = acquired;\n\n\t// Validate the raw MIME string before normalize/allowlist: normalizeMime\n\t// only strips parameters and matchesMimeAllowlist only checks startsWith,\n\t// so without this a crafted value like \"image/png\\r\\nX-Evil: 1\" would\n\t// reach the storage backend's ContentType header and be echoed by the\n\t// media file serving route.\n\tif (!CONTENT_TYPE_RE.test(acquired.mimeType)) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid content type\");\n\t}\n\tlet filename = input.filename;\n\tlet mimeType = normalizeMime(acquired.mimeType);\n\tlet size = bytes.byteLength;\n\n\tif (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) {\n\t\treturn fail(\"INVALID_TYPE\", \"File type not allowed\");\n\t}\n\tif (bytes.byteLength > rawMax) {\n\t\treturn fail(\"PAYLOAD_TOO_LARGE\", `File exceeds maximum size of ${formatFileSize(rawMax)}`);\n\t}\n\n\ttry {\n\t\tif (hooks.beforeUpload) {\n\t\t\tconst processed = await hooks.beforeUpload({\n\t\t\t\tname: filename,\n\t\t\t\ttype: mimeType,\n\t\t\t\tsize: bytes.byteLength,\n\t\t\t});\n\t\t\tfilename = processed.name;\n\t\t\tmimeType = normalizeMime(processed.type);\n\t\t\tsize = processed.size;\n\t\t\tif (!CONTENT_TYPE_RE.test(processed.type)) {\n\t\t\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid content type\");\n\t\t\t}\n\t\t\tif (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) {\n\t\t\t\treturn fail(\"INVALID_TYPE\", \"File type not allowed\");\n\t\t\t}\n\t\t}\n\t\tconst contentHash = await computeContentHash(bytes);\n\t\tconst repo = new MediaRepository(db);\n\n\t\tconst existing = await repo.findByContentHash(contentHash);\n\t\tif (existing) {\n\t\t\treturn { success: true, data: { item: withUrl(existing), deduplicated: true } };\n\t\t}\n\n\t\tconst storageKey = `${ulid()}${path.extname(filename)}`;\n\t\tawait storage.upload({ key: storageKey, body: bytes, contentType: mimeType });\n\n\t\ttry {\n\t\t\tconst enriched = await enrichImageMetadata(bytes, mimeType);\n\t\t\tconst item = await repo.create({\n\t\t\t\tfilename,\n\t\t\t\tmimeType,\n\t\t\t\tsize,\n\t\t\t\twidth: enriched.width,\n\t\t\t\theight: enriched.height,\n\t\t\t\talt: input.alt,\n\t\t\t\tstorageKey,\n\t\t\t\tcontentHash,\n\t\t\t\tblurhash: enriched.blurhash,\n\t\t\t\tdominantColor: enriched.dominantColor,\n\t\t\t\tauthorId: input.authorId,\n\t\t\t});\n\t\t\treturn { success: true, data: { item: withUrl(item) } };\n\t\t} catch (error) {\n\t\t\t// Don't leave an orphaned object in storage when record creation fails\n\t\t\ttry {\n\t\t\t\tawait storage.delete(storageKey);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t} catch {\n\t\treturn fail(\"UPLOAD_ERROR\", \"Upload failed\");\n\t}\n}\n","import type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { listTablesLike } from \"../database/dialect-helpers.js\";\nimport type { Database } from \"../database/types.js\";\n\n/** Rewrite stored locales to the exact casing used by the site configuration. */\nexport async function repairLocaleCasing(\n\tdb: Kysely<Database>,\n\tconfiguredLocales: readonly string[],\n): Promise<void> {\n\tconst tableNames = await listTablesLike(db, \"ec_%\");\n\n\tfor (const tableName of tableNames) {\n\t\tconst table = sql.ref(tableName);\n\t\tfor (const locale of configuredLocales) {\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${table} AS target\n\t\t\t\tSET locale = ${locale}\n\t\t\t\tWHERE lower(target.locale) = lower(${locale})\n\t\t\t\t\tAND target.locale != ${locale}\n\t\t\t\t\tAND NOT EXISTS (\n\t\t\t\t\t\tSELECT 1\n\t\t\t\t\t\tFROM ${table} AS existing\n\t\t\t\t\t\tWHERE existing.slug = target.slug AND existing.locale = ${locale}\n\t\t\t\t\t)\n\t\t\t\t\tAND target.id = (\n\t\t\t\t\t\tSELECT MIN(candidate.id)\n\t\t\t\t\t\tFROM ${table} AS candidate\n\t\t\t\t\t\tWHERE candidate.slug = target.slug\n\t\t\t\t\t\t\tAND lower(candidate.locale) = lower(${locale})\n\t\t\t\t\t\t\tAND candidate.locale != ${locale}\n\t\t\t\t\t)\n\t\t\t`.execute(db);\n\t\t}\n\t}\n}\n","import type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nconst REPAIR_GUIDE =\n\t\"https://docs.emdashcms.com/guides/internationalization/#repairing-taxonomy-locale-mismatches\";\n\ninterface TaxonomyLocaleMismatch {\n\tsource: \"definitions\" | \"terms\";\n\tlocale: string;\n}\n\nexport async function warnAboutUnconfiguredTaxonomyLocales(\n\tdb: Kysely<Database>,\n\tconfiguredLocales: readonly string[],\n\tdefinitionLocales?: readonly string[],\n): Promise<void> {\n\tconst supportedLocales = configuredLocales.length > 0 ? configuredLocales : [\"en\"];\n\tconst definitionRows =\n\t\tdefinitionLocales === undefined\n\t\t\t? await db\n\t\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t\t.select(\"locale\")\n\t\t\t\t\t.distinct()\n\t\t\t\t\t.where(\"locale\", \"not in\", supportedLocales)\n\t\t\t\t\t.execute()\n\t\t\t: [...new Set(definitionLocales)]\n\t\t\t\t\t.filter((locale) => !supportedLocales.includes(locale))\n\t\t\t\t\t.map((locale) => ({ locale }));\n\tconst termRows = await db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.select(\"locale\")\n\t\t.distinct()\n\t\t.where(\"locale\", \"not in\", supportedLocales)\n\t\t.execute();\n\tconst mismatches: TaxonomyLocaleMismatch[] = [\n\t\t...definitionRows.map(({ locale }) => ({ source: \"definitions\" as const, locale })),\n\t\t...termRows.map(({ locale }) => ({ source: \"terms\" as const, locale })),\n\t].toSorted((a, b) => a.source.localeCompare(b.source) || a.locale.localeCompare(b.locale));\n\tif (mismatches.length === 0) return;\n\n\tconst details = mismatches.map(({ source, locale }) => `${source}: ${locale}`).join(\"; \");\n\tconsole.warn(\n\t\t`EmDash: Taxonomy rows use locales outside the configured locales (${supportedLocales.join(\", \")}): ${details}. ` +\n\t\t\t`Locale-scoped reads may not return these rows. Review and repair them explicitly: ${REPAIR_GUIDE}`,\n\t);\n}\n","import { adaptSandboxEntry } from \"../adapt-sandbox-entry.js\";\nimport { normalizeManifestHook } from \"../manifest-schema.js\";\nimport { ContentSaveRejectedError } from \"../save-rejection.js\";\nimport type { PluginManifest, ResolvedPlugin } from \"../types.js\";\nimport type { PageMetadataContribution } from \"../types.js\";\nimport { inspectSandboxHookResult } from \"./hook-result.js\";\nimport type { SandboxedPluginInstance } from \"./types.js\";\n\nconst SANDBOX_REJECTION = Symbol(\"emdash:sandbox-save-rejection\");\n\nexport interface SandboxSaveRejectionDetails {\n\tpluginId: string;\n\treason: string;\n}\n\nconst VALID_METADATA_KINDS = new Set([\"meta\", \"property\", \"link\", \"jsonld\"]);\nconst VALID_LINK_REL = new Set([\n\t\"canonical\",\n\t\"alternate\",\n\t\"author\",\n\t\"license\",\n\t\"nlweb\",\n\t\"site.standard.document\",\n]);\n\nfunction isValidMetadataContribution(value: unknown): value is PageMetadataContribution {\n\tif (!value || typeof value !== \"object\" || !(\"kind\" in value)) return false;\n\tconst contribution = value as Record<string, unknown>;\n\tif (typeof contribution.kind !== \"string\" || !VALID_METADATA_KINDS.has(contribution.kind)) {\n\t\treturn false;\n\t}\n\tif (contribution.kind === \"meta\") {\n\t\treturn typeof contribution.name === \"string\" && typeof contribution.content === \"string\";\n\t}\n\tif (contribution.kind === \"property\") {\n\t\treturn typeof contribution.property === \"string\" && typeof contribution.content === \"string\";\n\t}\n\tif (contribution.kind === \"link\") {\n\t\treturn (\n\t\t\ttypeof contribution.href === \"string\" &&\n\t\t\ttypeof contribution.rel === \"string\" &&\n\t\t\tVALID_LINK_REL.has(contribution.rel)\n\t\t);\n\t}\n\treturn contribution.graph != null && typeof contribution.graph === \"object\";\n}\n\nexport function getSandboxSaveRejectionDetails(error: unknown): SandboxSaveRejectionDetails | null {\n\tif (!(error instanceof ContentSaveRejectedError) || !(SANDBOX_REJECTION in error)) return null;\n\tconst details: unknown = Object.getOwnPropertyDescriptor(error, SANDBOX_REJECTION)?.value;\n\tif (!details || typeof details !== \"object\") return null;\n\tif (!(\"pluginId\" in details) || typeof details.pluginId !== \"string\") return null;\n\tif (!(\"reason\" in details) || typeof details.reason !== \"string\") return null;\n\treturn { pluginId: details.pluginId, reason: details.reason };\n}\n\n/**\n * Represent an isolate-backed plugin inside the normal host hook pipeline.\n * The proxy contains metadata and RPC handlers only; plugin code remains in\n * the runner isolate.\n */\nexport function createSandboxedPluginProxy(\n\tmanifest: PluginManifest,\n\tinstance: SandboxedPluginInstance,\n): ResolvedPlugin {\n\tconst hooks: Record<string, unknown> = {};\n\tfor (const entry of manifest.hooks) {\n\t\tconst hook = normalizeManifestHook(entry);\n\t\t// Raw page fragments and site render components remain trusted-only.\n\t\tif (hook.name === \"page:fragments\") continue;\n\n\t\thooks[hook.name] = {\n\t\t\thandler: async (event: unknown) => {\n\t\t\t\tconst result = await instance.invokeHook(hook.name, event);\n\t\t\t\tif (hook.name === \"page:metadata\") {\n\t\t\t\t\tif (result == null) return null;\n\t\t\t\t\tconst contributions = (Array.isArray(result) ? result : [result]).filter(\n\t\t\t\t\t\tisValidMetadataContribution,\n\t\t\t\t\t);\n\t\t\t\t\treturn contributions.length > 0 ? contributions : null;\n\t\t\t\t}\n\t\t\t\tif (hook.name !== \"content:beforeSave\") return result;\n\n\t\t\t\tconst inspection = inspectSandboxHookResult(result);\n\t\t\t\tif (inspection.kind === \"error\") {\n\t\t\t\t\tconst error = new ContentSaveRejectedError(\"Save rejected by a sandboxed plugin\");\n\t\t\t\t\treturn Promise.reject(\n\t\t\t\t\t\tObject.assign(error, {\n\t\t\t\t\t\t\t[SANDBOX_REJECTION]: {\n\t\t\t\t\t\t\t\tpluginId: manifest.id,\n\t\t\t\t\t\t\t\treason: inspection.error.reason,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (inspection.kind === \"malformed\") {\n\t\t\t\t\tthrow new Error(`Sandboxed plugin \"${manifest.id}\" returned an invalid hook result`);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t\tpriority: hook.priority,\n\t\t\ttimeout: hook.timeout,\n\t\t\tdependencies: hook.dependencies,\n\t\t\terrorPolicy: hook.errorPolicy,\n\t\t\texclusive: hook.exclusive,\n\t\t};\n\t}\n\n\treturn adaptSandboxEntry(\n\t\t{ hooks },\n\t\t{\n\t\t\tid: manifest.id,\n\t\t\tversion: manifest.version,\n\t\t\tentrypoint: \"\",\n\t\t\tformat: \"standard\",\n\t\t\tcapabilities: manifest.capabilities,\n\t\t\tallowedHosts: manifest.allowedHosts,\n\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- descriptor typing predates composite manifest indexes; adapter preserves them\n\t\t\tstorage: manifest.storage as never,\n\t\t\tadminPages: manifest.admin.pages,\n\t\t\tadminWidgets: manifest.admin.widgets,\n\t\t\teditorPanels: manifest.admin.editorPanels,\n\t\t\teditorActions: manifest.admin.editorActions,\n\t\t\tsettingsSchema: manifest.admin.settingsSchema,\n\t\t\tportableTextBlocks: manifest.admin.portableTextBlocks,\n\t\t\tfieldWidgets: manifest.admin.fieldWidgets,\n\t\t},\n\t);\n}\n","import { createSiteInfo, type SiteInfoOptions } from \"../context.js\";\nimport type { SandboxOptions } from \"./types.js\";\n\n/**\n * Build platform sandbox options with the same normalized site context used\n * by trusted plugin hooks and routes.\n */\nexport function createSandboxRunnerOptions(\n\toptions: Omit<SandboxOptions, \"siteInfo\">,\n\tsiteInfo?: SiteInfoOptions,\n): SandboxOptions {\n\treturn {\n\t\t...options,\n\t\tsiteInfo: createSiteInfo(siteInfo ?? {}),\n\t};\n}\n","import type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tMediaUsageRepository,\n\ttype MediaUsageCleanupCandidate,\n\ttype MediaUsageCleanupCursor,\n} from \"../../database/repositories/media-usage.js\";\nimport type { Database } from \"../../database/types.js\";\n\nexport const MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT = 250;\nexport const MEDIA_USAGE_CLEANUP_DELETE_LIMIT = 50;\nexport const MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT = 49;\nexport const MEDIA_USAGE_CLEANUP_INTERVAL_MS = 60 * 1000;\nexport const MEDIA_USAGE_CLEANUP_LEASE_MS = 5 * 60 * 1000;\nexport const MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS = 60 * 60 * 1000;\nconst MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS = 5 * 1000;\n\nexport interface MediaUsageCleanupResult {\n\tstatus: \"completed\" | \"failed\" | \"skipped\";\n\tcandidateRows: number;\n\tdeletedRows: number;\n\tdeletedOrphans: number;\n\tdeletedStale: number;\n\tdeletedAbandoned: number;\n\tdeletedWriteLeases: number;\n\tbacklogLowerBound: number;\n\tscanHasMore: boolean;\n\tdurationMs: number;\n}\n\ninterface CleanupCandidates {\n\torphanIds: string[];\n\tstaleIds: string[];\n\tabandonedIds: string[];\n\tentries: CleanupCandidateEntry[];\n}\n\ntype CleanupTarget = \"orphan\" | \"stale\" | \"abandoned\";\n\ninterface CleanupCandidateEntry {\n\tcandidate: MediaUsageCleanupCandidate;\n\ttarget: CleanupTarget | null;\n}\n\n/**\n * Reclaims a bounded window of obsolete media-usage occurrences.\n *\n * The persisted claim makes a cron tick single-flight across Worker isolates\n * and Node processes.\n */\nexport async function cleanupMediaUsage(db: Kysely<Database>): Promise<MediaUsageCleanupResult> {\n\tconst startedMs = Date.now();\n\tconst canIssueStatement = () => withinBudget(startedMs);\n\tconst repo = new MediaUsageRepository(db);\n\tconst leaseToken = ulid();\n\tconst claim = await repo.claimMediaUsageCleanup({\n\t\tleaseToken,\n\t\tleaseDurationSeconds: MEDIA_USAGE_CLEANUP_LEASE_MS / 1000,\n\t\tnextEligibleDelaySeconds: MEDIA_USAGE_CLEANUP_INTERVAL_MS / 1000,\n\t\tsweepSafetyWindowSeconds: MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS / 1000,\n\t});\n\tif (!claim) return emptyResult(\"skipped\", elapsedSince(startedMs));\n\n\tlet candidateRows = 0;\n\tlet deletedOrphans = 0;\n\tlet deletedStale = 0;\n\tlet deletedAbandoned = 0;\n\tlet deletedWriteLeases = 0;\n\tlet backlogLowerBound = 0;\n\tlet scanHasMore = false;\n\tlet nextCursor = claim.cursor;\n\tlet sweepComplete = false;\n\n\ttry {\n\t\tif (canIssueStatement()) {\n\t\t\tdeletedWriteLeases = await repo.deleteExpiredGenerationWriteLeases(\n\t\t\t\tMEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT,\n\t\t\t\tcleanupLease(leaseToken),\n\t\t\t\tcanIssueStatement,\n\t\t\t);\n\t\t}\n\n\t\tconst cutoff = claim.scanBeforeAt;\n\t\tconst candidates = await repo.findMediaUsageCleanupCandidates({\n\t\t\tcutoff,\n\t\t\tcursor: claim.cursor,\n\t\t\tlimit: MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT,\n\t\t\tcleanupLease: cleanupLease(leaseToken),\n\t\t\tcanIssueStatement,\n\t\t});\n\t\tif (candidates) {\n\t\t\tcandidateRows = candidates.length;\n\t\t\tscanHasMore = candidates.length === MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT;\n\n\t\t\tconst selected = selectCleanupCandidates(candidates, claim.claimedAt);\n\t\t\tbacklogLowerBound =\n\t\t\t\tselected.orphanIds.length + selected.staleIds.length + selected.abandonedIds.length;\n\t\t\tconst completedTargets = new Set<CleanupTarget>();\n\t\t\tlet canContinue = true;\n\n\t\t\tif (canIssueStatement() && selected.orphanIds.length > 0) {\n\t\t\t\tdeletedOrphans = await repo.deleteOrphanOccurrencesOlderThan(\n\t\t\t\t\tcutoff,\n\t\t\t\t\tselected.orphanIds.length,\n\t\t\t\t\t{\n\t\t\t\t\t\tcandidateIds: selected.orphanIds,\n\t\t\t\t\t\tcleanupLease: cleanupLease(leaseToken),\n\t\t\t\t\t\tcanIssueStatement,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tcanContinue = deletedOrphans === selected.orphanIds.length;\n\t\t\t\tif (canContinue) completedTargets.add(\"orphan\");\n\t\t\t}\n\t\t\tif (canContinue && canIssueStatement() && selected.staleIds.length > 0) {\n\t\t\t\tdeletedStale = await repo.deleteStaleGenerationsOlderThan(\n\t\t\t\t\tcutoff,\n\t\t\t\t\tselected.staleIds.length,\n\t\t\t\t\t{\n\t\t\t\t\t\tcandidateIds: selected.staleIds,\n\t\t\t\t\t\tcleanupLease: cleanupLease(leaseToken),\n\t\t\t\t\t\tcanIssueStatement,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tcanContinue = deletedStale === selected.staleIds.length;\n\t\t\t\tif (canContinue) completedTargets.add(\"stale\");\n\t\t\t}\n\t\t\tif (canContinue && canIssueStatement() && selected.abandonedIds.length > 0) {\n\t\t\t\tdeletedAbandoned = await repo.deleteAbandonedGenerationsOlderThan(\n\t\t\t\t\tcutoff,\n\t\t\t\t\tselected.abandonedIds.length,\n\t\t\t\t\t{\n\t\t\t\t\t\tcandidateIds: selected.abandonedIds,\n\t\t\t\t\t\tcleanupLease: cleanupLease(leaseToken),\n\t\t\t\t\t\tcanIssueStatement,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tif (deletedAbandoned === selected.abandonedIds.length) {\n\t\t\t\t\tcompletedTargets.add(\"abandoned\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst hasIncompleteTargets = selected.entries.some(\n\t\t\t\t(entry) => entry.target !== null && !completedTargets.has(entry.target),\n\t\t\t);\n\t\t\tsweepComplete =\n\t\t\t\t!scanHasMore && selected.entries.length === candidates.length && !hasIncompleteTargets;\n\t\t\tnextCursor = sweepComplete\n\t\t\t\t? null\n\t\t\t\t: cursorAfterCompletedCandidates(selected, completedTargets, claim.cursor);\n\t\t}\n\n\t\tconst durationMs = elapsedSince(startedMs);\n\t\tconst completed = await repo.completeMediaUsageCleanup({\n\t\t\tleaseToken,\n\t\t\tnextCursor,\n\t\t\tsweepComplete,\n\t\t\tcandidateCount: candidateRows,\n\t\t\tdeletedOrphans,\n\t\t\tdeletedStale,\n\t\t\tdeletedAbandoned,\n\t\t\tdeletedWriteLeases,\n\t\t\tbacklogLowerBound,\n\t\t\tscanHasMore,\n\t\t\tdurationMs,\n\t\t});\n\n\t\treturn {\n\t\t\tstatus: completed ? \"completed\" : \"skipped\",\n\t\t\tcandidateRows,\n\t\t\tdeletedRows: deletedOrphans + deletedStale + deletedAbandoned,\n\t\t\tdeletedOrphans,\n\t\t\tdeletedStale,\n\t\t\tdeletedAbandoned,\n\t\t\tdeletedWriteLeases,\n\t\t\tbacklogLowerBound,\n\t\t\tscanHasMore,\n\t\t\tdurationMs,\n\t\t};\n\t} catch (error) {\n\t\tconst durationMs = elapsedSince(startedMs);\n\t\tconst failures = Math.min(claim.consecutiveFailures + 1, 5);\n\t\ttry {\n\t\t\tawait repo.failMediaUsageCleanup({\n\t\t\t\tleaseToken,\n\t\t\t\tretryDelaySeconds: failureDelayMs(failures) / 1000,\n\t\t\t\tconsecutiveFailures: failures,\n\t\t\t\tdurationMs,\n\t\t\t\terrorCode: \"MEDIA_USAGE_CLEANUP_FAILED\",\n\t\t\t});\n\t\t} catch (failureError) {\n\t\t\tconsole.error(\"[media-usage-cleanup] Failed to record cleanup failure:\", failureError);\n\t\t}\n\t\tconsole.error(\"[media-usage-cleanup] Cleanup failed:\", error);\n\t\treturn {\n\t\t\t...emptyResult(\"failed\", durationMs),\n\t\t\tcandidateRows,\n\t\t\tdeletedRows: deletedOrphans + deletedStale + deletedAbandoned,\n\t\t\tdeletedOrphans,\n\t\t\tdeletedStale,\n\t\t\tdeletedAbandoned,\n\t\t\tdeletedWriteLeases,\n\t\t\tbacklogLowerBound,\n\t\t\tscanHasMore,\n\t\t};\n\t}\n}\n\nfunction selectCleanupCandidates(\n\tcandidates: readonly MediaUsageCleanupCandidate[],\n\tactiveLeaseAt: string,\n): CleanupCandidates {\n\tconst orphanIds: string[] = [];\n\tconst staleIds: string[] = [];\n\tconst abandonedIds: string[] = [];\n\tconst entries: CleanupCandidateEntry[] = [];\n\n\tfor (const candidate of candidates) {\n\t\tif (hasActiveWriteLease(candidate, activeLeaseAt)) {\n\t\t\tentries.push({ candidate, target: null });\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst target = cleanupTarget(candidate);\n\t\tif (target === null) {\n\t\t\tentries.push({ candidate, target: null });\n\t\t\tcontinue;\n\t\t}\n\t\tif (\n\t\t\torphanIds.length + staleIds.length + abandonedIds.length >=\n\t\t\tMEDIA_USAGE_CLEANUP_DELETE_LIMIT\n\t\t) {\n\t\t\tbreak;\n\t\t}\n\t\tif (target === \"orphan\") orphanIds.push(candidate.id);\n\t\tif (target === \"stale\") staleIds.push(candidate.id);\n\t\tif (target === \"abandoned\") abandonedIds.push(candidate.id);\n\t\tentries.push({ candidate, target });\n\t}\n\n\treturn { orphanIds, staleIds, abandonedIds, entries };\n}\n\nfunction cleanupTarget(candidate: MediaUsageCleanupCandidate): CleanupTarget | null {\n\tif (candidate.currentGeneration === null) return \"orphan\";\n\tif (candidate.currentGeneration === candidate.generation || candidate.indexedAt === null)\n\t\treturn null;\n\treturn candidate.createdAt < candidate.indexedAt ? \"stale\" : \"abandoned\";\n}\n\nfunction hasActiveWriteLease(\n\tcandidate: MediaUsageCleanupCandidate,\n\tactiveLeaseAt: string,\n): boolean {\n\treturn candidate.writeLeaseExpiresAt !== null && candidate.writeLeaseExpiresAt > activeLeaseAt;\n}\n\nfunction cursorFor(candidate: MediaUsageCleanupCandidate): MediaUsageCleanupCursor {\n\treturn { createdAt: candidate.createdAt, id: candidate.id };\n}\n\nfunction cursorAfterCompletedCandidates(\n\tselected: CleanupCandidates,\n\tcompletedTargets: ReadonlySet<CleanupTarget>,\n\tpriorCursor: MediaUsageCleanupCursor | null,\n): MediaUsageCleanupCursor | null {\n\tlet cursor = priorCursor;\n\tfor (const entry of selected.entries) {\n\t\tif (entry.target !== null && !completedTargets.has(entry.target)) break;\n\t\tcursor = cursorFor(entry.candidate);\n\t}\n\treturn cursor;\n}\n\nfunction emptyResult(\n\tstatus: Extract<MediaUsageCleanupResult[\"status\"], \"failed\" | \"skipped\">,\n\tdurationMs: number,\n): MediaUsageCleanupResult {\n\treturn {\n\t\tstatus,\n\t\tcandidateRows: 0,\n\t\tdeletedRows: 0,\n\t\tdeletedOrphans: 0,\n\t\tdeletedStale: 0,\n\t\tdeletedAbandoned: 0,\n\t\tdeletedWriteLeases: 0,\n\t\tbacklogLowerBound: 0,\n\t\tscanHasMore: false,\n\t\tdurationMs,\n\t};\n}\n\nfunction withinBudget(startedMs: number): boolean {\n\treturn elapsedSince(startedMs) < MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS;\n}\n\nfunction elapsedSince(startedMs: number): number {\n\treturn Math.max(0, Date.now() - startedMs);\n}\n\nfunction failureDelayMs(consecutiveFailures: number): number {\n\treturn Math.min(2 ** (consecutiveFailures - 1), 15) * MEDIA_USAGE_CLEANUP_INTERVAL_MS;\n}\n\nfunction cleanupLease(leaseToken: string) {\n\treturn { leaseToken };\n}\n","/**\n * System cleanup\n *\n * Runs periodic maintenance tasks that prevent unbounded accumulation of\n * expired or stale data. Called from cron scheduler ticks and (for latency-\n * sensitive subsystems) inline during relevant requests.\n *\n * Each subsystem cleanup is independent and non-fatal -- if one fails, the\n * rest still run. Failures are logged but never surface to callers.\n */\n\nimport { createKyselyAdapter, type AuthTables } from \"@emdash-cms/auth/adapters/kysely\";\nimport type { Kysely } from \"kysely\";\n\nimport { cleanupExpiredChallenges } from \"./auth/challenge-store.js\";\nimport { MediaRepository } from \"./database/repositories/media.js\";\nimport { RedirectRepository } from \"./database/repositories/redirect.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport type { Database } from \"./database/types.js\";\nimport { removeUploadAttempt } from \"./media/upload-attempts.js\";\nimport { cleanupMediaUsage } from \"./media/usage/cleanup.js\";\nimport type { Storage } from \"./storage/types.js\";\n\n/**\n * Result of a system cleanup run.\n * Each field is the number of rows deleted, or -1 if the cleanup failed.\n */\nexport interface CleanupResult {\n\tchallenges: number;\n\texpiredTokens: number;\n\tpendingUploads: number;\n\tpendingUploadFiles: number;\n\tuploadAttempts: number;\n\trevisionsPruned: number;\n\tmediaUsage: number;\n\t/** Oldest 404-log rows deleted to restore the hard cap. */\n\tnotFoundLog: number;\n\t/** Transfer operations whose staging area was collected. */\n\ttransferStaging: number;\n}\n\nconst REVISION_KEEP_COUNT = 50;\nconst REVISION_PRUNE_BATCH_SIZE = 10;\n\n/**\n * Run all system cleanup tasks.\n *\n * Safe to call frequently -- each subsystem tolerates repeated calls, and\n * repeated calls with nothing to clean are cheap.\n *\n * @param db - The database instance\n * @param storage - Optional storage backend for deleting orphaned files.\n * When omitted, pending upload DB rows are still deleted but the\n * corresponding files in object storage are not removed.\n */\nexport async function runSystemCleanup(\n\tdb: Kysely<Database>,\n\tstorage?: Storage,\n): Promise<CleanupResult> {\n\tconst result: CleanupResult = {\n\t\tchallenges: -1,\n\t\texpiredTokens: -1,\n\t\tpendingUploads: -1,\n\t\tpendingUploadFiles: -1,\n\t\tuploadAttempts: -1,\n\t\trevisionsPruned: -1,\n\t\tmediaUsage: -1,\n\t\tnotFoundLog: -1,\n\t\ttransferStaging: -1,\n\t};\n\n\t// 1. Passkey challenges (expire after 60s, clean anything past 5 min)\n\ttry {\n\t\tresult.challenges = await cleanupExpiredChallenges(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired challenges:\", error);\n\t}\n\n\t// 2. Magic link / invite / signup tokens\n\ttry {\n\t\t// Cast needed: Database extends AuthTables but uses Generated<> wrappers\n\t\t// that confuse structural checks. The adapter casts internally anyway.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Database uses Generated<> wrappers incompatible with AuthTables structurally; safe at runtime\n\t\tconst authAdapter = createKyselyAdapter(db as unknown as Kysely<AuthTables>);\n\t\tawait authAdapter.deleteExpiredTokens();\n\t\tresult.expiredTokens = 0; // deleteExpiredTokens returns void\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean expired tokens:\", error);\n\t}\n\n\t// 3. Pending media uploads (abandoned after 1 hour)\n\t// Delete DB rows first, then remove corresponding files from storage.\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst orphanedKeys = await mediaRepo.cleanupPendingUploads();\n\t\tresult.pendingUploads = orphanedKeys.length;\n\n\t\t// Delete orphaned files from object storage\n\t\tif (storage && orphanedKeys.length > 0) {\n\t\t\tlet filesDeleted = 0;\n\t\t\tfor (const key of orphanedKeys) {\n\t\t\t\ttry {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tfilesDeleted++;\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Log per-file failures but continue -- storage.delete is\n\t\t\t\t\t// documented as idempotent, so this is an unexpected error.\n\t\t\t\t\tconsole.error(`[cleanup] Failed to delete storage file ${key}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.pendingUploadFiles = filesDeleted;\n\t\t} else {\n\t\t\tresult.pendingUploadFiles = 0;\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean pending uploads:\", error);\n\t}\n\n\t// 4. Uploaded objects that lost publication races or outlived their media row\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst completedAttemptsDeleted = await mediaRepo.deleteCompletedUploadAttempts();\n\t\tif (!storage) {\n\t\t\tresult.uploadAttempts = completedAttemptsDeleted;\n\t\t} else {\n\t\t\tconst storageKeys = await mediaRepo.findUploadAttemptsForCleanup();\n\t\t\tlet attemptsDeleted = completedAttemptsDeleted;\n\t\t\tfor (const storageKey of storageKeys) {\n\t\t\t\tif (await removeUploadAttempt(storage, mediaRepo, storageKey)) {\n\t\t\t\t\tattemptsDeleted++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.uploadAttempts = attemptsDeleted;\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean media upload attempts:\", error);\n\t}\n\n\ttry {\n\t\tresult.revisionsPruned = await pruneQueuedRevisions(db);\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to prune revisions:\", error);\n\t}\n\n\ttry {\n\t\tconst mediaUsage = await cleanupMediaUsage(db);\n\t\tresult.mediaUsage = mediaUsage.status === \"failed\" ? -1 : mediaUsage.deletedRows;\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to clean media usage:\", error);\n\t}\n\n\ttry {\n\t\tresult.notFoundLog = await new RedirectRepository(db).cleanup404Log();\n\t} catch (error) {\n\t\tconsole.error(\"[cleanup] Failed to cap the 404 log:\", error);\n\t}\n\n\tif (storage) {\n\t\ttry {\n\t\t\tconst { collectTransferStaging } = await import(\"./transfer/gc.js\");\n\t\t\tresult.transferStaging = (await collectTransferStaging(db, storage)).collected;\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[transfer] Failed to collect transfer staging:\", error);\n\t\t}\n\t} else {\n\t\tresult.transferStaging = 0;\n\t}\n\n\treturn result;\n}\n\nasync function pruneQueuedRevisions(db: Kysely<Database>): Promise<number> {\n\tconst queued = await db\n\t\t.selectFrom(\"_emdash_revision_prune_queue\")\n\t\t.selectAll()\n\t\t.orderBy(\"revision_id\")\n\t\t.limit(REVISION_PRUNE_BATCH_SIZE)\n\t\t.execute();\n\tconst revisionRepo = new RevisionRepository(db);\n\tlet totalPruned = 0;\n\n\tfor (const row of queued) {\n\t\ttry {\n\t\t\ttotalPruned += await revisionRepo.pruneQueuedEntry(\n\t\t\t\trow.collection,\n\t\t\t\trow.entry_id,\n\t\t\t\trow.revision_id,\n\t\t\t\tREVISION_KEEP_COUNT,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn totalPruned;\n}\n","/**\n * Built-in Default Comment Moderator\n *\n * Registers comment:moderate as an exclusive hook.\n * Implements the 4-step decision logic:\n * 1. Auto-approve authenticated CMS users (if configured)\n * 2. If moderation is \"none\" → approved\n * 3. If moderation is \"first_time\" and returning commenter → approved\n * 4. Otherwise → pending\n *\n * This moderator does not read `metadata` — it only uses collection settings\n * and prior approval count. Plugin moderators (AI, Akismet) replace this.\n */\n\nimport type { CommentModerateEvent, ModerationDecision, PluginContext } from \"../plugins/types.js\";\n\n/** Plugin ID for the built-in default comment moderator */\nexport const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = \"emdash-default-comment-moderator\";\n\n/**\n * The comment:moderate handler for the built-in default moderator.\n */\nexport async function defaultCommentModerate(\n\tevent: CommentModerateEvent,\n\t_ctx: PluginContext,\n): Promise<ModerationDecision> {\n\tconst { comment, collectionSettings, priorApprovedCount } = event;\n\n\t// 1. Auto-approve authenticated CMS users if configured\n\tif (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) {\n\t\treturn { status: \"approved\", reason: \"Authenticated CMS user\" };\n\t}\n\n\t// 2. If moderation is \"none\" → approved\n\tif (collectionSettings.commentsModeration === \"none\") {\n\t\treturn { status: \"approved\", reason: \"Moderation disabled\" };\n\t}\n\n\t// 3. If moderation is \"first_time\" and returning commenter → approved\n\tif (collectionSettings.commentsModeration === \"first_time\" && priorApprovedCount > 0) {\n\t\treturn { status: \"approved\", reason: \"Returning commenter\" };\n\t}\n\n\t// 4. Otherwise → pending\n\treturn { status: \"pending\", reason: \"Held for review\" };\n}\n","/**\n * Scheduled publishing sweep\n *\n * Promotes content whose scheduled publish time has passed. Driven by the\n * platform scheduler alongside cron ticks and system cleanup — never by a\n * request. On Node the cron scheduler's maintenance pass calls it; on\n * Cloudflare the Worker's `scheduled()` handler does.\n *\n * Like `runSystemCleanup`, each collection sweep is independent and non-fatal:\n * one collection failing must not stop the rest.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { handleContentPublish } from \"./api/handlers/content.js\";\nimport { ContentRepository } from \"./database/repositories/content.js\";\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport type { Database } from \"./database/types.js\";\nimport { SchemaRegistry } from \"./schema/registry.js\";\n\n/** A content item that was promoted to published by a sweep. */\nexport interface PublishedRef {\n\tcollection: string;\n\tid: string;\n}\n\n/**\n * Default cap on items promoted per collection in a single sweep. Bounds the\n * publish/webhook fan-out of one tick so a large backlog can't exhaust a Worker\n * invocation's CPU/subrequest budget; the remainder drains on later ticks.\n */\nexport const SCHEDULED_PUBLISH_BATCH_LIMIT = 100;\nconst SCHEDULED_PUBLISH_CURSOR_PREFIX = \"emdash:scheduled-publish-cursor:\";\n\ninterface ScheduledPublishCursor {\n\tscheduledAt: string;\n\tid: string;\n}\n\nfunction scheduledPublishCursorKey(collection: string): string {\n\treturn `${SCHEDULED_PUBLISH_CURSOR_PREFIX}${collection}`;\n}\n\nfunction isScheduledPublishCursor(value: unknown): value is ScheduledPublishCursor {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t\"scheduledAt\" in value &&\n\t\ttypeof value.scheduledAt === \"string\" &&\n\t\t\"id\" in value &&\n\t\ttypeof value.id === \"string\"\n\t);\n}\n\n/**\n * Publishes a single content item. Mirrors the relevant subset of\n * `handleContentPublish`'s return shape. Production callers pass\n * `EmDashRuntime.handleContentPublish` so `content:afterPublish` hooks fire\n * (search indexing, webhooks, syndication); the default falls back to the raw\n * handler (no hooks) for callers that have only a `db`.\n */\nexport type ScheduledPublishFn = (\n\tcollection: string,\n\tid: string,\n\toptions: {\n\t\tpublishedAt?: string;\n\t\trequireScheduledDue?: boolean;\n\t\texpectedScheduledAt?: string;\n\t\tcurrentTime?: Date;\n\t},\n) => Promise<{ success: boolean; error?: { code?: string } }>;\n\nexport interface PublishDueContentOptions {\n\t/**\n\t * Publish callback. Production callers pass the runtime's\n\t * `handleContentPublish` so `content:afterPublish` hooks fire (search\n\t * indexing, webhooks, syndication). Defaults to the raw DB handler (no hooks).\n\t */\n\tpublish?: ScheduledPublishFn;\n\t/**\n\t * Invoked after each collection's batch with the items promoted in that\n\t * batch. Lets request-less callers (the Cloudflare `scheduled()` handler)\n\t * purge edge-cache tags incrementally instead of only after the whole sweep,\n\t * so a runtime killed mid-sweep strands at most one batch behind stale cache\n\t * rather than everything published so far. Failures are logged, never fatal.\n\t */\n\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t/**\n\t * Maximum items promoted per collection per sweep. Defaults to\n\t * `SCHEDULED_PUBLISH_BATCH_LIMIT`. Pass `0` (or a negative) for unbounded.\n\t */\n\tlimit?: number;\n\t/** Clock value shared by due selection and the final publication fence. */\n\tcurrentTime?: Date;\n}\n\n/**\n * Publish every content item whose `scheduled_at` is in the past.\n *\n * Iterates all collections, finds due items (`findReadyToPublish` returns both\n * scheduled drafts and published entries with pending scheduled changes), and\n * publishes each. `publish()` clears `scheduled_at`, so a second sweep is a\n * no-op — safe to run on every tick.\n *\n * Bounded per collection by `limit` (default `SCHEDULED_PUBLISH_BATCH_LIMIT`):\n * a large backlog drains across successive ticks rather than in one unbounded\n * pass. After each collection's batch, `onPublished` (if given) is awaited so\n * cache-tag invalidation happens incrementally, not just at the very end.\n *\n * Returns every item it promoted so request-less callers (the Cloudflare\n * `scheduled()` handler) can also act on the full set.\n */\nexport async function publishDueContent(\n\tdb: Kysely<Database>,\n\toptions: PublishDueContentOptions = {},\n): Promise<PublishedRef[]> {\n\tconst {\n\t\tpublish,\n\t\tonPublished,\n\t\tlimit = SCHEDULED_PUBLISH_BATCH_LIMIT,\n\t\tcurrentTime = new Date(),\n\t} = options;\n\tconst published: PublishedRef[] = [];\n\n\tlet collections;\n\ttry {\n\t\tcollections = await new SchemaRegistry(db).listCollections();\n\t} catch (error) {\n\t\tconsole.error(\"[scheduled-publish] Failed to list collections:\", error);\n\t\treturn published;\n\t}\n\n\tconst repo = new ContentRepository(db);\n\tconst optionsRepo = new OptionsRepository(db);\n\tconst doPublish: ScheduledPublishFn =\n\t\tpublish ?? ((collection, id, opts) => handleContentPublish(db, collection, id, opts));\n\t// 0 / negative means unbounded; findReadyToPublish treats that as \"no LIMIT\".\n\tconst batchLimit = limit > 0 ? limit : undefined;\n\n\tfor (const collection of collections) {\n\t\ttry {\n\t\t\tlet cursor: ScheduledPublishCursor | undefined;\n\t\t\tlet cursorRevision: string | null = null;\n\t\t\tif (batchLimit !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stored = await optionsRepo.getVersioned(scheduledPublishCursorKey(collection.slug));\n\t\t\t\t\tcursorRevision = stored?.revision ?? null;\n\t\t\t\t\tif (isScheduledPublishCursor(stored?.value)) cursor = stored.value;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[scheduled-publish] Failed to read cursor for \"${collection.slug}\":`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tlet due = await repo.findReadyToPublish(collection.slug, batchLimit, currentTime, cursor);\n\t\t\tif (cursor && due.length === 0) {\n\t\t\t\tdue = await repo.findReadyToPublish(collection.slug, batchLimit, currentTime);\n\t\t\t}\n\t\t\tconst batch: PublishedRef[] = [];\n\t\t\tfor (const item of due) {\n\t\t\t\t// First publication of a scheduled draft should record the intended\n\t\t\t\t// scheduled time, not the (later) sweep time. Items already published\n\t\t\t\t// with pending draft changes keep their original published_at.\n\t\t\t\tconst publishedAt = item.publishedAt == null ? (item.scheduledAt ?? undefined) : undefined;\n\t\t\t\tconst result = await doPublish(collection.slug, item.id, {\n\t\t\t\t\tpublishedAt,\n\t\t\t\t\trequireScheduledDue: true,\n\t\t\t\t\texpectedScheduledAt: item.scheduledAt ?? undefined,\n\t\t\t\t\tcurrentTime,\n\t\t\t\t});\n\t\t\t\tif (result.success) {\n\t\t\t\t\tbatch.push({ collection: collection.slug, id: item.id });\n\t\t\t\t} else if (result.error?.code === \"NOT_DUE\" || result.error?.code === \"PUBLISH_REJECTED\") {\n\t\t\t\t\t// Unscheduled or rescheduled between selection and publish — the\n\t\t\t\t\t// editor changed their mind; skip quietly, not a failure.\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[scheduled-publish] Failed to publish ${collection.slug}/${item.id}:`,\n\t\t\t\t\t\tresult.error,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst lastAttempted = due.at(-1);\n\t\t\tif (batchLimit !== undefined && lastAttempted?.scheduledAt) {\n\t\t\t\ttry {\n\t\t\t\t\tawait optionsRepo.compareAndSet(\n\t\t\t\t\t\tscheduledPublishCursorKey(collection.slug),\n\t\t\t\t\t\tcursorRevision,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tscheduledAt: lastAttempted.scheduledAt,\n\t\t\t\t\t\t\tid: lastAttempted.id,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[scheduled-publish] Failed to save cursor for \"${collection.slug}\":`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (batch.length > 0) {\n\t\t\t\tpublished.push(...batch);\n\t\t\t\tif (onPublished) {\n\t\t\t\t\t// Purge this batch's cache tags before moving to the next\n\t\t\t\t\t// collection, so a mid-sweep kill can't strand already-published\n\t\t\t\t\t// content behind stale cache.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait onPublished(batch);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[scheduled-publish] onPublished failed after \"${collection.slug}\" batch:`,\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(`[scheduled-publish] Sweep failed for \"${collection.slug}\":`, error);\n\t\t}\n\t}\n\n\treturn published;\n}\n","/**\n * EmDashRuntime - Core runtime for EmDash CMS\n *\n * Manages database, storage, plugins (trusted + sandboxed), hooks, and\n * provides handlers for content/media operations.\n *\n * Created once per worker lifetime, cached and reused across requests.\n */\n\nimport { getLocaleDir, resolveLocale } from \"@emdash-cms/admin/locales\";\nimport { Permissions } from \"@emdash-cms/auth\";\nimport type { Element } from \"@emdash-cms/blocks\";\nimport {\n\tnormalizePluginPagePath,\n\tvalidateBlockResponse,\n\tvalidateContentEditorActionResponse,\n\ttype BlockValidationPolicy,\n\ttype PluginUiContext,\n} from \"@emdash-cms/blocks/server\";\nimport { isJsonPostRouteContract } from \"@emdash-cms/plugin-types\";\nimport { Kysely, type Dialect } from \"kysely\";\nimport virtualConfig from \"virtual:emdash/config\";\nimport { z } from \"zod\";\n\nimport { ErrorCode } from \"./api/errors.js\";\nimport { buildManifestCollections } from \"./api/handlers/manifest.js\";\nimport {\n\thandleMediaUpload as uploadMedia,\n\ttype MediaUploadInput,\n} from \"./api/handlers/media-upload.js\";\nimport { validateRev } from \"./api/rev.js\";\nimport { getSiteBaseUrl } from \"./api/site-url.js\";\nimport type {\n\tEmDashConfig,\n\tPluginAdminPage,\n\tPluginDashboardWidget,\n} from \"./astro/integration/runtime.js\";\nimport type { EmDashManifest } from \"./astro/types.js\";\nimport { getAuthMode } from \"./auth/mode.js\";\nimport { getTrustedProxyHeaders } from \"./auth/trusted-proxy.js\";\nimport { lookupContentAuthor, sendCommentNotification } from \"./comments/notifications.js\";\nimport type { ContentFieldFilters } from \"./content-list-query.js\";\nimport { keepKnownFields, staleStoredKeys } from \"./content/known-fields.js\";\nimport { isSqlite } from \"./database/dialect-helpers.js\";\nimport { kyselyLogOption } from \"./database/instrumentation.js\";\nimport {\n\tenforceRuntimeMigrationPolicy,\n\tPendingMigrationsError,\n\ttype RuntimeMigrationMode,\n} from \"./database/migrations/policy.js\";\nimport {\n\tConcurrentMigrationTimeoutError,\n\tMIGRATION_RACE_WAIT_MS,\n} from \"./database/migrations/runner.js\";\nimport { AuditRepository } from \"./database/repositories/audit.js\";\nimport { CommentRepository } from \"./database/repositories/comment.js\";\nimport type { CommentStatus } from \"./database/repositories/comment.js\";\nimport { ContentRepository } from \"./database/repositories/content.js\";\nimport { RevisionRepository } from \"./database/repositories/revision.js\";\nimport { selectTaxonomyDefs } from \"./database/repositories/taxonomy-def.js\";\nimport { ContentMutationConflictError } from \"./database/repositories/types.js\";\nimport type {\n\tContentItem as ContentItemInternal,\n\tContentDateField,\n} from \"./database/repositories/types.js\";\nimport type { ImageValue } from \"./fields/types.js\";\nimport { getI18nConfig, resolveContentCreateLocale } from \"./i18n/config.js\";\nimport { repairLocaleCasing } from \"./i18n/repair-locale-casing.js\";\nimport { warnAboutUnconfiguredTaxonomyLocales } from \"./i18n/taxonomy-locale-diagnostic.js\";\nimport { normalizeMediaValue } from \"./media/normalize.js\";\nimport type { MediaProvider, MediaProviderCapabilities } from \"./media/types.js\";\nimport { activateMediaUsageCapture } from \"./media/usage/activation.js\";\nimport {\n\tdeleteContentMediaUsage,\n\tfindNonTranslatableSiblingContentIds,\n\tmarkContentMediaUsageCollectionStale,\n\trefreshContentMediaUsageAfterWrite,\n} from \"./media/usage/content-refresh.js\";\nimport { processMediaUsageWorkAfterWrite } from \"./media/usage/work-processor.js\";\nimport {\n\tscheduledPolicyRejectionKey,\n\ttype ScheduledPolicyRejection,\n} from \"./plugins/content-policy.js\";\nimport { createCommentAccess, createTaxonomyAccessWithWrite } from \"./plugins/context.js\";\nimport type { ContentActionCallbacks, ContentWriteGuard } from \"./plugins/context.js\";\nimport type { PluginContentCacheInvalidator } from \"./plugins/routes.js\";\nimport {\n\tcreateSandboxedPluginProxy,\n\tgetSandboxSaveRejectionDetails,\n} from \"./plugins/sandbox/proxy.js\";\nimport { createSandboxRunnerOptions } from \"./plugins/sandbox/runner-options.js\";\nimport { getSandboxRouteErrorDetails } from \"./plugins/sandbox/types.js\";\nimport type {\n\tSandboxedPluginInstance,\n\tSandboxRunner,\n\tSandboxRunnerFactory,\n} from \"./plugins/sandbox/types.js\";\nimport type {\n\tActorInfo,\n\tContentActionOrigin,\n\tContentItem as PluginContentItem,\n\tResolvedPlugin,\n\tMediaItem,\n\tMediaMetadataPatch,\n\tPluginManifest,\n\tPluginCapability,\n\tPluginStorageConfig,\n\tPluginMcpManifestConfig,\n\tPluginEditorAction,\n\tPluginEditorPanel,\n\tPublicPageContext,\n\tPageMetadataContribution,\n\tPageFragmentContribution,\n\tPortableTextBlockConfig,\n\tFieldWidgetConfig,\n\tSettingField,\n\tUserInfo,\n\tCollectionCommentSettings,\n\tModerationDecision,\n\tPluginComment,\n\tPluginCommentStatus,\n\tContentWriteInput,\n\tPluginContentCreateCallback,\n\tVersionedContentItem,\n} from \"./plugins/types.js\";\nimport { normalizePluginCapabilities } from \"./plugins/types.js\";\nimport { recordSchedulerHeartbeatSafely } from \"./scheduler-health.js\";\nimport { primeRegisteredCollections } from \"./schema/collection-slugs-cache.js\";\nimport type { CollectionWithFields } from \"./schema/types.js\";\nimport { isMissingTableError } from \"./utils/db-errors.js\";\nimport { hashString } from \"./utils/hash.js\";\nimport { createInitLock, type InitLock, initWithLock } from \"./utils/init-lock.js\";\nimport { createSingleFlightCache, singleFlightCached } from \"./utils/single-flight-cache.js\";\nimport { COMMIT, VERSION } from \"./version.js\";\n\nconst LEADING_SLASH_PATTERN = /^\\//;\nconst LOCALE_CASING_REPAIR_OPTION = \"emdash:repair_locale_casing\";\n\nconst PLUGIN_COMMENT_STATUSES = new Set<string>([\"approved\", \"pending\", \"spam\"]);\n\nfunction assertPluginCommentStatus(\n\tvalue: string,\n\tname: \"status\" | \"expectedStatus\",\n): asserts value is PluginCommentStatus {\n\tif (!PLUGIN_COMMENT_STATUSES.has(value)) {\n\t\tthrow Object.assign(new Error(`${name} must be one of: approved, pending, spam`), {\n\t\t\tcode: \"COMMENT_STATUS_INVALID\",\n\t\t});\n\t}\n}\n\nfunction getLocaleCasingRepairVersion(locales: readonly string[]): string | null {\n\tif (locales.length === 0) return null;\n\treturn `1:${locales.toSorted().join(\",\")}`;\n}\n\n/**\n * Parse a JSON column expected to contain an array of strings.\n *\n * Throws on malformed JSON rather than returning []; callers are responsible\n * for deciding how to handle/log the error. Empty string / null inputs return\n * [] (they represent \"no value\"). Non-string array entries are filtered out.\n */\nfunction parseStringArray(raw: string | null | undefined): string[] {\n\tif (!raw) return [];\n\tconst parsed: unknown = JSON.parse(raw);\n\tif (!Array.isArray(parsed)) return [];\n\treturn parsed.filter((v): v is string => typeof v === \"string\");\n}\n\nfunction isModerationDecision(value: unknown): value is ModerationDecision {\n\tif (typeof value !== \"object\" || value === null || !(\"status\" in value)) return false;\n\tif (value.status !== \"pending\" && value.status !== \"approved\" && value.status !== \"spam\") {\n\t\treturn false;\n\t}\n\treturn !(\"reason\" in value) || value.reason === undefined || typeof value.reason === \"string\";\n}\n\n/** Combined result from a single-pass page contribution collection */\ninterface PageContributions {\n\tmetadata: PageMetadataContribution[];\n\tfragments: PageFragmentContribution[];\n}\n\nimport { after } from \"./after.js\";\nimport { maybeRunScheduledBackup } from \"./api/handlers/backup.js\";\nimport { loadBundleFromR2 } from \"./api/handlers/marketplace.js\";\nimport { runSystemCleanup } from \"./cleanup.js\";\nimport {\n\tDEFAULT_COMMENT_MODERATOR_PLUGIN_ID,\n\tdefaultCommentModerate,\n} from \"./comments/moderator.js\";\nimport { submitPublicComment } from \"./comments/public-submission.js\";\nimport {\n\tCommentStatusConflictError,\n\tcreateComment,\n\tmoderateComment,\n\ttype CommentCreateInput,\n\ttype CommentCreateResult,\n\ttype CommentHookRunner,\n} from \"./comments/service.js\";\nimport { validateEncryptionKeyAtStartup } from \"./config/secrets.js\";\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport {\n\thandleContentList,\n\thandleContentAuthors,\n\thandleContentGet,\n\thandleContentGetIncludingTrashed,\n\thandleContentCreate,\n\thandleContentUpdate,\n\thandleContentDelete,\n\thandleContentDuplicate,\n\thandleContentRestore,\n\thandleContentPermanentDelete,\n\thandleContentListTrashed,\n\thandleContentCountTrashed,\n\thandleContentPublish,\n\thandleContentUnpublish,\n\thandleContentSchedule,\n\thandleContentUnschedule,\n\thandleScheduledPolicyRejection,\n\thandleContentCountScheduled,\n\thandleContentDiscardDraft,\n\thandleContentCompare,\n\thandleContentTranslations,\n\thandleMediaList,\n\thandleMediaGet,\n\thandleMediaCreate,\n\thandleMediaRegisterUpload,\n\thandleMediaUpdate,\n\thandleMediaReplaceMetadata,\n\thandleMediaDelete,\n\thandleRevisionList,\n\thandleRevisionGet,\n\thandleRevisionRestore,\n\tSchemaRegistry,\n\tSchemaError,\n\tnormalizeBlocksData,\n\tresolveBlockTypes,\n\ttype Database,\n\ttype Storage,\n} from \"./index.js\";\nimport { getDb } from \"./loader.js\";\nimport { isRecord } from \"./plugin-utils.js\";\nimport { CronExecutor, type InvokeCronHookFn } from \"./plugins/cron.js\";\nimport { definePlugin } from \"./plugins/define-plugin.js\";\nimport { DEV_CONSOLE_EMAIL_PLUGIN_ID, devConsoleEmailDeliver } from \"./plugins/email-console.js\";\nimport { EmailPipeline } from \"./plugins/email.js\";\nimport {\n\tcreateHookPipeline,\n\tgetActiveContentSaveHookName,\n\tresolveExclusiveHooks as resolveExclusiveHooksShared,\n\ttype HookPipeline,\n} from \"./plugins/hooks.js\";\nimport { disableRuntimePlugin, enableRuntimePlugin } from \"./plugins/lifecycle.js\";\nimport { HOOK_NAMES, normalizeManifestRoute } from \"./plugins/manifest-schema.js\";\nimport { updatePluginMediaMetadata } from \"./plugins/media.js\";\nimport { extractRequestMeta, sanitizeHeadersForSandbox } from \"./plugins/request-meta.js\";\nimport { PluginRouteRequestError } from \"./plugins/route-wire.js\";\nimport {\n\tbuildRouteMeta,\n\tparseRouteInput,\n\tPluginRouteRegistry,\n\ttoRouteCallerInfo,\n\ttype RouteCallerInput,\n\ttype RouteMeta,\n} from \"./plugins/routes.js\";\nimport { isContentSaveRejection } from \"./plugins/save-rejection.js\";\nimport type { CronScheduler } from \"./plugins/scheduler/types.js\";\nimport { PluginStateRepository } from \"./plugins/state.js\";\nimport { syncDeclaredStorageIndexes } from \"./plugins/storage-indexes.js\";\nimport {\n\tgetRegistryConfigInput,\n\tresolveManifestRegistryConfig,\n\tresolveRegistryConfigForSandbox,\n} from \"./registry/config.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { publishDueContent, type PublishedRef } from \"./scheduled-publish.js\";\nimport { FTSManager } from \"./search/fts-manager.js\";\nimport { invalidateSiteSettingsCache } from \"./settings/index.js\";\nimport { assertSiteWriteAllowed } from \"./transfer/fence.js\";\n\nconst DRAFT_ONLY_UPDATE_KEYS = new Set([\n\t\"data\",\n\t\"slug\",\n\t\"locale\",\n\t\"skipRevision\",\n\t\"actor\",\n\t\"migrateBlocks\",\n\t\"replaceBlocks\",\n]);\n\n/** Field types whose schema is an array, so a stored blank string can never validate. */\nconst ARRAY_FIELD_TYPES = new Set<string>([\"portableText\", \"multiSelect\", \"repeater\"]);\nconst MAX_DRAFT_STAGE_ATTEMPTS = 32;\nconst PLUGIN_INVOCATION_RELEASE_GRACE_MS = 60_000;\n\ntype ContentPolicyHookName =\n\t| \"content:beforePublish\"\n\t| \"content:beforeSchedule\"\n\t| \"content:beforeUnpublish\";\n\ninterface ContentPolicyMutationOptions {\n\t_rev?: string;\n\tactor?: ActorInfo;\n\torigin?: ContentActionOrigin;\n\tpluginInvocationId?: string;\n}\n\ntype ContentPolicyCheck =\n\t| { allowed: true; revision: string }\n\t| {\n\t\t\tallowed: false;\n\t\t\trevision: string;\n\t\t\terror: { code: string; message: string };\n\t\t\tcancellation?: { pluginId: string; reason: string };\n\t };\n\n/**\n * Sandboxed plugin entry from virtual module\n */\nexport interface SandboxedPluginEntry {\n\tid: string;\n\tversion: string;\n\toptions: Record<string, unknown>;\n\tcode: string;\n\t/** Capabilities the plugin requests */\n\tcapabilities: PluginCapability[];\n\t/** Allowed hosts for network:fetch */\n\tallowedHosts: string[];\n\t/** Declared storage collections */\n\tstorage: PluginStorageConfig;\n\t/** Serialized MCP declarations emitted at plugin build time. */\n\tmcp?: PluginMcpManifestConfig;\n\t/** Route declarations (name + public/permission/cacheControl), used for route auth decisions */\n\troutes?: PluginManifest[\"routes\"];\n\t/** Hook declarations this plugin implements */\n\thooks?: PluginManifest[\"hooks\"];\n\t/** Admin pages */\n\tadminPages?: Array<{ path: string; label?: string; icon?: string }>;\n\t/** Dashboard widgets */\n\tadminWidgets?: Array<{ id: string; title?: string; size?: string }>;\n\t/** Saved-entry Block Kit panels. */\n\teditorPanels?: PluginManifest[\"admin\"][\"editorPanels\"];\n\t/** Saved-entry host-rendered actions. */\n\teditorActions?: PluginManifest[\"admin\"][\"editorActions\"];\n\t/** Settings schema for the auto-generated admin settings form */\n\tsettingsSchema?: Record<string, SettingField>;\n\t/** Portable Text block types contributed to the editor (declarative Block Kit) */\n\tportableTextBlocks?: PortableTextBlockConfig[];\n\t/** Field widget types contributed for schema-field editing UIs */\n\tfieldWidgets?: FieldWidgetConfig[];\n\t/** Admin entry module */\n\tadminEntry?: string;\n\t/**\n\t * Exclusive hooks this plugin should be auto-selected for.\n\t * Weaker than an existing admin DB selection — config order wins when no selection exists.\n\t */\n\tpreferred?: string[];\n}\n\nexport type ResolvedPluginEditorExtension =\n\t| {\n\t\t\tkind: \"panel\";\n\t\t\textension: PluginEditorPanel;\n\t\t\tpolicy: BlockValidationPolicy;\n\t\t\tcapabilities: readonly PluginCapability[];\n\t }\n\t| {\n\t\t\tkind: \"action\";\n\t\t\textension: PluginEditorAction;\n\t\t\tpolicy: BlockValidationPolicy;\n\t\t\tcapabilities: readonly PluginCapability[];\n\t };\n\nexport interface PluginEditorExtensionDispatch {\n\tui: PluginUiContext;\n\tkind: \"panel\" | \"action\";\n\tpolicy: BlockValidationPolicy;\n}\n\n/**\n * Media provider entry from virtual module\n */\nexport interface MediaProviderEntry {\n\tid: string;\n\tname: string;\n\ticon?: string;\n\tcapabilities: MediaProviderCapabilities;\n\t/** Factory function to create the provider instance */\n\tcreateProvider: (ctx: MediaProviderContext) => MediaProvider;\n}\n\n/**\n * Context passed to media provider factory functions\n */\nexport interface MediaProviderContext {\n\tdb: Kysely<Database>;\n\t/**\n\t * Resolver for the live connection, preferred over `db` by providers that\n\t * query EmDash's database. Resolves the current request/event-scoped\n\t * connection from ALS so connection-backed adapters (Postgres over\n\t * Hyperdrive) don't reuse the per-isolate singleton's socket across events.\n\t * Providers should resolve per operation rather than capturing `db` once.\n\t * Omitted-safe: falls back to `db` for stateless adapters (D1, Node SQLite).\n\t */\n\tgetDb?: () => Kysely<Database>;\n\tstorage: Storage | null;\n}\n\n/**\n * Builds the timer-based scheduler that drives cron ticks and maintenance.\n * Injected via `virtual:emdash/scheduler` so the platform — not core — decides\n * whether a long-lived heartbeat exists.\n */\nexport type CreateSchedulerFn = (executor: CronExecutor) => CronScheduler;\n\n/**\n * Dependencies injected from virtual modules (middleware reads these)\n */\nexport interface RuntimeDependencies {\n\tconfig: EmDashConfig;\n\t/** Effective migration mode, resolved once by the runtime entrypoint. */\n\tmigrationMode?: RuntimeMigrationMode;\n\tplugins: ResolvedPlugin[];\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateDialect: (config: any) => Dialect;\n\t/**\n\t * Factory for a dialect that batches same-turn reads into one round trip\n\t * ({@link EmDashRuntime.create} uses it for the cold-start read phase).\n\t * Present only on batching backends (D1, DO); absent backends fall back to\n\t * the singleton. Returns a fresh connection each call — it must never be the\n\t * long-lived singleton, whose coalescing buffer would be shared across\n\t * requests.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateCoalescingDialect?: (config: any) => Dialect | null;\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcreateStorage: ((config: any) => Storage) | null;\n\tsandboxEnabled: boolean;\n\t/** sandbox: false escape hatch - load sandboxed plugins in-process */\n\tsandboxBypassed?: boolean;\n\t/**\n\t * Factory for the timer-based cron/maintenance heartbeat. Supplied by the\n\t * generated `virtual:emdash/scheduler` module: a `NodeCronScheduler` factory\n\t * on long-lived runtimes (Node/Bun), or `null` on serverless adapters where\n\t * an external driver (e.g. the Cloudflare Worker's `scheduled()` Cron\n\t * Trigger) calls `runScheduledTasks()` instead. When absent or null, the\n\t * runtime starts no scheduler. Keeping the platform decision in the\n\t * integration means core has no adapter-specific runtime checks.\n\t */\n\tcreateScheduler?: CreateSchedulerFn | null;\n\t/** Media provider entries from virtual module */\n\tmediaProviderEntries?: MediaProviderEntry[];\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\t/** Factory function supplied by the active platform adapter. */\n\tcreateSandboxRunner: SandboxRunnerFactory | null;\n\t/** Clock used by cron scheduling, task execution, and scheduled publishing. */\n\tnow?: () => Date;\n\t/** Site values supplied by hosts that do not load Astro's virtual config. */\n\tsiteInfo?: {\n\t\tname?: string;\n\t\turl?: string;\n\t\tlocale?: string;\n\t\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n\t};\n}\n\n/**\n * Constructor parameters for `EmDashRuntime`.\n *\n * Production code should use `EmDashRuntime.create()` which discovers and\n * loads all parts (database, plugins, hooks, cron, etc.) and then calls the\n * constructor. Direct construction is supported for callers that already\n * have all the dependencies in hand — for example, integration tests that\n * supply a pre-migrated database and an empty plugin set.\n *\n * Every field corresponds 1:1 to internal state set on the runtime — none of\n * these are derived. If you don't have a value for one, see what `create()`\n * passes for that field as the canonical default.\n */\nexport interface EmDashRuntimeParts {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n\tconfiguredPlugins: ResolvedPlugin[];\n\tsandboxedPlugins: Map<string, SandboxedPluginInstance>;\n\tsandboxedPluginEntries: SandboxedPluginEntry[];\n\thooks: HookPipeline;\n\tenabledPlugins: Set<string>;\n\tpluginStates: Map<string, string>;\n\tconfig: EmDashConfig;\n\tmediaProviders: Map<string, MediaProvider>;\n\tmediaProviderEntries: MediaProviderEntry[];\n\tcronExecutor: CronExecutor | null;\n\tcronScheduler: CronScheduler | null;\n\temailPipeline: EmailPipeline | null;\n\tallPipelinePlugins: ResolvedPlugin[];\n\tpipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tgetDb?: () => Kysely<Database>;\n\t\tbeforeContentWrite?: ContentWriteGuard;\n\t\tcontentCreate?: PluginContentCreateCallback;\n\t\tcontentActions?: ContentActionCallbacks;\n\t\tnow?: () => Date;\n\t\tstorage?: Storage;\n\t\tcommentModerate?: (\n\t\t\tpluginId: string,\n\t\t\tid: string,\n\t\t\tstatus: PluginCommentStatus,\n\t\t\texpectedStatus: PluginCommentStatus,\n\t\t) => Promise<PluginComment>;\n\t\tsiteInfo?: {\n\t\t\tsiteName?: string;\n\t\t\tsiteUrl?: string;\n\t\t\tlocale?: string;\n\t\t\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n\t\t};\n\t};\n\truntimeDeps: RuntimeDependencies;\n\tpipelineRef: { current: HookPipeline };\n}\n\n/**\n * A `ContentSaveRejectedError` carries a message the plugin wrote for the\n * editor; every other exception stays internal and is replaced by a generic\n * message so hook internals cannot leak through the API.\n */\nfunction beforeSaveFailure(error: unknown) {\n\tif (isContentSaveRejection(error)) {\n\t\tconst sandbox = getSandboxSaveRejectionDetails(error);\n\t\tif (sandbox) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: ErrorCode.SAVE_REJECTED,\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t\tdetails: sandbox,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false as const,\n\t\t\terror: { code: ErrorCode.SAVE_REJECTED, message: error.message },\n\t\t};\n\t}\n\tconsole.error(\"EmDash: content:beforeSave hook failed:\", error);\n\treturn {\n\t\tsuccess: false as const,\n\t\terror: {\n\t\t\tcode: ErrorCode.CONTENT_HOOK_ERROR,\n\t\t\tmessage: \"A plugin hook failed while saving content\",\n\t\t},\n\t};\n}\n\n/**\n * Convert a ContentItem to Record<string, unknown> for hook consumption.\n * Hooks receive the full item as a flat record.\n */\nfunction contentItemToRecord(item: ContentItemInternal): Record<string, unknown> {\n\treturn { ...item };\n}\n\nfunction toPluginContentItem(item: ContentItemInternal): PluginContentItem {\n\treturn {\n\t\tid: item.id,\n\t\ttype: item.type,\n\t\tslug: item.slug,\n\t\tstatus: item.status,\n\t\tlocale: item.locale,\n\t\tdata: item.data,\n\t\tcreatedAt: item.createdAt,\n\t\tupdatedAt: item.updatedAt,\n\t\tpublishedAt: item.publishedAt,\n\t\tscheduledAt: item.scheduledAt,\n\t\tauthorId: item.authorId,\n\t\ttranslationGroup: item.translationGroup,\n\t\tliveRevisionId: item.liveRevisionId,\n\t\tdraftRevisionId: item.draftRevisionId,\n\t\tversion: item.version,\n\t\t...(item.seo === undefined ? {} : { seo: item.seo }),\n\t};\n}\n\n/**\n * Db init lock reclaim deadline. Derived from the migration race wait so\n * they can't drift apart: a healthy init can legitimately block for the\n * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold\n * connect and migrator work, before it should be presumed dead. The outer\n * runtime init lock (middleware.ts) must use a strictly larger deadline —\n * it wraps create() → getDatabase() → this lock, and equal deadlines would\n * let the outer reclaim while the inner is legitimately still working.\n */\nexport const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;\n\n/**\n * Db cache + its init lock live on globalThis behind a Symbol: the bundler\n * can duplicate this module across SSR chunks (same reasoning as\n * request-cache.ts), and a duplicated cache/lock would mean concurrent\n * independent db inits — and duplicate migrators — per isolate.\n */\nconst DB_HOLDER_KEY = Symbol.for(\"emdash:db-cache\");\ninterface DbHolder {\n\tcache: Map<string, Kysely<Database>>;\n\tlock: InitLock;\n\t/**\n\t * Recent migration failures, keyed like `cache`. A failed migration is\n\t * near-certain to fail again immediately (schema conflict, broken\n\t * migration), and without this every request in a warm isolate would\n\t * re-attempt it — on Workers with Postgres that stampedes the database\n\t * through the migration advisory lock (#1744). Entries expire after\n\t * DB_INIT_FAILURE_BACKOFF_MS; a successful init clears them.\n\t */\n\tfailures: Map<string, { at: number; message: string }>;\n}\nconst globalSymbolStore = globalThis as Record<symbol, unknown>;\nfunction getDbHolder(): DbHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;\n\tif (!holder) {\n\t\tholder = {\n\t\t\tcache: new Map<string, Kysely<Database>>(),\n\t\t\tlock: createInitLock(),\n\t\t\tfailures: new Map(),\n\t\t};\n\t\tglobalSymbolStore[DB_HOLDER_KEY] = holder;\n\t}\n\t// A holder created by an older copy of this module (dev-server HMR keeps\n\t// globalThis across reloads) may predate the failures map.\n\tholder.failures ??= new Map();\n\treturn holder;\n}\n\n/**\n * After a database init fails (migrations threw), skip re-attempting for\n * this long. Cold isolates still get one attempt each, so a transient\n * failure heals on its own; a persistently failing migration is retried at\n * most once per backoff window per isolate instead of on every request.\n */\nconst DB_INIT_FAILURE_BACKOFF_MS = 30_000;\n\n/**\n * Auto-seed runs at most once per isolate per database. Its lock + \"done\" set\n * live on globalThis (same bundler-duplication reasoning as the db cache) so a\n * reclaimed-and-rerun `create()` can't seed a second time concurrently. The\n * lock polls rather than sharing a promise, so it is safe to await across a\n * cancelled owner in workerd.\n */\nconst SEED_HOLDER_KEY = Symbol.for(\"emdash:seed-state\");\ninterface SeedHolder {\n\tdone: Set<string>;\n\tlock: InitLock;\n}\nfunction getSeedHolder(): SeedHolder {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below\n\tlet holder = globalSymbolStore[SEED_HOLDER_KEY] as SeedHolder | undefined;\n\tif (!holder) {\n\t\tholder = { done: new Set<string>(), lock: createInitLock() };\n\t\tglobalSymbolStore[SEED_HOLDER_KEY] = holder;\n\t}\n\treturn holder;\n}\nconst storageCache = new Map<string, Storage>();\nconst sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();\nconst sandboxedManifestCache = new Map<string, PluginManifest>();\n/**\n * Per-tier sets of `${pluginId}:${version}` keys present in\n * `sandboxedPluginCache`. Used during sync to know which entries belong\n * to which install source so we can invalidate only what belongs to the\n * tier currently being synced.\n */\nconst marketplacePluginKeys = new Set<string>();\nconst registryPluginKeys = new Set<string>();\n/**\n * Manifest metadata for runtime-installed sandboxed plugins (marketplace\n * and registry both). Keyed by `pluginId`; readers don't care which\n * source the plugin came from. Named `marketplace*` for legacy reasons.\n */\nconst marketplaceManifestCache = new Map<\n\tstring,\n\t{\n\t\tid: string;\n\t\tversion: string;\n\t\tadmin?: {\n\t\t\tpages?: PluginAdminPage[];\n\t\t\twidgets?: PluginDashboardWidget[];\n\t\t\tsettingsSchema?: Record<string, SettingField>;\n\t\t\teditorPanels?: PluginManifest[\"admin\"][\"editorPanels\"];\n\t\t\teditorActions?: PluginManifest[\"admin\"][\"editorActions\"];\n\t\t};\n\t\tmcp?: PluginMcpManifestConfig;\n\t\tstorage?: PluginManifest[\"storage\"];\n\t\tallowedHosts?: string[];\n\t\tcapabilities?: PluginCapability[];\n\t}\n>();\n/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */\nconst sandboxedRouteMetaCache = new Map<string, Map<string, RouteMeta>>();\nlet sandboxRunner: SandboxRunner | null = null;\n\nfunction allowedBrowserImageHosts(\n\tcapabilities: readonly PluginCapability[],\n\tallowedHosts: readonly string[],\n): readonly string[] {\n\tif (\n\t\tcapabilities.includes(\"network:request:unrestricted\") ||\n\t\tcapabilities.includes(\"network:fetch:any\")\n\t) {\n\t\treturn [\"*\"];\n\t}\n\tif (capabilities.includes(\"network:request\") || capabilities.includes(\"network:fetch\")) {\n\t\treturn allowedHosts;\n\t}\n\treturn [];\n}\n\n/**\n * EmDashRuntime - singleton per worker\n */\nexport class EmDashRuntime {\n\t/**\n\t * The singleton database instance (worker-lifetime cached).\n\t * Use the `db` getter instead — it checks the request context first\n\t * for per-request overrides (D1 read replica sessions, DO multi-site).\n\t */\n\tprivate readonly _db: Kysely<Database>;\n\treadonly storage: Storage | null;\n\treadonly configuredPlugins: ResolvedPlugin[];\n\treadonly sandboxedPlugins: Map<string, SandboxedPluginInstance>;\n\treadonly sandboxedPluginEntries: SandboxedPluginEntry[];\n\t/**\n\t * Schema registry bound to the current request/event-scoped connection.\n\t * Built per access (SchemaRegistry just wraps a db) against `this.db`, the\n\t * ALS-aware getter — never a captured snapshot of the singleton. On a\n\t * connection-backed adapter (Postgres over Hyperdrive) a captured singleton\n\t * would query a socket opened by an earlier event and trip workerd's\n\t * cross-request I/O guard; the catch in handlers like handleContentUpdate\n\t * would then silently treat a revision-enabled collection as non-revisioned\n\t * and write draft edits to live columns. Same reasoning as the per-call\n\t * registry in _buildManifest().\n\t */\n\tget schemaRegistry(): SchemaRegistry {\n\t\treturn new SchemaRegistry(this.db);\n\t}\n\tprivate _hooks!: HookPipeline;\n\treadonly config: EmDashConfig;\n\treadonly mediaProviders: Map<string, MediaProvider>;\n\treadonly mediaProviderEntries: MediaProviderEntry[];\n\treadonly cronExecutor: CronExecutor | null;\n\treadonly email: EmailPipeline | null;\n\n\tprivate cronScheduler: CronScheduler | null;\n\tprivate enabledPlugins: Set<string>;\n\tprivate pluginStates: Map<string, string>;\n\tprivate readonly activePluginContentActions = new Map<string, number>();\n\tprivate readonly pendingPluginAfterHooks = new Map<string, Array<() => Promise<void>>>();\n\tprivate readonly activePluginInvocations = new Set<string>();\n\t/** Timed-out sandbox invocations whose later actions must schedule hooks without another flush. */\n\tprivate readonly releasedPluginInvocations = new Set<string>();\n\tprivate readonly pluginInvocationCacheInvalidators = new Map<\n\t\tstring,\n\t\tPluginContentCacheInvalidator\n\t>();\n\tprivate pluginContentCacheInvalidator?: PluginContentCacheInvalidator;\n\n\t/**\n\t * Isolate-lifetime guard so FTS indexes are verified at most once per\n\t * worker rather than on every admin request. See ensureSearchHealthy().\n\t * Uses the poison-immune single-flight cache (never a shared awaitable\n\t * promise) so a cancelled first caller can't wedge later ones.\n\t */\n\tprivate readonly _searchHealthCache = createSingleFlightCache<void>();\n\n\t/** Current hook pipeline. Use the `hooks` getter for external access. */\n\tget hooks(): HookPipeline {\n\t\treturn this._hooks;\n\t}\n\n\t/** All plugins eligible for the hook pipeline (includes built-in plugins).\n\t * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */\n\tprivate allPipelinePlugins: ResolvedPlugin[];\n\t/** Guards the once-per-process plugin storage-index sync. */\n\tprivate storageIndexesSynced = false;\n\t/** Factory options for the hook pipeline context factory */\n\tprivate pipelineFactoryOptions: {\n\t\tdb: Kysely<Database>;\n\t\tgetDb?: () => Kysely<Database>;\n\t\tbeforeContentWrite?: ContentWriteGuard;\n\t\tcontentCreate?: PluginContentCreateCallback;\n\t\tcontentActions?: ContentActionCallbacks;\n\t\tnow?: () => Date;\n\t\tstorage?: Storage;\n\t\tcommentModerate?: (\n\t\t\tpluginId: string,\n\t\t\tid: string,\n\t\t\tstatus: PluginCommentStatus,\n\t\t\texpectedStatus: PluginCommentStatus,\n\t\t) => Promise<PluginComment>;\n\t\tsiteInfo?: {\n\t\t\tsiteName?: string;\n\t\t\tsiteUrl?: string;\n\t\t\tlocale?: string;\n\t\t\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n\t\t};\n\t};\n\t/** Dependencies needed for exclusive hook resolution */\n\tprivate runtimeDeps: RuntimeDependencies;\n\t/** Mutable ref for the cron invokeCronHook closure to read the current pipeline */\n\tprivate pipelineRef!: { current: HookPipeline };\n\tprivate readonly commentModerationInProgress = new Set<string>();\n\n\t/**\n\t * Get the database instance for the current request.\n\t *\n\t * Checks the ALS-based request context first — middleware sets a\n\t * per-request Kysely instance there for D1 read replica sessions\n\t * or DO preview databases. Falls back to the singleton instance.\n\t */\n\tget db(): Kysely<Database> {\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.db) {\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- db in context is set by middleware with correct type\n\t\t\treturn ctx.db as Kysely<Database>;\n\t\t}\n\t\treturn this._db;\n\t}\n\n\t/** Bind the platform cache provider used by plugin actions, including request-less cron hooks. */\n\tsetPluginContentCacheInvalidator(\n\t\tinvalidateContentCache: PluginContentCacheInvalidator | undefined,\n\t): void {\n\t\tthis.pluginContentCacheInvalidator = invalidateContentCache;\n\t}\n\n\tconstructor(parts: EmDashRuntimeParts) {\n\t\tthis._db = parts.db;\n\t\tthis.storage = parts.storage;\n\t\tthis.configuredPlugins = parts.configuredPlugins;\n\t\tthis.sandboxedPlugins = parts.sandboxedPlugins;\n\t\tthis.sandboxedPluginEntries = parts.sandboxedPluginEntries;\n\t\tthis._hooks = parts.hooks;\n\t\tthis.enabledPlugins = parts.enabledPlugins;\n\t\tthis.pluginStates = parts.pluginStates;\n\t\tthis.config = parts.config;\n\t\tthis.mediaProviders = parts.mediaProviders;\n\t\tthis.mediaProviderEntries = parts.mediaProviderEntries;\n\t\tthis.cronExecutor = parts.cronExecutor;\n\t\tthis.cronScheduler = parts.cronScheduler;\n\t\tthis.email = parts.emailPipeline;\n\t\tthis.allPipelinePlugins = parts.allPipelinePlugins;\n\t\tthis.pipelineFactoryOptions = parts.pipelineFactoryOptions;\n\t\tthis.runtimeDeps = parts.runtimeDeps;\n\t\tthis.pipelineRef = parts.pipelineRef;\n\t}\n\n\t/**\n\t * Get the sandbox runner instance (for marketplace install/update)\n\t */\n\tgetSandboxRunner(): SandboxRunner | null {\n\t\treturn sandboxRunner;\n\t}\n\n\t/**\n\t * Whether the sandbox bypass mode (sandbox: false) is active.\n\t * Marketplace install/update handlers use this to skip the\n\t * SANDBOX_NOT_AVAILABLE gate, since the bypass path loads\n\t * marketplace plugins in-process via syncMarketplacePlugins().\n\t */\n\tisSandboxBypassed(): boolean {\n\t\treturn this.runtimeDeps.sandboxBypassed === true;\n\t}\n\n\t/**\n\t * Publish any content whose scheduled time has passed.\n\t * Returns the items promoted so callers can invalidate their cache tags.\n\t */\n\tasync publishScheduled(): Promise<PublishedRef[]> {\n\t\treturn this.publishScheduledWithFence();\n\t}\n\n\tprivate async publishScheduledWithFence(\n\t\tonPublished?: (refs: PublishedRef[]) => Promise<void>,\n\t): Promise<PublishedRef[]> {\n\t\tconst recordWrite = await assertSiteWriteAllowed(this.db);\n\t\tconst currentTime = this.runtimeDeps.now?.() ?? new Date();\n\t\tconst published = await publishDueContent(this.db, {\n\t\t\tpublish: (collection, id, options) =>\n\t\t\t\tthis.handleContentPublish(collection, id, {\n\t\t\t\t\t...options,\n\t\t\t\t\torigin: { source: \"scheduler\" },\n\t\t\t\t}),\n\t\t\tonPublished,\n\t\t\tcurrentTime,\n\t\t});\n\t\tif (published.length > 0) await recordWrite();\n\t\treturn published;\n\t}\n\n\t/**\n\t * Run the full scheduled-maintenance batch: cron tasks, scheduled\n\t * publishing, and system cleanup. For request-less drivers — the\n\t * Cloudflare `scheduled()` handler invokes this from a Cron Trigger.\n\t * (On Node the timer-based scheduler drives the same work itself.)\n\t *\n\t * Each step is independent and non-fatal. Returns the content promoted\n\t * by the publishing sweep so the caller can purge edge-cache tags.\n\t *\n\t * `onPublished` (optional) is awaited after each collection's batch so a\n\t * request-less driver can invalidate edge-cache tags incrementally rather\n\t * than only after the whole sweep — bounding stale-cache exposure if the\n\t * runtime is killed mid-sweep.\n\t */\n\tasync runScheduledTasks(\n\t\toptions: {\n\t\t\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t\t\tinvalidateContentCache?: PluginContentCacheInvalidator;\n\t\t} = {},\n\t): Promise<{ published: PublishedRef[] }> {\n\t\tconst { published } = await this.runScheduledTasksWithStats(options);\n\t\treturn { published };\n\t}\n\n\tasync runScheduledTasksWithStats(\n\t\toptions: {\n\t\t\tonPublished?: (refs: PublishedRef[]) => Promise<void>;\n\t\t\tinvalidateContentCache?: PluginContentCacheInvalidator;\n\t\t} = {},\n\t): Promise<{ processed: number; published: PublishedRef[] }> {\n\t\tif (options.invalidateContentCache) {\n\t\t\tthis.pluginContentCacheInvalidator = options.invalidateContentCache;\n\t\t}\n\t\tlet processed = 0;\n\t\tif (this.cronExecutor) {\n\t\t\ttry {\n\t\t\t\tprocessed = await this.cronExecutor.tick();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[cron] Tick failed:\", error);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.cronExecutor.recoverStaleLocks();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[cron] Stale lock recovery failed:\", error);\n\t\t\t}\n\t\t}\n\n\t\tlet published: PublishedRef[] = [];\n\t\ttry {\n\t\t\tpublished = await this.publishScheduledWithFence(options.onPublished);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[scheduled-publish] Sweep failed:\", error);\n\t\t}\n\n\t\ttry {\n\t\t\tawait runSystemCleanup(this.db, this.storage ?? undefined);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.syncPluginStorageIndexesOnce();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[plugins] Storage index sync failed:\", error);\n\t\t}\n\n\t\t// Never throws; no-op unless scheduled backups are enabled and due.\n\t\tawait maybeRunScheduledBackup(this.db, this.storage ?? undefined);\n\t\tawait recordSchedulerHeartbeatSafely(this.db);\n\n\t\treturn { processed, published };\n\t}\n\n\t/**\n\t * Materialize plugin-declared storage indexes, once per process.\n\t *\n\t * Called from the scheduler path, not from request handlers — configured\n\t * plugins have no install handler, so the tick is their only sync moment.\n\t */\n\tasync syncPluginStorageIndexesOnce(): Promise<void> {\n\t\tif (this.storageIndexesSynced) return;\n\t\tthis.storageIndexesSynced = true;\n\t\t// Sandboxed marketplace/registry plugins never join allPipelinePlugins;\n\t\t// their manifests are cached at bundle load. Without them, plugins\n\t\t// installed before this feature shipped would never get their indexes.\n\t\tawait syncDeclaredStorageIndexes(this.db, [\n\t\t\t...this.allPipelinePlugins,\n\t\t\t...marketplaceManifestCache.values(),\n\t\t]);\n\t}\n\n\t/**\n\t * Stop the cron scheduler gracefully.\n\t * Call during worker shutdown or hot-reload.\n\t */\n\tasync stopCron(): Promise<void> {\n\t\tif (this.cronScheduler) {\n\t\t\tawait this.cronScheduler.stop();\n\t\t}\n\t}\n\n\t/** Stop background work and discard loaded sandbox isolates. */\n\tasync shutdown(): Promise<void> {\n\t\tawait this.stopCron();\n\t\tsandboxRunner?.setCommentModerate?.(null);\n\t\tawait sandboxRunner?.terminateAll();\n\t\tsandboxRunner = null;\n\t\tsandboxedPluginCache.clear();\n\t\tsandboxedManifestCache.clear();\n\t\tsandboxedRouteMetaCache.clear();\n\t\tmarketplaceManifestCache.clear();\n\t\tmarketplacePluginKeys.clear();\n\t\tregistryPluginKeys.clear();\n\t}\n\n\t/**\n\t * Update in-memory plugin status and rebuild the hook pipeline.\n\t *\n\t * Rebuilding the pipeline ensures disabled plugins' hooks stop firing\n\t * and re-enabled plugins' hooks start firing again without a restart.\n\t * Exclusive hook selections are re-resolved after each rebuild.\n\t */\n\tasync setPluginStatus(pluginId: string, status: \"active\" | \"inactive\"): Promise<void> {\n\t\tthis.pluginStates.set(pluginId, status);\n\t\tif (status === \"active\") {\n\t\t\tthis.setSandboxedPluginActive(pluginId, true);\n\t\t\tthis.enabledPlugins.add(pluginId);\n\t\t\tawait this.rebuildHookPipeline();\n\t\t\tawait this._hooks.runPluginActivate(pluginId);\n\t\t} else {\n\t\t\ttry {\n\t\t\t\t// Deactivate hooks retain access until their cleanup has finished.\n\t\t\t\tawait this._hooks.runPluginDeactivate(pluginId);\n\t\t\t} finally {\n\t\t\t\tthis.setSandboxedPluginActive(pluginId, false);\n\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t\tawait this.rebuildHookPipeline();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setSandboxedPluginActive(pluginId: string, active: boolean): void {\n\t\tfor (const [key, plugin] of this.sandboxedPlugins) {\n\t\t\tif (key.slice(0, key.lastIndexOf(\":\")) === pluginId) {\n\t\t\t\tplugin.setActive?.(active);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync handlePluginEnable(pluginId: string) {\n\t\treturn enableRuntimePlugin(this, pluginId);\n\t}\n\n\tasync handlePluginDisable(pluginId: string) {\n\t\treturn disableRuntimePlugin(this, pluginId);\n\t}\n\n\t/** Dispatch teardown lifecycle while the runtime-installed isolate is still loaded. */\n\tasync runPluginUninstallLifecycle(pluginId: string, deleteData: boolean): Promise<void> {\n\t\tconst active = this.isPluginEnabled(pluginId);\n\t\tif (active) {\n\t\t\ttry {\n\t\t\t\tawait this._hooks.runPluginDeactivate(pluginId);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Plugin ${pluginId} deactivate hook failed during uninstall:`, error);\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tconst results = await this._hooks.runPluginUninstall(pluginId, deleteData);\n\t\t\tif (results.length > 0) return;\n\t\t\tawait this.findSandboxedPlugin(pluginId)?.invokeHook(\"plugin:uninstall\", { deleteData });\n\t\t} catch (error) {\n\t\t\tconsole.error(`EmDash: Plugin ${pluginId} uninstall hook failed:`, error);\n\t\t}\n\t}\n\n\tasync runPluginInstallLifecycle(pluginId: string): Promise<void> {\n\t\tawait this._hooks.runPluginInstall(pluginId);\n\t\tawait this._hooks.runPluginActivate(pluginId);\n\t}\n\n\tasync runPluginActivateLifecycle(pluginId: string): Promise<void> {\n\t\tawait this._hooks.runPluginActivate(pluginId);\n\t}\n\n\t/**\n\t * Rebuild the hook pipeline from the current set of enabled plugins.\n\t *\n\t * Filters `allPipelinePlugins` to only those in `enabledPlugins`,\n\t * creates a fresh HookPipeline, re-resolves exclusive hook selections,\n\t * and re-wires the context factory so existing references (cron\n\t * callbacks, email pipeline) use the new pipeline.\n\t */\n\tprivate async rebuildHookPipeline(): Promise<void> {\n\t\tconst enabledList = this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id));\n\t\tconst newPipeline = createHookPipeline(enabledList, this.pipelineFactoryOptions);\n\n\t\t// Re-resolve exclusive hooks against the new pipeline\n\t\tawait EmDashRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);\n\n\t\t// Carry over context factory options from the old pipeline so that\n\t\t// email, cron reschedule, and other wired-in options are preserved.\n\t\t// The old pipeline's contextFactoryOptions were built up incrementally\n\t\t// via setContextFactory calls during create(). We replay them here.\n\t\tif (this.email) {\n\t\t\t// db/getDb are already wired by createHookPipeline above (they live in\n\t\t\t// pipelineFactoryOptions), so the merge only adds emailPipeline.\n\t\t\tnewPipeline.setContextFactory({ emailPipeline: this.email });\n\t\t}\n\t\tnewPipeline.setContextFactory({\n\t\t\t// Plugin schedules remain database-backed when no in-process scheduler\n\t\t\t// exists; an external trigger is responsible for invoking due tasks.\n\t\t\tcronReschedule: () => this.cronScheduler?.reschedule(),\n\t\t});\n\n\t\t// Update the email pipeline to use the new hook pipeline\n\t\tif (this.email) {\n\t\t\tthis.email.setPipeline(newPipeline);\n\t\t}\n\n\t\t// Update the mutable ref so the cron closure dispatches through\n\t\t// the new pipeline without needing to reconstruct the CronExecutor.\n\t\tthis.pipelineRef.current = newPipeline;\n\n\t\tthis._hooks = newPipeline;\n\t}\n\n\t/**\n\t * Synchronize marketplace plugin runtime state with DB + storage.\n\t *\n\t * Ensures install/update/uninstall changes take effect immediately in the\n\t * current worker: loads newly active plugins and removes uninstalled ones.\n\t */\n\tasync syncMarketplacePlugins(): Promise<void> {\n\t\tif (!this.config.marketplace) return;\n\n\t\t// In sandbox bypass mode (sandbox: false), the noop runner reports\n\t\t// unavailable but we still want admin metadata for newly installed\n\t\t// marketplace plugins to refresh in-process. Hooks/routes still won't\n\t\t// execute (matches the cold-start bypass behavior), but Configure\n\t\t// links and admin pages appear immediately.\n\t\tif (this.runtimeDeps.sandboxBypassed) {\n\t\t\tawait this.syncMarketplacePluginsBypassed();\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.syncSandboxedSourcePlugins(\"marketplace\");\n\t}\n\n\t/**\n\t * Synchronize registry plugin runtime state with DB + storage.\n\t *\n\t * Mirrors {@link syncMarketplacePlugins} for plugins installed via the\n\t * experimental decentralized plugin registry. Called after install,\n\t * update, and uninstall handlers complete.\n\t */\n\tasync syncRegistryPlugins(): Promise<void> {\n\t\tif (!getRegistryConfigInput(this.config.registry, this.config.experimental?.registry)) return;\n\t\tawait this.syncSandboxedSourcePlugins(\"registry\");\n\t}\n\n\t/**\n\t * Internal: reconcile in-memory sandboxed-plugin state with the\n\t * `_plugin_state` table for the given source tier. Shared\n\t * implementation behind {@link syncMarketplacePlugins} and\n\t * {@link syncRegistryPlugins}.\n\t *\n\t * Each source tier has its own key set in `${source}PluginKeys` so a\n\t * sync for one tier doesn't invalidate the other.\n\t */\n\tprivate async syncSandboxedSourcePlugins(source: \"marketplace\" | \"registry\"): Promise<void> {\n\t\tif (!this.storage) return;\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) return;\n\n\t\tconst keySet = source === \"marketplace\" ? marketplacePluginKeys : registryPluginKeys;\n\t\tlet pipelineChanged = false;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(this.db);\n\t\t\tconst states =\n\t\t\t\tsource === \"marketplace\"\n\t\t\t\t\t? await stateRepo.getMarketplacePlugins()\n\t\t\t\t\t: await stateRepo.getRegistryPlugins();\n\n\t\t\tconst desired = new Map<string, string>();\n\t\t\tfor (const state of states) {\n\t\t\t\tthis.pluginStates.set(state.pluginId, state.status);\n\t\t\t\tif (state.status === \"active\") {\n\t\t\t\t\tthis.enabledPlugins.add(state.pluginId);\n\t\t\t\t} else {\n\t\t\t\t\tthis.enabledPlugins.delete(state.pluginId);\n\t\t\t\t}\n\t\t\t\tif (state.status !== \"active\") continue;\n\t\t\t\t// Marketplace plugins use `marketplaceVersion` when present;\n\t\t\t\t// registry plugins always use `version`.\n\t\t\t\tconst desiredVersion =\n\t\t\t\t\tsource === \"marketplace\" ? (state.marketplaceVersion ?? state.version) : state.version;\n\t\t\t\tdesired.set(state.pluginId, desiredVersion);\n\t\t\t}\n\n\t\t\t// Remove uninstalled or no-longer-active plugins from memory.\n\t\t\tconst keysToRemove: string[] = [];\n\t\t\tfor (const key of keySet) {\n\t\t\t\tconst [pluginId] = key.split(\":\");\n\t\t\t\tif (!pluginId) continue;\n\t\t\t\tconst desiredVersion = desired.get(pluginId);\n\t\t\t\tif (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;\n\t\t\t\tkeysToRemove.push(key);\n\t\t\t}\n\n\t\t\tfor (const key of keysToRemove) {\n\t\t\t\tconst [pluginId] = key.split(\":\");\n\t\t\t\tif (!pluginId) continue;\n\t\t\t\tconst desiredVersion = desired.get(pluginId);\n\t\t\t\tif (!desiredVersion) {\n\t\t\t\t\tthis.pluginStates.delete(pluginId);\n\t\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t\t}\n\n\t\t\t\tconst existing = sandboxedPluginCache.get(key);\n\t\t\t\tif (existing) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait existing.terminate();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.warn(`EmDash: Failed to terminate sandboxed plugin ${key}:`, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsandboxedPluginCache.delete(key);\n\t\t\t\tsandboxedManifestCache.delete(key);\n\t\t\t\tthis.sandboxedPlugins.delete(key);\n\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\tkeySet.delete(key);\n\t\t\t\tpipelineChanged = true;\n\t\t\t\tif (pluginId) {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t\tmarketplaceManifestCache.delete(pluginId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Load newly active plugins.\n\t\t\tfor (const [pluginId, version] of desired) {\n\t\t\t\tconst key = `${pluginId}:${version}`;\n\t\t\t\tif (sandboxedPluginCache.has(key)) {\n\t\t\t\t\tkeySet.add(key);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst bundle = await loadBundleFromR2(this.storage, pluginId, version, source);\n\t\t\t\tif (!bundle) {\n\t\t\t\t\tconsole.warn(`EmDash: ${source} plugin ${pluginId}@${version} not found in R2`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);\n\t\t\t\tsandboxedPluginCache.set(key, loaded);\n\t\t\t\tsandboxedManifestCache.set(key, bundle.manifest);\n\t\t\t\tthis.sandboxedPlugins.set(key, loaded);\n\t\t\t\tthis.allPipelinePlugins.push(createSandboxedPluginProxy(bundle.manifest, loaded));\n\t\t\t\tkeySet.add(key);\n\t\t\t\tpipelineChanged = true;\n\n\t\t\t\t// Cache manifest admin config for getManifest()\n\t\t\t\tmarketplaceManifestCache.set(pluginId, {\n\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t\tmcp: bundle.manifest.mcp,\n\t\t\t\t\tstorage: bundle.manifest.storage,\n\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts,\n\t\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t\t});\n\n\t\t\t\t// Cache route metadata from manifest for auth decisions\n\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\tconst routeMetaMap = new Map<string, RouteMeta>();\n\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\trouteMetaMap.set(normalized.name, buildRouteMeta(normalized));\n\t\t\t\t\t}\n\t\t\t\t\tsandboxedRouteMetaCache.set(pluginId, routeMetaMap);\n\t\t\t\t} else {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (pipelineChanged) {\n\t\t\t\tawait this.rebuildHookPipeline();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(`EmDash: Failed to sync ${source} plugins:`, error);\n\t\t}\n\t}\n\n\t/**\n\t * Remove a plugin from the in-memory pipeline lists by ID.\n\t * Mutates allPipelinePlugins and configuredPlugins in place.\n\t */\n\tprivate removePluginFromLists(pluginId: string): void {\n\t\tconst allIdx = this.allPipelinePlugins.findIndex((p) => p.id === pluginId);\n\t\tif (allIdx !== -1) this.allPipelinePlugins.splice(allIdx, 1);\n\t\tconst configIdx = this.configuredPlugins.findIndex((p) => p.id === pluginId);\n\t\tif (configIdx !== -1) this.configuredPlugins.splice(configIdx, 1);\n\t}\n\n\t/**\n\t * Sync marketplace plugin metadata in sandbox: false bypass mode.\n\t *\n\t * In bypass mode the noop runner can't load plugins, but admin pages,\n\t * widgets, and route metadata still need to refresh in-process when an\n\t * admin installs/updates/uninstalls a marketplace plugin. Otherwise the\n\t * admin UI shows stale data until the server restarts.\n\t *\n\t * Hooks and routes still won't execute under bypass (matches the\n\t * cold-start bypass behavior in loadMarketplacePluginsBypassed).\n\t *\n\t * Known limitation: bypass plugins are loaded via `import(dataUrl)`,\n\t * which Node's ESM cache keys on the full URL. Updates create fresh\n\t * module objects, but old ones remain cached for the worker's lifetime.\n\t * In practice this is a few KB per update — only matters for sites with\n\t * very frequent marketplace updates running long-lived processes. The\n\t * fix would be vm.SourceTextModule for explicit lifecycle management.\n\t */\n\tprivate async syncMarketplacePluginsBypassed(): Promise<void> {\n\t\tif (!this.storage) return;\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(this.db);\n\t\t\tconst marketplaceStates = await stateRepo.getMarketplacePlugins();\n\n\t\t\tconst desired = new Map<string, string>();\n\t\t\tfor (const state of marketplaceStates) {\n\t\t\t\tthis.pluginStates.set(state.pluginId, state.status);\n\t\t\t\tif (state.status === \"active\") {\n\t\t\t\t\tthis.enabledPlugins.add(state.pluginId);\n\t\t\t\t} else {\n\t\t\t\t\tthis.enabledPlugins.delete(state.pluginId);\n\t\t\t\t}\n\t\t\t\tif (state.status !== \"active\") continue;\n\t\t\t\tdesired.set(state.pluginId, state.marketplaceVersion ?? state.version);\n\t\t\t}\n\n\t\t\t// Drop metadata for plugins no longer active.\n\t\t\tconst toRemove: string[] = [];\n\t\t\tfor (const pluginId of marketplaceManifestCache.keys()) {\n\t\t\t\tif (!desired.has(pluginId)) toRemove.push(pluginId);\n\t\t\t}\n\t\t\tfor (const pluginId of toRemove) {\n\t\t\t\t// Fire plugin:deactivate hook before removal\n\t\t\t\tconst resolved = this.allPipelinePlugins.find((p) => p.id === pluginId);\n\t\t\t\tif (resolved) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst deactivateHook = resolved.hooks?.[\"plugin:deactivate\"];\n\t\t\t\t\t\tif (deactivateHook) {\n\t\t\t\t\t\t\tconst handler =\n\t\t\t\t\t\t\t\ttypeof deactivateHook === \"function\" ? deactivateHook : deactivateHook.handler;\n\t\t\t\t\t\t\tif (typeof handler === \"function\") {\n\t\t\t\t\t\t\t\t// Sandbox-bypass cleanup: the plugin context isn't constructable\n\t\t\t\t\t\t\t\t// here (no DB binding, no media, etc.), but well-behaved\n\t\t\t\t\t\t\t\t// deactivate hooks should be no-op safe. If a hook does require\n\t\t\t\t\t\t\t\t// ctx, it throws and the surrounding catch logs it.\n\t\t\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- best-effort cleanup; see comment above\n\t\t\t\t\t\t\t\tawait handler({ pluginId }, {} as never);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.warn(`[emdash] plugin:deactivate hook failed for ${pluginId}:`, err);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tmarketplaceManifestCache.delete(pluginId);\n\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t// Remove from pipeline lists too (mutate in place since the\n\t\t\t\t// arrays are readonly references but mutable contents)\n\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\tthis.enabledPlugins.delete(pluginId);\n\t\t\t}\n\n\t\t\t// Load plugin code, adapt as trusted plugins, and add to pipeline lists\n\t\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\t\t\tconst newPlugins: ResolvedPlugin[] = [];\n\t\t\tfor (const [pluginId, version] of desired) {\n\t\t\t\tconst bundle = await loadBundleFromR2(this.storage, pluginId, version);\n\t\t\t\tif (!bundle) {\n\t\t\t\t\tconsole.warn(`EmDash: Marketplace plugin ${pluginId}@${version} not found in R2`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tmarketplaceManifestCache.set(pluginId, {\n\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t\tmcp: bundle.manifest.mcp,\n\t\t\t\t\tstorage: bundle.manifest.storage,\n\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts,\n\t\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t\t});\n\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\tconst routeMetaMap = new Map<string, RouteMeta>();\n\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\trouteMetaMap.set(normalized.name, buildRouteMeta(normalized));\n\t\t\t\t\t}\n\t\t\t\t\tsandboxedRouteMetaCache.set(pluginId, routeMetaMap);\n\t\t\t\t} else {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(pluginId);\n\t\t\t\t}\n\n\t\t\t\t// Skip if already in the pipeline at this version\n\t\t\t\tconst existing = this.allPipelinePlugins.find((p) => p.id === pluginId);\n\t\t\t\tif (existing && existing.version === bundle.manifest.version) continue;\n\n\t\t\t\t// Remove any older version\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.removePluginFromLists(pluginId);\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString(\"base64\")}`;\n\t\t\t\t\t// Dynamic data: import returns `any` from a base64-encoded module.\n\t\t\t\t\t// We trust the bundle to be shaped like a plugin (built by plugin-cli);\n\t\t\t\t\t// adaptSandboxEntry then validates fields it cares about.\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle\n\t\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tunknown\n\t\t\t\t\t>;\n\t\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t\t>[0];\n\t\t\t\t\tconst adapted = adaptSandboxEntry(pluginDef, {\n\t\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\t\tentrypoint: \"\",\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities ?? [],\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts ?? [],\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\t\tstorage: (bundle.manifest.storage ?? {}) as never,\n\t\t\t\t\t\tadminPages: bundle.manifest.admin?.pages,\n\t\t\t\t\t\tadminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({\n\t\t\t\t\t\t\tid: w.id,\n\t\t\t\t\t\t\ttitle: w.title,\n\t\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tsettingsSchema: bundle.manifest.admin?.settingsSchema,\n\t\t\t\t\t\teditorPanels: bundle.manifest.admin?.editorPanels,\n\t\t\t\t\t\teditorActions: bundle.manifest.admin?.editorActions,\n\t\t\t\t\t});\n\t\t\t\t\tnewPlugins.push(adapted);\n\t\t\t\t\tthis.allPipelinePlugins.push(adapted);\n\t\t\t\t\tthis.configuredPlugins.push(adapted);\n\t\t\t\t\tthis.enabledPlugins.add(adapted.id);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`EmDash: Failed to load marketplace plugin ${pluginId}@${version} in-process:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If anything changed, rebuild the hook pipeline so new/removed\n\t\t\t// plugins take effect immediately without a server restart.\n\t\t\tif (toRemove.length > 0 || newPlugins.length > 0) {\n\t\t\t\tawait this.rebuildHookPipeline();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"EmDash: Failed to sync marketplace plugins (bypass):\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create and initialize the runtime\n\t */\n\tstatic async create(\n\t\tdeps: RuntimeDependencies,\n\t\ttimings?: Array<{ name: string; dur: number; desc?: string }>,\n\t): Promise<EmDashRuntime> {\n\t\t// Helper: time a phase and push into the shared timings array when\n\t\t// provided. Uses performance.now() — monotonic across async boundaries.\n\t\t// No-op when `timings` wasn't passed (preserves backwards compatibility\n\t\t// with callers that don't care about per-phase breakdown).\n\t\tconst phase = async <T>(name: string, desc: string, fn: () => Promise<T>): Promise<T> => {\n\t\t\tif (!timings) return fn();\n\t\t\tconst t0 = performance.now();\n\t\t\ttry {\n\t\t\t\treturn await fn();\n\t\t\t} finally {\n\t\t\t\ttimings.push({ name, dur: performance.now() - t0, desc });\n\t\t\t}\n\t\t};\n\n\t\t// Initialize the database and enforce its configured migration policy.\n\t\tconst db = await phase(\"rt.db\", \"DB init + migration policy\", () =>\n\t\t\tEmDashRuntime.getDatabase(deps),\n\t\t);\n\n\t\t// Resolver for the live connection, mirroring the `get db()` getter\n\t\t// below (which can't be used here — the runtime instance doesn't exist\n\t\t// yet). Long-lived subsystems built during create() (cron executor,\n\t\t// plugin context factory, media providers) capture this resolver rather\n\t\t// than the `db` snapshot, so a connection-backed adapter (Postgres over\n\t\t// Hyperdrive) serves their queries from the current request/event-scoped\n\t\t// connection in ALS instead of the per-isolate singleton — whose socket\n\t\t// belongs to an earlier request and would trip workerd's cross-request\n\t\t// I/O guard. Stateless adapters (D1, Node SQLite) set no ALS db on most\n\t\t// paths, so this falls back to the singleton: unchanged behavior.\n\t\tconst resolveDb = (): Kysely<Database> => {\n\t\t\tconst ctx = getRequestContext();\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- ALS db is typed unknown to avoid a circular import; middleware always sets a Kysely<Database>\n\t\t\treturn (ctx?.db as Kysely<Database> | undefined) ?? db;\n\t\t};\n\n\t\t// Validate EMDASH_ENCRYPTION_KEY once here so a malformed value\n\t\t// surfaces in startup logs. Plugin secret-setting operations re-read\n\t\t// the key and fail closed at their own boundary, so validation here\n\t\t// does not block unrelated request paths.\n\t\tawait phase(\"rt.secrets\", \"Validate encryption key\", () => validateEncryptionKeyAtStartup());\n\n\t\t// FTS verify/repair is deferred off the cold-start hot path.\n\t\t// See EmDashRuntime.ensureSearchHealthy().\n\n\t\t// Initialize storage (sync)\n\t\tconst storage = EmDashRuntime.getStorage(deps);\n\n\t\tlet pluginStates: Map<string, string> = new Map();\n\t\tconst configuredLocales: string[] =\n\t\t\tvirtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];\n\t\tconst localeCasingRepairVersion = getLocaleCasingRepairVersion(configuredLocales);\n\t\tlet storedLocaleCasingRepairVersion: string | undefined;\n\t\tlet siteInfo:\n\t\t\t| {\n\t\t\t\t\tsiteName?: string;\n\t\t\t\t\tsiteUrl?: string;\n\t\t\t\t\tlocale?: string;\n\t\t\t\t\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n\t\t\t }\n\t\t\t| undefined;\n\t\t// \"Already set up\" by default so a read failure (e.g. tables absent on a\n\t\t// pre-migration db) skips seeding rather than seeding a half-built db.\n\t\tlet seedGate = { collectionCount: 1, setupDone: true };\n\n\t\t// Seeding must only touch the configured singleton, never a borrowed\n\t\t// per-request db (playground / DO preview) or the loader-fallback db.\n\t\tconst reqCtx = getRequestContext();\n\t\tconst ownsConfiguredDb = !!deps.config.database && !(reqCtx?.dbIsIsolated && reqCtx.db);\n\n\t\t// Run the init reads on a coalescing connection so the concurrent\n\t\t// same-turn reads flush as one batch() round trip. The singleton can't:\n\t\t// its plain SqliteAdapter reports supportsMultipleConnections=false, so\n\t\t// Kysely's connection mutex serializes concurrent reads into N round\n\t\t// trips. The connection is per-init and discarded — the long-lived\n\t\t// singleton must never coalesce or one request's reads could land in\n\t\t// another's batch. Backends without a coalescing dialect (Node/SQLite)\n\t\t// fall back to the singleton, where serialization costs nothing.\n\t\tlet readDb = db;\n\t\tlet readDbDisposable: Kysely<Database> | undefined;\n\t\tconst disposeReadDb = async () => {\n\t\t\tconst disposable = readDbDisposable;\n\t\t\treadDbDisposable = undefined;\n\t\t\tif (!disposable) return;\n\t\t\ttry {\n\t\t\t\tawait disposable.destroy();\n\t\t\t} catch {\n\t\t\t\t// Non-fatal — the underlying binding is shared and needs no teardown.\n\t\t\t}\n\t\t};\n\t\tif (ownsConfiguredDb && deps.createCoalescingDialect && deps.config.database) {\n\t\t\ttry {\n\t\t\t\tconst dialect = deps.createCoalescingDialect(deps.config.database.config);\n\t\t\t\tif (dialect) {\n\t\t\t\t\treadDb = new Kysely<Database>({ dialect, log: kyselyLogOption() });\n\t\t\t\t\treadDbDisposable = readDb;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treadDb = db;\n\t\t\t}\n\t\t}\n\t\tconst optionsRepo = new OptionsRepository(readDb);\n\t\tlet missingManualSchemaError: unknown;\n\t\tconst captureMissingManualSchema = (error: unknown) => {\n\t\t\tif ((deps.migrationMode ?? \"auto\") === \"manual\" && isMissingTableError(error)) {\n\t\t\t\tmissingManualSchemaError ??= error;\n\t\t\t}\n\t\t};\n\n\t\tconst readSiteInfo = async () => {\n\t\t\tconst siteOpts = await optionsRepo.getMany<string>([\n\t\t\t\t\"emdash:site_title\",\n\t\t\t\t\"emdash:site_url\",\n\t\t\t\t\"emdash:locale\",\n\t\t\t\tLOCALE_CASING_REPAIR_OPTION,\n\t\t\t]);\n\t\t\tstoredLocaleCasingRepairVersion = siteOpts.get(LOCALE_CASING_REPAIR_OPTION);\n\t\t\treturn {\n\t\t\t\tsiteName: deps.siteInfo?.name ?? siteOpts.get(\"emdash:site_title\") ?? undefined,\n\t\t\t\tsiteUrl: deps.siteInfo?.url ?? siteOpts.get(\"emdash:site_url\") ?? undefined,\n\t\t\t\tlocale: deps.siteInfo?.locale ?? siteOpts.get(\"emdash:locale\") ?? undefined,\n\t\t\t\t// trailingSlash is a build-time Astro routing decision, not a\n\t\t\t\t// user-editable setting, so it comes from the Astro config\n\t\t\t\t// (virtual:emdash/config), not the options table.\n\t\t\t\ttrailingSlash: deps.siteInfo?.trailingSlash ?? virtualConfig?.trailingSlash,\n\t\t\t};\n\t\t};\n\n\t\tconst coldStartReads: Array<Promise<void>> = [\n\t\t\tphase(\"rt.plugins\", \"Plugin states\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst states = await readDb\n\t\t\t\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t\t\t\t.select([\"plugin_id\", \"status\"])\n\t\t\t\t\t\t.execute();\n\t\t\t\t\tpluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));\n\t\t\t\t} catch (error) {\n\t\t\t\t\tcaptureMissingManualSchema(error);\n\t\t\t\t\t// _plugin_state may not exist yet on a pre-migration db.\n\t\t\t\t}\n\t\t\t}),\n\t\t\tphase(\"rt.site\", \"Site info options\", async () => {\n\t\t\t\ttry {\n\t\t\t\t\tsiteInfo = await readSiteInfo();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tcaptureMissingManualSchema(error);\n\t\t\t\t\t// options may not exist yet on a pre-migration db.\n\t\t\t\t}\n\t\t\t}),\n\t\t];\n\n\t\tif (ownsConfiguredDb) {\n\t\t\tcoldStartReads.push(\n\t\t\t\tphase(\"rt.seedcheck\", \"Auto-seed gate\", async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Selecting the slugs instead of COUNT(*) costs the same\n\t\t\t\t\t\t// round trip and primes the registered-collections cache,\n\t\t\t\t\t\t// so the first render on this isolate skips its own lookup.\n\t\t\t\t\t\tconst [collectionRows, setupOption] = await Promise.all([\n\t\t\t\t\t\t\treadDb.selectFrom(\"_emdash_collections\").select(\"slug\").execute(),\n\t\t\t\t\t\t\treadDb\n\t\t\t\t\t\t\t\t.selectFrom(\"options\")\n\t\t\t\t\t\t\t\t.select(\"value\")\n\t\t\t\t\t\t\t\t.where(\"name\", \"=\", \"emdash:setup_complete\")\n\t\t\t\t\t\t\t\t.executeTakeFirst(),\n\t\t\t\t\t\t]);\n\t\t\t\t\t\tprimeRegisteredCollections(collectionRows.map((row) => row.slug));\n\t\t\t\t\t\tconst setupDone = (() => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn !!setupOption && JSON.parse(setupOption.value) === true;\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})();\n\t\t\t\t\t\tseedGate = { collectionCount: collectionRows.length, setupDone };\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tcaptureMissingManualSchema(error);\n\t\t\t\t\t\t// Leave the \"already set up\" default so a read failure never\n\t\t\t\t\t\t// triggers a seed onto a half-built db.\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tawait Promise.all(coldStartReads);\n\t\tif (missingManualSchemaError) {\n\t\t\tawait disposeReadDb();\n\t\t\tthrow missingManualSchemaError;\n\t\t}\n\n\t\tif (\n\t\t\tlocaleCasingRepairVersion &&\n\t\t\t(configuredLocales.some(\n\t\t\t\t(locale) => locale.includes(\"-\") || locale !== locale.toLowerCase(),\n\t\t\t) ||\n\t\t\t\tstoredLocaleCasingRepairVersion !== undefined) &&\n\t\t\tstoredLocaleCasingRepairVersion !== localeCasingRepairVersion\n\t\t) {\n\t\t\tawait phase(\"rt.locale\", \"Repair locale casing\", async () => {\n\t\t\t\tawait repairLocaleCasing(db, configuredLocales);\n\t\t\t\tawait new OptionsRepository(db).set(LOCALE_CASING_REPAIR_OPTION, localeCasingRepairVersion);\n\t\t\t});\n\t\t}\n\n\t\t// Auto-seed the default schema for a first load that skipped the setup\n\t\t// wizard (the wizard and dev-bypass apply seeds explicitly). Run under a\n\t\t// per-isolate lock keyed by the configured db so a reclaimed-and-rerun\n\t\t// create() can't apply the seed a second time concurrently.\n\t\tif (seedGate.collectionCount === 0 && !seedGate.setupDone) {\n\t\t\ttry {\n\t\t\t\tconst activation = await activateMediaUsageCapture(db, { writersDrained: true });\n\t\t\t\tif (activation.outcome !== \"active\") {\n\t\t\t\t\tthrow new Error(\"Fresh-site media usage activation did not complete\");\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tawait disposeReadDb();\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst seedKey = deps.config.database?.entrypoint ?? \"default\";\n\t\t\tconst seedHolder = getSeedHolder();\n\t\t\ttry {\n\t\t\t\tawait initWithLock(\n\t\t\t\t\tseedHolder.lock,\n\t\t\t\t\t() => (seedHolder.done.has(seedKey) ? true : undefined),\n\t\t\t\t\tasync () => {\n\t\t\t\t\t\tconst { applySeed } = await import(\"./seed/apply.js\");\n\t\t\t\t\t\tconst { loadSeed } = await import(\"./seed/load.js\");\n\t\t\t\t\t\tconst { validateSeed } = await import(\"./seed/validate.js\");\n\n\t\t\t\t\t\tconst seed = await loadSeed();\n\t\t\t\t\t\tconst validation = validateSeed(seed);\n\t\t\t\t\t\tif (validation.valid) {\n\t\t\t\t\t\t\tconst seedResult = await applySeed(db, seed, { onConflict: \"skip\" });\n\t\t\t\t\t\t\tconsole.log(\"Auto-seeded default collections\");\n\t\t\t\t\t\t\tif (seedResult.taxonomies.skipped > 0) {\n\t\t\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t\t\t`[auto-seed] Kept ${seedResult.taxonomies.skipped} existing taxonomy definition(s) instead of the seed's. Edit them in the admin, or run \\`emdash seed <file> --on-conflict update\\` to replace them (this also overwrites other seeded records).`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tseedHolder.done.add(seedKey);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t},\n\t\t\t\t\t{ deadlineMs: DB_INIT_DEADLINE_MS, anchor: (promise) => after(() => promise) },\n\t\t\t\t);\n\t\t\t\t// The site-info read ran before the seed wrote its defaults, so\n\t\t\t\t// refresh the snapshot the plugin context sees.\n\t\t\t\ttry {\n\t\t\t\t\tsiteInfo = await readSiteInfo();\n\t\t\t\t} catch {\n\t\t\t\t\t// Non-fatal — plugin context falls back to undefined fields.\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Non-fatal — a failed seed (e.g. missing seed module) leaves the\n\t\t\t\t// site un-seeded; a later request retries.\n\t\t\t}\n\t\t}\n\n\t\t// The read connection is single-use; everything below uses the singleton.\n\t\tawait disposeReadDb();\n\n\t\tconst enabledPlugins = new Set<string>();\n\t\tfor (const plugin of deps.plugins) {\n\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t}\n\t\t}\n\n\t\t// Build the full list of pipeline-eligible plugins: all configured\n\t\t// plugins (regardless of current enabled status) plus built-in plugins.\n\t\t// rebuildHookPipeline() filters this to only enabled plugins.\n\t\tconst allPipelinePlugins: ResolvedPlugin[] = [...deps.plugins];\n\n\t\t// Collected bypassed plugins (sandbox: false escape hatch).\n\t\t// These need to be added to BOTH the pipeline (for hooks) AND the\n\t\t// configuredPlugins list (for route dispatch).\n\t\tconst bypassedPluginsList: ResolvedPlugin[] = [];\n\n\t\t// In dev mode, register a built-in console email provider.\n\t\t// It participates in exclusive hook resolution like any other plugin —\n\t\t// auto-selected when it's the sole provider, overridden when a real one is configured.\n\t\t// Gated by import.meta.env.DEV to prevent silent email loss in production.\n\t\tif (import.meta.env.DEV) {\n\t\t\ttry {\n\t\t\t\tconst devConsolePlugin = definePlugin({\n\t\t\t\t\tid: DEV_CONSOLE_EMAIL_PLUGIN_ID,\n\t\t\t\t\tversion: \"0.0.0\",\n\t\t\t\t\tcapabilities: [\"hooks.email-transport:register\"],\n\t\t\t\t\thooks: {\n\t\t\t\t\t\t\"email:deliver\": {\n\t\t\t\t\t\t\texclusive: true,\n\t\t\t\t\t\t\thandler: devConsoleEmailDeliver,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\tallPipelinePlugins.push(devConsolePlugin);\n\t\t\t\t// Built-in plugins are always enabled\n\t\t\t\tenabledPlugins.add(devConsolePlugin.id);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\"[email] Failed to register dev console email provider:\", error);\n\t\t\t}\n\t\t}\n\n\t\t// Register built-in default comment moderator.\n\t\t// Always present as a fallback: exclusive hook resolution selects a\n\t\t// single plugin moderator (e.g. AI moderation) over it unless the site\n\t\t// has already stored a comment:moderate selection.\n\t\ttry {\n\t\t\tconst defaultModeratorPlugin = definePlugin({\n\t\t\t\tid: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,\n\t\t\t\tversion: \"0.0.0\",\n\t\t\t\tcapabilities: [\"users:read\"],\n\t\t\t\thooks: {\n\t\t\t\t\t\"comment:moderate\": {\n\t\t\t\t\t\texclusive: true,\n\t\t\t\t\t\thandler: defaultCommentModerate,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\t\t\tallPipelinePlugins.push(defaultModeratorPlugin);\n\t\t\t// Built-in plugins are always enabled\n\t\t\tenabledPlugins.add(defaultModeratorPlugin.id);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[comments] Failed to register default moderator:\", error);\n\t\t}\n\n\t\t// sandbox: false escape hatch - load sandboxed plugin entries in-process\n\t\t// as trusted plugins (no isolation) so they participate in the hook pipeline.\n\t\t// Block this on Cloudflare Workers where dynamic import(dataUrl) is not\n\t\t// available and running untrusted code in-process is a security risk.\n\t\tif (deps.sandboxBypassed && deps.sandboxedPluginEntries.length > 0) {\n\t\t\tconst isCfWorkers =\n\t\t\t\ttypeof navigator !== \"undefined\" &&\n\t\t\t\ttypeof navigator.userAgent === \"string\" &&\n\t\t\t\tnavigator.userAgent.includes(\"Cloudflare-Workers\");\n\t\t\tif (isCfWorkers) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"sandbox: false is not supported in Cloudflare Workers. \" +\n\t\t\t\t\t\t\"Remove the sandbox: false option or use the Cloudflare sandbox runner.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsole.info(\n\t\t\t\t\"EmDash: Sandbox disabled (sandbox: false). \" +\n\t\t\t\t\t\"Sandboxed plugins will run in-process without isolation.\",\n\t\t\t);\n\t\t\tconst bypassedPlugins = await EmDashRuntime.loadBypassedPlugins(deps.sandboxedPluginEntries);\n\t\t\tfor (const plugin of bypassedPlugins) {\n\t\t\t\tallPipelinePlugins.push(plugin);\n\t\t\t\tbypassedPluginsList.push(plugin);\n\t\t\t\t// Respect plugin state: only enable if active or no record exists.\n\t\t\t\t// Plugins an admin previously disabled should stay disabled.\n\t\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// In bypass mode, also load marketplace plugins from R2 as trusted\n\t\t// in-process plugins BEFORE pipeline creation. They need to be in the\n\t\t// pipeline to participate in hook dispatch.\n\t\tif (deps.sandboxBypassed && deps.config.marketplace && storage) {\n\t\t\tconst marketplaceBypassed = await EmDashRuntime.loadMarketplacePluginsBypassed(db, storage);\n\t\t\tfor (const plugin of marketplaceBypassed) {\n\t\t\t\tallPipelinePlugins.push(plugin);\n\t\t\t\tbypassedPluginsList.push(plugin);\n\t\t\t\tconst status = pluginStates.get(plugin.id);\n\t\t\t\tif (status === undefined || status === \"active\") {\n\t\t\t\t\tenabledPlugins.add(plugin.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Load sandboxed plugins (build-time, sandbox runner path)\n\t\tconst sandboxedPluginPool = await phase(\"rt.sandbox\", \"Sandboxed plugins\", () =>\n\t\t\tEmDashRuntime.loadSandboxedPlugins(deps, db, storage, siteInfo),\n\t\t);\n\t\tfor (const [key, plugin] of sandboxedPluginPool) {\n\t\t\tconst pluginId = key.slice(0, key.lastIndexOf(\":\"));\n\t\t\tconst status = pluginStates.get(pluginId);\n\t\t\tplugin.setActive?.(status === undefined || status === \"active\");\n\t\t}\n\n\t\t// Cold-start: load marketplace- and registry-installed plugins from\n\t\t// site R2 via the sandbox runner. The two tiers only depend on the\n\t\t// sandbox phase above, not on each other, so when both are enabled\n\t\t// they run concurrently instead of paying two sequential loads.\n\t\t// In bypass mode marketplace plugins were already handled above.\n\t\tconst installedTierPhases: Promise<void>[] = [];\n\t\tif (deps.config.marketplace && storage && !deps.sandboxBypassed) {\n\t\t\tinstalledTierPhases.push(\n\t\t\t\tphase(\"rt.market\", \"Marketplace plugins\", () =>\n\t\t\t\t\tEmDashRuntime.loadInstalledSandboxedPlugins(\n\t\t\t\t\t\t\"marketplace\",\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tstorage,\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tsandboxedPluginPool,\n\t\t\t\t\t\tsiteInfo,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Cold-start: load registry-installed plugins from site R2\n\t\tif (\n\t\t\tgetRegistryConfigInput(deps.config.registry, deps.config.experimental?.registry) &&\n\t\t\tstorage\n\t\t) {\n\t\t\tinstalledTierPhases.push(\n\t\t\t\tphase(\"rt.registry\", \"Registry plugins\", () =>\n\t\t\t\t\tEmDashRuntime.loadInstalledSandboxedPlugins(\n\t\t\t\t\t\t\"registry\",\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tstorage,\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tsandboxedPluginPool,\n\t\t\t\t\t\tsiteInfo,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tif (installedTierPhases.length > 0) {\n\t\t\tawait Promise.all(installedTierPhases);\n\t\t}\n\n\t\tconst configuredSandboxIds = new Set(deps.sandboxedPluginEntries.map((entry) => entry.id));\n\t\tconst sandboxedPlugins = new Map(\n\t\t\t[...sandboxedPluginPool].filter(([pluginKey]) => {\n\t\t\t\tconst pluginId = pluginKey.slice(0, pluginKey.lastIndexOf(\":\"));\n\t\t\t\treturn configuredSandboxIds.has(pluginId) || pluginStates.get(pluginId) === \"active\";\n\t\t\t}),\n\t\t);\n\n\t\t// Add isolate-backed proxies to the same host pipeline as trusted\n\t\t// plugins. The proxy carries only manifest metadata and RPC handlers;\n\t\t// plugin code remains inside the configured runner.\n\t\tfor (const [pluginKey, instance] of sandboxedPlugins) {\n\t\t\tconst manifest = sandboxedManifestCache.get(pluginKey);\n\t\t\tif (!manifest) continue;\n\t\t\tallPipelinePlugins.push(createSandboxedPluginProxy(manifest, instance));\n\t\t\tconst status = pluginStates.get(manifest.id);\n\t\t\tif (status === undefined || status === \"active\") enabledPlugins.add(manifest.id);\n\t\t}\n\n\t\t// Create hook pipeline. getDb travels here (not just via the email\n\t\t// setContextFactory call below) so it survives rebuildHookPipeline(),\n\t\t// which reconstructs the factory from pipelineFactoryOptions. Without it,\n\t\t// toggling a plugin on an email-less deployment would silently revert\n\t\t// plugin contexts to the singleton db — re-breaking connection-backed\n\t\t// adapters. See #1622.\n\t\tconst runtimeRef: { current: EmDashRuntime | null } = { current: null };\n\t\tconst requireRuntime = (): EmDashRuntime => {\n\t\t\tif (!runtimeRef.current) throw new Error(\"EmDash runtime is not ready\");\n\t\t\treturn runtimeRef.current;\n\t\t};\n\t\tconst contentActions: ContentActionCallbacks = {\n\t\t\tbegin: (pluginId, invocationId, invalidateContentCache) =>\n\t\t\t\trequireRuntime().beginPluginInvocation(pluginId, invocationId, invalidateContentCache),\n\t\t\tflush: (pluginId, invocationId, final) =>\n\t\t\t\trequireRuntime().flushPluginAfterHooks(pluginId, invocationId, final),\n\t\t\tgetVersioned: (pluginId, collection, id) =>\n\t\t\t\trequireRuntime().pluginGetVersioned(pluginId, collection, id),\n\t\t\tpublish: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>\n\t\t\t\trequireRuntime().pluginPublish(\n\t\t\t\t\tpluginId,\n\t\t\t\t\tcollection,\n\t\t\t\t\tid,\n\t\t\t\t\toptions,\n\t\t\t\t\tinvocationId,\n\t\t\t\t\tinvalidateContentCache,\n\t\t\t\t),\n\t\t\tunpublish: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>\n\t\t\t\trequireRuntime().pluginUnpublish(\n\t\t\t\t\tpluginId,\n\t\t\t\t\tcollection,\n\t\t\t\t\tid,\n\t\t\t\t\toptions,\n\t\t\t\t\tinvocationId,\n\t\t\t\t\tinvalidateContentCache,\n\t\t\t\t),\n\t\t\tschedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>\n\t\t\t\trequireRuntime().pluginSchedule(\n\t\t\t\t\tpluginId,\n\t\t\t\t\tcollection,\n\t\t\t\t\tid,\n\t\t\t\t\toptions,\n\t\t\t\t\tinvocationId,\n\t\t\t\t\tinvalidateContentCache,\n\t\t\t\t),\n\t\t\tunschedule: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>\n\t\t\t\trequireRuntime().pluginUnschedule(\n\t\t\t\t\tpluginId,\n\t\t\t\t\tcollection,\n\t\t\t\t\tid,\n\t\t\t\t\toptions,\n\t\t\t\t\tinvocationId,\n\t\t\t\t\tinvalidateContentCache,\n\t\t\t\t),\n\t\t\tgetTrashedVersioned: (pluginId, collection, id) =>\n\t\t\t\trequireRuntime().pluginGetTrashedVersioned(pluginId, collection, id),\n\t\t\trestore: (pluginId, collection, id, options, invocationId, invalidateContentCache) =>\n\t\t\t\trequireRuntime().pluginRestore(\n\t\t\t\t\tpluginId,\n\t\t\t\t\tcollection,\n\t\t\t\t\tid,\n\t\t\t\t\toptions,\n\t\t\t\t\tinvocationId,\n\t\t\t\t\tinvalidateContentCache,\n\t\t\t\t),\n\t\t};\n\t\tconst pipelineFactoryOptions = {\n\t\t\tdb,\n\t\t\tgetDb: resolveDb,\n\t\t\tbeforeContentWrite: () => assertSiteWriteAllowed(resolveDb()),\n\t\t\tcontentActions,\n\t\t\tnow: deps.now,\n\t\t\tstorage: storage ?? undefined,\n\t\t\tsiteInfo,\n\t\t\tcommentModerate: (\n\t\t\t\tpluginId: string,\n\t\t\t\tid: string,\n\t\t\t\tstatus: PluginCommentStatus,\n\t\t\t\texpectedStatus: PluginCommentStatus,\n\t\t\t) => {\n\t\t\t\tconst current = runtimeRef.current;\n\t\t\t\tif (!current) throw new Error(\"Comment moderation is unavailable during runtime startup\");\n\t\t\t\treturn current.handlePluginCommentModerate(pluginId, id, status, expectedStatus);\n\t\t\t},\n\t\t};\n\t\tconst enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));\n\t\tconst pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);\n\n\t\t// Initialize media providers\n\t\tconst mediaProviders = new Map<string, MediaProvider>();\n\t\tconst mediaProviderEntries = deps.mediaProviderEntries ?? [];\n\t\tconst providerContext: MediaProviderContext = { db, storage, getDb: resolveDb };\n\n\t\tfor (const entry of mediaProviderEntries) {\n\t\t\ttry {\n\t\t\t\tconst provider = entry.createProvider(providerContext);\n\t\t\t\tmediaProviders.set(entry.id, provider);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(`Failed to initialize media provider \"${entry.id}\":`, error);\n\t\t\t}\n\t\t}\n\n\t\t// Resolve exclusive hooks — auto-select providers and sync with DB\n\t\tawait phase(\"rt.hooks\", \"Exclusive hook resolution\", () =>\n\t\t\tEmDashRuntime.resolveExclusiveHooks(pipeline, db, deps),\n\t\t);\n\n\t\t// ── Email pipeline ───────────────────────────────────────────────\n\t\t// The email pipeline orchestrates beforeSend → deliver → afterSend.\n\t\t// The dev console provider was registered above and will be auto-selected\n\t\t// by resolveExclusiveHooks if it's the sole email:deliver provider.\n\t\tconst emailPipeline = new EmailPipeline(pipeline);\n\n\t\t// Wire email send into sandbox runner (created earlier but without\n\t\t// email pipeline since it didn't exist yet)\n\t\tif (sandboxRunner) {\n\t\t\tsandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));\n\t\t}\n\n\t\t// ── Cron system ──────────────────────────────────────────────────\n\t\t// Create executor with a hook dispatch function that uses the pipeline.\n\t\t// The callback reads from a mutable ref so that rebuildHookPipeline()\n\t\t// can swap the pipeline without reconstructing the CronExecutor.\n\t\tconst pipelineRef = { current: pipeline };\n\t\tconst invokeCronHook: InvokeCronHookFn = async (pluginId, event) => {\n\t\t\tconst result = await pipelineRef.current.invokeCronHook(pluginId, event);\n\t\t\tif (!result.success && result.error) {\n\t\t\t\tthrow result.error;\n\t\t\t}\n\t\t};\n\n\t\t// Wire email pipeline into context factory (independent of cron —\n\t\t// must not be inside the cron try/catch or ctx.email breaks when cron fails).\n\t\t// db/getDb were already set via pipelineFactoryOptions above; merge only\n\t\t// adds emailPipeline.\n\t\tpipeline.setContextFactory({ emailPipeline });\n\n\t\tlet cronExecutor: CronExecutor | null = null;\n\t\tlet cronScheduler: CronScheduler | null = null;\n\t\t// Populated with the constructed runtime just before this method returns,\n\t\t// so the timer scheduler's cleanup can route scheduled publishing through\n\t\t// the runtime wrapper (firing content:afterPublish hooks). The first tick\n\t\t// is ≥1s out, well after the synchronous assignment below.\n\t\tawait phase(\"rt.cron\", \"Cron init (recovery deferred post-response)\", async () => {\n\t\t\ttry {\n\t\t\t\tcronExecutor = new CronExecutor(resolveDb, invokeCronHook, deps.now);\n\t\t\t\t// Plugin schedules are always database-backed. On long-lived runtimes this\n\t\t\t\t// callback also wakes the timer; on Cloudflare the external Cron Trigger\n\t\t\t\t// drives execution, so rescheduling is intentionally a no-op.\n\t\t\t\tpipeline.setContextFactory({\n\t\t\t\t\tcronReschedule: () => cronScheduler?.reschedule(),\n\t\t\t\t});\n\n\t\t\t\t// Recover stale locks from previous crashes. Pure bookkeeping\n\t\t\t\t// against the _emdash_cron_tasks table — no request needs the\n\t\t\t\t// result — so we defer it past the response via after(). On\n\t\t\t\t// Cloudflare this goes into waitUntil (extending the worker\n\t\t\t\t// lifetime); on Node it's fire-and-forget (the process stays\n\t\t\t\t// up anyway). Saves one cold-start write per D1 isolate.\n\t\t\t\tconst executorForRecovery = cronExecutor;\n\t\t\t\tafter(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst recovered = await executorForRecovery.recoverStaleLocks();\n\t\t\t\t\t\tif (recovered > 0) {\n\t\t\t\t\t\t\tconsole.log(`[cron] Recovered ${recovered} stale task lock(s)`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Keep the `[cron]` prefix so a failure is easy to trace back\n\t\t\t\t\t\t// rather than surfacing as a generic deferred-task error.\n\t\t\t\t\t\tconsole.error(\"[cron] Failed to recover stale task locks:\", error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\t// The platform decides whether a long-lived timer heartbeat exists.\n\t\t\t\t// `createScheduler` is injected by the generated virtual:emdash/scheduler\n\t\t\t\t// module: a NodeCronScheduler factory on Node/Bun, or null on serverless\n\t\t\t\t// adapters (e.g. Cloudflare) where the Worker's `scheduled()` handler\n\t\t\t\t// drives runScheduledTasks() instead. No adapter check lives here.\n\t\t\t\tif (deps.createScheduler) {\n\t\t\t\t\tconst scheduler = deps.createScheduler(cronExecutor);\n\t\t\t\t\tcronScheduler = scheduler;\n\n\t\t\t\t\t// Run scheduled publishing and system cleanup alongside each tick.\n\t\t\t\t\t// Pass storage so cleanupPendingUploads can delete orphaned files.\n\t\t\t\t\tscheduler.setSystemCleanup(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Route through the runtime so content:afterPublish hooks fire.\n\t\t\t\t\t\t\t// Falls back to the raw handler if (improbably) the tick beats\n\t\t\t\t\t\t\t// the post-construction ref assignment.\n\t\t\t\t\t\t\tconst runtime = runtimeRef.current;\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\tawait runtime.publishScheduled();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tconst recordWrite = await assertSiteWriteAllowed(db);\n\t\t\t\t\t\t\t\tif ((await publishDueContent(db)).length > 0) await recordWrite();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error(\"[scheduled-publish] Sweep failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait runSystemCleanup(db, storage ?? undefined);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t// Non-fatal -- individual cleanup failures are already logged\n\t\t\t\t\t\t\t// by runSystemCleanup. This catches unexpected errors.\n\t\t\t\t\t\t\tconsole.error(\"[cleanup] System cleanup failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait runtimeRef.current?.syncPluginStorageIndexesOnce();\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error(\"[plugins] Storage index sync failed:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Never throws; no-op unless scheduled backups are enabled and due.\n\t\t\t\t\t\tawait maybeRunScheduledBackup(db, storage ?? undefined);\n\t\t\t\t\t\tawait recordSchedulerHeartbeatSafely(db);\n\t\t\t\t\t});\n\t\t\t\t\t// start() is void on the timer scheduler but the interface\n\t\t\t\t\t// allows a promise (alarm-backed schedulers); we don't block on it.\n\t\t\t\t\tvoid scheduler.start();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\"[cron] Failed to initialize cron system:\", error);\n\t\t\t\t// Non-fatal — CMS works without cron\n\t\t\t}\n\t\t});\n\t\tsandboxRunner?.setCronReschedule?.(() => cronScheduler?.reschedule());\n\n\t\tconst runtime = new EmDashRuntime({\n\t\t\tdb,\n\t\t\tstorage,\n\t\t\t// Include bypassed sandboxed plugins in configuredPlugins so route\n\t\t\t// dispatch can find them under sandbox: false (they're treated as\n\t\t\t// trusted plugins for the duration of the bypass).\n\t\t\tconfiguredPlugins: [...deps.plugins, ...bypassedPluginsList],\n\t\t\tsandboxedPlugins,\n\t\t\tsandboxedPluginEntries: deps.sandboxedPluginEntries,\n\t\t\thooks: pipeline,\n\t\t\tenabledPlugins,\n\t\t\tpluginStates,\n\t\t\tconfig: deps.config,\n\t\t\tmediaProviders,\n\t\t\tmediaProviderEntries,\n\t\t\tcronExecutor,\n\t\t\tcronScheduler,\n\t\t\temailPipeline,\n\t\t\tallPipelinePlugins,\n\t\t\tpipelineFactoryOptions,\n\t\t\truntimeDeps: deps,\n\t\t\tpipelineRef,\n\t\t});\n\t\tconst contentCreate: PluginContentCreateCallback = async (\n\t\t\t_pluginId,\n\t\t\tcollection,\n\t\t\tinput,\n\t\t\toptions,\n\t\t) => {\n\t\t\tconst { seo, ...data } = input;\n\t\t\tconst result = await runtime.handleContentCreate(\n\t\t\t\tcollection,\n\t\t\t\t{\n\t\t\t\t\tdata,\n\t\t\t\t\tseo,\n\t\t\t\t\tlocale: resolveContentCreateLocale(options?.locale),\n\t\t\t\t\ttranslationOf: options?.translationOf,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tskipSaveHooks: options?.sandboxOrigin\n\t\t\t\t\t\t? options.originHook !== undefined\n\t\t\t\t\t\t: getActiveContentSaveHookName() != null,\n\t\t\t\t\texcludeAfterSavePluginId: _pluginId,\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tthrow Object.assign(new Error(result.error.message), {\n\t\t\t\t\tname: result.error.code,\n\t\t\t\t\tcode: result.error.code,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn result.data.item;\n\t\t};\n\t\truntime.pipelineFactoryOptions.contentCreate = contentCreate;\n\t\tpipeline.setContextFactory({ contentCreate });\n\t\tsandboxRunner?.setContentCreate?.(contentCreate);\n\t\t// Hand the constructed instance to the scheduler-cleanup closure so the\n\t\t// timer-driven sweep can fire publish hooks (see runtimeRef above).\n\t\truntimeRef.current = runtime;\n\t\tsandboxRunner?.setCommentModerate?.((pluginId, id, status, expectedStatus) =>\n\t\t\truntime.handlePluginCommentModerate(pluginId, id, status, expectedStatus),\n\t\t);\n\t\tsandboxRunner?.setContentActions?.(contentActions);\n\t\treturn runtime;\n\t}\n\n\t/**\n\t * Get a media provider by ID\n\t */\n\tgetMediaProvider(providerId: string): MediaProvider | undefined {\n\t\treturn this.mediaProviders.get(providerId);\n\t}\n\n\t/**\n\t * Get all media provider entries (for admin UI)\n\t */\n\tgetMediaProviderList(): Array<{\n\t\tid: string;\n\t\tname: string;\n\t\ticon?: string;\n\t\tcapabilities: MediaProviderCapabilities;\n\t}> {\n\t\treturn this.mediaProviderEntries.map((e) => ({\n\t\t\tid: e.id,\n\t\t\tname: e.name,\n\t\t\ticon: e.icon,\n\t\t\tcapabilities: e.capabilities,\n\t\t}));\n\t}\n\n\t/**\n\t * Get or create database instance\n\t */\n\tprivate static async getDatabase(deps: RuntimeDependencies): Promise<Kysely<Database>> {\n\t\t// Only use the per-request `ctx.db` when it's an isolated instance\n\t\t// (playground / DO preview). Plain D1 Sessions set `ctx.db` on every\n\t\t// anonymous request — if we captured one of those session-bound\n\t\t// Kyselys into the cached runtime, every request would accidentally\n\t\t// share one request's session. The configured `deps.createDialect`\n\t\t// path gives us a fresh singleton instead.\n\t\tconst ctx = getRequestContext();\n\t\tif (ctx?.dbIsIsolated && ctx.db) {\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- db in context is typed as unknown to avoid circular deps\n\t\t\treturn ctx.db as Kysely<Database>;\n\t\t}\n\n\t\tconst dbConfig = deps.config.database;\n\n\t\t// If no database configured in integration, try to get from loader\n\t\tif (!dbConfig) {\n\t\t\ttry {\n\t\t\t\treturn await getDb();\n\t\t\t} catch {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"EmDash database not configured. Either configure database in astro.config.mjs or use emdashLoader in live.config.ts\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst cacheKey = dbConfig.entrypoint;\n\n\t\t// Waiters poll the cache rather than sharing the initializing request's\n\t\t// promise: if the request that owns the init is cancelled mid-await\n\t\t// (e.g. client disconnect during cold migrations), a shared promise\n\t\t// never settles — and the owner's `finally` that would clear it never\n\t\t// runs — deadlocking every later request in the isolate. Prevention:\n\t\t// the in-flight init is anchored via after()/waitUntil so a cancelled\n\t\t// owner's init still completes and populates the cache. Net: a stale\n\t\t// lock is reclaimed after a deadline.\n\t\tconst holder = getDbHolder();\n\n\t\tconst throwIfBackingOff = () => {\n\t\t\tconst failure = holder.failures.get(cacheKey);\n\t\t\tif (!failure) return;\n\t\t\tif (Date.now() - failure.at < DB_INIT_FAILURE_BACKOFF_MS) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Database initialization is backing off after a recent migration failure: ${failure.message}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Expired — drop it so the map only ever holds active backoffs.\n\t\t\tholder.failures.delete(cacheKey);\n\t\t};\n\t\tthrowIfBackingOff();\n\n\t\treturn initWithLock(\n\t\t\tholder.lock,\n\t\t\t() => holder.cache.get(cacheKey),\n\t\t\tasync (isCurrentClaim) => {\n\t\t\t\t// Re-check under the lock: a request that entered the wait loop\n\t\t\t\t// before the failure was recorded must not immediately re-run\n\t\t\t\t// the failing migration when the lock frees up.\n\t\t\t\tthrowIfBackingOff();\n\n\t\t\t\tconst dialect = deps.createDialect(dbConfig.config);\n\t\t\t\tconst db = new Kysely<Database>({ dialect, log: kyselyLogOption() });\n\n\t\t\t\ttry {\n\t\t\t\t\tawait enforceRuntimeMigrationPolicy(db, deps.migrationMode ?? \"auto\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Timing out behind another instance's in-flight migrations\n\t\t\t\t\t// is not a failure of OUR migration — the holder may just be\n\t\t\t\t\t// slow. Don't back off for it: the next request waits again\n\t\t\t\t\t// and init recovers the moment the holder finishes.\n\t\t\t\t\tif (\n\t\t\t\t\t\t!(error instanceof ConcurrentMigrationTimeoutError) &&\n\t\t\t\t\t\t!(error instanceof PendingMigrationsError)\n\t\t\t\t\t) {\n\t\t\t\t\t\tholder.failures.set(cacheKey, {\n\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// Every attempt builds a fresh dialect/pool; close it or each\n\t\t\t\t\t// failed init leaks its connection(s) (#1744 observed these\n\t\t\t\t\t// piling up as idle Postgres connections).\n\t\t\t\t\tawait db.destroy().catch(() => {});\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tholder.failures.delete(cacheKey);\n\n\t\t\t\t// Note: legacy installs may carry a stray `emdash:manifest_cache`\n\t\t\t\t// row in the options table from versions that persisted a JSON\n\t\t\t\t// manifest. The runtime no longer reads or writes it. We do not\n\t\t\t\t// proactively delete it: the row is a few hundred bytes of dead\n\t\t\t\t// weight and is never on the read path, whereas a one-shot\n\t\t\t\t// cleanup-flag check costs an extra `options.get()` on every\n\t\t\t\t// isolate cold boot forever. Cheaper to leave it.\n\n\t\t\t\t// This returns a migrated but possibly unseeded db; create() runs\n\t\t\t\t// the seed gate and applies the seed, batched with its other init\n\t\t\t\t// reads.\n\n\t\t\t\t// Publish only while still the current owner: a reclaimed slow\n\t\t\t\t// init must not flip the cached Kysely identity back after the\n\t\t\t\t// reclaimer has published its own. The unpublished instance is\n\t\t\t\t// still returned and fully valid for the request that built it.\n\t\t\t\tif (isCurrentClaim()) {\n\t\t\t\t\tholder.cache.set(cacheKey, db);\n\t\t\t\t}\n\t\t\t\treturn db;\n\t\t\t},\n\t\t\t{\n\t\t\t\tdeadlineMs: DB_INIT_DEADLINE_MS,\n\t\t\t\tanchor: (promise) => after(() => promise),\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Get or create storage instance\n\t */\n\tprivate static getStorage(deps: RuntimeDependencies): Storage | null {\n\t\tconst storageConfig = deps.config.storage;\n\t\tif (!storageConfig || !deps.createStorage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst cacheKey = storageConfig.entrypoint;\n\t\tconst cached = storageCache.get(cacheKey);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\tconst storage = deps.createStorage(storageConfig.config);\n\t\tstorageCache.set(cacheKey, storage);\n\t\treturn storage;\n\t}\n\n\t/**\n\t * Load sandboxed plugin entries as trusted in-process plugins.\n\t * Used by the sandbox: false debugging escape hatch.\n\t *\n\t * Imports each plugin's bundled ESM code via a data URL, adapts it\n\t * with adaptSandboxEntry, and returns ResolvedPlugin objects ready\n\t * to be merged into the pipeline plugin list.\n\t */\n\tprivate static async loadBypassedPlugins(\n\t\tentries: SandboxedPluginEntry[],\n\t): Promise<ResolvedPlugin[]> {\n\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\t\tconst plugins: ResolvedPlugin[] = [];\n\t\tfor (const entry of entries) {\n\t\t\ttry {\n\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(entry.code).toString(\"base64\")}`;\n\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields.\n\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<string, unknown>;\n\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t>[0];\n\t\t\t\t// PluginDescriptor.storage's TypeScript type is narrower than what\n\t\t\t\t// adaptSandboxEntry actually accepts at runtime — it copies indexes\n\t\t\t\t// through to PluginStorageConfig which supports composite indexes\n\t\t\t\t// (string[][]). Pass the raw entry.storage with a structural cast\n\t\t\t\t// to preserve composite index declarations.\n\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through to PluginStorageConfig which supports composite indexes\n\t\t\t\t// Preserve admin metadata so plugin-management APIs can derive\n\t\t\t\t// hasAdminPages / hasDashboardWidgets correctly. Without this,\n\t\t\t\t// the admin UI hides Configure links and dashboard widgets for\n\t\t\t\t// bypassed plugins even though they declared them.\n\t\t\t\t// SandboxedPluginEntry uses looser types than PluginDescriptor\n\t\t\t\t// (label?, size: string), so coerce to the descriptor shape.\n\t\t\t\tconst adminPages = entry.adminPages?.map((p) => ({\n\t\t\t\t\tpath: p.path,\n\t\t\t\t\tlabel: p.label ?? p.path,\n\t\t\t\t\ticon: p.icon,\n\t\t\t\t}));\n\t\t\t\tconst adminWidgets:\n\t\t\t\t\t| Array<{\n\t\t\t\t\t\t\tid: string;\n\t\t\t\t\t\t\ttitle?: string;\n\t\t\t\t\t\t\tsize?: \"full\" | \"half\" | \"third\";\n\t\t\t\t\t }>\n\t\t\t\t\t| undefined = entry.adminWidgets?.map((w) => {\n\t\t\t\t\tconst size: \"full\" | \"half\" | \"third\" | undefined =\n\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined;\n\t\t\t\t\treturn { id: w.id, title: w.title, size };\n\t\t\t\t});\n\t\t\t\tconst resolved = adaptSandboxEntry(pluginDef, {\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\tversion: entry.version,\n\t\t\t\t\tentrypoint: \"\",\n\t\t\t\t\tcapabilities: entry.capabilities,\n\t\t\t\t\tallowedHosts: entry.allowedHosts,\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\tstorage: entry.storage as never,\n\t\t\t\t\tadminPages,\n\t\t\t\t\tadminWidgets,\n\t\t\t\t\tsettingsSchema: entry.settingsSchema,\n\t\t\t\t\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\t\tfieldWidgets: entry.fieldWidgets,\n\t\t\t\t\teditorPanels: entry.editorPanels,\n\t\t\t\t\teditorActions: entry.editorActions,\n\t\t\t\t});\n\t\t\t\tplugins.push(resolved);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`EmDash: Loaded plugin ${entry.id}:${entry.version} in-process (sandbox bypassed)`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Failed to load sandboxed plugin ${entry.id} in-process:`, error);\n\t\t\t}\n\t\t}\n\t\treturn plugins;\n\t}\n\n\t/**\n\t * Load sandboxed plugins using SandboxRunner\n\t */\n\tprivate static async loadSandboxedPlugins(\n\t\tdeps: RuntimeDependencies,\n\t\tdb: Kysely<Database>,\n\t\tmediaStorage?: Storage | null,\n\t\tsiteInfo?: {\n\t\t\tsiteName?: string;\n\t\t\tsiteUrl?: string;\n\t\t\tlocale?: string;\n\t\t\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n\t\t},\n\t): Promise<Map<string, SandboxedPluginInstance>> {\n\t\t// Check if sandboxing is enabled\n\t\tif (!deps.sandboxEnabled) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Create sandbox runner if not exists\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner(\n\t\t\t\tcreateSandboxRunnerOptions(\n\t\t\t\t\t{\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tbeforeContentWrite: () => assertSiteWriteAllowed(db),\n\t\t\t\t\t\ttaxonomyWrite: createTaxonomyAccessWithWrite(db),\n\t\t\t\t\t\tnow: deps.now,\n\t\t\t\t\t\tmediaStorage: mediaStorage\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tupload: (opts) =>\n\t\t\t\t\t\t\t\t\t\tmediaStorage.upload({\n\t\t\t\t\t\t\t\t\t\t\tkey: opts.key,\n\t\t\t\t\t\t\t\t\t\t\tbody: opts.body,\n\t\t\t\t\t\t\t\t\t\t\tcontentType: opts.contentType,\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\tdownload: (key) => mediaStorage.download(key),\n\t\t\t\t\t\t\t\t\tdelete: (key) => mediaStorage.delete(key),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t},\n\t\t\t\t\tsiteInfo,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tif (!sandboxRunner) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Check if the runner is actually available (has required bindings).\n\t\t// Warn regardless of whether there are plugins to load, so operators\n\t\t// see the issue even if no marketplace plugins are installed yet.\n\t\tif (!sandboxRunner.isAvailable()) {\n\t\t\tconst reason = sandboxRunner.unavailableReason?.();\n\t\t\tconsole.warn(\n\t\t\t\treason\n\t\t\t\t\t? `EmDash: Plugin sandbox is configured but not available on this platform: ${reason}. ` +\n\t\t\t\t\t\t\t\"Sandboxed plugins will not be loaded.\"\n\t\t\t\t\t: \"EmDash: Plugin sandbox is configured but not available on this platform. \" +\n\t\t\t\t\t\t\t\"Sandboxed plugins will not be loaded. \" +\n\t\t\t\t\t\t\t\"If using @emdash-cms/sandbox-workerd/sandbox, ensure workerd is installed.\",\n\t\t\t);\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\tif (deps.sandboxedPluginEntries.length === 0) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// sandbox: false escape hatch is handled separately (before pipeline\n\t\t// creation) via loadBypassedPlugins. If we somehow reach here with the\n\t\t// flag set, just return — the plugins are already in the trusted pipeline.\n\t\tif (deps.sandboxBypassed) {\n\t\t\treturn sandboxedPluginCache;\n\t\t}\n\n\t\t// Load each sandboxed plugin via sandbox runner\n\t\tfor (const entry of deps.sandboxedPluginEntries) {\n\t\t\tconst pluginKey = `${entry.id}:${entry.version}`;\n\t\t\tif (sandboxedPluginCache.has(pluginKey)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst adminPages = entry.adminPages?.map((page) => ({\n\t\t\t\t\tpath: page.path,\n\t\t\t\t\tlabel: page.label ?? page.path,\n\t\t\t\t\ticon: page.icon,\n\t\t\t\t}));\n\t\t\t\tconst adminWidgets: PluginDashboardWidget[] | undefined = entry.adminWidgets?.map(\n\t\t\t\t\t(widget) => ({\n\t\t\t\t\t\tid: widget.id,\n\t\t\t\t\t\ttitle: widget.title,\n\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\twidget.size === \"full\" || widget.size === \"half\" || widget.size === \"third\"\n\t\t\t\t\t\t\t\t? widget.size\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tconst capabilities = normalizePluginCapabilities(entry.capabilities ?? []);\n\n\t\t\t\t// Build manifest from entry's declared config\n\t\t\t\tconst manifest: PluginManifest = {\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\tversion: entry.version,\n\t\t\t\t\tcapabilities,\n\t\t\t\t\tallowedHosts: entry.allowedHosts ?? [],\n\t\t\t\t\tstorage: entry.storage ?? {},\n\t\t\t\t\t// Descriptors built before hook metadata was emitted must keep\n\t\t\t\t\t// working. Invoking an absent hook is a no-op in both runners.\n\t\t\t\t\thooks: entry.hooks ?? [...HOOK_NAMES],\n\t\t\t\t\troutes: entry.routes ?? [],\n\t\t\t\t\tadmin: {\n\t\t\t\t\t\tpages: adminPages,\n\t\t\t\t\t\twidgets: adminWidgets,\n\t\t\t\t\t\tsettingsSchema: entry.settingsSchema,\n\t\t\t\t\t\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\t\t\tfieldWidgets: entry.fieldWidgets,\n\t\t\t\t\t\teditorPanels: entry.editorPanels,\n\t\t\t\t\t\teditorActions: entry.editorActions,\n\t\t\t\t\t},\n\t\t\t\t\tmcp: entry.mcp,\n\t\t\t\t};\n\n\t\t\t\tconst plugin = await sandboxRunner.load(manifest, entry.code);\n\t\t\t\tsandboxedPluginCache.set(pluginKey, plugin);\n\t\t\t\tsandboxedManifestCache.set(pluginKey, manifest);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(\", \")}]`,\n\t\t\t\t);\n\n\t\t\t\tif (manifest.routes.length > 0) {\n\t\t\t\t\tconst routeMetaMap = new Map<string, RouteMeta>();\n\t\t\t\t\tfor (const routeEntry of manifest.routes) {\n\t\t\t\t\t\tconst normalized = normalizeManifestRoute(routeEntry);\n\t\t\t\t\t\trouteMetaMap.set(normalized.name, buildRouteMeta(normalized));\n\t\t\t\t\t}\n\t\t\t\t\tsandboxedRouteMetaCache.set(entry.id, routeMetaMap);\n\t\t\t\t} else {\n\t\t\t\t\tsandboxedRouteMetaCache.delete(entry.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn sandboxedPluginCache;\n\t}\n\n\t/**\n\t * Cold-start: load marketplace-installed plugins from site-local R2 storage\n\t *\n\t * Queries _plugin_state for source='marketplace' rows, fetches each bundle\n\t * from R2, and loads via SandboxRunner.\n\t */\n\t/**\n\t * Cold-start load of all active sandboxed plugins for one install\n\t * tier (marketplace or registry) from site-local R2.\n\t *\n\t * Mirrors {@link syncSandboxedSourcePlugins} but runs once at runtime\n\t * creation, before request traffic arrives; the sync method runs on\n\t * demand after install / update / uninstall handlers.\n\t */\n\tprivate static async loadInstalledSandboxedPlugins(\n\t\tsource: \"marketplace\" | \"registry\",\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t\tdeps: RuntimeDependencies,\n\t\tcache: Map<string, SandboxedPluginInstance>,\n\t\tsiteInfo?: {\n\t\t\tsiteName?: string;\n\t\t\tsiteUrl?: string;\n\t\t\tlocale?: string;\n\t\t\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n\t\t},\n\t): Promise<void> {\n\t\t// Ensure sandbox runner exists with media storage wired up.\n\t\t// (storage here is the media Storage adapter from the runtime.)\n\t\tif (!sandboxRunner && deps.createSandboxRunner) {\n\t\t\tsandboxRunner = deps.createSandboxRunner(\n\t\t\t\tcreateSandboxRunnerOptions(\n\t\t\t\t\t{\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\tbeforeContentWrite: () => assertSiteWriteAllowed(db),\n\t\t\t\t\t\ttaxonomyWrite: createTaxonomyAccessWithWrite(db),\n\t\t\t\t\t\tnow: deps.now,\n\t\t\t\t\t\tmediaStorage: {\n\t\t\t\t\t\t\tupload: (opts) =>\n\t\t\t\t\t\t\t\tstorage.upload({\n\t\t\t\t\t\t\t\t\tkey: opts.key,\n\t\t\t\t\t\t\t\t\tbody: opts.body,\n\t\t\t\t\t\t\t\t\tcontentType: opts.contentType,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tdownload: (key) => storage.download(key),\n\t\t\t\t\t\t\tdelete: (key) => storage.delete(key),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tsiteInfo,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\t// In sandbox bypass mode, marketplace plugins are loaded in-process\n\t\t// BEFORE pipeline creation by EmDashRuntime.create(). Skip here.\n\t\tif (deps.sandboxBypassed) return;\n\n\t\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst keySet = source === \"marketplace\" ? marketplacePluginKeys : registryPluginKeys;\n\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst plugins =\n\t\t\t\tsource === \"marketplace\"\n\t\t\t\t\t? await stateRepo.getMarketplacePlugins()\n\t\t\t\t\t: await stateRepo.getRegistryPlugins();\n\n\t\t\tfor (const plugin of plugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\n\t\t\t\t// Marketplace plugins record the live version in\n\t\t\t\t// `marketplaceVersion`; registry plugins use `version` directly.\n\t\t\t\tconst version =\n\t\t\t\t\tsource === \"marketplace\" ? (plugin.marketplaceVersion ?? plugin.version) : plugin.version;\n\t\t\t\tconst pluginKey = `${plugin.pluginId}:${version}`;\n\n\t\t\t\t// Skip if already loaded (shouldn't happen, but guard)\n\t\t\t\tif (cache.has(pluginKey)) continue;\n\n\t\t\t\ttry {\n\t\t\t\t\tconst bundle = await loadBundleFromR2(storage, plugin.pluginId, version, source);\n\t\t\t\t\tif (!bundle) {\n\t\t\t\t\t\tconsole.warn(`EmDash: ${source} plugin ${plugin.pluginId}@${version} not found in R2`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);\n\t\t\t\t\tcache.set(pluginKey, loaded);\n\t\t\t\t\tsandboxedManifestCache.set(pluginKey, bundle.manifest);\n\t\t\t\t\tkeySet.add(pluginKey);\n\n\t\t\t\t\t// Cache manifest admin config for getManifest()\n\t\t\t\t\tmarketplaceManifestCache.set(plugin.pluginId, {\n\t\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t\t\tmcp: bundle.manifest.mcp,\n\t\t\t\t\t\tstorage: bundle.manifest.storage,\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts,\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Cache route metadata from manifest for auth decisions\n\t\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\t\tconst routeMeta = new Map<string, RouteMeta>();\n\t\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\t\trouteMeta.set(normalized.name, buildRouteMeta(normalized));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);\n\t\t\t\t\t}\n\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`EmDash: Loaded ${source} plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(\", \")}]`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`EmDash: Failed to load ${source} plugin ${plugin.pluginId}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// _plugin_state table may not exist yet (pre-migration)\n\t\t}\n\t}\n\n\t/**\n\t * Cold-start: load marketplace plugins in bypass mode (sandbox: false).\n\t *\n\t * Each active marketplace bundle is read, evaluated via data URL, adapted\n\t * with adaptSandboxEntry, and returned as a ResolvedPlugin. The caller is\n\t * responsible for merging these into allPipelinePlugins / configuredPlugins\n\t * BEFORE the hook pipeline is created, so hooks and routes register in\n\t * the trusted pipeline.\n\t *\n\t * Also caches manifest and route metadata so admin UI / getManifest() work.\n\t *\n\t * Returns ResolvedPlugins to be merged into the pipeline.\n\t */\n\tprivate static async loadMarketplacePluginsBypassed(\n\t\tdb: Kysely<Database>,\n\t\tstorage: Storage,\n\t): Promise<ResolvedPlugin[]> {\n\t\tconst resolved: ResolvedPlugin[] = [];\n\t\ttry {\n\t\t\tconst stateRepo = new PluginStateRepository(db);\n\t\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\t\t\tif (marketplacePlugins.length === 0) return resolved;\n\n\t\t\tconsole.info(\n\t\t\t\t\"EmDash: Sandbox disabled (sandbox: false). \" +\n\t\t\t\t\t\"Marketplace plugins will run in-process without isolation.\",\n\t\t\t);\n\n\t\t\tconst { adaptSandboxEntry } = await import(\"./plugins/adapt-sandbox-entry.js\");\n\n\t\t\tfor (const plugin of marketplacePlugins) {\n\t\t\t\tif (plugin.status !== \"active\") continue;\n\t\t\t\tconst version = plugin.marketplaceVersion ?? plugin.version;\n\t\t\t\ttry {\n\t\t\t\t\tconst bundle = await loadBundleFromR2(storage, plugin.pluginId, version);\n\t\t\t\t\tif (!bundle) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`EmDash: Marketplace plugin ${plugin.pluginId}@${version} not found in R2`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Cache manifest and route metadata for admin UI and route auth\n\t\t\t\t\tmarketplaceManifestCache.set(plugin.pluginId, {\n\t\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\t\tadmin: bundle.manifest.admin,\n\t\t\t\t\t\tmcp: bundle.manifest.mcp,\n\t\t\t\t\t\tstorage: bundle.manifest.storage,\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts,\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t\t\t});\n\t\t\t\t\tif (bundle.manifest.routes.length > 0) {\n\t\t\t\t\t\tconst routeMeta = new Map<string, RouteMeta>();\n\t\t\t\t\t\tfor (const entry of bundle.manifest.routes) {\n\t\t\t\t\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\t\t\t\t\trouteMeta.set(normalized.name, buildRouteMeta(normalized));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Evaluate the bundled ESM and adapt it as a trusted plugin\n\t\t\t\t\tconst dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString(\"base64\")}`;\n\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields.\n\t\t\t\t\tconst pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\tunknown\n\t\t\t\t\t>;\n\t\t\t\t\tconst pluginDef = (pluginModule.default ?? pluginModule) as Parameters<\n\t\t\t\t\t\ttypeof adaptSandboxEntry\n\t\t\t\t\t>[0];\n\t\t\t\t\tconst adapted = adaptSandboxEntry(pluginDef, {\n\t\t\t\t\t\tid: bundle.manifest.id,\n\t\t\t\t\t\tversion: bundle.manifest.version,\n\t\t\t\t\t\tentrypoint: \"\",\n\t\t\t\t\t\tcapabilities: bundle.manifest.capabilities ?? [],\n\t\t\t\t\t\tallowedHosts: bundle.manifest.allowedHosts ?? [],\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through\n\t\t\t\t\t\tstorage: (bundle.manifest.storage ?? {}) as never,\n\t\t\t\t\t\tadminPages: bundle.manifest.admin?.pages,\n\t\t\t\t\t\tadminWidgets: bundle.manifest.admin?.widgets?.map((w) => ({\n\t\t\t\t\t\t\tid: w.id,\n\t\t\t\t\t\t\ttitle: w.title,\n\t\t\t\t\t\t\tsize:\n\t\t\t\t\t\t\t\tw.size === \"full\" || w.size === \"half\" || w.size === \"third\" ? w.size : undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tsettingsSchema: bundle.manifest.admin?.settingsSchema,\n\t\t\t\t\t\teditorPanels: bundle.manifest.admin?.editorPanels,\n\t\t\t\t\t\teditorActions: bundle.manifest.admin?.editorActions,\n\t\t\t\t\t});\n\t\t\t\t\tresolved.push(adapted);\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`EmDash: Loaded marketplace plugin ${plugin.pluginId}@${version} in-process (sandbox bypassed)`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`EmDash: Failed to load marketplace plugin ${plugin.pluginId} in-process:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// _plugin_state table may not exist yet\n\t\t}\n\t\treturn resolved;\n\t}\n\n\t/**\n\t * Resolve exclusive hook selections on startup.\n\t *\n\t * Delegates to the shared resolveExclusiveHooks() in hooks.ts.\n\t * The runtime version considers all pipeline providers as \"active\" since\n\t * the pipeline was already built from only active/enabled plugins.\n\t */\n\tprivate static async resolveExclusiveHooks(\n\t\tpipeline: HookPipeline,\n\t\tdb: Kysely<Database>,\n\t\tdeps: RuntimeDependencies,\n\t): Promise<void> {\n\t\tconst exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();\n\t\tif (exclusiveHookNames.length === 0) return;\n\n\t\tlet optionsRepo: OptionsRepository;\n\t\ttry {\n\t\t\toptionsRepo = new OptionsRepository(db);\n\t\t} catch {\n\t\t\treturn; // Options table may not exist yet\n\t\t}\n\n\t\t// Build preferred hints from sandboxed plugin entries\n\t\tconst preferredHints = new Map<string, string[]>();\n\t\tfor (const entry of deps.sandboxedPluginEntries) {\n\t\t\tif (entry.preferred && entry.preferred.length > 0) {\n\t\t\t\tpreferredHints.set(entry.id, entry.preferred);\n\t\t\t}\n\t\t}\n\n\t\t// The pipeline was created from only enabled plugins, so all providers\n\t\t// in it are active. The isActive check always returns true.\n\t\tawait resolveExclusiveHooksShared({\n\t\t\tpipeline,\n\t\t\tisActive: () => true,\n\t\t\tgetOption: (key) => optionsRepo.get<string>(key),\n\t\t\tgetOptions: (keys) => optionsRepo.getMany<string>(keys),\n\t\t\tsetOption: (key, value) => optionsRepo.set(key, value),\n\t\t\tdeleteOption: async (key) => {\n\t\t\t\tawait optionsRepo.delete(key);\n\t\t\t},\n\t\t\tpreferredHints,\n\t\t\tfallbackProviders: new Set([DEFAULT_COMMENT_MODERATOR_PLUGIN_ID]),\n\t\t});\n\t}\n\n\t// =========================================================================\n\t// Manifest\n\t// =========================================================================\n\n\t/**\n\t * Build the admin manifest from the live database.\n\t *\n\t * Used by the admin UI (sidebar collections, content editor field\n\t * dispatch, manifest endpoint) and by WordPress import — it's never\n\t * read on a public request, so this isn't on any anonymous hot path.\n\t *\n\t * No cross-request cache. The previous worker-isolate cache produced\n\t * a class of cross-isolate staleness bugs (#776, #873, #876, #877)\n\t * because Cloudflare Workers keeps multiple warm isolates per region\n\t * and there's no fan-out primitive to invalidate them in step. The\n\t * cache existed to amortize an N+1 schema query pattern; now that\n\t * `listCollectionsWithFields()` does the same work in two queries,\n\t * the rebuild is fast enough to pay on every admin request.\n\t *\n\t * Within a single request, `requestCached` deduplicates concurrent\n\t * callers (the manifest endpoint and an admin SSR template, say).\n\t */\n\tgetManifest(): Promise<EmDashManifest> {\n\t\treturn requestCached(\"emdash:manifest\", () => this._buildManifest());\n\t}\n\n\t/**\n\t * Build the manifest from the database.\n\t *\n\t * Constant query shapes via `listCollectionsWithFields()` — one query\n\t * for collections, one batched query for fields (chunked at\n\t * `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter\n\t * limit). Typical sites stay well under the chunk threshold, so this\n\t * is two queries in practice; never N+1.\n\t */\n\tprivate async _buildManifest(): Promise<EmDashManifest> {\n\t\t// Build collections from the live database.\n\t\t// Use this.db (ALS-aware getter) so playground mode picks up the\n\t\t// per-session DO database instead of the hardcoded singleton.\n\t\tconst manifestCollections = await buildManifestCollections({}, this.db);\n\n\t\t// Build plugins manifest\n\t\tconst manifestPlugins: Record<\n\t\t\tstring,\n\t\t\t{\n\t\t\t\tversion?: string;\n\t\t\t\tenabled?: boolean;\n\t\t\t\tsandboxed?: boolean;\n\t\t\t\tadminMode?: \"react\" | \"blocks\" | \"none\";\n\t\t\t\tadminPages?: Array<{ path: string; label?: string; icon?: string }>;\n\t\t\t\tdashboardWidgets?: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\ttitle?: string;\n\t\t\t\t\tsize?: string;\n\t\t\t\t}>;\n\t\t\t\tportableTextBlocks?: Array<{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tlabel: string;\n\t\t\t\t\ticon?: string;\n\t\t\t\t\tdescription?: string;\n\t\t\t\t\tplaceholder?: string;\n\t\t\t\t\tfields?: Element[];\n\t\t\t\t\tcategory?: string;\n\t\t\t\t}>;\n\t\t\t\tfieldWidgets?: Array<{\n\t\t\t\t\tname: string;\n\t\t\t\t\tlabel: string;\n\t\t\t\t\tfieldTypes: string[];\n\t\t\t\t\telements?: Element[];\n\t\t\t\t}>;\n\t\t\t\teditorPanels?: PluginManifest[\"admin\"][\"editorPanels\"];\n\t\t\t\teditorActions?: PluginManifest[\"admin\"][\"editorActions\"];\n\t\t\t}\n\t\t> = {};\n\n\t\tfor (const plugin of this.configuredPlugins) {\n\t\t\tconst status = this.pluginStates.get(plugin.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\t// Determine admin mode: has admin entry → react, has pages/widgets → blocks, else none\n\t\t\tconst hasAdminEntry = !!plugin.admin?.entry;\n\t\t\tconst hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;\n\t\t\tconst hasEditorExtensions =\n\t\t\t\t(plugin.admin?.editorPanels?.length ?? 0) > 0 ||\n\t\t\t\t(plugin.admin?.editorActions?.length ?? 0) > 0;\n\t\t\tlet adminMode: \"react\" | \"blocks\" | \"none\" = \"none\";\n\t\t\tif (hasAdminEntry) {\n\t\t\t\tadminMode = \"react\";\n\t\t\t} else if (hasAdminPages || hasWidgets || hasEditorExtensions) {\n\t\t\t\tadminMode = \"blocks\";\n\t\t\t}\n\n\t\t\tmanifestPlugins[plugin.id] = {\n\t\t\t\tversion: plugin.version,\n\t\t\t\tenabled,\n\t\t\t\tadminMode,\n\t\t\t\tadminPages: plugin.admin?.pages ?? [],\n\t\t\t\tdashboardWidgets: plugin.admin?.widgets ?? [],\n\t\t\t\tportableTextBlocks: plugin.admin?.portableTextBlocks,\n\t\t\t\tfieldWidgets: plugin.admin?.fieldWidgets,\n\t\t\t\teditorPanels: plugin.admin?.editorPanels,\n\t\t\t\teditorActions: plugin.admin?.editorActions,\n\t\t\t};\n\t\t}\n\n\t\t// Add sandboxed plugins (use entries for admin config)\n\t\tfor (const entry of this.sandboxedPluginEntries) {\n\t\t\tconst status = this.pluginStates.get(entry.id);\n\t\t\tconst enabled = status === undefined || status === \"active\";\n\n\t\t\tconst hasAdminPages = (entry.adminPages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;\n\t\t\tconst hasEditorExtensions =\n\t\t\t\t(entry.editorPanels?.length ?? 0) > 0 || (entry.editorActions?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[entry.id] = {\n\t\t\t\tversion: entry.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\t// `adminMode` reflects only admin pages/widgets. A plugin can\n\t\t\t\t// contribute portableTextBlocks/fieldWidgets with adminMode \"none\" —\n\t\t\t\t// the admin reads those from the manifest regardless, so don't gate\n\t\t\t\t// admin contributions on `adminMode`.\n\t\t\t\tadminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: entry.adminPages ?? [],\n\t\t\t\tdashboardWidgets: entry.adminWidgets ?? [],\n\t\t\t\tportableTextBlocks: entry.portableTextBlocks,\n\t\t\t\tfieldWidgets: entry.fieldWidgets,\n\t\t\t\teditorPanels: entry.editorPanels,\n\t\t\t\teditorActions: entry.editorActions,\n\t\t\t};\n\t\t}\n\n\t\t// Add marketplace-installed plugins (dynamically loaded from R2)\n\t\tfor (const [pluginId, meta] of marketplaceManifestCache) {\n\t\t\t// Skip if already included from build-time config\n\t\t\tif (manifestPlugins[pluginId]) continue;\n\n\t\t\tconst status = this.pluginStates.get(pluginId);\n\t\t\tconst enabled = status === \"active\";\n\n\t\t\tconst pages = meta.admin?.pages;\n\t\t\tconst widgets = meta.admin?.widgets;\n\t\t\tconst hasAdminPages = (pages?.length ?? 0) > 0;\n\t\t\tconst hasWidgets = (widgets?.length ?? 0) > 0;\n\t\t\tconst hasEditorExtensions =\n\t\t\t\t(meta.admin?.editorPanels?.length ?? 0) > 0 || (meta.admin?.editorActions?.length ?? 0) > 0;\n\n\t\t\tmanifestPlugins[pluginId] = {\n\t\t\t\tversion: meta.version,\n\t\t\t\tenabled,\n\t\t\t\tsandboxed: true,\n\t\t\t\tadminMode: hasAdminPages || hasWidgets || hasEditorExtensions ? \"blocks\" : \"none\",\n\t\t\t\tadminPages: pages ?? [],\n\t\t\t\tdashboardWidgets: widgets ?? [],\n\t\t\t\teditorPanels: meta.admin?.editorPanels,\n\t\t\t\teditorActions: meta.admin?.editorActions,\n\t\t\t};\n\t\t}\n\n\t\t// Build taxonomies from database\n\t\tlet manifestTaxonomies: Array<{\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tlabel: string;\n\t\t\tlabelSingular?: string;\n\t\t\thierarchical: boolean;\n\t\t\tcollections: string[];\n\t\t\tlocale: string;\n\t\t\ttranslationGroup: string;\n\t\t}> = [];\n\t\tlet taxonomyDefinitionLocales: string[] = [];\n\t\ttry {\n\t\t\tconst rows = await selectTaxonomyDefs(this.db).orderBy(\"d.name\").execute();\n\t\t\ttaxonomyDefinitionLocales = rows.map((row) => row.locale);\n\t\t\tmanifestTaxonomies = rows.map((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tlabel: row.label,\n\t\t\t\tlabelSingular: row.label_singular ?? undefined,\n\t\t\t\thierarchical: row.hierarchical === 1,\n\t\t\t\tcollections: parseStringArray(row.collections).toSorted(),\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group ?? row.id,\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tconsole.debug(\"EmDash: Could not load taxonomy definitions:\", error);\n\t\t}\n\n\t\ttry {\n\t\t\tconst configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];\n\t\t\tawait warnAboutUnconfiguredTaxonomyLocales(\n\t\t\t\tthis.db,\n\t\t\t\tconfiguredLocales,\n\t\t\t\ttaxonomyDefinitionLocales,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[i18n] taxonomy locale diagnostic failed:\", error);\n\t\t}\n\n\t\t// Build manifest hash\n\t\tconst manifestHash = await hashString(\n\t\t\tJSON.stringify(manifestCollections) +\n\t\t\t\tJSON.stringify(manifestPlugins) +\n\t\t\t\tJSON.stringify(manifestTaxonomies),\n\t\t);\n\n\t\t// Determine auth mode\n\t\tconst authMode = getAuthMode(this.config);\n\t\tconst authModeValue = authMode.type === \"external\" ? authMode.providerType : \"passkey\";\n\n\t\t// Include i18n config if enabled (read from virtual module to avoid SSR module singleton mismatch)\n\t\tconst i18nConfig = virtualConfig?.i18n ?? getI18nConfig();\n\t\tconst i18n =\n\t\t\ti18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1\n\t\t\t\t? {\n\t\t\t\t\t\tdefaultLocale: i18nConfig.defaultLocale,\n\t\t\t\t\t\tlocales: i18nConfig.locales,\n\t\t\t\t\t\tprefixDefaultLocale: i18nConfig.prefixDefaultLocale,\n\t\t\t\t\t}\n\t\t\t\t: undefined;\n\n\t\tconst registryConfig = resolveRegistryConfigForSandbox({\n\t\t\tregistry: this.config.registry,\n\t\t\texperimentalRegistry: this.config.experimental?.registry,\n\t\t});\n\t\tconst { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(\n\t\t\tregistryConfig.input,\n\t\t\t{ fieldPrefix: registryConfig.fieldPrefix },\n\t\t);\n\t\tif (registryConfigurationError) {\n\t\t\tconsole.error(\n\t\t\t\t`EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`,\n\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tversion: VERSION,\n\t\t\tcommit: COMMIT,\n\t\t\tastroVersion: this.config.astroVersion,\n\t\t\thash: manifestHash,\n\t\t\tcollections: manifestCollections,\n\t\t\tplugins: manifestPlugins,\n\t\t\ttaxonomies: manifestTaxonomies,\n\t\t\tauthMode: authModeValue,\n\t\t\ti18n,\n\t\t\tcontentLocale: {\n\t\t\t\tdefaultLocale: i18nConfig?.defaultLocale ?? \"en\",\n\t\t\t\timplicit: i18nConfig === null,\n\t\t\t},\n\t\t\tmarketplace: !!this.config.marketplace,\n\t\t\tregistry,\n\t\t\tregistryConfigurationError,\n\t\t};\n\t}\n\n\t/**\n\t * Verify and repair FTS indexes on demand. Runs at most once per worker\n\t * lifetime.\n\t *\n\t * Originally called from `EmDashRuntime.create()`, but on a busy D1 link\n\t * (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold\n\t * start for a modest-sized site — more than every other init phase\n\t * combined. Anonymous public reads never touch the search write path,\n\t * so the cost isn't paid back for the vast majority of requests.\n\t *\n\t * Instead, search endpoints call this lazily: the first request that\n\t * actually needs the index pays the verify cost (usually fast — no\n\t * rebuild needed), everyone else runs cold-free.\n\t *\n\t * Uses the runtime's singleton database (`this._db`) rather than the\n\t * request-scoped DB. Verify reads only, but `rebuildIndex` writes, and\n\t * a GET search request on D1 carries a `first-unconstrained` session\n\t * that's free to route at a read replica — unsafe for writes. The\n\t * singleton always goes through the default binding, which the D1\n\t * adapter will promote to `first-primary` for write statements.\n\t *\n\t * Safe to call concurrently: repeated callers share the same in-flight\n\t * promise. Errors are swallowed internally so callers don't need to\n\t * defend against FTS not existing yet (pre-setup).\n\t */\n\tasync ensureSearchHealthy(): Promise<void> {\n\t\t// Non-SQLite has no FTS to verify; the check is a cheap synchronous\n\t\t// branch, no need to cache it.\n\t\tif (!isSqlite(this._db)) return;\n\t\ttry {\n\t\t\tawait singleFlightCached(\n\t\t\t\tthis._searchHealthCache,\n\t\t\t\tasync () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst ftsManager = new FTSManager(this._db);\n\t\t\t\t\t\tconst repaired = await ftsManager.verifyAndRepairAll();\n\t\t\t\t\t\tif (repaired > 0) {\n\t\t\t\t\t\t\tconsole.log(`Repaired ${repaired} corrupted FTS index(es)`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// FTS tables may not exist yet (pre-setup). Non-fatal — cache\n\t\t\t\t\t\t// the \"checked\" state regardless so we don't re-scan.\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t\t);\n\t\t} catch {\n\t\t\t// This check is best-effort and must never fail the calling request.\n\t\t\t// The inner body already swallows verify errors; this guards the\n\t\t\t// outer failure modes (owner timeout, waiter give-up) so a slow FTS\n\t\t\t// scan degrades to \"unverified\", not a 500 on admin/search routes.\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Content Handlers\n\t// =========================================================================\n\n\tasync handleContentList(\n\t\tcollection: string,\n\t\tparams: {\n\t\t\tcursor?: string;\n\t\t\tlimit?: number;\n\t\t\tstatus?: string;\n\t\t\torderBy?: string;\n\t\t\torder?: \"asc\" | \"desc\";\n\t\t\tlocale?: string;\n\t\t\tq?: string;\n\t\t\tauthorId?: string;\n\t\t\tdateField?: ContentDateField;\n\t\t\tdateFrom?: string;\n\t\t\tdateTo?: string;\n\t\t\tbylines?: string[];\n\t\t\tbylinesNone?: boolean;\n\t\t\tincludeInferredBylines?: boolean;\n\t\t\tfieldFilters?: ContentFieldFilters;\n\t\t},\n\t) {\n\t\treturn handleContentList(this.db, collection, params);\n\t}\n\n\tasync handleContentAuthors(collection: string) {\n\t\treturn handleContentAuthors(this.db, collection);\n\t}\n\n\tasync handleContentGet(collection: string, id: string, locale?: string) {\n\t\tconst result = await handleContentGet(this.db, collection, id, locale);\n\t\treturn this.hydrateDraftData(result);\n\t}\n\n\tasync handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {\n\t\tconst result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);\n\t\treturn this.hydrateDraftData(result);\n\t}\n\n\t/**\n\t * If the response item has a `draftRevisionId`, replace `item.data` with\n\t * the draft revision's data and expose the original published values as\n\t * `liveData`. This makes the content_get / content_update round-trip\n\t * intuitive — read returns the latest content the caller has saved\n\t * (their pending draft), with the previously-published values still\n\t * accessible for compare-style flows.\n\t *\n\t * No-op when no draft exists or the response is an error.\n\t */\n\tprivate async hydrateDraftData<T>(\n\t\tresult: T,\n\t\toptions: { includeStagedSlug?: boolean; strict?: boolean } = {},\n\t): Promise<T> {\n\t\tif (!result || typeof result !== \"object\") return result;\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape probed below\n\t\tconst r = result as {\n\t\t\tsuccess?: boolean;\n\t\t\tdata?: { item?: Record<string, unknown> };\n\t\t};\n\t\tif (!r.success || !r.data?.item) return result;\n\t\tconst item = r.data.item;\n\t\tconst draftRevisionId = typeof item.draftRevisionId === \"string\" ? item.draftRevisionId : null;\n\t\tif (!draftRevisionId) return result;\n\t\ttry {\n\t\t\tconst revision = await new RevisionRepository(this.db).findById(draftRevisionId);\n\t\t\tif (!revision) {\n\t\t\t\tif (options.strict) throw new Error(`Draft revision not found: ${draftRevisionId}`);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tconst liveData =\n\t\t\t\titem.data && typeof item.data === \"object\"\n\t\t\t\t\t? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to object above\n\t\t\t\t\t\t(item.data as Record<string, unknown>)\n\t\t\t\t\t: {};\n\t\t\t// Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the\n\t\t\t// revision data — those are handler-internal markers and don't\n\t\t\t// belong in the surfaced `data` field. Match syncDataColumns at\n\t\t\t// content.ts:~1119.\n\t\t\tconst revisionData: Record<string, unknown> = {};\n\t\t\tfor (const [key, value] of Object.entries(revision.data)) {\n\t\t\t\tif (!key.startsWith(\"_\")) revisionData[key] = value;\n\t\t\t}\n\t\t\tconst mergedData = { ...liveData, ...revisionData };\n\t\t\t// Return a clone rather than mutating in place. The response\n\t\t\t// object isn't retained by the runtime today, but a future\n\t\t\t// request-cache layer would observe stale-after-mutation bugs;\n\t\t\t// cloning closes that footgun.\n\t\t\t// `r.data` was narrowed to `{ item?: ... }` at the top of this\n\t\t\t// method; spread its other keys (e.g. `_rev`) alongside the\n\t\t\t// hydrated item without going back through `unknown`.\n\t\t\treturn {\n\t\t\t\t...result,\n\t\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape preserved; result has been narrowed to the {success,data:{item}} envelope\n\t\t\t\tdata: {\n\t\t\t\t\t...r.data,\n\t\t\t\t\titem: {\n\t\t\t\t\t\t...item,\n\t\t\t\t\t\t...(options.includeStagedSlug && typeof revision.data._slug === \"string\"\n\t\t\t\t\t\t\t? { slug: revision.data._slug }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\tdata: mergedData,\n\t\t\t\t\t\tliveData,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (options.strict) throw error;\n\t\t\t// Non-fatal — fall back to the unhydrated response. Log so the\n\t\t\t// failure isn't completely silent (the response will look stale\n\t\t\t// to the caller but no error is raised).\n\t\t\tconsole.error(\"[emdash] draft hydration failed:\", error);\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tasync handleContentCreate(\n\t\tcollection: string,\n\t\tbody: {\n\t\t\tdata: Record<string, unknown>;\n\t\t\tseo?: ContentWriteInput[\"seo\"];\n\t\t\tslug?: string | null;\n\t\t\tstatus?: string;\n\t\t\tauthorId?: string;\n\t\t\tbylines?: Array<{ bylineId: string; roleLabel?: string | null }>;\n\t\t\tlocale?: string;\n\t\t\ttranslationOf?: string;\n\t\t\ttaxonomies?: Record<string, string[]>;\n\t\t\tactor?: ActorInfo;\n\t\t\tmigrateBlocks?: boolean;\n\t\t\treplaceBlocks?: boolean;\n\t\t},\n\t\toptions: { skipSaveHooks?: boolean; excludeAfterSavePluginId?: string } = {},\n\t) {\n\t\tconst actor = body.actor ? { ...body.actor } : undefined;\n\t\tlet locale: string;\n\t\ttry {\n\t\t\tlocale = resolveContentCreateLocale(body.locale);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : \"Invalid locale\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tlet translationSource: ContentItemInternal | null = null;\n\t\tlet sharedFieldSlugs: string[] = [];\n\t\tif (body.translationOf) {\n\t\t\tconst repo = new ContentRepository(this.db);\n\t\t\tconst source = await repo.findById(collection, body.translationOf);\n\t\t\tif (!source) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Translation source content not found\" },\n\t\t\t\t};\n\t\t\t}\n\t\t\ttranslationSource = source;\n\t\t\tsharedFieldSlugs = (\n\t\t\t\tawait this.db\n\t\t\t\t\t.selectFrom(\"_emdash_fields as field\")\n\t\t\t\t\t.innerJoin(\"_emdash_collections as collection\", \"collection.id\", \"field.collection_id\")\n\t\t\t\t\t.select(\"field.slug\")\n\t\t\t\t\t.where(\"collection.slug\", \"=\", collection)\n\t\t\t\t\t.where(\"field.translatable\", \"=\", 0)\n\t\t\t\t\t.execute()\n\t\t\t).map((field) => field.slug);\n\t\t\tconst siblings = await repo.findTranslations(\n\t\t\t\tcollection,\n\t\t\t\tsource.translationGroup ?? source.id,\n\t\t\t);\n\t\t\tif (siblings.some((sibling) => sibling.locale?.toLowerCase() === locale.toLowerCase())) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\t\tmessage: `Translation already exists in locale \"${locale}\" for this content item`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Run beforeSave hooks (trusted plugins)\n\t\tlet processedData = body.data;\n\t\tif (!options.skipSaveHooks && this.hooks.hasHooks(\"content:beforeSave\")) {\n\t\t\ttry {\n\t\t\t\tconst hookResult = await this.hooks.runContentBeforeSave(\n\t\t\t\t\tbody.data,\n\t\t\t\t\tcollection,\n\t\t\t\t\ttrue,\n\t\t\t\t\tundefined,\n\t\t\t\t\tactor,\n\t\t\t\t);\n\t\t\t\tprocessedData = hookResult.content;\n\t\t\t} catch (error) {\n\t\t\t\treturn beforeSaveFailure(error);\n\t\t\t}\n\t\t}\n\n\t\tif (translationSource) {\n\t\t\tprocessedData = { ...processedData };\n\t\t\tfor (const slug of sharedFieldSlugs) {\n\t\t\t\tif (Object.hasOwn(translationSource.data, slug)) {\n\t\t\t\t\tprocessedData[slug] = translationSource.data[slug];\n\t\t\t\t} else {\n\t\t\t\t\tdelete processedData[slug];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst collectionInfo = await this.schemaRegistry\n\t\t\t.getCollectionWithFields(collection)\n\t\t\t.catch(() => null);\n\t\tconst resolvedBlockTypes = collectionInfo?.fields.some((field) => field.type === \"blocks\")\n\t\t\t? await resolveBlockTypes(this.db)\n\t\t\t: undefined;\n\t\tif (collectionInfo) {\n\t\t\ttry {\n\t\t\t\tprocessedData = await normalizeBlocksData(\n\t\t\t\t\tthis.db,\n\t\t\t\t\tcollectionInfo,\n\t\t\t\t\tprocessedData,\n\t\t\t\t\ttranslationSource?.data,\n\t\t\t\t\t{\n\t\t\t\t\t\tmigrateBlocks: body.migrateBlocks,\n\t\t\t\t\t\treplaceBlocks: body.replaceBlocks,\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tresolvedBlockTypes,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SchemaError) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\t\terror: { code: error.code, message: error.message, details: error.details },\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tprocessedData = await this.normalizeFieldValues(\n\t\t\tcollection,\n\t\t\tprocessedData,\n\t\t\tcollectionInfo,\n\t\t\tresolvedBlockTypes,\n\t\t);\n\n\t\t// Validate against the collection schema. Hook output is validated\n\t\t// rather than `body.data` so plugins that mutate field values can't\n\t\t// sneak invalid data past.\n\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\tconst validation = await validateContentData(this.db, collection, processedData, {\n\t\t\tpartial: false,\n\t\t});\n\t\tif (!validation.ok) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: validation.error,\n\t\t\t};\n\t\t}\n\n\t\t// Create the content\n\t\tconst {\n\t\t\tactor: _discardedActor,\n\t\t\tmigrateBlocks: _discardedMigrateBlocks,\n\t\t\treplaceBlocks: _discardedReplaceBlocks,\n\t\t\t...contentBody\n\t\t} = body;\n\t\tconst result = await handleContentCreate(this.db, collection, {\n\t\t\t...contentBody,\n\t\t\tdata: processedData,\n\t\t\tlocale,\n\t\t\tauthorId: body.authorId,\n\t\t\tbylines: body.bylines,\n\t\t});\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (!options.skipSaveHooks && result.success && result.data) {\n\t\t\tthis.runAfterSaveHooks(\n\t\t\t\tcontentItemToRecord(result.data.item),\n\t\t\t\tcollection,\n\t\t\t\ttrue,\n\t\t\t\tactor,\n\t\t\t\toptions.excludeAfterSavePluginId,\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUpdate(\n\t\tcollection: string,\n\t\tid: string,\n\t\tbody: {\n\t\t\tdata?: Record<string, unknown>;\n\t\t\tslug?: string | null;\n\t\t\tstatus?: string;\n\t\t\tauthorId?: string | null;\n\t\t\tbylines?: Array<{ bylineId: string; roleLabel?: string | null }>;\n\t\t\tseo?: {\n\t\t\t\ttitle?: string | null;\n\t\t\t\tdescription?: string | null;\n\t\t\t\timage?: string | null;\n\t\t\t\tcanonical?: string | null;\n\t\t\t\tnoIndex?: boolean;\n\t\t\t};\n\t\t\ttaxonomies?: Record<string, string[]>;\n\t\t\tpublishedAt?: string | null;\n\t\t\tlocale?: string;\n\t\t\t/** Replace the previous autosave revision after staging this save. */\n\t\t\tskipRevision?: boolean;\n\t\t\t_rev?: string;\n\t\t\t/**\n\t\t\t * Acting user for this save. Used for revision attribution and\n\t\t\t * passed to content hooks; never changes entry ownership.\n\t\t\t */\n\t\t\tactor?: ActorInfo;\n\t\t\tmigrateBlocks?: boolean;\n\t\t\treplaceBlocks?: boolean;\n\t\t},\n\t) {\n\t\tconst actor = body.actor ? { ...body.actor } : undefined;\n\n\t\t// Resolve slug → ID if needed (before any lookups)\n\t\tconst repo = new ContentRepository(this.db);\n\t\tconst resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);\n\t\tconst resolvedId = resolvedItem?.id ?? id;\n\n\t\t// Validate _rev early — before draft revision writes which modify updated_at.\n\t\t// After validation, strip _rev so the handler doesn't double-check against\n\t\t// the now-modified timestamp.\n\t\tif (body._rev) {\n\t\t\tif (!resolvedItem) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"NOT_FOUND\", message: `Content item not found: ${id}` },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst revCheck = validateRev(body._rev, resolvedItem);\n\t\t\tif (!revCheck.valid) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"CONFLICT\", message: revCheck.message },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tconst {\n\t\t\t_rev: _discardedRev,\n\t\t\tactor: _discardedActor,\n\t\t\tmigrateBlocks,\n\t\t\treplaceBlocks,\n\t\t\t...bodyWithoutRev\n\t\t} = body;\n\t\tconst blockWriteOptions = { migrateBlocks, replaceBlocks };\n\n\t\t// Loaded once and threaded through normalization, the stale-key drop and the draft\n\t\t// merge below: each of those needs the field list and the registry does not cache.\n\t\tconst collectionInfo = bodyWithoutRev.data\n\t\t\t? await this.schemaRegistry.getCollectionWithFields(collection).catch(() => null)\n\t\t\t: null;\n\t\tconst knownFieldSlugs = new Set((collectionInfo?.fields ?? []).map((f) => f.slug));\n\n\t\t// Run beforeSave hooks if data is provided\n\t\tlet processedData = bodyWithoutRev.data;\n\t\tlet resolvedBlockTypes: Awaited<ReturnType<typeof resolveBlockTypes>> | undefined;\n\t\tif (bodyWithoutRev.data) {\n\t\t\tif (this.hooks.hasHooks(\"content:beforeSave\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst hookResult = await this.hooks.runContentBeforeSave(\n\t\t\t\t\t\tbodyWithoutRev.data,\n\t\t\t\t\t\tcollection,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tresolvedItem?.id,\n\t\t\t\t\t\tactor,\n\t\t\t\t\t);\n\t\t\t\t\tprocessedData = hookResult.content;\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn beforeSaveFailure(error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tprocessedData = await this.normalizeFieldValues(\n\t\t\t\tcollection,\n\t\t\t\tprocessedData!,\n\t\t\t\tcollectionInfo,\n\t\t\t\tundefined,\n\t\t\t\tfalse,\n\t\t\t);\n\n\t\t\t// Drop unknown field keys the entry already stores (e.g. a deleted field\n\t\t\t// stranded in a draft revision) before validation, while still rejecting\n\t\t\t// unknown keys the entry has never stored.\n\t\t\tif (collectionInfo?.fields) {\n\t\t\t\tprocessedData = await this.dropUnknownKeysAlreadyStored(\n\t\t\t\t\tprocessedData,\n\t\t\t\t\tresolvedItem,\n\t\t\t\t\tknownFieldSlugs,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcollectionInfo?.fields.some(\n\t\t\t\t\t(field) => field.type === \"blocks\" && Object.hasOwn(processedData!, field.slug),\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tresolvedBlockTypes = await resolveBlockTypes(this.db);\n\t\t\t}\n\n\t\t\tif (!collectionInfo?.supports?.includes(\"revisions\")) {\n\t\t\t\tif (collectionInfo) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tprocessedData = await normalizeBlocksData(\n\t\t\t\t\t\t\tthis.db,\n\t\t\t\t\t\t\tcollectionInfo,\n\t\t\t\t\t\t\tprocessedData,\n\t\t\t\t\t\t\tresolvedItem?.data,\n\t\t\t\t\t\t\tblockWriteOptions,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tresolvedBlockTypes,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tprocessedData = await this.normalizeFieldValues(\n\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\tprocessedData,\n\t\t\t\t\t\t\tcollectionInfo,\n\t\t\t\t\t\t\tresolvedBlockTypes,\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (error instanceof SchemaError) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\t\t\t\terror: { code: error.code, message: error.message, details: error.details },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\t\t\tconst validation = await validateContentData(this.db, collection, processedData, {\n\t\t\t\t\tpartial: true,\n\t\t\t\t});\n\t\t\t\tif (!validation.ok) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\t\terror: validation.error,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Draft-aware revision handling (if collection supports revisions)\n\t\t// Content table columns = published data (never written by saves).\n\t\t// Draft data lives only in the revisions table.\n\t\tlet usesDraftRevisions = false;\n\t\tlet draftStorageChanged = false;\n\t\tif (processedData) {\n\t\t\tif (collectionInfo?.supports?.includes(\"revisions\")) {\n\t\t\t\tusesDraftRevisions = true;\n\t\t\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\t\t\tlet existing = await repo.findById(collection, resolvedId);\n\n\t\t\t\tfor (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {\n\t\t\t\t\tlet baseData: Record<string, unknown>;\n\t\t\t\t\tif (existing.draftRevisionId) {\n\t\t\t\t\t\tconst draftRevision = await revisionRepo.findById(existing.draftRevisionId);\n\t\t\t\t\t\tbaseData = draftRevision?.data ?? existing.data;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbaseData = existing.data;\n\t\t\t\t\t}\n\t\t\t\t\tlet attemptData = processedData;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tattemptData = await normalizeBlocksData(\n\t\t\t\t\t\t\tthis.db,\n\t\t\t\t\t\t\tcollectionInfo,\n\t\t\t\t\t\t\tprocessedData,\n\t\t\t\t\t\t\tbaseData,\n\t\t\t\t\t\t\tblockWriteOptions,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tresolvedBlockTypes,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tattemptData = await this.normalizeFieldValues(\n\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\tattemptData,\n\t\t\t\t\t\t\tcollectionInfo,\n\t\t\t\t\t\t\tresolvedBlockTypes,\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (error instanceof SchemaError) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\t\t\t\terror: { code: error.code, message: error.message, details: error.details },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t\tconst { validateContentData } = await import(\"./api/handlers/validation.js\");\n\t\t\t\t\tconst validation = await validateContentData(this.db, collection, attemptData, {\n\t\t\t\t\t\tpartial: true,\n\t\t\t\t\t});\n\t\t\t\t\tif (!validation.ok) {\n\t\t\t\t\t\treturn { success: false as const, error: validation.error };\n\t\t\t\t\t}\n\n\t\t\t\t\t// Written without the keys the collection has no field for, so an entry\n\t\t\t\t\t// carrying a deleted field's value sheds it on its next save instead of\n\t\t\t\t\t// carrying it through every revision that follows.\n\t\t\t\t\tconst mergedData = collectionInfo?.fields\n\t\t\t\t\t\t? keepKnownFields({ ...baseData, ...attemptData }, knownFieldSlugs)\n\t\t\t\t\t\t: { ...baseData, ...attemptData };\n\t\t\t\t\tif (bodyWithoutRev.slug !== undefined) {\n\t\t\t\t\t\tmergedData._slug = bodyWithoutRev.slug;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\t\t\tcollection,\n\t\t\t\t\t\tentryId: resolvedId,\n\t\t\t\t\t\tdata: mergedData,\n\t\t\t\t\t\tauthorId: actor?.id,\n\t\t\t\t\t});\n\n\t\t\t\t\tlet staged: boolean;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstaged = await repo.replaceDraftRevision(collection, resolvedId, revision.id, existing);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);\n\t\t\t\t\t\t} catch (cleanupError) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t`[emdash] Failed to clean up unstaged revision ${revision.id}:`,\n\t\t\t\t\t\t\t\tcleanupError,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!staged) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);\n\t\t\t\t\t\t} catch (cleanupError) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t`[emdash] Failed to clean up unstaged revision ${revision.id}:`,\n\t\t\t\t\t\t\t\tcleanupError,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (body._rev || attempt === MAX_DRAFT_STAGE_ATTEMPTS - 1) {\n\t\t\t\t\t\t\tconst error = new ContentMutationConflictError();\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\t\t\t\terror: { code: \"CONFLICT\", message: error.message },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\texisting = await repo.findById(collection, resolvedId);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tdraftStorageChanged = true;\n\t\t\t\t\tprocessedData = attemptData;\n\n\t\t\t\t\tif (bodyWithoutRev.skipRevision && existing.draftRevisionId) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait revisionRepo.deleteIfUnreferenced(\n\t\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\t\tresolvedId,\n\t\t\t\t\t\t\t\texisting.draftRevisionId,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t`[emdash] Failed to clean up superseded revision ${existing.draftRevisionId}:`,\n\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tafter(async () => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait revisionRepo.pruneQueuedEntry(collection, resolvedId, revision.id, 50);\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t\t`[revisions] Failed to prune revisions for ${collection}/${resolvedId}:`,\n\t\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Public HTML comes from live columns / SEO / taxonomies, not draft revisions.\n\t\tconst liveMetaTouched = Object.entries(bodyWithoutRev).some(\n\t\t\t([key, value]) => value !== undefined && !DRAFT_ONLY_UPDATE_KEYS.has(key),\n\t\t);\n\n\t\t// Update the content table:\n\t\t// - If collection uses draft revisions: only update metadata (no data fields, no slug)\n\t\t// - Otherwise: update everything as before\n\t\tconst result =\n\t\t\tusesDraftRevisions && !liveMetaTouched\n\t\t\t\t? await handleContentGet(this.db, collection, resolvedId)\n\t\t\t\t: await handleContentUpdate(this.db, collection, resolvedId, {\n\t\t\t\t\t\t...bodyWithoutRev,\n\t\t\t\t\t\tdata: usesDraftRevisions ? undefined : processedData,\n\t\t\t\t\t\tslug: usesDraftRevisions ? undefined : bodyWithoutRev.slug,\n\t\t\t\t\t\tauthorId: bodyWithoutRev.authorId,\n\t\t\t\t\t\tbylines: bodyWithoutRev.bylines,\n\t\t\t\t\t});\n\n\t\tconst liveContentChanged = usesDraftRevisions\n\t\t\t? liveMetaTouched\n\t\t\t: Boolean(processedData || bodyWithoutRev.slug !== undefined || liveMetaTouched);\n\n\t\t// Hydrate draft data BEFORE firing afterSave hooks so the hook sees\n\t\t// the same effective data the response surfaces — for revision-\n\t\t// supporting collections, that's the just-saved draft, not the live\n\t\t// columns.\n\t\tconst hydrated = await this.hydrateDraftData(result);\n\t\tif (hydrated.success && hydrated.data) {\n\t\t\tconst contentIdsToRefresh = [resolvedId];\n\t\t\tif (!usesDraftRevisions && processedData) {\n\t\t\t\tcontentIdsToRefresh.push(\n\t\t\t\t\t...(await this.findSyncedSiblingsForUsageRefresh(\n\t\t\t\t\t\tcollection,\n\t\t\t\t\t\tresolvedId,\n\t\t\t\t\t\thydrated.data.item.translationGroup,\n\t\t\t\t\t\tprocessedData,\n\t\t\t\t\t)),\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, contentIdsToRefresh);\n\t\t} else if (draftStorageChanged) {\n\t\t\ttry {\n\t\t\t\tawait markContentMediaUsageCollectionStale(this.db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`[media-usage] Failed to mark ${collection} stale:`, error);\n\t\t\t}\n\t\t}\n\n\t\t// Run afterSave hooks (fire-and-forget)\n\t\tif (hydrated.success && hydrated.data) {\n\t\t\tthis.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false, actor);\n\t\t}\n\n\t\tif (hydrated.success) {\n\t\t\treturn { ...hydrated, liveContentChanged };\n\t\t}\n\t\treturn hydrated;\n\t}\n\n\tasync handleContentDelete(collection: string, id: string) {\n\t\t// Run beforeDelete hooks (trusted plugins)\n\t\tif (this.hooks.hasHooks(\"content:beforeDelete\")) {\n\t\t\tconst { allowed } = await this.hooks.runContentBeforeDelete(id, collection);\n\t\t\tif (!allowed) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"DELETE_BLOCKED\",\n\t\t\t\t\t\tmessage: \"Delete blocked by plugin hook\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);\n\n\t\t// Delete the content\n\t\tconst result = await handleContentDelete(this.db, collection, id);\n\t\tif (result.success) {\n\t\t\tawait this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.id]);\n\t\t}\n\n\t\t// Run afterDelete hooks (deferred past the response via after())\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection, false);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Trash Handlers\n\t// =========================================================================\n\n\tasync handleContentListTrashed(\n\t\tcollection: string,\n\t\tparams: { cursor?: string; limit?: number; locale?: string } = {},\n\t) {\n\t\treturn handleContentListTrashed(this.db, collection, params);\n\t}\n\n\tasync handleContentRestore(\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: {\n\t\t\t_rev?: string;\n\t\t\torigin?: ContentActionOrigin;\n\t\t\tpluginInvocationId?: string;\n\t\t} = {},\n\t) {\n\t\tconst result = await handleContentRestore(this.db, collection, id, { _rev: options._rev });\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterRestore hooks (fire-and-forget)\n\t\tif (result.success) {\n\t\t\tthis.runAfterRestoreHooks(\n\t\t\t\tcontentItemToRecord(result.data.item),\n\t\t\t\tcollection,\n\t\t\t\toptions.origin?.source === \"plugin\" ? options.origin.pluginId : undefined,\n\t\t\t\toptions.pluginInvocationId,\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentPermanentDelete(collection: string, id: string) {\n\t\tconst policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);\n\t\tconst result = await handleContentPermanentDelete(this.db, collection, id);\n\t\tif (result.success) {\n\t\t\tawait this.clearScheduledPolicyRejection(collection, result.data.id, policyRejectionRevision);\n\t\t\tawait this.deleteContentUsageAfterSuccessfulPermanentDelete(collection, result.data.id);\n\t\t}\n\n\t\t// Run afterDelete hooks so plugins (e.g. AI Search) can clean up\n\t\tif (result.success) {\n\t\t\tthis.runAfterDeleteHooks(id, collection, true);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentCountTrashed(collection: string, params: { locale?: string } = {}) {\n\t\treturn handleContentCountTrashed(this.db, collection, params);\n\t}\n\n\tasync handleContentDuplicate(collection: string, id: string, authorId?: string) {\n\t\tconst result = await handleContentDuplicate(this.db, collection, id, authorId);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Publishing & Scheduling Handlers\n\t// =========================================================================\n\n\tprivate pluginVersionedResult(\n\t\tresult:\n\t\t\t| { success: true; data: { item: ContentItemInternal; _rev?: string } }\n\t\t\t| { success: false; error: { code: string; message: string } },\n\t): VersionedContentItem {\n\t\tif (!result.success) {\n\t\t\tthrow Object.assign(new Error(result.error.message), { code: result.error.code });\n\t\t}\n\t\tif (!result.data._rev) throw new Error(\"Content action returned no revision\");\n\t\treturn { item: toPluginContentItem(result.data.item), _rev: result.data._rev };\n\t}\n\n\tprivate async runPluginContentAction<T>(\n\t\tpluginId: string,\n\t\taction: string,\n\t\tcollection: string,\n\t\tid: string,\n\t\tfn: (resolvedId: string) => Promise<T>,\n\t\tinvocationId?: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<T> {\n\t\tlet invocationInvalidator: PluginContentCacheInvalidator | undefined;\n\t\tif (invocationId) {\n\t\t\tconst invocationKey = this.pluginInvocationKey(pluginId, invocationId);\n\t\t\tif (\n\t\t\t\t!this.activePluginInvocations.has(invocationKey) &&\n\t\t\t\t!this.releasedPluginInvocations.has(invocationKey)\n\t\t\t) {\n\t\t\t\tthrow Object.assign(new Error(\"Plugin content action invocation has expired\"), {\n\t\t\t\t\tcode: \"CONTENT_ACTION_INVOCATION_EXPIRED\",\n\t\t\t\t});\n\t\t\t}\n\t\t\tinvocationInvalidator = this.pluginInvocationCacheInvalidators.get(invocationKey);\n\t\t}\n\t\tconst recordWrite = await assertSiteWriteAllowed(this.db);\n\t\tconst repo = new ContentRepository(this.db);\n\t\tconst item =\n\t\t\taction === \"restore\"\n\t\t\t\t? await repo.findByIdOrSlugIncludingTrashed(collection, id)\n\t\t\t\t: await repo.findByIdOrSlug(collection, id);\n\t\tconst resolvedId = item?.id ?? id;\n\t\tconst key = this.pluginContentActionKey(collection, resolvedId);\n\t\tif ((this.activePluginContentActions.get(key) ?? 0) > 0) {\n\t\t\tthrow Object.assign(new Error(\"Plugin content action re-entered itself\"), {\n\t\t\t\tcode: \"CONTENT_ACTION_REENTRANT\",\n\t\t\t});\n\t\t}\n\t\tthis.retainPluginContentAction(key);\n\t\ttry {\n\t\t\tconst result = await fn(resolvedId);\n\t\t\tawait recordWrite();\n\t\t\tconst invalidator =\n\t\t\t\tinvalidateContentCache ?? invocationInvalidator ?? this.pluginContentCacheInvalidator;\n\t\t\tif (invalidator) {\n\t\t\t\ttry {\n\t\t\t\t\tawait invalidator([collection, resolvedId]);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[plugin-content-action] Cache invalidation failed for ${collection}/${resolvedId}:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tthis.releasePluginContentAction(key);\n\t\t}\n\t}\n\n\tprivate pluginContentActionKey(collection: string, id: string): string {\n\t\treturn JSON.stringify([collection, id]);\n\t}\n\n\tprivate retainPluginContentAction(key: string): void {\n\t\tthis.activePluginContentActions.set(key, (this.activePluginContentActions.get(key) ?? 0) + 1);\n\t}\n\n\tprivate releasePluginContentAction(key: string): void {\n\t\tconst remaining = (this.activePluginContentActions.get(key) ?? 1) - 1;\n\t\tif (remaining > 0) this.activePluginContentActions.set(key, remaining);\n\t\telse this.activePluginContentActions.delete(key);\n\t}\n\n\tprivate async pluginGetVersioned(\n\t\t_pluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t): Promise<VersionedContentItem | null> {\n\t\tconst result = await this.handleContentGet(collection, id);\n\t\tif (!result.success) {\n\t\t\tif (result.error.code === \"NOT_FOUND\") return null;\n\t\t\tthrow Object.assign(new Error(result.error.message), { code: result.error.code });\n\t\t}\n\t\tif (!result.data._rev) throw new Error(\"Content read returned no revision\");\n\t\treturn { item: toPluginContentItem(result.data.item), _rev: result.data._rev };\n\t}\n\n\tprivate async pluginGetTrashedVersioned(\n\t\t_pluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t): Promise<VersionedContentItem | null> {\n\t\tconst result = await this.handleContentGetIncludingTrashed(collection, id);\n\t\tif (!result.success) {\n\t\t\tif (result.error.code === \"NOT_FOUND\") return null;\n\t\t\tthrow Object.assign(new Error(result.error.message), { code: result.error.code });\n\t\t}\n\t\tif (!(await new ContentRepository(this.db).isTrashed(collection, result.data.item.id))) {\n\t\t\treturn null;\n\t\t}\n\t\tif (!result.data._rev) throw new Error(\"Trashed content read returned no revision\");\n\t\treturn { item: toPluginContentItem(result.data.item), _rev: result.data._rev };\n\t}\n\n\tprivate pluginPublish(\n\t\tpluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { _rev: string },\n\t\tinvocationId?: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<VersionedContentItem> {\n\t\treturn this.runPluginContentAction(\n\t\t\tpluginId,\n\t\t\t\"publish\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tasync (resolvedId) =>\n\t\t\t\tthis.pluginVersionedResult(\n\t\t\t\t\tawait this.handleContentPublish(collection, resolvedId, {\n\t\t\t\t\t\t_rev: options._rev,\n\t\t\t\t\t\torigin: { source: \"plugin\", pluginId },\n\t\t\t\t\t\tpluginInvocationId: invocationId,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\tinvocationId,\n\t\t\tinvalidateContentCache,\n\t\t);\n\t}\n\n\tprivate pluginUnpublish(\n\t\tpluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { _rev: string },\n\t\tinvocationId?: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<VersionedContentItem> {\n\t\treturn this.runPluginContentAction(\n\t\t\tpluginId,\n\t\t\t\"unpublish\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tasync (resolvedId) =>\n\t\t\t\tthis.pluginVersionedResult(\n\t\t\t\t\tawait this.handleContentUnpublish(collection, resolvedId, {\n\t\t\t\t\t\t_rev: options._rev,\n\t\t\t\t\t\torigin: { source: \"plugin\", pluginId },\n\t\t\t\t\t\tpluginInvocationId: invocationId,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\tinvocationId,\n\t\t\tinvalidateContentCache,\n\t\t);\n\t}\n\n\tprivate pluginSchedule(\n\t\tpluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { scheduledAt: string; _rev: string },\n\t\tinvocationId?: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<VersionedContentItem> {\n\t\treturn this.runPluginContentAction(\n\t\t\tpluginId,\n\t\t\t\"schedule\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tasync (resolvedId) =>\n\t\t\t\tthis.pluginVersionedResult(\n\t\t\t\t\tawait this.handleContentSchedule(collection, resolvedId, options.scheduledAt, {\n\t\t\t\t\t\t_rev: options._rev,\n\t\t\t\t\t\torigin: { source: \"plugin\", pluginId },\n\t\t\t\t\t\tpluginInvocationId: invocationId,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\tinvocationId,\n\t\t\tinvalidateContentCache,\n\t\t);\n\t}\n\n\tprivate pluginUnschedule(\n\t\tpluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { _rev: string },\n\t\tinvocationId?: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<VersionedContentItem> {\n\t\treturn this.runPluginContentAction(\n\t\t\tpluginId,\n\t\t\t\"unschedule\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tasync (resolvedId) =>\n\t\t\t\tthis.pluginVersionedResult(\n\t\t\t\t\tawait this.handleContentUnschedule(collection, resolvedId, {\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\torigin: { source: \"plugin\", pluginId },\n\t\t\t\t\t\tpluginInvocationId: invocationId,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\tinvocationId,\n\t\t\tinvalidateContentCache,\n\t\t);\n\t}\n\n\tprivate pluginRestore(\n\t\tpluginId: string,\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: { _rev: string },\n\t\tinvocationId?: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<VersionedContentItem> {\n\t\treturn this.runPluginContentAction(\n\t\t\tpluginId,\n\t\t\t\"restore\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tasync (resolvedId) =>\n\t\t\t\tthis.pluginVersionedResult(\n\t\t\t\t\tawait this.handleContentRestore(collection, resolvedId, {\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\torigin: { source: \"plugin\", pluginId },\n\t\t\t\t\t\tpluginInvocationId: invocationId,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\tinvocationId,\n\t\t\tinvalidateContentCache,\n\t\t);\n\t}\n\n\tprivate async checkContentPolicy(\n\t\tname: ContentPolicyHookName,\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: ContentPolicyMutationOptions,\n\t\trejectionCode: \"PUBLISH_REJECTED\" | \"SCHEDULE_REJECTED\" | \"UNPUBLISH_REJECTED\",\n\t\tfailureCode: \"CONTENT_PUBLISH_ERROR\" | \"CONTENT_SCHEDULE_ERROR\" | \"CONTENT_UNPUBLISH_ERROR\",\n\t\tscheduledAt?: string,\n\t): Promise<ContentPolicyCheck> {\n\t\tlet current = await handleContentGet(this.db, collection, id);\n\t\tif (!current.success) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\trevision: options._rev ?? \"\",\n\t\t\t\terror: current.error,\n\t\t\t};\n\t\t}\n\n\t\tconst revision = current.data._rev;\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\trevision: options._rev ?? \"\",\n\t\t\t\terror: { code: failureCode, message: \"Failed to read content revision\" },\n\t\t\t};\n\t\t}\n\t\tif (options._rev !== undefined && options._rev !== revision) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\trevision,\n\t\t\t\terror: { code: \"CONFLICT\", message: \"Revision precondition did not match\" },\n\t\t\t};\n\t\t}\n\n\t\tif (!this.hooks.hasHooks(name)) return { allowed: true, revision };\n\t\tif (name !== \"content:beforeUnpublish\") {\n\t\t\ttry {\n\t\t\t\tcurrent = await this.hydrateDraftData(current, {\n\t\t\t\t\tincludeStagedSlug: true,\n\t\t\t\t\tstrict: true,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`[content-policy] ${name} draft hydration failed:`, error);\n\t\t\t\treturn {\n\t\t\t\t\tallowed: false,\n\t\t\t\t\trevision,\n\t\t\t\t\terror: { code: failureCode, message: `Failed to read content for ${name} policy` },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst origin = options.origin ?? { source: \"system\" };\n\t\tconst actor =\n\t\t\toptions.actor &&\n\t\t\t(origin.source === \"api\" || origin.source === \"mcp\" || origin.source === \"visual-editor\")\n\t\t\t\t? { ...options.actor, source: origin.source }\n\t\t\t\t: undefined;\n\t\ttry {\n\t\t\tconst decision = await this.hooks.runContentPolicy(name, {\n\t\t\t\tcontent: contentItemToRecord(current.data.item),\n\t\t\t\tcollection,\n\t\t\t\torigin,\n\t\t\t\tactor,\n\t\t\t\t...(scheduledAt === undefined ? {} : { scheduledAt }),\n\t\t\t});\n\t\t\tif (decision.cancellation) {\n\t\t\t\treturn {\n\t\t\t\t\tallowed: false,\n\t\t\t\t\trevision,\n\t\t\t\t\terror: { code: rejectionCode, message: decision.cancellation.reason },\n\t\t\t\t\tcancellation: decision.cancellation,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { allowed: true, revision };\n\t\t} catch (error) {\n\t\t\tconsole.error(`[content-policy] ${name} failed:`, error);\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\trevision,\n\t\t\t\terror: { code: failureCode, message: `Failed to run ${name} policy` },\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate async getScheduledPolicyRejectionRevision(\n\t\tcollection: string,\n\t\tid: string,\n\t): Promise<string | undefined> {\n\t\ttry {\n\t\t\treturn (\n\t\t\t\tawait new OptionsRepository(this.db).getVersioned(\n\t\t\t\t\tscheduledPolicyRejectionKey(collection, id),\n\t\t\t\t)\n\t\t\t)?.revision;\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[content-policy] Failed to read scheduled rejection for ${collection}/${id}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async clearScheduledPolicyRejection(\n\t\tcollection: string,\n\t\tid: string,\n\t\texpectedRevision: string | undefined,\n\t): Promise<void> {\n\t\tif (expectedRevision === undefined) return;\n\t\ttry {\n\t\t\tawait new OptionsRepository(this.db).compareAndDelete(\n\t\t\t\tscheduledPolicyRejectionKey(collection, id),\n\t\t\t\texpectedRevision,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[content-policy] Failed to clear scheduled rejection for ${collection}/${id}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate createScheduledPolicyRejection(\n\t\tcollection: string,\n\t\tid: string,\n\t\tcancellation: { pluginId: string; reason: string },\n\t): ScheduledPolicyRejection {\n\t\treturn {\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tpluginId: cancellation.pluginId,\n\t\t\treason: cancellation.reason,\n\t\t\trejectedAt: (this.runtimeDeps.now?.() ?? new Date()).toISOString(),\n\t\t};\n\t}\n\n\tasync handleContentPublish(\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: {\n\t\t\tpublishedAt?: string;\n\t\t\trequireScheduledDue?: boolean;\n\t\t\texpectedScheduledAt?: string;\n\t\t\t_rev?: string;\n\t\t\tcurrentTime?: Date;\n\t\t\tactor?: ActorInfo;\n\t\t\torigin?: ContentActionOrigin;\n\t\t\tpluginInvocationId?: string;\n\t\t} = {},\n\t) {\n\t\tconst policy = await this.checkContentPolicy(\n\t\t\t\"content:beforePublish\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\toptions,\n\t\t\t\"PUBLISH_REJECTED\",\n\t\t\t\"CONTENT_PUBLISH_ERROR\",\n\t\t);\n\t\tif (!policy.allowed) {\n\t\t\tif (policy.cancellation && options.origin?.source === \"scheduler\") {\n\t\t\t\tconst unscheduled = await handleScheduledPolicyRejection(this.db, collection, id, {\n\t\t\t\t\t_rev: policy.revision,\n\t\t\t\t\trejection: this.createScheduledPolicyRejection(collection, id, policy.cancellation),\n\t\t\t\t});\n\t\t\t\tif (!unscheduled.success) return unscheduled;\n\t\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [unscheduled.data.item.id]);\n\t\t\t\tthis.runAfterUnscheduleHooks(contentItemToRecord(unscheduled.data.item), collection);\n\t\t\t}\n\t\t\treturn { success: false as const, error: policy.error };\n\t\t}\n\t\tconst policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);\n\t\tconst {\n\t\t\tactor: _actor,\n\t\t\torigin: _origin,\n\t\t\tpluginInvocationId: _pluginInvocationId,\n\t\t\t...handlerOptions\n\t\t} = options;\n\t\tconst result = await handleContentPublish(this.db, collection, id, {\n\t\t\t...handlerOptions,\n\t\t\t_rev: policy.revision,\n\t\t});\n\t\tif (result.success && result.data) {\n\t\t\tawait this.clearScheduledPolicyRejection(\n\t\t\t\tcollection,\n\t\t\t\tresult.data.item.id,\n\t\t\t\tpolicyRejectionRevision,\n\t\t\t);\n\t\t\tconst { item } = result.data;\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [\n\t\t\t\titem.id,\n\t\t\t\t...(await this.findSyncedSiblingsForUsageRefresh(\n\t\t\t\t\tcollection,\n\t\t\t\t\titem.id,\n\t\t\t\t\titem.translationGroup,\n\t\t\t\t\titem.data,\n\t\t\t\t\t{ absentAsCleared: true },\n\t\t\t\t)),\n\t\t\t]);\n\t\t}\n\n\t\t// Run afterPublish hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterPublishHooks(\n\t\t\t\tcontentItemToRecord(result.data.item),\n\t\t\t\tcollection,\n\t\t\t\toptions.origin?.source === \"plugin\" ? options.origin.pluginId : undefined,\n\t\t\t\toptions.pluginInvocationId,\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUnpublish(\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: ContentPolicyMutationOptions = {},\n\t) {\n\t\tconst policy = await this.checkContentPolicy(\n\t\t\t\"content:beforeUnpublish\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\toptions,\n\t\t\t\"UNPUBLISH_REJECTED\",\n\t\t\t\"CONTENT_UNPUBLISH_ERROR\",\n\t\t);\n\t\tif (!policy.allowed) return { success: false as const, error: policy.error };\n\t\tconst result = await handleContentUnpublish(this.db, collection, id, {\n\t\t\t_rev: policy.revision,\n\t\t});\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterUnpublish hooks (deferred past the response via after())\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterUnpublishHooks(\n\t\t\t\tcontentItemToRecord(result.data.item),\n\t\t\t\tcollection,\n\t\t\t\toptions.origin?.source === \"plugin\" ? options.origin.pluginId : undefined,\n\t\t\t\toptions.pluginInvocationId,\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentSchedule(\n\t\tcollection: string,\n\t\tid: string,\n\t\tscheduledAt: string,\n\t\toptions: ContentPolicyMutationOptions = {},\n\t) {\n\t\tconst policy = await this.checkContentPolicy(\n\t\t\t\"content:beforeSchedule\",\n\t\t\tcollection,\n\t\t\tid,\n\t\t\toptions,\n\t\t\t\"SCHEDULE_REJECTED\",\n\t\t\t\"CONTENT_SCHEDULE_ERROR\",\n\t\t\tscheduledAt,\n\t\t);\n\t\tif (!policy.allowed) return { success: false as const, error: policy.error };\n\t\tconst policyRejectionRevision = await this.getScheduledPolicyRejectionRevision(collection, id);\n\t\tconst result = await handleContentSchedule(\n\t\t\tthis.db,\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tscheduledAt,\n\t\t\tthis.runtimeDeps.now?.() ?? new Date(),\n\t\t\tpolicy.revision,\n\t\t);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.clearScheduledPolicyRejection(\n\t\t\t\tcollection,\n\t\t\t\tresult.data.item.id,\n\t\t\t\tpolicyRejectionRevision,\n\t\t\t);\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterSchedule hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterScheduleHooks(\n\t\t\t\tcontentItemToRecord(result.data.item),\n\t\t\t\tcollection,\n\t\t\t\toptions.origin?.source === \"plugin\" ? options.origin.pluginId : undefined,\n\t\t\t\toptions.pluginInvocationId,\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentUnschedule(\n\t\tcollection: string,\n\t\tid: string,\n\t\toptions: {\n\t\t\t_rev?: string;\n\t\t\torigin?: ContentActionOrigin;\n\t\t\tpluginInvocationId?: string;\n\t\t} = {},\n\t) {\n\t\tconst result = await handleContentUnschedule(this.db, collection, id, { _rev: options._rev });\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\n\t\t// Run afterUnschedule hooks (fire-and-forget)\n\t\tif (result.success && result.data) {\n\t\t\tthis.runAfterUnscheduleHooks(\n\t\t\t\tcontentItemToRecord(result.data.item),\n\t\t\t\tcollection,\n\t\t\t\toptions.origin?.source === \"plugin\" ? options.origin.pluginId : undefined,\n\t\t\t\toptions.pluginInvocationId,\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleContentCountScheduled(collection: string) {\n\t\treturn handleContentCountScheduled(this.db, collection);\n\t}\n\n\tasync handleContentDiscardDraft(collection: string, id: string, options: { _rev?: string } = {}) {\n\t\tconst result = await handleContentDiscardDraft(this.db, collection, id, options);\n\t\tif (result.success && result.data) {\n\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(collection, [result.data.item.id]);\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync handleContentCompare(collection: string, id: string) {\n\t\treturn handleContentCompare(this.db, collection, id);\n\t}\n\n\tasync handleContentTranslations(collection: string, id: string) {\n\t\treturn handleContentTranslations(this.db, collection, id);\n\t}\n\n\t// =========================================================================\n\t// Media Handlers\n\t// =========================================================================\n\n\tasync handleMediaList(params: {\n\t\tcursor?: string;\n\t\tpage?: number;\n\t\tlimit?: number;\n\t\tmimeType?: string | readonly string[];\n\t\tq?: string;\n\t\tfolderId?: string | null;\n\t}) {\n\t\treturn handleMediaList(this.db, params);\n\t}\n\n\tasync handleMediaGet(id: string) {\n\t\treturn handleMediaGet(this.db, id);\n\t}\n\n\tasync handleMediaUpload(input: MediaUploadInput) {\n\t\tif (!this.storage) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: { code: \"NO_STORAGE\", message: \"Storage not configured\" },\n\t\t\t};\n\t\t}\n\t\tconst result = await uploadMedia(this.db, this.storage, input, {\n\t\t\tbeforeUpload: async (file) => {\n\t\t\t\tif (!this.hooks.hasHooks(\"media:beforeUpload\")) return file;\n\t\t\t\treturn (await this.hooks.runMediaBeforeUpload(file)).file;\n\t\t\t},\n\t\t});\n\t\tif (result.success && !result.data.deduplicated && this.hooks.hasHooks(\"media:afterUpload\")) {\n\t\t\tconst item = result.data.item;\n\t\t\tawait this.hooks.runMediaAfterUpload({\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\turl: item.url,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync handleMediaRegisterUpload(input: { storageKey: string; authorId?: string }) {\n\t\tif (!this.storage) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: { code: \"NO_STORAGE\", message: \"Storage not configured\" },\n\t\t\t};\n\t\t}\n\t\tconst result = await handleMediaRegisterUpload(this.db, this.storage, input);\n\n\t\tif (result.success && this.hooks.hasHooks(\"media:afterUpload\")) {\n\t\t\tconst item = result.data.item;\n\t\t\tconst mediaItem: MediaItem = {\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t};\n\t\t\tthis.hooks\n\t\t\t\t.runMediaAfterUpload(mediaItem)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterUpload hook error:\", err));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleMediaCreate(input: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\twidth?: number;\n\t\theight?: number;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tblurhash?: string;\n\t\tdominantColor?: string;\n\t\tauthorId?: string;\n\t\tfolderId?: string | null;\n\t}) {\n\t\t// Run beforeUpload hooks\n\t\tlet processedInput = input;\n\t\tif (this.hooks.hasHooks(\"media:beforeUpload\")) {\n\t\t\tconst hookResult = await this.hooks.runMediaBeforeUpload({\n\t\t\t\tname: input.filename,\n\t\t\t\ttype: input.mimeType,\n\t\t\t\tsize: input.size || 0,\n\t\t\t});\n\t\t\tprocessedInput = {\n\t\t\t\t...input,\n\t\t\t\tfilename: hookResult.file.name,\n\t\t\t\tmimeType: hookResult.file.type,\n\t\t\t\tsize: hookResult.file.size,\n\t\t\t};\n\t\t}\n\n\t\t// Create the media record\n\t\tconst result = await handleMediaCreate(this.db, processedInput);\n\n\t\t// Run afterUpload hooks (fire-and-forget)\n\t\tif (result.success && this.hooks.hasHooks(\"media:afterUpload\")) {\n\t\t\tconst item = result.data.item;\n\t\t\tconst mediaItem: MediaItem = {\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t};\n\t\t\tthis.hooks\n\t\t\t\t.runMediaAfterUpload(mediaItem)\n\t\t\t\t.catch((err) => console.error(\"EmDash afterUpload hook error:\", err));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync handleMediaUpdate(\n\t\tid: string,\n\t\tinput: {\n\t\t\talt?: string;\n\t\t\tcaption?: string;\n\t\t\twidth?: number;\n\t\t\theight?: number;\n\t\t\tfolderId?: string | null;\n\t\t\tfocalX?: number | null;\n\t\t\tfocalY?: number | null;\n\t\t},\n\t) {\n\t\tconst result = await handleMediaUpdate(this.db, id, input);\n\t\t// Resolved media references in site settings (`logo`, `favicon`,\n\t\t// `seo.defaultOgImage`) bake in the media row's `contentType`,\n\t\t// `width`, and `height`. A metadata edit invalidates that snapshot\n\t\t// for every entry point: REST routes, MCP tools, plugin code, and\n\t\t// any future caller of `handleMediaUpdate`. Cross-isolate staleness\n\t\t// remains bounded by isolate lifetime.\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync handlePluginMediaMetadataUpdate(id: string, patch: MediaMetadataPatch) {\n\t\treturn updatePluginMediaMetadata(this.db, id, patch);\n\t}\n\n\tasync handleMediaReplaceMetadata(\n\t\tid: string,\n\t\texpectedStorageKey: string,\n\t\tinput: { size: number; width: number; height: number; contentHash: string },\n\t) {\n\t\tconst result = await handleMediaReplaceMetadata(this.db, id, expectedStorageKey, input);\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync handleMediaDelete(id: string) {\n\t\tconst result = await handleMediaDelete(this.db, id, this.storage);\n\t\t// Same reasoning as `handleMediaUpdate`: if the deleted media row\n\t\t// was referenced by a setting, the cached resolved URL now points\n\t\t// at a 404. Invalidation is unconditional on success — cheaper than\n\t\t// querying which settings reference the id.\n\t\tif (result.success) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t\treturn result;\n\t}\n\n\tprivate commentHooks(): CommentHookRunner {\n\t\treturn {\n\t\t\trunBeforeCreate: (event) => this.hooks.runCommentBeforeCreate(event),\n\t\t\trunModerate: async (event) => {\n\t\t\t\tconst result = await this.hooks.invokeExclusiveHook(\"comment:moderate\", event);\n\t\t\t\tif (!result) return { status: \"pending\", reason: \"No moderator configured\" };\n\t\t\t\tif (result.error) {\n\t\t\t\t\tconsole.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);\n\t\t\t\t\treturn { status: \"pending\", reason: \"Moderation error\" };\n\t\t\t\t}\n\t\t\t\tif (!isModerationDecision(result.result)) {\n\t\t\t\t\treturn { status: \"pending\", reason: \"Invalid moderation result\" };\n\t\t\t\t}\n\t\t\t\treturn result.result;\n\t\t\t},\n\t\t\tfireAfterCreate: (event) => {\n\t\t\t\tvoid this.hooks\n\t\t\t\t\t.runCommentAfterCreate(event)\n\t\t\t\t\t.catch((error) =>\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\"[comments] afterCreate error:\",\n\t\t\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t},\n\t\t\tfireAfterModerate: (event) => {\n\t\t\t\treturn this.hooks\n\t\t\t\t\t.runCommentAfterModerate(event)\n\t\t\t\t\t.catch((error) =>\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\"[comments] afterModerate error:\",\n\t\t\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t},\n\t\t};\n\t}\n\n\tasync handleCommentCreate(\n\t\tinput: CommentCreateInput,\n\t\tsettings: CollectionCommentSettings,\n\t\tcontentInfo?: Parameters<typeof createComment>[4],\n\t): Promise<CommentCreateResult | null> {\n\t\treturn createComment(this.db, input, settings, this.commentHooks(), contentInfo);\n\t}\n\n\tasync handlePublicCommentSubmission(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\trequest: Request,\n\t\tuser?: UserInfo,\n\t): Promise<Response> {\n\t\treturn submitPublicComment(this, collection, contentId, request, user);\n\t}\n\n\tprivate async sendCommentApproval(\n\t\tcomment: Awaited<ReturnType<CommentRepository[\"findById\"]>>,\n\t\trequest?: Request,\n\t) {\n\t\tif (!comment || !this.email) return;\n\t\ttry {\n\t\t\tconst adminBaseUrl = await getSiteBaseUrl(\n\t\t\t\tthis.db,\n\t\t\t\trequest ?? new Request(\"https://plugin.invalid/\"),\n\t\t\t\tthis.config,\n\t\t\t);\n\t\t\tconst content = await lookupContentAuthor(this.db, comment.collection, comment.contentId);\n\t\t\tif (content?.author) {\n\t\t\t\tawait sendCommentNotification({\n\t\t\t\t\temail: this.email,\n\t\t\t\t\tcomment,\n\t\t\t\t\tcontentAuthor: content.author,\n\t\t\t\t\tadminBaseUrl,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t\"[comments] notification error:\",\n\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async moderateCommentWithOrigin(\n\t\tid: string,\n\t\tstatus: CommentStatus,\n\t\texpectedStatus: CommentStatus,\n\t\torigin:\n\t\t\t| { source: \"admin\"; userId: string; name: string | null }\n\t\t\t| { source: \"plugin\"; pluginId: string },\n\t\trequest?: Request,\n\t) {\n\t\tif (this.commentModerationInProgress.has(id)) {\n\t\t\tconst current = await new CommentRepository(this.db).findById(id);\n\t\t\tif (current && current.status !== expectedStatus) {\n\t\t\t\tthrow new CommentStatusConflictError(current.status);\n\t\t\t}\n\t\t\tthrow Object.assign(new Error(\"Comment moderation is already in progress\"), {\n\t\t\t\tcode: \"COMMENT_MODERATION_IN_PROGRESS\",\n\t\t\t});\n\t\t}\n\t\tthis.commentModerationInProgress.add(id);\n\t\ttry {\n\t\t\treturn await moderateComment(\n\t\t\t\tthis.db,\n\t\t\t\tid,\n\t\t\t\tstatus,\n\t\t\t\texpectedStatus,\n\t\t\t\torigin,\n\t\t\t\tthis.commentHooks(),\n\t\t\t\t(comment) => this.sendCommentApproval(comment, request),\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.commentModerationInProgress.delete(id);\n\t\t}\n\t}\n\n\tasync handleCommentModerate(\n\t\tid: string,\n\t\tstatus: CommentStatus,\n\t\tmoderator: { id: string; name: string | null },\n\t\texpectedStatus?: CommentStatus,\n\t\trequest?: Request,\n\t) {\n\t\tlet expected = expectedStatus;\n\t\tif (!expected) {\n\t\t\tconst current = await new CommentRepository(this.db).findById(id);\n\t\t\tif (!current || current.status === \"trash\") return null;\n\t\t\texpected = current.status;\n\t\t}\n\t\treturn this.moderateCommentWithOrigin(\n\t\t\tid,\n\t\t\tstatus,\n\t\t\texpected,\n\t\t\t{ source: \"admin\", userId: moderator.id, name: moderator.name },\n\t\t\trequest,\n\t\t);\n\t}\n\n\tasync handlePluginCommentModerate(\n\t\tpluginId: string,\n\t\tid: string,\n\t\tstatus: PluginCommentStatus,\n\t\texpectedStatus: PluginCommentStatus,\n\t): Promise<PluginComment> {\n\t\tassertPluginCommentStatus(status, \"status\");\n\t\tassertPluginCommentStatus(expectedStatus, \"expectedStatus\");\n\t\tconst updated = await this.moderateCommentWithOrigin(id, status, expectedStatus, {\n\t\t\tsource: \"plugin\",\n\t\t\tpluginId,\n\t\t});\n\t\tif (!updated) throw new Error(`Comment not found: ${id}`);\n\t\tconst result = await createCommentAccess(this.db).get(id);\n\t\tif (!result) throw new Error(`Comment not found: ${id}`);\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Revision Handlers\n\t// =========================================================================\n\n\tasync handleRevisionList(collection: string, entryId: string, params: { limit?: number } = {}) {\n\t\treturn handleRevisionList(this.db, collection, entryId, params);\n\t}\n\n\tasync handleRevisionGet(revisionId: string) {\n\t\treturn handleRevisionGet(this.db, revisionId);\n\t}\n\n\tasync handleRevisionRestore(revisionId: string, callerUserId: string) {\n\t\t// Discover the parent entry up front so we can branch on whether\n\t\t// the collection uses draft revisions.\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst collectionInfo = await this.schemaRegistry.getCollectionWithFields(revision.collection);\n\t\tconst usesDraftRevisions = collectionInfo?.supports?.includes(\"revisions\") ?? false;\n\n\t\t// Non-revision collections: keep the legacy behavior of writing the\n\t\t// revision's data straight onto the live row. This preserves\n\t\t// behavior for collections that opt out of the draft model.\n\t\tif (!usesDraftRevisions) {\n\t\t\tconst result = await handleRevisionRestore(this.db, revisionId, callerUserId);\n\t\t\tif (result.success) {\n\t\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);\n\t\t\t}\n\t\t\treturn this.hydrateDraftData(result);\n\t\t}\n\n\t\t// Revision-capable collections: restore is \"make this revision the\n\t\t// current draft\". The live row's data columns are left untouched\n\t\t// (only `draft_revision_id` changes — no `updated_at` stamp, since\n\t\t// restoring to draft is the same kind of draft-only staging as\n\t\t// Save/Autosave and must not register a phantom modification for\n\t\t// sitemap <lastmod> / JSON-LD dateModified, #2143). The caller\n\t\t// must then `content_publish` to promote the restored draft to\n\t\t// live, matching the documented tool contract.\n\t\ttry {\n\t\t\tconst contentRepo = new ContentRepository(this.db);\n\t\t\tconst newDraftId = await contentRepo.restoreDraftRevision(\n\t\t\t\trevision.collection,\n\t\t\t\trevision.entryId,\n\t\t\t\trevision.data,\n\t\t\t\tcallerUserId,\n\t\t\t);\n\t\t\tif (!newDraftId) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\t\tmessage: `Content item not found: ${revision.entryId}`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tafter(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait revisionRepo.pruneQueuedEntry(\n\t\t\t\t\t\trevision.collection,\n\t\t\t\t\t\trevision.entryId,\n\t\t\t\t\t\tnewDraftId,\n\t\t\t\t\t\t50,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`,\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Return the freshly-fetched item with the new draft hydrated\n\t\t\t// onto `data`. Without this the response would echo the live\n\t\t\t// columns and the next `content_get` would surface different\n\t\t\t// values (the bug that motivated this rewrite).\n\t\t\tconst refetched = await handleContentGet(this.db, revision.collection, revision.entryId);\n\t\t\tconst hydrated = await this.hydrateDraftData(refetched);\n\t\t\tif (hydrated.success) {\n\t\t\t\tawait this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);\n\t\t\t}\n\t\t\treturn hydrated;\n\t\t} catch (error) {\n\t\t\tif (error instanceof ContentMutationConflictError) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false as const,\n\t\t\t\t\terror: { code: \"CONFLICT\", message: error.message },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconsole.error(\"[emdash] revision restore failed:\", error);\n\t\t\treturn {\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"REVISION_RESTORE_ERROR\",\n\t\t\t\t\tmessage: \"Failed to restore revision\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate async findSyncedSiblingsForUsageRefresh(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\ttranslationGroup: string | null | undefined,\n\t\tdata: Record<string, unknown>,\n\t\toptions: { absentAsCleared?: boolean } = {},\n\t): Promise<string[]> {\n\t\ttry {\n\t\t\treturn await findNonTranslatableSiblingContentIds(\n\t\t\t\tthis.db,\n\t\t\t\tcollection,\n\t\t\t\tcontentId,\n\t\t\t\ttranslationGroup,\n\t\t\t\tdata,\n\t\t\t\toptions,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[media-usage] Failed to discover synced i18n siblings for ${collection}/${contentId}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t\ttry {\n\t\t\t\tawait markContentMediaUsageCollectionStale(\n\t\t\t\t\tthis.db,\n\t\t\t\t\tcollection,\n\t\t\t\t\t\"CONTENT_USAGE_REFRESH_ERROR\",\n\t\t\t\t);\n\t\t\t} catch (staleError) {\n\t\t\t\tconsole.error(`[media-usage] Failed to mark ${collection} stale:`, staleError);\n\t\t\t}\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tprivate async refreshContentUsageAfterSuccessfulWrite(\n\t\tcollection: string,\n\t\tcontentIds: readonly string[],\n\t): Promise<void> {\n\t\tfor (const contentId of new Set(contentIds)) {\n\t\t\ttry {\n\t\t\t\tconst work = await processMediaUsageWorkAfterWrite(this.db, collection, contentId);\n\t\t\t\tif (work.outcome !== \"inactive\") continue;\n\t\t\t\tawait refreshContentMediaUsageAfterWrite(this.db, collection, contentId);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[media-usage] Failed after content write ${collection}/${contentId}:`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async deleteContentUsageAfterSuccessfulPermanentDelete(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst work = await processMediaUsageWorkAfterWrite(this.db, collection, contentId);\n\t\t\tif (work.outcome !== \"inactive\") return;\n\t\t\tconst result = await deleteContentMediaUsage(this.db, collection, contentId);\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[media-usage] Failed after permanent content delete ${collection}/${contentId}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Plugin Routes\n\t// =========================================================================\n\n\tprivate getSandboxedAdminDefinition(pluginId: string): {\n\t\tpages: string[];\n\t\twidgets: string[];\n\t\tpolicy: BlockValidationPolicy;\n\t} | null {\n\t\tconst entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);\n\t\tif (entry) {\n\t\t\tconst pages = (entry.adminPages ?? []).map((page) => normalizePluginPagePath(page.path));\n\t\t\tconst imageHosts = allowedBrowserImageHosts(entry.capabilities, entry.allowedHosts);\n\t\t\treturn {\n\t\t\t\tpages,\n\t\t\t\twidgets: (entry.adminWidgets ?? []).map((widget) => widget.id),\n\t\t\t\tpolicy: { pluginPagePaths: pages, allowedImageHosts: imageHosts },\n\t\t\t};\n\t\t}\n\n\t\tconst manifest = marketplaceManifestCache.get(pluginId);\n\t\tif (!manifest) return null;\n\t\tconst pages = (manifest.admin?.pages ?? []).map((page) => normalizePluginPagePath(page.path));\n\t\tconst imageHosts = allowedBrowserImageHosts(\n\t\t\tmanifest.capabilities ?? [],\n\t\t\tmanifest.allowedHosts ?? [],\n\t\t);\n\t\treturn {\n\t\t\tpages,\n\t\t\twidgets: (manifest.admin?.widgets ?? []).map((widget) => widget.id),\n\t\t\tpolicy: { pluginPagePaths: pages, allowedImageHosts: imageHosts },\n\t\t};\n\t}\n\n\tprivate resolvePluginUiContext(\n\t\tdefinition: { pages: string[]; widgets: string[] },\n\t\tbody: unknown,\n\t\trequest: Request,\n\t): { context?: PluginUiContext; error?: { code: string; message: string } } {\n\t\tif (typeof body !== \"object\" || body === null || !(\"page\" in body)) return {};\n\t\tconst page = body.page;\n\t\tif (typeof page !== \"string\") {\n\t\t\treturn {\n\t\t\t\terror: { code: \"INVALID_PLUGIN_UI_CONTEXT\", message: \"Plugin UI page must be a string\" },\n\t\t\t};\n\t\t}\n\n\t\tlet surface: PluginUiContext[\"surface\"];\n\t\tif (page.startsWith(\"widget:\")) {\n\t\t\tif (!definition.widgets.includes(page.slice(\"widget:\".length))) {\n\t\t\t\treturn {\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"INVALID_PLUGIN_UI_CONTEXT\",\n\t\t\t\t\t\tmessage: \"Plugin dashboard widget is not declared\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tsurface = \"dashboard-widget\";\n\t\t} else {\n\t\t\tif (!definition.pages.includes(normalizePluginPagePath(page))) {\n\t\t\t\treturn {\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"INVALID_PLUGIN_UI_CONTEXT\",\n\t\t\t\t\t\tmessage: \"Plugin admin page is not declared\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tsurface = \"admin-page\";\n\t\t}\n\n\t\tconst locale = resolveLocale(request);\n\t\treturn { context: { surface, locale, direction: getLocaleDir(locale) } };\n\t}\n\n\tprivate validateSandboxedAdminResponse(\n\t\tpluginId: string,\n\t\tdefinition: { policy: BlockValidationPolicy },\n\t\tresult: {\n\t\t\tsuccess: boolean;\n\t\t\tdata?: unknown;\n\t\t\terror?: { code: string; message: string };\n\t\t\tstatus?: number;\n\t\t},\n\t) {\n\t\tif (!result.success) return result;\n\t\tconst validation = validateBlockResponse(result.data, definition.policy);\n\t\tif (validation.valid) return result;\n\n\t\tconsole.error(\n\t\t\t`EmDash: Sandboxed plugin ${pluginId} returned invalid Block Kit content:`,\n\t\t\tvalidation.errors,\n\t\t);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\tstatus: 502,\n\t\t\terror: {\n\t\t\t\tcode: \"INVALID_BLOCK_RESPONSE\",\n\t\t\t\tmessage: \"Plugin returned invalid Block Kit content\",\n\t\t\t},\n\t\t};\n\t}\n\n\tgetPluginEditorExtension(\n\t\tpluginId: string,\n\t\tkind: \"panel\" | \"action\",\n\t\textensionId: string,\n\t\tcollection: string,\n\t): ResolvedPluginEditorExtension | null {\n\t\tif (!this.isPluginEnabled(pluginId)) return null;\n\n\t\tlet panels: PluginEditorPanel[] | undefined;\n\t\tlet actions: PluginEditorAction[] | undefined;\n\t\tlet pages: string[] = [];\n\t\tlet capabilities: readonly PluginCapability[] = [];\n\t\tlet allowedHosts: readonly string[] = [];\n\n\t\tconst configured = this.configuredPlugins.find((plugin) => plugin.id === pluginId);\n\t\tif (configured) {\n\t\t\tif (configured.admin.entry) return null;\n\t\t\tpanels = configured.admin.editorPanels;\n\t\t\tactions = configured.admin.editorActions;\n\t\t\tpages = (configured.admin.pages ?? []).map((page) => page.path);\n\t\t\tcapabilities = configured.capabilities;\n\t\t\tallowedHosts = configured.allowedHosts;\n\t\t} else {\n\t\t\tconst entry = this.sandboxedPluginEntries.find((candidate) => candidate.id === pluginId);\n\t\t\tif (entry) {\n\t\t\t\tpanels = entry.editorPanels;\n\t\t\t\tactions = entry.editorActions;\n\t\t\t\tpages = (entry.adminPages ?? []).map((page) => page.path);\n\t\t\t\tcapabilities = entry.capabilities;\n\t\t\t\tallowedHosts = entry.allowedHosts;\n\t\t\t} else {\n\t\t\t\tconst manifest = marketplaceManifestCache.get(pluginId);\n\t\t\t\tif (!manifest) return null;\n\t\t\t\tpanels = manifest.admin?.editorPanels;\n\t\t\t\tactions = manifest.admin?.editorActions;\n\t\t\t\tpages = (manifest.admin?.pages ?? []).map((page) => page.path);\n\t\t\t\tcapabilities = manifest.capabilities ?? [];\n\t\t\t\tallowedHosts = manifest.allowedHosts ?? [];\n\t\t\t}\n\t\t}\n\n\t\tconst normalizedCapabilities = normalizePluginCapabilities(capabilities);\n\t\tconst policy = {\n\t\t\tpluginPagePaths: pages,\n\t\t\tallowedImageHosts: allowedBrowserImageHosts(capabilities, allowedHosts),\n\t\t};\n\t\tif (kind === \"panel\") {\n\t\t\tconst matches = panels?.filter((panel) => panel.id === extensionId) ?? [];\n\t\t\tconst extension = matches[0];\n\t\t\tif (\n\t\t\t\tmatches.length !== 1 ||\n\t\t\t\t!extension ||\n\t\t\t\t(extension.collections && !extension.collections.includes(collection))\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn { kind, extension, policy, capabilities: normalizedCapabilities };\n\t\t}\n\t\tconst matches = actions?.filter((action) => action.id === extensionId) ?? [];\n\t\tconst extension = matches[0];\n\t\tif (\n\t\t\tmatches.length !== 1 ||\n\t\t\t!extension ||\n\t\t\t(extension.collections && !extension.collections.includes(collection)) ||\n\t\t\t(extension.style === \"danger\" && !extension.confirm)\n\t\t) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { kind, extension, policy, capabilities: normalizedCapabilities };\n\t}\n\n\tasync getPluginEditorDraftSchema(collection: string) {\n\t\treturn this.schemaRegistry.getCollectionWithFields(collection);\n\t}\n\n\tprivate validatePluginEditorExtensionResponse(\n\t\tpluginId: string,\n\t\tdispatch: PluginEditorExtensionDispatch,\n\t\tresult: {\n\t\t\tsuccess: boolean;\n\t\t\tdata?: unknown;\n\t\t\terror?: { code: string; message: string };\n\t\t\tstatus?: number;\n\t\t},\n\t) {\n\t\tif (!result.success) return result;\n\t\tconst validation =\n\t\t\tdispatch.kind === \"panel\"\n\t\t\t\t? validateBlockResponse(result.data, dispatch.policy)\n\t\t\t\t: validateContentEditorActionResponse(result.data, dispatch.policy);\n\t\tif (validation.valid) return result;\n\n\t\tconsole.error(\n\t\t\t`EmDash: Plugin ${pluginId} returned an invalid content editor ${dispatch.kind} response:`,\n\t\t\tvalidation.errors,\n\t\t);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\tstatus: 502,\n\t\t\terror: {\n\t\t\t\tcode:\n\t\t\t\t\tdispatch.kind === \"panel\" ? \"INVALID_BLOCK_RESPONSE\" : \"INVALID_EDITOR_ACTION_RESPONSE\",\n\t\t\t\tmessage: \"Plugin returned an invalid editor extension response\",\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Get route metadata for a plugin route without invoking the handler.\n\t * Used by the catch-all route to decide auth before dispatch.\n\t * Returns null if the plugin or route doesn't exist.\n\t */\n\tgetPluginRouteMeta(pluginId: string, path: string): RouteMeta | null {\n\t\tif (!this.isPluginEnabled(pluginId)) return null;\n\n\t\tconst routeKey = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\t// Check trusted plugins first\n\t\tconst trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (trustedPlugin) {\n\t\t\tconst route = trustedPlugin.routes[routeKey];\n\t\t\tif (!route) return null;\n\t\t\treturn buildRouteMeta(route);\n\t\t}\n\n\t\t// Check sandboxed plugin route metadata cache\n\t\tconst meta = sandboxedRouteMetaCache.get(pluginId);\n\t\tif (meta) {\n\t\t\tconst routeMeta = meta.get(routeKey);\n\t\t\tif (routeMeta) return routeMeta;\n\t\t}\n\n\t\t// The \"admin\" route is implicitly available for any sandboxed plugin\n\t\t// that declares admin pages or widgets. This handles plugins installed\n\t\t// from bundles that predate the explicit admin route requirement.\n\t\tif (routeKey === \"admin\") {\n\t\t\tconst manifestMeta = marketplaceManifestCache.get(pluginId);\n\t\t\tif (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) {\n\t\t\t\treturn { public: false };\n\t\t\t}\n\t\t\t// Also check build-time sandboxed entries\n\t\t\tconst entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\t\tif (entry?.adminPages?.length || entry?.adminWidgets?.length) {\n\t\t\t\treturn { public: false };\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: if the plugin exists in the sandbox cache, allow the route.\n\t\t// The sandbox runner will return an error if the route doesn't actually exist.\n\t\tif (this.findSandboxedPlugin(pluginId)) {\n\t\t\treturn { public: false };\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Resolve the settings schema for a runtime-installed (marketplace or\n\t * registry) plugin from its cached manifest. Returns `{}` for a known\n\t * plugin without a schema and `null` for unknown plugins, matching the\n\t * contract of `getPluginSettingsSchema` for build-time plugins.\n\t */\n\tgetRuntimePluginSettingsSchema(pluginId: string): Record<string, SettingField> | null {\n\t\tconst meta = marketplaceManifestCache.get(pluginId);\n\t\tif (!meta) return null;\n\t\treturn meta.admin?.settingsSchema ?? {};\n\t}\n\n\tasync handlePluginApiRoute(\n\t\tpluginId: string,\n\t\tmethod: string,\n\t\tpath: string,\n\t\trequest: Request,\n\t\tuser?: RouteCallerInput | null,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t\teditorDispatch?: PluginEditorExtensionDispatch,\n\t) {\n\t\tif (!this.isPluginEnabled(pluginId)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not enabled: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tconst normalizedMethod = method.toUpperCase();\n\t\tconst routeMeta = this.getPluginRouteMeta(pluginId, path);\n\t\tif (routeMeta?.methods && !routeMeta.methods.some((allowed) => allowed === normalizedMethod)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tstatus: 405,\n\t\t\t\terror: { code: \"METHOD_NOT_ALLOWED\", message: \"Method not allowed\" },\n\t\t\t};\n\t\t}\n\n\t\t// Authenticated caller for `ctx.user`. Undefined for public routes\n\t\t// (the catch-all only forwards the caller after private-route auth)\n\t\t// and for machine tokens with no bound user.\n\t\tconst caller = user ? toRouteCallerInfo(user) : undefined;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await parseRouteInput(request, routeMeta?.request);\n\t\t} catch (error) {\n\t\t\tif (error instanceof PluginRouteRequestError) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\tstatus: error.status,\n\t\t\t\t\terror: { code: \"INVALID_PLUGIN_REQUEST\", message: error.message },\n\t\t\t\t};\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tconst routeKey = path.replace(LEADING_SLASH_PATTERN, \"\");\n\t\tconst adminDefinition =\n\t\t\t!editorDispatch && routeKey === \"admin\" ? this.getSandboxedAdminDefinition(pluginId) : null;\n\t\tconst uiResult = adminDefinition\n\t\t\t? this.resolvePluginUiContext(adminDefinition, body, request)\n\t\t\t: {};\n\t\tif (uiResult.error) return { success: false, status: 400, error: uiResult.error };\n\n\t\t// Check trusted (configured) plugins first — this must match the\n\t\t// resolution order in getPluginRouteMeta to avoid auth/execution mismatches.\n\t\tconst trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);\n\t\tif (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {\n\t\t\tconst routeRegistry = new PluginRouteRegistry({\n\t\t\t\t...this.pipelineFactoryOptions,\n\t\t\t\tcontentActions: this.contentActionsWithInvalidator(invalidateContentCache),\n\t\t\t\temailPipeline: this.email ?? undefined,\n\t\t\t\tcronReschedule: () => this.cronScheduler?.reschedule(),\n\t\t\t\ttrustedProxyHeaders: getTrustedProxyHeaders(this.config),\n\t\t\t});\n\t\t\trouteRegistry.register(trustedPlugin);\n\n\t\t\tconst result = await routeRegistry.invoke(pluginId, routeKey, {\n\t\t\t\trequest,\n\t\t\t\tbody,\n\t\t\t\tuser: caller,\n\t\t\t\tui: editorDispatch?.ui ?? uiResult.context,\n\t\t\t});\n\t\t\treturn editorDispatch\n\t\t\t\t? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result)\n\t\t\t\t: adminDefinition\n\t\t\t\t\t? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result)\n\t\t\t\t\t: result;\n\t\t}\n\n\t\t// Check sandboxed (marketplace) plugins second\n\t\tconst sandboxedPlugin = this.findSandboxedPlugin(pluginId);\n\t\tif (sandboxedPlugin) {\n\t\t\tconst result = await this.handleSandboxedRoute(\n\t\t\t\tsandboxedPlugin,\n\t\t\t\tpath,\n\t\t\t\trequest,\n\t\t\t\tbody,\n\t\t\t\trouteMeta ?? { public: false },\n\t\t\t\tcaller,\n\t\t\t\teditorDispatch?.ui ?? uiResult.context,\n\t\t\t\tinvalidateContentCache,\n\t\t\t);\n\t\t\treturn editorDispatch\n\t\t\t\t? this.validatePluginEditorExtensionResponse(pluginId, editorDispatch, result)\n\t\t\t\t: adminDefinition\n\t\t\t\t\t? this.validateSandboxedAdminResponse(pluginId, adminDefinition, result)\n\t\t\t\t\t: result;\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t};\n\t}\n\n\tprivate contentActionsWithInvalidator(\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): ContentActionCallbacks | undefined {\n\t\tconst actions = this.pipelineFactoryOptions.contentActions;\n\t\tif (!actions || !invalidateContentCache) return actions;\n\t\treturn {\n\t\t\t...actions,\n\t\t\tbegin: (pluginId, invocationId, invocationInvalidator) =>\n\t\t\t\tactions.begin?.(pluginId, invocationId, invocationInvalidator ?? invalidateContentCache),\n\t\t\tpublish: (pluginId, collection, id, options, invocationId) =>\n\t\t\t\tactions.publish(pluginId, collection, id, options, invocationId, invalidateContentCache),\n\t\t\tunpublish: (pluginId, collection, id, options, invocationId) =>\n\t\t\t\tactions.unpublish(pluginId, collection, id, options, invocationId, invalidateContentCache),\n\t\t\tschedule: (pluginId, collection, id, options, invocationId) =>\n\t\t\t\tactions.schedule(pluginId, collection, id, options, invocationId, invalidateContentCache),\n\t\t\tunschedule: (pluginId, collection, id, options, invocationId) =>\n\t\t\t\tactions.unschedule(pluginId, collection, id, options, invocationId, invalidateContentCache),\n\t\t\trestore: (pluginId, collection, id, options, invocationId) =>\n\t\t\t\tactions.restore(pluginId, collection, id, options, invocationId, invalidateContentCache),\n\t\t};\n\t}\n\n\tasync getPluginMcpTools(pluginId?: string) {\n\t\tconst tools: Array<{\n\t\t\tpluginId: string;\n\t\t\tname: string;\n\t\t\tdescription: string;\n\t\t\troute: string;\n\t\t\tpermission: string;\n\t\t\tdestructive: boolean;\n\t\t\tinputSchema: z.ZodType;\n\t\t\toutputSchema?: z.ZodType;\n\t\t}> = [];\n\t\tconst seen = new Set<string>();\n\n\t\tfor (const plugin of this.configuredPlugins) {\n\t\t\tif (pluginId && plugin.id !== pluginId) continue;\n\t\t\tfor (const [name, tool] of Object.entries(plugin.mcp?.tools ?? {})) {\n\t\t\t\tconst route = plugin.routes[tool.route];\n\t\t\t\tif (\n\t\t\t\t\t!route ||\n\t\t\t\t\troute.public ||\n\t\t\t\t\troute.response === \"raw\" ||\n\t\t\t\t\t!isJsonPostRouteContract(route) ||\n\t\t\t\t\t!route.permission ||\n\t\t\t\t\t!Object.hasOwn(Permissions, route.permission)\n\t\t\t\t)\n\t\t\t\t\tcontinue;\n\t\t\t\tconst key = `${plugin.id}__${name}`;\n\t\t\t\tif (seen.has(key)) continue;\n\t\t\t\tseen.add(key);\n\t\t\t\ttools.push({\n\t\t\t\t\tpluginId: plugin.id,\n\t\t\t\t\tname,\n\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\troute: tool.route,\n\t\t\t\t\tpermission: route.permission,\n\t\t\t\t\tdestructive: tool.destructive ?? false,\n\t\t\t\t\tinputSchema: tool.input,\n\t\t\t\t\toutputSchema: tool.output,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst addManifestTools = (id: string, mcp: PluginMcpManifestConfig | undefined) => {\n\t\t\tif (pluginId && id !== pluginId) return;\n\t\t\tfor (const tool of mcp?.tools ?? []) {\n\t\t\t\tconst key = `${id}__${tool.name}`;\n\t\t\t\tconst routeMeta = this.getPluginRouteMeta(id, tool.route);\n\t\t\t\tif (\n\t\t\t\t\tseen.has(key) ||\n\t\t\t\t\t!routeMeta ||\n\t\t\t\t\trouteMeta.public ||\n\t\t\t\t\trouteMeta.response === \"raw\" ||\n\t\t\t\t\t!isJsonPostRouteContract(routeMeta) ||\n\t\t\t\t\trouteMeta.permission !== tool.permission ||\n\t\t\t\t\t!Object.hasOwn(Permissions, tool.permission)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tseen.add(key);\n\t\t\t\ttools.push({\n\t\t\t\t\tpluginId: id,\n\t\t\t\t\tname: tool.name,\n\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\troute: tool.route,\n\t\t\t\t\tpermission: tool.permission,\n\t\t\t\t\tdestructive: tool.destructive,\n\t\t\t\t\tinputSchema: z.fromJSONSchema({ ...tool.inputSchema }),\n\t\t\t\t\toutputSchema: tool.outputSchema ? z.fromJSONSchema({ ...tool.outputSchema }) : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\tfor (const entry of this.sandboxedPluginEntries) addManifestTools(entry.id, entry.mcp);\n\t\tfor (const [id, manifest] of marketplaceManifestCache) addManifestTools(id, manifest.mcp);\n\n\t\treturn tools;\n\t}\n\n\tasync getEnabledPluginMcpTools() {\n\t\tconst [tools, states] = await Promise.all([\n\t\t\tthis.getPluginMcpTools(),\n\t\t\tnew PluginStateRepository(this.db).getAll(),\n\t\t]);\n\t\tconst stateByPlugin = new Map(states.map((state) => [state.pluginId, state]));\n\t\treturn tools.filter((tool) => {\n\t\t\tconst state = stateByPlugin.get(tool.pluginId);\n\t\t\tif (\n\t\t\t\t!state?.mcpToolsEnabled ||\n\t\t\t\tstate.status !== \"active\" ||\n\t\t\t\t!this.isPluginEnabled(tool.pluginId)\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst consented = state.mcpToolsConsent;\n\t\t\treturn consented === this.serializePluginMcpConsent(tools, tool.pluginId);\n\t\t});\n\t}\n\n\tserializePluginMcpConsent(\n\t\ttools: Awaited<ReturnType<EmDashRuntime[\"getPluginMcpTools\"]>>,\n\t\tpluginId: string,\n\t): string {\n\t\treturn JSON.stringify(\n\t\t\ttools\n\t\t\t\t.filter((tool) => tool.pluginId === pluginId)\n\t\t\t\t.map((tool) => ({\n\t\t\t\t\tname: tool.name,\n\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\troute: tool.route,\n\t\t\t\t\tpermission: tool.permission,\n\t\t\t\t\tdestructive: tool.destructive,\n\t\t\t\t\tinputSchema: z.toJSONSchema(tool.inputSchema, { target: \"draft-7\" }),\n\t\t\t\t\t...(tool.outputSchema\n\t\t\t\t\t\t? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: \"draft-7\" }) }\n\t\t\t\t\t\t: {}),\n\t\t\t\t}))\n\t\t\t\t.toSorted((a, b) => a.name.localeCompare(b.name)),\n\t\t);\n\t}\n\n\tasync handlePluginMcpTool(\n\t\tpluginId: string,\n\t\ttoolName: string,\n\t\troute: string,\n\t\tinput: unknown,\n\t\tactorId: string,\n\t\trequest: Request,\n\t\tcaller?: RouteCallerInput | null,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t) {\n\t\tconst requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));\n\t\tconst audit = new AuditRepository(this.db);\n\t\tconst headers = new Headers(request.headers);\n\t\theaders.delete(\"content-length\");\n\t\theaders.delete(\"content-encoding\");\n\t\tconst internalRequest = new Request(request.url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders,\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t\tconst result = await this.handlePluginApiRoute(\n\t\t\tpluginId,\n\t\t\t\"POST\",\n\t\t\troute,\n\t\t\tinternalRequest,\n\t\t\tcaller,\n\t\t\tinvalidateContentCache,\n\t\t);\n\t\tawait audit.log({\n\t\t\tactorId,\n\t\t\tactorIp: requestMeta.ip ?? undefined,\n\t\t\taction: \"plugin_tool_invoke\",\n\t\t\tresourceType: \"plugin_mcp_tool\",\n\t\t\tresourceId: `${pluginId}__${toolName}`,\n\t\t\tdetails: { pluginId, tool: toolName, route },\n\t\t\tstatus: result.success ? \"success\" : \"failure\",\n\t\t});\n\t\treturn result;\n\t}\n\n\tasync handlePluginMcpDenied(\n\t\tpluginId: string,\n\t\ttoolName: string,\n\t\troute: string,\n\t\tactorId: string,\n\t\trequest: Request,\n\t\treason: string,\n\t): Promise<void> {\n\t\tconst requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));\n\t\tawait new AuditRepository(this.db).log({\n\t\t\tactorId,\n\t\t\tactorIp: requestMeta.ip ?? undefined,\n\t\t\taction: \"plugin_tool_invoke\",\n\t\t\tresourceType: \"plugin_mcp_tool\",\n\t\t\tresourceId: `${pluginId}__${toolName}`,\n\t\t\tdetails: { pluginId, tool: toolName, route, reason },\n\t\t\tstatus: \"denied\",\n\t\t});\n\t}\n\n\t// =========================================================================\n\t// Sandboxed Plugin Helpers\n\t// =========================================================================\n\n\tprivate findSandboxedPlugin(pluginId: string): SandboxedPluginInstance | undefined {\n\t\tfor (const [key, plugin] of this.sandboxedPlugins) {\n\t\t\tif (key.startsWith(pluginId + \":\")) {\n\t\t\t\treturn plugin;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Drop incoming keys that have no matching collection field when the entry\n\t * already stores them in its live data or current draft revision.\n\t *\n\t * Draft revisions keep the full `data` JSON, so deleting a field can strand\n\t * the old value; this lets a read-then-write save succeed without allowing\n\t * genuinely unknown keys.\n\t */\n\tprivate async dropUnknownKeysAlreadyStored(\n\t\tdata: Record<string, unknown>,\n\t\texisting: { data: Record<string, unknown>; draftRevisionId?: string | null } | null,\n\t\tknownFieldSlugs: ReadonlySet<string>,\n\t): Promise<Record<string, unknown>> {\n\t\tif (!existing) return data;\n\n\t\tlet stored: Record<string, unknown> = existing.data ?? {};\n\t\tif (existing.draftRevisionId) {\n\t\t\tconst draft = await new RevisionRepository(this.db)\n\t\t\t\t.findById(existing.draftRevisionId)\n\t\t\t\t.catch(() => null);\n\t\t\tif (draft?.data) stored = draft.data;\n\t\t}\n\n\t\tconst stale = staleStoredKeys(data, stored, knownFieldSlugs);\n\t\tif (stale.length === 0) return data;\n\n\t\tconst result = { ...data };\n\t\tfor (const key of stale) delete result[key];\n\t\treturn result;\n\t}\n\n\t/**\n\t * Normalize field values in content data before validation.\n\t * Turns a blank string in an array-valued field into `null`, and fills\n\t * missing image/file dimensions, storageKey, mimeType, and filename from providers.\n\t */\n\tprivate async normalizeFieldValues(\n\t\tcollection: string,\n\t\tdata: Record<string, unknown>,\n\t\tpreloaded?: CollectionWithFields | null,\n\t\tpreloadedBlockTypes?: Awaited<ReturnType<typeof resolveBlockTypes>>,\n\t\tincludeBlocks = true,\n\t): Promise<Record<string, unknown>> {\n\t\tlet collectionInfo = preloaded;\n\t\tif (collectionInfo === undefined) {\n\t\t\ttry {\n\t\t\t\tcollectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);\n\t\t\t} catch {\n\t\t\t\treturn data;\n\t\t\t}\n\t\t}\n\t\tif (!collectionInfo?.fields) return data;\n\n\t\tconst result = { ...data };\n\t\tfor (const field of collectionInfo.fields) {\n\t\t\tconst value = result[field.slug];\n\t\t\tif (ARRAY_FIELD_TYPES.has(field.type) && typeof value === \"string\" && !value.trim()) {\n\t\t\t\tresult[field.slug] = null;\n\t\t\t}\n\t\t}\n\n\t\tconst imageFields = collectionInfo.fields.filter(\n\t\t\t(f) => f.type === \"image\" || f.type === \"file\",\n\t\t);\n\t\t// Repeater fields can contain image sub-fields, whose values need the same normalization\n\t\t// (a bare media id posted inside a repeater item would otherwise be stored verbatim and\n\t\t// render as \"Image not found\" in the admin).\n\t\tconst repeaterFields = collectionInfo.fields.filter(\n\t\t\t(f) => f.type === \"repeater\" && Array.isArray(f.validation?.subFields),\n\t\t);\n\t\tconst blockFields = includeBlocks\n\t\t\t? collectionInfo.fields.filter((field) => field.type === \"blocks\")\n\t\t\t: [];\n\t\tif (imageFields.length === 0 && repeaterFields.length === 0 && blockFields.length === 0) {\n\t\t\treturn result;\n\t\t}\n\n\t\tconst getProvider = (id: string) => this.getMediaProvider(id);\n\n\t\tfor (const field of imageFields) {\n\t\t\tconst value = result[field.slug];\n\t\t\tif (value == null) continue;\n\n\t\t\ttry {\n\t\t\t\t// Only image fields carry a dark variant.\n\t\t\t\tconst normalized =\n\t\t\t\t\tfield.type === \"image\"\n\t\t\t\t\t\t? await normalizeImageValue(value, getProvider)\n\t\t\t\t\t\t: await normalizeMediaValue(value, getProvider);\n\t\t\t\tif (normalized) {\n\t\t\t\t\tresult[field.slug] = normalized;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Don't fail the save if normalization fails for a single field\n\t\t\t}\n\t\t}\n\n\t\tfor (const field of repeaterFields) {\n\t\t\tconst value = result[field.slug];\n\t\t\tif (!Array.isArray(value)) continue;\n\n\t\t\tconst mediaSubFieldSlugs = (field.validation?.subFields ?? [])\n\t\t\t\t.filter((sub) => sub.type === \"image\")\n\t\t\t\t.map((sub) => sub.slug);\n\t\t\tif (mediaSubFieldSlugs.length === 0) continue;\n\n\t\t\tconst items: unknown[] = value;\n\t\t\tresult[field.slug] = await Promise.all(\n\t\t\t\titems.map(async (item) => {\n\t\t\t\t\tif (!isRecord(item)) return item;\n\t\t\t\t\tconst normalizedItem: Record<string, unknown> = { ...item };\n\t\t\t\t\tfor (const slug of mediaSubFieldSlugs) {\n\t\t\t\t\t\tconst subValue = normalizedItem[slug];\n\t\t\t\t\t\tif (subValue == null) continue;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst normalized = await normalizeImageValue(subValue, getProvider);\n\t\t\t\t\t\t\tif (normalized) {\n\t\t\t\t\t\t\t\tnormalizedItem[slug] = normalized;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Don't fail the save if normalization fails for a single sub-field\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn normalizedItem;\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (blockFields.length > 0) {\n\t\t\tconst blockTypes = preloadedBlockTypes ?? (await resolveBlockTypes(this.db));\n\t\t\tfor (const field of blockFields) {\n\t\t\t\tconst value = result[field.slug];\n\t\t\t\tif (!Array.isArray(value)) continue;\n\t\t\t\tresult[field.slug] = await Promise.all(\n\t\t\t\t\tvalue.map(async (block) => {\n\t\t\t\t\t\tif (!isRecord(block) || typeof block._type !== \"string\") return block;\n\t\t\t\t\t\tconst type = blockTypes.get(block._type);\n\t\t\t\t\t\tconst version = type?.versions.find(\n\t\t\t\t\t\t\t(candidate) => candidate.version === block._version,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!version || version.unsupportedTypes?.length) return block;\n\t\t\t\t\t\tconst normalizedBlock: Record<string, unknown> = { ...block };\n\t\t\t\t\t\tfor (const nestedField of version.fields) {\n\t\t\t\t\t\t\tconst nestedValue = normalizedBlock[nestedField.slug];\n\t\t\t\t\t\t\tif (nestedValue == null) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tif (nestedField.type === \"image\") {\n\t\t\t\t\t\t\t\t\tconst normalized = await normalizeImageValue(nestedValue, getProvider);\n\t\t\t\t\t\t\t\t\tif (normalized) normalizedBlock[nestedField.slug] = normalized;\n\t\t\t\t\t\t\t\t} else if (nestedField.type === \"file\") {\n\t\t\t\t\t\t\t\t\tconst normalized = await normalizeMediaValue(nestedValue, getProvider);\n\t\t\t\t\t\t\t\t\tif (normalized) normalizedBlock[nestedField.slug] = normalized;\n\t\t\t\t\t\t\t\t} else if (nestedField.type === \"repeater\" && Array.isArray(nestedValue)) {\n\t\t\t\t\t\t\t\t\tconst imageSlugs = (nestedField.validation?.subFields ?? [])\n\t\t\t\t\t\t\t\t\t\t.filter((subField) => subField.type === \"image\")\n\t\t\t\t\t\t\t\t\t\t.map((subField) => subField.slug);\n\t\t\t\t\t\t\t\t\tnormalizedBlock[nestedField.slug] = await Promise.all(\n\t\t\t\t\t\t\t\t\t\tnestedValue.map(async (item) => {\n\t\t\t\t\t\t\t\t\t\t\tif (!isRecord(item)) return item;\n\t\t\t\t\t\t\t\t\t\t\tconst normalizedItem = { ...item };\n\t\t\t\t\t\t\t\t\t\t\tfor (const slug of imageSlugs) {\n\t\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\t\tconst normalized = await normalizeImageValue(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnormalizedItem[slug],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetProvider,\n\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (normalized) normalizedItem[slug] = normalized;\n\t\t\t\t\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\treturn normalizedItem;\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn normalizedBlock;\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate runAfterSaveHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t\tactor?: ActorInfo,\n\t\texcludePluginId?: string,\n\t): void {\n\t\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(\"content:afterSave\")) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.hooks.runContentAfterSave(content, collection, isNew, actor, excludePluginId);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"EmDash afterSave hook error:\", err);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate runAfterDeleteHooks(id: string, collection: string, permanent: boolean): void {\n\t\tafter(async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(\"content:afterDelete\")) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.hooks.runContentAfterDelete(id, collection, permanent);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"EmDash afterDelete hook error:\", err);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate runDeferredContentHook(\n\t\tname:\n\t\t\t| \"content:afterPublish\"\n\t\t\t| \"content:afterUnpublish\"\n\t\t\t| \"content:afterRestore\"\n\t\t\t| \"content:afterSchedule\"\n\t\t\t| \"content:afterUnschedule\",\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tafterPluginId?: string,\n\t\tpluginInvocationId?: string,\n\t): void {\n\t\tconst label = name.slice(\"content:\".length);\n\n\t\tconst invoke = async () => {\n\t\t\t// Trusted plugins\n\t\t\tif (this.hooks.hasHooks(name)) {\n\t\t\t\ttry {\n\t\t\t\t\tswitch (name) {\n\t\t\t\t\t\tcase \"content:afterPublish\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterPublish(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterUnpublish\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterUnpublish(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterRestore\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterRestore(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterSchedule\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterSchedule(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"content:afterUnschedule\":\n\t\t\t\t\t\t\tawait this.hooks.runContentAfterUnschedule(content, collection);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(`EmDash ${label} hook error:`, err);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tconst contentId = typeof content.id === \"string\" ? content.id : undefined;\n\t\tconst guardKey =\n\t\t\tafterPluginId && contentId ? this.pluginContentActionKey(collection, contentId) : undefined;\n\t\tconst invokeWithGuard = async () => {\n\t\t\tif (guardKey) this.retainPluginContentAction(guardKey);\n\t\t\ttry {\n\t\t\t\tawait invoke();\n\t\t\t} finally {\n\t\t\t\tif (guardKey) this.releasePluginContentAction(guardKey);\n\t\t\t}\n\t\t};\n\t\tif (afterPluginId && pluginInvocationId && this.findSandboxedPlugin(afterPluginId)) {\n\t\t\tconst key = this.pluginInvocationKey(afterPluginId, pluginInvocationId);\n\t\t\tif (!this.activePluginInvocations.has(key) || this.releasedPluginInvocations.has(key)) {\n\t\t\t\tafter(invokeWithGuard);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst pending = this.pendingPluginAfterHooks.get(key) ?? [];\n\t\t\tpending.push(invokeWithGuard);\n\t\t\tthis.pendingPluginAfterHooks.set(key, pending);\n\t\t\treturn;\n\t\t}\n\t\tafter(invokeWithGuard);\n\t}\n\n\tprivate pluginInvocationKey(pluginId: string, invocationId: string): string {\n\t\treturn JSON.stringify([pluginId, invocationId]);\n\t}\n\n\tprivate beginPluginInvocation(\n\t\tpluginId: string,\n\t\tinvocationId: string,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): void {\n\t\tconst key = this.pluginInvocationKey(pluginId, invocationId);\n\t\tthis.pendingPluginAfterHooks.delete(key);\n\t\tthis.releasedPluginInvocations.delete(key);\n\t\tif (invalidateContentCache)\n\t\t\tthis.pluginInvocationCacheInvalidators.set(key, invalidateContentCache);\n\t\telse this.pluginInvocationCacheInvalidators.delete(key);\n\t\tthis.activePluginInvocations.add(key);\n\t}\n\n\tprivate flushPluginAfterHooks(\n\t\tpluginId: string,\n\t\tinvocationId?: string,\n\t\tfinal = true,\n\t): Promise<void> {\n\t\tif (!invocationId) return Promise.resolve();\n\t\tconst key = this.pluginInvocationKey(pluginId, invocationId);\n\t\tthis.releasedPluginInvocations.add(key);\n\t\tconst pending = this.pendingPluginAfterHooks.get(key) ?? [];\n\t\tthis.pendingPluginAfterHooks.delete(key);\n\t\tfor (const invoke of pending) after(invoke);\n\t\tif (final) {\n\t\t\tthis.releasedPluginInvocations.delete(key);\n\t\t\tthis.activePluginInvocations.delete(key);\n\t\t\tthis.pluginInvocationCacheInvalidators.delete(key);\n\t\t} else {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.releasedPluginInvocations.delete(key);\n\t\t\t\tthis.activePluginInvocations.delete(key);\n\t\t\t\tthis.pluginInvocationCacheInvalidators.delete(key);\n\t\t\t}, PLUGIN_INVOCATION_RELEASE_GRACE_MS);\n\t\t\tif (typeof timer === \"object\") timer.unref();\n\t\t}\n\t\treturn Promise.resolve();\n\t}\n\n\tprivate runAfterPublishHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tafterPluginId?: string,\n\t\tpluginInvocationId?: string,\n\t): void {\n\t\tthis.runDeferredContentHook(\n\t\t\t\"content:afterPublish\",\n\t\t\tcontent,\n\t\t\tcollection,\n\t\t\tafterPluginId,\n\t\t\tpluginInvocationId,\n\t\t);\n\t}\n\n\tprivate runAfterUnpublishHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tafterPluginId?: string,\n\t\tpluginInvocationId?: string,\n\t): void {\n\t\tthis.runDeferredContentHook(\n\t\t\t\"content:afterUnpublish\",\n\t\t\tcontent,\n\t\t\tcollection,\n\t\t\tafterPluginId,\n\t\t\tpluginInvocationId,\n\t\t);\n\t}\n\n\tprivate runAfterRestoreHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tafterPluginId?: string,\n\t\tpluginInvocationId?: string,\n\t): void {\n\t\tthis.runDeferredContentHook(\n\t\t\t\"content:afterRestore\",\n\t\t\tcontent,\n\t\t\tcollection,\n\t\t\tafterPluginId,\n\t\t\tpluginInvocationId,\n\t\t);\n\t}\n\n\tprivate runAfterScheduleHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tafterPluginId?: string,\n\t\tpluginInvocationId?: string,\n\t): void {\n\t\tthis.runDeferredContentHook(\n\t\t\t\"content:afterSchedule\",\n\t\t\tcontent,\n\t\t\tcollection,\n\t\t\tafterPluginId,\n\t\t\tpluginInvocationId,\n\t\t);\n\t}\n\n\tprivate runAfterUnscheduleHooks(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tafterPluginId?: string,\n\t\tpluginInvocationId?: string,\n\t): void {\n\t\tthis.runDeferredContentHook(\n\t\t\t\"content:afterUnschedule\",\n\t\t\tcontent,\n\t\t\tcollection,\n\t\t\tafterPluginId,\n\t\t\tpluginInvocationId,\n\t\t);\n\t}\n\n\tprivate async handleSandboxedRoute(\n\t\tplugin: SandboxedPluginInstance,\n\t\tpath: string,\n\t\trequest: Request,\n\t\tbody: unknown,\n\t\trouteMeta: RouteMeta,\n\t\tuser?: UserInfo,\n\t\tui?: PluginUiContext,\n\t\tinvalidateContentCache?: PluginContentCacheInvalidator,\n\t): Promise<{\n\t\tsuccess: boolean;\n\t\tdata?: unknown;\n\t\terror?: { code: string; message: string };\n\t\tstatus?: number;\n\t}> {\n\t\tconst routeName = path.replace(LEADING_SLASH_PATTERN, \"\");\n\n\t\ttry {\n\t\t\tconst declaredHeaders = routeMeta.request ? (routeMeta.request.headers ?? []) : undefined;\n\t\t\tconst headers = sanitizeHeadersForSandbox(request.headers, declaredHeaders);\n\t\t\tconst meta = extractRequestMeta(request, this.config);\n\t\t\tconst result = await plugin.invokeRoute(\n\t\t\t\trouteName,\n\t\t\t\tbody,\n\t\t\t\t{\n\t\t\t\t\turl: request.url,\n\t\t\t\t\tmethod: request.method,\n\t\t\t\t\theaders,\n\t\t\t\t\tmeta,\n\t\t\t\t\tuser,\n\t\t\t\t\tui,\n\t\t\t\t},\n\t\t\t\t{ invalidateContentCache },\n\t\t\t);\n\t\t\treturn { success: true, data: result };\n\t\t} catch (error) {\n\t\t\tif (error instanceof PluginRouteRequestError) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\tstatus: error.status,\n\t\t\t\t\terror: { code: \"INVALID_PLUGIN_REQUEST\", message: error.message },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconsole.error(`EmDash: Sandboxed plugin route error:`, error);\n\t\t\tconst sandboxRouteError = getSandboxRouteErrorDetails(error);\n\t\t\tif (sandboxRouteError) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\tstatus: sandboxRouteError.status,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: sandboxRouteError.code,\n\t\t\t\t\t\tmessage: sandboxRouteError.message,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ROUTE_ERROR\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Public Page Contributions\n\t// =========================================================================\n\n\t/**\n\t * Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext\n\t * object so results are collected once per page context per request, even when\n\t * multiple render components (EmDashHead, EmDashBodyStart, EmDashBodyEnd)\n\t * request contributions from the same page.\n\t */\n\tprivate pageContributionCache = new WeakMap<PublicPageContext, Promise<PageContributions>>();\n\n\t/**\n\t * Collect all page contributions (metadata + fragments) in a single pass.\n\t * Results are cached by page context object identity.\n\t */\n\tasync collectPageContributions(page: PublicPageContext): Promise<PageContributions> {\n\t\tconst cached = this.pageContributionCache.get(page);\n\t\tif (cached) return cached;\n\n\t\tconst promise = this.doCollectPageContributions(page);\n\t\tthis.pageContributionCache.set(page, promise);\n\t\treturn promise;\n\t}\n\n\tprivate async doCollectPageContributions(page: PublicPageContext): Promise<PageContributions> {\n\t\tconst metadata: PageMetadataContribution[] = [];\n\t\tconst fragments: PageFragmentContribution[] = [];\n\n\t\t// Trusted plugins via HookPipeline — both metadata and fragments\n\t\tif (this.hooks.hasHooks(\"page:metadata\")) {\n\t\t\tconst results = await this.hooks.runPageMetadata({ page });\n\t\t\tfor (const r of results) {\n\t\t\t\tmetadata.push(...r.contributions);\n\t\t\t}\n\t\t}\n\n\t\tif (this.hooks.hasHooks(\"page:fragments\")) {\n\t\t\tconst results = await this.hooks.runPageFragments({ page });\n\t\t\tfor (const r of results) {\n\t\t\t\tfragments.push(...r.contributions);\n\t\t\t}\n\t\t}\n\n\t\treturn { metadata, fragments };\n\t}\n\n\t/**\n\t * Collect page metadata contributions from trusted and sandboxed plugins.\n\t * Delegates to the single-pass collector and returns the metadata portion.\n\t */\n\tasync collectPageMetadata(page: PublicPageContext): Promise<PageMetadataContribution[]> {\n\t\tconst { metadata } = await this.collectPageContributions(page);\n\t\treturn metadata;\n\t}\n\n\t/**\n\t * Collect page fragment contributions from trusted plugins only.\n\t * Delegates to the single-pass collector and returns the fragments portion.\n\t */\n\tasync collectPageFragments(page: PublicPageContext): Promise<PageFragmentContribution[]> {\n\t\tconst { fragments } = await this.collectPageContributions(page);\n\t\treturn fragments;\n\t}\n\n\tprivate isPluginEnabled(pluginId: string): boolean {\n\t\tconst status = this.pluginStates.get(pluginId);\n\t\treturn status === undefined || status === \"active\";\n\t}\n}\n\n/**\n * Normalize an image field value together with its `darkVariant`. The media\n * normalizer only keeps the media item's own keys, so the variant is normalized\n * separately and reattached.\n */\nasync function normalizeImageValue(\n\tvalue: unknown,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<ImageValue | null> {\n\tconst primary = await normalizePrimaryImageValue(value, getProvider);\n\tif (!primary || !isRecord(value) || value.darkVariant == null) return primary;\n\tconst darkVariant = await normalizeMediaValue(value.darkVariant, getProvider);\n\treturn darkVariant ? { ...primary, darkVariant } : primary;\n}\n\n/**\n * Normalize the primary image of an image field value.\n *\n * A legacy string URL that the admin upgraded to `{ id: \"\", src: url }` so it\n * can carry a dark variant still has to normalize as a string: as an object it\n * counts as local media, which strips `src` and leaves nothing behind. The\n * upgrade outlives the variant — an editor can add one and remove it again —\n * so the shape decides, not the presence of `darkVariant`.\n */\nasync function normalizePrimaryImageValue(\n\tvalue: unknown,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<ImageValue | null> {\n\tif (isRecord(value) && !value.id && typeof value.src === \"string\" && !value.provider) {\n\t\treturn normalizeMediaValue(value.src, getProvider);\n\t}\n\treturn normalizeMediaValue(value, getProvider);\n}\n"],"mappings":"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"}