@michaelthielemann/kestrel 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +35 -110
  2. package/kestrel.config.ts +1 -1
  3. package/layers/access/server/middleware/00.ip-allowlist.ts +3 -18
  4. package/layers/access/server/middleware/access-guard.ts +25 -23
  5. package/layers/access/server/types.d.ts +1 -2
  6. package/layers/admin/app/components/AdminAccount.vue +1 -1
  7. package/layers/admin/app/components/BlockFields.vue +1 -2
  8. package/layers/admin/app/components/BlockPreview.vue +17 -5
  9. package/layers/admin/app/components/BlocksBody.vue +1 -1
  10. package/layers/admin/app/components/CollectionEditor.vue +35 -16
  11. package/layers/admin/app/components/CollectionList.vue +34 -3
  12. package/layers/admin/app/components/CollectionListBulkBar.vue +17 -3
  13. package/layers/admin/app/components/CollectionListFilterPanel.vue +1 -1
  14. package/layers/admin/app/components/CollectionListPager.vue +1 -1
  15. package/layers/admin/app/components/CollectionListTable.vue +41 -2
  16. package/layers/admin/app/components/EditorStatus.vue +8 -7
  17. package/layers/admin/app/components/LocaleBar.vue +3 -1
  18. package/layers/admin/app/components/PageFields.vue +2 -4
  19. package/layers/admin/app/components/SeoFields.vue +2 -4
  20. package/layers/admin/app/components/SingletonEditor.vue +3 -2
  21. package/layers/admin/app/composables/useAuth.ts +3 -3
  22. package/layers/admin/app/composables/useBlockTree.ts +1 -1
  23. package/layers/admin/app/composables/useBlocks.ts +1 -1
  24. package/layers/admin/app/composables/useCollectionOps.ts +45 -8
  25. package/layers/admin/app/composables/useCollections.ts +1 -2
  26. package/layers/admin/app/composables/useEditForm.ts +35 -17
  27. package/layers/admin/app/composables/useLinkResolver.ts +1 -1
  28. package/layers/admin/app/composables/useListBatchActions.ts +20 -3
  29. package/layers/admin/app/composables/useListColumns.ts +1 -1
  30. package/layers/admin/app/composables/useListFilterDraft.ts +3 -2
  31. package/layers/admin/app/composables/useListRows.ts +7 -2
  32. package/layers/admin/app/composables/useListUrlState.ts +1 -1
  33. package/layers/admin/app/composables/usePublishStatus.ts +2 -2
  34. package/layers/admin/app/layouts/admin.vue +0 -1
  35. package/layers/admin/app/pages/admin/[collection]/[id].vue +5 -4
  36. package/layers/admin/app/pages/admin/index.vue +1 -1
  37. package/layers/admin/app/pages/admin/references.vue +2 -2
  38. package/layers/admin/app/utils/block-tree.ts +1 -1
  39. package/layers/admin/app/utils/collection-ops.ts +28 -3
  40. package/layers/admin/app/utils/edit-form.ts +34 -2
  41. package/layers/admin/app/utils/editor-expose.ts +2 -0
  42. package/layers/admin/app/utils/editor-form-context.ts +1 -2
  43. package/layers/admin/app/utils/field-keys.ts +1 -1
  44. package/layers/admin/app/utils/list-columns.ts +3 -2
  45. package/layers/admin/app/utils/list-query.ts +2 -2
  46. package/layers/admin/app/utils/populate-blocks.ts +2 -2
  47. package/layers/admin/app/utils/reauth.ts +5 -6
  48. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +7 -0
  49. package/layers/collections/server/plugins/01.register.ts +9 -12
  50. package/layers/collections/server/plugins/02.register-relation-populate.ts +5 -7
  51. package/layers/{fields → core}/app/utils/field-factories.ts +1 -1
  52. package/layers/core/modules/auto-discovery/extract-block.ts +3 -3
  53. package/layers/core/modules/auto-discovery/index.ts +68 -11
  54. package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
  55. package/layers/core/modules/auto-discovery/scan.ts +77 -0
  56. package/layers/core/modules/auto-discovery/virtual.d.ts +5 -0
  57. package/layers/core/modules/component-namespace/index.ts +24 -0
  58. package/layers/core/modules/kestrel/index.ts +6 -1
  59. package/layers/core/modules/plugin-order/index.ts +69 -0
  60. package/layers/core/modules/plugin-order/plugin-order.ts +273 -0
  61. package/layers/core/nuxt.config.ts +1 -0
  62. package/layers/core/server/api/[...path].ts +98 -0
  63. package/layers/core/server/plugins/00.config.ts +12 -0
  64. package/layers/core/server/plugins/00.migrate.ts +1 -0
  65. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +5 -0
  66. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +5 -0
  67. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +5 -0
  68. package/layers/core/server/plugins/02.schema-sync.ts +2 -7
  69. package/layers/core/server/plugins/04.outbox-worker.ts +31 -0
  70. package/layers/core/server/plugins/05.reindex-refs.ts +5 -0
  71. package/layers/core/server/routes/__kestrel/dashboard.get.ts +29 -0
  72. package/layers/core/server/tasks/db/migrate-module.ts +83 -0
  73. package/layers/core/server/tasks/db/migrate-revisions.ts +20 -0
  74. package/layers/core/server/tasks/db/migrate.ts +2 -6
  75. package/layers/core/server/utils/server-config.ts +66 -3
  76. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +7 -0
  77. package/layers/fields/server/plugins/01.register-field-populate.ts +2 -2
  78. package/layers/media/app/components/KestrelImg.vue +3 -3
  79. package/layers/media/app/components/MediaLibrary.vue +2 -2
  80. package/layers/media/app/components/field/Media.vue +1 -1
  81. package/layers/media/app/composables/useMediaResolver.ts +1 -1
  82. package/layers/media/app/composables/useMediaUpload.ts +1 -1
  83. package/layers/media/app/composables/useMediaVariant.ts +2 -2
  84. package/layers/media/app/utils/ai-disclosure.ts +1 -1
  85. package/layers/media/app/utils/library.ts +1 -1
  86. package/layers/media/app/utils/ops.ts +1 -3
  87. package/layers/media/app/utils/picture-model.ts +2 -2
  88. package/layers/media/app/utils/variant-name.ts +1 -1
  89. package/layers/media/server/middleware/ondemand-variants.ts +6 -5
  90. package/layers/media/server/plugins/01.register-media-pipelines.ts +7 -0
  91. package/layers/media/server/plugins/02.register-media.ts +10 -7
  92. package/layers/media/server/plugins/04.variant-capture.ts +12 -8
  93. package/layers/media/server/plugins/05.media-cleanup.ts +5 -0
  94. package/layers/media/server/tasks/media/backfill.ts +3 -4
  95. package/layers/public/app/components/KestrelLink.vue +1 -1
  96. package/layers/public/app/pages/[...slug].vue +1 -1
  97. package/layers/public/app/pages/__kestrel/preview.vue +2 -2
  98. package/layers/public/app/utils/json-ld.ts +1 -1
  99. package/layers/public/app/utils/link-href.ts +1 -1
  100. package/layers/public/app/utils/page-head.ts +1 -1
  101. package/layers/public/modules/deploy-output/index.ts +4 -6
  102. package/layers/public/modules/prerender-routes/index.ts +4 -4
  103. package/layers/public/modules/prerender-routes/routes.ts +1 -1
  104. package/layers/public/modules/prune-media/index.ts +2 -2
  105. package/layers/public/server/delivery-live/serve.ts +111 -0
  106. package/layers/public/server/middleware/delivery-live-catchall.ts +7 -0
  107. package/layers/public/server/pipelines/index.ts +23 -0
  108. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +14 -0
  109. package/layers/public/server/plugins/01.register-public-pipelines.ts +7 -0
  110. package/layers/public/server/plugins/02.register-links.ts +2 -3
  111. package/layers/public/server/plugins/03.redirects.ts +41 -23
  112. package/layers/public/server/plugins/05.plan-publish.ts +5 -0
  113. package/layers/public/server/plugins/zz.publish.ts +79 -24
  114. package/layers/public/server/routes/llms-full.txt.get.ts +5 -5
  115. package/layers/public/server/routes/llms.txt.get.ts +4 -2
  116. package/layers/public/server/routes/redirects.json.get.ts +2 -2
  117. package/layers/public/server/routes/robots.txt.get.ts +2 -0
  118. package/layers/public/server/routes/sitemap.xml.get.ts +16 -6
  119. package/layers/public/server/tasks/publish/run.ts +9 -4
  120. package/layers/public/server/utils/publish/render-live.ts +26 -0
  121. package/layers/ui/app/components/field/Choice.vue +1 -1
  122. package/layers/ui/app/components/field/Datetime.vue +1 -1
  123. package/layers/ui/app/components/field/Json.vue +1 -2
  124. package/layers/ui/app/components/field/Layout.vue +2 -4
  125. package/layers/ui/app/components/field/Link.vue +1 -1
  126. package/layers/ui/app/components/field/LinkInternalPicker.vue +3 -3
  127. package/layers/ui/app/components/field/Number.vue +2 -2
  128. package/layers/ui/app/components/field/Relation.vue +1 -1
  129. package/layers/ui/app/components/field/Repeater.vue +1 -3
  130. package/layers/ui/app/components/field/Slug.vue +2 -3
  131. package/layers/ui/app/components/field/Text.vue +1 -1
  132. package/layers/ui/app/components/ui/Richtext.vue +1 -1
  133. package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
  134. package/layers/ui/app/composables/useLinkField.ts +1 -1
  135. package/layers/ui/app/composables/useRepeater.ts +1 -1
  136. package/layers/ui/app/i18n/de.ts +7 -7
  137. package/layers/ui/app/i18n/en.ts +7 -39
  138. package/layers/ui/app/utils/field-component.ts +1 -1
  139. package/layers/ui/app/utils/field-registry.ts +1 -1
  140. package/layers/ui/app/utils/icons.ts +2 -6
  141. package/layers/ui/app/utils/localized.ts +1 -1
  142. package/nuxt.config.ts +1 -1
  143. package/package.json +53 -19
  144. package/scripts/consumer-template-ci.mjs +157 -0
  145. package/scripts/coverage-all.mjs +163 -0
  146. package/scripts/dashboard.mjs +89 -0
  147. package/scripts/hash-password.mjs +1 -1
  148. package/scripts/lib/coverage-model.mjs +51 -0
  149. package/scripts/port-graph.mjs +350 -0
  150. package/scripts/typecheck.mjs +11 -0
  151. package/templates/starter/app/blocks/Prose.vue +1 -1
  152. package/layers/access/server/utils/csrf.ts +0 -39
  153. package/layers/access/server/utils/grant-registry.ts +0 -36
  154. package/layers/access/server/utils/guard.ts +0 -44
  155. package/layers/access/server/utils/ip-allowlist.ts +0 -79
  156. package/layers/access/server/utils/policy.ts +0 -98
  157. package/layers/access/server/utils/public-resources.ts +0 -11
  158. package/layers/access/server/utils/render-context.ts +0 -41
  159. package/layers/access/server/utils/require-admin.ts +0 -15
  160. package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
  161. package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
  162. package/layers/auth/server/api/auth/login.post.ts +0 -38
  163. package/layers/auth/server/api/auth/logout.post.ts +0 -9
  164. package/layers/auth/server/api/auth/session.get.ts +0 -1
  165. package/layers/auth/server/utils/admin-credential.ts +0 -42
  166. package/layers/auth/server/utils/client-ip.ts +0 -88
  167. package/layers/auth/server/utils/login-throttle.ts +0 -126
  168. package/layers/auth/server/utils/password.ts +0 -52
  169. package/layers/auth/server/utils/session-cookie.ts +0 -44
  170. package/layers/auth/server/utils/session-epoch.ts +0 -96
  171. package/layers/auth/server/utils/session.ts +0 -88
  172. package/layers/collections/server/collections/pages.ts +0 -19
  173. package/layers/collections/server/utils/populate-relations.ts +0 -79
  174. package/layers/core/app/utils/bulk-actions.ts +0 -9
  175. package/layers/core/app/utils/filter-ops.ts +0 -98
  176. package/layers/core/app/utils/list-limits.ts +0 -27
  177. package/layers/core/app/utils/locale-path.ts +0 -12
  178. package/layers/core/app/utils/slugify.ts +0 -16
  179. package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
  180. package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
  181. package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
  182. package/layers/core/server/api/[collection]/[id].get.ts +0 -11
  183. package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
  184. package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
  185. package/layers/core/server/api/[collection]/index.get.ts +0 -24
  186. package/layers/core/server/api/[collection]/index.post.ts +0 -8
  187. package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
  188. package/layers/core/server/api/[collection]/index.put.ts +0 -24
  189. package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
  190. package/layers/core/server/api/[collection]/options.get.ts +0 -18
  191. package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
  192. package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
  193. package/layers/core/server/api/[collection]/translations.get.ts +0 -25
  194. package/layers/core/server/api/collections/[name].get.ts +0 -4
  195. package/layers/core/server/api/collections/index.get.ts +0 -3
  196. package/layers/core/server/api/references/broken.get.ts +0 -9
  197. package/layers/core/server/api/references/referrers.get.ts +0 -24
  198. package/layers/core/server/database/record-refs.ts +0 -20
  199. package/layers/core/server/plugins/03.record-refs.ts +0 -12
  200. package/layers/core/server/schema/bootstrap.ts +0 -41
  201. package/layers/core/server/schema/desired.ts +0 -99
  202. package/layers/core/server/schema/dev-sync.ts +0 -23
  203. package/layers/core/server/schema/dialect.ts +0 -61
  204. package/layers/core/server/schema/diff.ts +0 -110
  205. package/layers/core/server/schema/introspect.ts +0 -68
  206. package/layers/core/server/schema/model.ts +0 -61
  207. package/layers/core/server/schema/render-sqlite.ts +0 -89
  208. package/layers/core/server/schema/sync.ts +0 -201
  209. package/layers/core/server/utils/blocks.ts +0 -6
  210. package/layers/core/server/utils/collection-types.ts +0 -28
  211. package/layers/core/server/utils/crud.ts +0 -578
  212. package/layers/core/server/utils/db.ts +0 -26
  213. package/layers/core/server/utils/defineCollection.ts +0 -213
  214. package/layers/core/server/utils/duplicate.ts +0 -162
  215. package/layers/core/server/utils/field-layout.ts +0 -125
  216. package/layers/core/server/utils/filter-predicate.ts +0 -101
  217. package/layers/core/server/utils/http.ts +0 -50
  218. package/layers/core/server/utils/if-unmodified.ts +0 -13
  219. package/layers/core/server/utils/kestrel-config.ts +0 -451
  220. package/layers/core/server/utils/key-lock.ts +0 -33
  221. package/layers/core/server/utils/locale.ts +0 -53
  222. package/layers/core/server/utils/page-route.ts +0 -58
  223. package/layers/core/server/utils/page-slug.ts +0 -127
  224. package/layers/core/server/utils/picker.ts +0 -82
  225. package/layers/core/server/utils/populate.ts +0 -50
  226. package/layers/core/server/utils/prerender-memo.ts +0 -23
  227. package/layers/core/server/utils/read-capture.ts +0 -37
  228. package/layers/core/server/utils/record-ref-index.ts +0 -200
  229. package/layers/core/server/utils/registry.ts +0 -24
  230. package/layers/core/server/utils/resolve-scope.ts +0 -79
  231. package/layers/core/server/utils/seo.ts +0 -42
  232. package/layers/core/server/utils/serialize-collection.ts +0 -147
  233. package/layers/core/server/utils/sql.ts +0 -2
  234. package/layers/core/server/utils/storage.local.ts +0 -117
  235. package/layers/core/server/utils/storage.s3.ts +0 -197
  236. package/layers/core/server/utils/storage.ts +0 -90
  237. package/layers/core/server/utils/write-effects.ts +0 -40
  238. package/layers/core/server/utils/write-events.ts +0 -31
  239. package/layers/fields/app/utils/condition.ts +0 -71
  240. package/layers/fields/app/utils/define-block.ts +0 -13
  241. package/layers/fields/app/utils/field-constraints.ts +0 -17
  242. package/layers/fields/app/utils/field-meta.ts +0 -33
  243. package/layers/fields/app/utils/field-validate.ts +0 -75
  244. package/layers/fields/app/utils/field-value.ts +0 -7
  245. package/layers/fields/app/utils/richtext-links.ts +0 -48
  246. package/layers/fields/server/api/blocks/index.get.ts +0 -9
  247. package/layers/fields/server/field-registry/index.ts +0 -249
  248. package/layers/fields/server/field-registry/naming.ts +0 -10
  249. package/layers/fields/server/field-registry/sanitize.ts +0 -43
  250. package/layers/fields/server/field-registry/types.ts +0 -19
  251. package/layers/fields/server/utils/block-ids.ts +0 -43
  252. package/layers/fields/server/utils/block-populate.ts +0 -41
  253. package/layers/fields/server/utils/buildCollection.ts +0 -90
  254. package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
  255. package/layers/fields/server/utils/buildTable.ts +0 -113
  256. package/layers/fields/server/utils/conditional-required.ts +0 -25
  257. package/layers/fields/server/utils/defineBlock.ts +0 -86
  258. package/layers/fields/server/utils/defineFieldType.ts +0 -33
  259. package/layers/fields/server/utils/extract-refs.ts +0 -152
  260. package/layers/fields/server/utils/field-populate.ts +0 -68
  261. package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
  262. package/layers/media/server/api/media/[id].delete.ts +0 -18
  263. package/layers/media/server/api/media/[id].get.ts +0 -10
  264. package/layers/media/server/api/media/[id].patch.test.ts +0 -66
  265. package/layers/media/server/api/media/[id].patch.ts +0 -65
  266. package/layers/media/server/api/media/backfill.post.ts +0 -18
  267. package/layers/media/server/api/media/copy.post.ts +0 -7
  268. package/layers/media/server/api/media/delete.post.ts +0 -13
  269. package/layers/media/server/api/media/folders.post.ts +0 -18
  270. package/layers/media/server/api/media/index.get.ts +0 -15
  271. package/layers/media/server/api/media/index.post.ts +0 -173
  272. package/layers/media/server/api/media/library.get.ts +0 -17
  273. package/layers/media/server/api/media/move.post.ts +0 -7
  274. package/layers/media/server/api/media/rename.post.ts +0 -7
  275. package/layers/media/server/api/media/resolve.get.ts +0 -13
  276. package/layers/media/server/collections/media-settings.ts +0 -25
  277. package/layers/media/server/collections/media.ts +0 -43
  278. package/layers/media/server/database/folders.ts +0 -10
  279. package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
  280. package/layers/media/server/schema-tables/folders.ts +0 -4
  281. package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
  282. package/layers/media/server/utils/ai-signal-detect.ts +0 -155
  283. package/layers/media/server/utils/backfill.ts +0 -144
  284. package/layers/media/server/utils/derive.ts +0 -59
  285. package/layers/media/server/utils/folder-paths.ts +0 -42
  286. package/layers/media/server/utils/folders.ts +0 -12
  287. package/layers/media/server/utils/library.ts +0 -91
  288. package/layers/media/server/utils/media-cleanup.ts +0 -27
  289. package/layers/media/server/utils/media-enabled.ts +0 -23
  290. package/layers/media/server/utils/media-ops.ts +0 -118
  291. package/layers/media/server/utils/media-write.ts +0 -13
  292. package/layers/media/server/utils/naming.ts +0 -52
  293. package/layers/media/server/utils/ondemand.ts +0 -69
  294. package/layers/media/server/utils/persist-upload.ts +0 -62
  295. package/layers/media/server/utils/populate.ts +0 -47
  296. package/layers/media/server/utils/record.ts +0 -71
  297. package/layers/media/server/utils/relocate-ops.ts +0 -341
  298. package/layers/media/server/utils/relocate-plan.ts +0 -63
  299. package/layers/media/server/utils/resolve.ts +0 -98
  300. package/layers/media/server/utils/sanitize-svg.ts +0 -12
  301. package/layers/media/server/utils/sniff.ts +0 -68
  302. package/layers/media/server/utils/storage-relocate.ts +0 -139
  303. package/layers/media/server/utils/translations.ts +0 -19
  304. package/layers/media/server/utils/usages.ts +0 -66
  305. package/layers/media/server/utils/variant-capture.ts +0 -70
  306. package/layers/media/server/utils/variants.ts +0 -123
  307. package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
  308. package/layers/public/server/api/links/resolve.get.ts +0 -19
  309. package/layers/public/server/api/preview.get.ts +0 -28
  310. package/layers/public/server/api/preview.post.ts +0 -93
  311. package/layers/public/server/api/publish-status.get.ts +0 -58
  312. package/layers/public/server/api/publish.post.ts +0 -84
  313. package/layers/public/server/api/route.get.ts +0 -54
  314. package/layers/public/server/collections/redirects.ts +0 -75
  315. package/layers/public/server/collections/site.ts +0 -44
  316. package/layers/public/server/database/publish-deps.ts +0 -12
  317. package/layers/public/server/database/publish-status.ts +0 -19
  318. package/layers/public/server/schema-tables/publish-deps.ts +0 -4
  319. package/layers/public/server/schema-tables/publish-status.ts +0 -4
  320. package/layers/public/server/utils/link-resolve.ts +0 -38
  321. package/layers/public/server/utils/llms-full.ts +0 -125
  322. package/layers/public/server/utils/llms.ts +0 -52
  323. package/layers/public/server/utils/page-resolve.ts +0 -208
  324. package/layers/public/server/utils/populate-links.ts +0 -66
  325. package/layers/public/server/utils/preview-token.ts +0 -109
  326. package/layers/public/server/utils/publish/coalesce.ts +0 -22
  327. package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
  328. package/layers/public/server/utils/publish/deps.ts +0 -87
  329. package/layers/public/server/utils/publish/invalidation.ts +0 -225
  330. package/layers/public/server/utils/publish/pending.ts +0 -74
  331. package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
  332. package/layers/public/server/utils/publish/publish-status.ts +0 -72
  333. package/layers/public/server/utils/publish/publisher.ts +0 -362
  334. package/layers/public/server/utils/publish/queue.ts +0 -67
  335. package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
  336. package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
  337. package/layers/public/server/utils/publish/route-for-record.ts +0 -16
  338. package/layers/public/server/utils/publish/route-keys.ts +0 -13
  339. package/layers/public/server/utils/richtext-markdown.ts +0 -260
  340. package/layers/public/server/utils/site-url.ts +0 -40
  341. package/layers/public/server/utils/sitemap.ts +0 -99
@@ -1,14 +1,9 @@
1
1
  import collections from '#kestrel/collections'
2
2
  import schemaTables from '#kestrel/schema-tables'
3
3
  import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
4
- import type { BuiltCollection } from '../utils/collection-types'
5
- import type { CollectionDef } from '../utils/defineCollection'
4
+ import { describeOp, desiredFromCollections, isDestructive, planOps, runDevSchemaSync, sqlite, useDb } from '@michaelthielemann/kestrel-core'
5
+ import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
6
6
  import { resolveServerKestrel, serverRuntimeConfig } from '../utils/server-config'
7
- import { desiredFromCollections } from '../schema/bootstrap'
8
- import { planOps, describeOp, isDestructive, type SyncDb } from '../schema/sync'
9
- import { sqlite } from '../schema/dialect'
10
- import { runDevSchemaSync } from '../schema/dev-sync'
11
-
12
7
  // Dev-only schema auto-sync (ADR-0002). Runs after 00.migrate, so built-in collections are already
13
8
  // migrated and this is a no-op on them; it additively reconciles the live DB to whatever the registered
14
9
  // collections (+ the discovered standalone tables) now compile to — so adding a collection or a field
@@ -0,0 +1,31 @@
1
+ import { buildOutboxPipelines, makeTicker, registerPipeline, useDb } from '@michaelthielemann/kestrel-core'
2
+ /** Hard-coded to `content` — the only module with an outbox table today; extend alongside
3
+ * `pipelines/outbox.ts` if a second module gets one. */
4
+ const OUTBOX_MODULE = 'content'
5
+ const POLL_INTERVAL_MS = 1000
6
+
7
+ /**
8
+ * The outbox worker: NOT a Nitro `scheduledTask` (those run per-request-worker in a way that doesn't suit
9
+ * an always-on background loop here) — a plain `setInterval` loop started at plugin init, `unref`'d so it
10
+ * never keeps the process alive on its own (a test/dev server can still exit while it's running).
11
+ *
12
+ * Polls every tick, unconditionally — no `PRAGMA data_version` short-circuit: `useDb()` is the process-
13
+ * wide singleton connection every write also goes through, and a connection never observes its OWN writes
14
+ * bumping `data_version` (only a write from a DIFFERENT connection does), so gating on it here would go
15
+ * permanently blind after the first write this same connection ever made. The poll query itself is a
16
+ * covered index search (see `outbox.ts`'s `readPendingOutbox`), cheap enough at a 1s interval to not need
17
+ * a short-circuit at all.
18
+ */
19
+ export default defineNitroPlugin(() => {
20
+ // `makeTicker` takes `useDb` itself (a thunk), not its result — nothing here calls it, so the db is
21
+ // still untouched at plugin-init time; the interval callback is the first thing that ever does.
22
+ const tick = makeTicker(useDb, OUTBOX_MODULE)
23
+
24
+ // Registration only — nothing here resolves a pipeline or reads the collection registry at init.
25
+ for (const def of buildOutboxPipelines()) registerPipeline(def)
26
+
27
+ const timer = setInterval(() => {
28
+ tick().catch((error: unknown) => console.error('[kestrel] outbox worker tick failed', error))
29
+ }, POLL_INTERVAL_MS)
30
+ timer.unref?.()
31
+ })
@@ -0,0 +1,5 @@
1
+ import { registerReindexRefs } from '@michaelthielemann/kestrel-core'
2
+
3
+ export default defineNitroPlugin(() => {
4
+ registerReindexRefs()
5
+ })
@@ -0,0 +1,29 @@
1
+ import { allCollections, buildPipelineIndex, renderDashboard } from '@michaelthielemann/kestrel-core'
2
+ import { PLUGIN_ORDER } from '../../../modules/plugin-order/plugin-order'
3
+
4
+ /**
5
+ * Dev-only introspection dashboard: gathers live registry state, so it reflects the CONSUMER's own
6
+ * collections/pipelines/overrides — not a static analysis of Kestrel's own source. File-based
7
+ * `server/routes/**` modules are compiled into production builds unconditionally, so the
8
+ * `import.meta.dev` check below is the real guard: Nitro inlines it to `false` in production and
9
+ * dead-code-eliminates everything past it, leaving only the 404.
10
+ */
11
+ export default defineEventHandler((event) => {
12
+ if (!import.meta.dev) {
13
+ throw createError({ statusCode: 404, statusMessage: 'Not found' })
14
+ }
15
+ const collections = allCollections().map((c) => ({
16
+ name: c.name,
17
+ mode: c.def.mode,
18
+ translatable: Boolean(c.def.translatable),
19
+ fieldCount: Object.keys(c.def.fields).length,
20
+ }))
21
+ const html = renderDashboard({
22
+ pipelines: buildPipelineIndex(),
23
+ pluginOrder: PLUGIN_ORDER.map((p) => ({ layer: p.layer, file: p.file, after: p.after })),
24
+ collections,
25
+ generatedAt: new Date().toISOString(),
26
+ })
27
+ setHeader(event, 'content-type', 'text/html; charset=utf-8')
28
+ return html
29
+ })
@@ -0,0 +1,83 @@
1
+ import collections from '#kestrel/collections'
2
+ import schemaTables from '#kestrel/schema-tables'
3
+ import moduleManifests from '#kestrel/module-manifests'
4
+ import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
5
+ import type { OwnershipManifest } from '@michaelthielemann/kestrel-contracts'
6
+ import { buildContentManifest, describeOp, desiredFromCollections, isDestructive, opTable, orderManifests, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
7
+ import type { BuiltCollection, CollectionDef, Dialect, SchemaSnapshot, SyncDb } from '@michaelthielemann/kestrel-core'
8
+ import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
9
+ // The per-module counterpart to `db:migrate` (ADR-0012): runs the SAME diff/apply engine, but scoped to
10
+ // one ownership manifest's tables at a time, in a fixed order — `content` (the collection/base schema
11
+ // other modules are conceptually layered on) first, then every discovered module manifest
12
+ // (`module-order.ts`'s `MODULE_MIGRATION_ORDER`), then an `unmanaged` catch-all for any table no
13
+ // manifest yet claims (never silently skipped). Payload/trigger mirror `db:migrate` exactly — {module:
14
+ // "media"}=one module only, otherwise every module in order; {check:true}=dry-run, {force:true}=rebuilds,
15
+ // {drop:[names]}=drop tables. Same dev/prod trigger shape as `db:migrate` (see that file's header).
16
+ type ModulePayload = { module?: string; check?: boolean; force?: boolean; drop?: string[] }
17
+ type ModuleResult =
18
+ | { module: string; check: true; pending: string[]; destructive: string[] }
19
+ | { module: string; applied: string[]; skipped: string[] }
20
+
21
+ function runOne(
22
+ module: string,
23
+ tables: readonly string[],
24
+ client: SyncDb,
25
+ desired: SchemaSnapshot,
26
+ dialect: Dialect,
27
+ p: ModulePayload,
28
+ ): ModuleResult {
29
+ if (p.check === true) {
30
+ const ops = planOps(client, desired, dialect).filter((op) => tables.includes(opTable(op)))
31
+ const destructive = ops.filter(isDestructive).map(describeOp)
32
+ return { module, check: true, pending: dialect.render(ops), destructive }
33
+ }
34
+
35
+ const { applied, skipped } = syncSchema(client, desired, {
36
+ allowDestructive: p.force === true,
37
+ dropTables: Array.isArray(p.drop) ? p.drop : undefined,
38
+ tables,
39
+ }, dialect)
40
+ if (skipped.length) {
41
+ console.warn(`[kestrel] db:migrate-module(${module}) withheld ${skipped.length} destructive change(s) — opt in with {"force":true} (rebuilds) / {"drop":[…]} (drops):\n - ${skipped.map(describeOp).join('\n - ')}`)
42
+ }
43
+ console.info(`[kestrel] db:migrate-module(${module}) applied ${applied.length} statement(s)`)
44
+ return { module, applied, skipped: skipped.map(describeOp) }
45
+ }
46
+
47
+ export default defineTask<ModuleResult[]>({
48
+ meta: {
49
+ name: 'db:migrate-module',
50
+ description: 'Per-module schema migration, ordered (content → media → publishing → unmanaged). {module:"name"}=one module only, otherwise every module in order. {check}/{force}/{drop} match db:migrate.',
51
+ },
52
+ run({ payload }) {
53
+ const p = (payload ?? {}) as ModulePayload
54
+ const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
55
+ toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
56
+ extraTables: schemaTables as AnySQLiteTable[],
57
+ })
58
+ const client = (useDb() as unknown as { $client: SyncDb }).$client
59
+ const dialect = sqlite
60
+
61
+ const manifests = orderManifests([buildContentManifest(), ...(moduleManifests as OwnershipManifest[])])
62
+ const claimed = new Set(manifests.flatMap((m) => m.tables))
63
+ // The `unmanaged` bucket's table list: every table any pending op touches that no manifest claims —
64
+ // computed up front so both `check` and apply modes scope to the SAME set (rather than an unfiltered
65
+ // rescan, which in apply mode would re-touch already-migrated tables and in check mode would re-list
66
+ // every module's pending ops a second time under `unmanaged`).
67
+ const unmanagedTables = [...new Set(planOps(client, desired, dialect).map(opTable))].filter((t) => !claimed.has(t))
68
+
69
+ if (p.module) {
70
+ const manifest = manifests.find((m) => m.module === p.module)
71
+ const tables = manifest ? manifest.tables : p.module === 'unmanaged' ? unmanagedTables : undefined
72
+ if (!tables) {
73
+ const known = [...manifests.map((m) => m.module), 'unmanaged'].join(', ')
74
+ throw new Error(`kestrel: db:migrate-module: unknown module "${p.module}" — known: ${known}`)
75
+ }
76
+ return { result: [runOne(p.module, tables, client, desired, dialect, p)] }
77
+ }
78
+
79
+ const results = manifests.map((m) => runOne(m.module, m.tables, client, desired, dialect, p))
80
+ results.push(runOne('unmanaged', unmanagedTables, client, desired, dialect, p))
81
+ return { result: results }
82
+ },
83
+ })
@@ -0,0 +1,20 @@
1
+ import { migrateRevisions, useDb, type MigrateRevisionsResult } from '@michaelthielemann/kestrel-core'
2
+
3
+ // Thin Nitro wrapper (defineTask is a runtime global) around the core, independently-unit-tested
4
+ // `migrateRevisions` (db/revision-migration.ts) — same sync/task split as db:migrate (schema/sync.ts +
5
+ // tasks/db/migrate.ts). Dev: GET/POST http://localhost:3000/_nitro/tasks/db:migrate-revisions ; prod: call
6
+ // runTask('db:migrate-revisions', { payload }) from inside the process — the built node-server has no task
7
+ // CLI/endpoint. Payload: {force:true} required, or the run refuses.
8
+ export default defineTask<MigrateRevisionsResult[]>({
9
+ meta: {
10
+ name: 'db:migrate-revisions',
11
+ description: 'Seed revision 1 for every existing row with no revision history yet. Requires {"force":true}.',
12
+ },
13
+ run({ payload }) {
14
+ const p = (payload ?? {}) as { force?: boolean }
15
+ const result = migrateRevisions(useDb(), { force: p.force === true })
16
+ const totalSeeded = result.reduce((n, r) => n + r.seeded, 0)
17
+ console.info(`[kestrel] db:migrate-revisions seeded ${totalSeeded} revision(s) across ${result.length} collection(s)`)
18
+ return { result }
19
+ },
20
+ })
@@ -1,13 +1,9 @@
1
1
  import collections from '#kestrel/collections'
2
2
  import schemaTables from '#kestrel/schema-tables'
3
3
  import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
4
- import type { BuiltCollection } from '../../utils/collection-types'
5
- import type { CollectionDef } from '../../utils/defineCollection'
4
+ import { describeOp, desiredFromCollections, isDestructive, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
5
+ import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
6
6
  import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
7
- import { desiredFromCollections } from '../../schema/bootstrap'
8
- import { planOps, syncSchema, isDestructive, describeOp, type SyncDb } from '../../schema/sync'
9
- import { sqlite } from '../../schema/dialect'
10
-
11
7
  // Explicit schema-migrate step for production (ADR-0002). Dev auto-syncs additively at boot
12
8
  // (02.schema-sync); prod boot never auto-DDLs, so an operator runs this task. Triggering (Nuxt 4.4 /
13
9
  // Nitro 2.13 — there is NO `nuxi task run`):
@@ -1,14 +1,14 @@
1
1
  import kestrelConfig from '../../../../kestrel.config'
2
- import { resolveKestrel } from './kestrel-config'
2
+ import { resolveKestrel, type ResolvedKestrel } from '@michaelthielemann/kestrel-core'
3
3
 
4
4
  /**
5
5
  * The Nitro runtimeConfig, or `undefined` for non-Nitro callers (node scripts, build) so they don't
6
6
  * throw on `useRuntimeConfig()`. Server utils prefer the values the kestrel module put here (from the
7
7
  * consumer's `kestrel: {}`), falling back to `resolveServerKestrel()` when it isn't populated.
8
8
  */
9
- export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; public?: Record<string, unknown> } | undefined {
9
+ export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> } | undefined {
10
10
  return typeof useRuntimeConfig === 'function'
11
- ? (useRuntimeConfig() as { kestrel?: Record<string, unknown>; public?: Record<string, unknown> })
11
+ ? (useRuntimeConfig() as { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> })
12
12
  : undefined
13
13
  }
14
14
 
@@ -16,3 +16,66 @@ export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; publ
16
16
  export function resolveServerKestrel(): ReturnType<typeof resolveKestrel> {
17
17
  return resolveKestrel(kestrelConfig, process.env, process.cwd())
18
18
  }
19
+
20
+ /**
21
+ * The full `ResolvedKestrel` shape, merging every server util's own `runtimeConfig ?? resolveServerKestrel()`
22
+ * precedence into ONE call — the boot-time config-wiring plugin (`00.config.ts`) calls this once and
23
+ * pushes the result into the (eventually package-side) config provider, so package code never calls
24
+ * `useRuntimeConfig()` itself. Per-field precedence matches each field's existing reader exactly (the
25
+ * kestrel module writes a CLOSED set of fields into `runtimeConfig.kestrel`/`.public` — see
26
+ * `layers/core/modules/kestrel/index.ts`; anything it doesn't write there falls through to the
27
+ * `resolveServerKestrel()` base for every reader, unchanged from before this function existed).
28
+ */
29
+ export function resolveServerKestrelConfig(): ResolvedKestrel {
30
+ const rc = serverRuntimeConfig()
31
+ const fallback = resolveServerKestrel()
32
+ const srv = (rc?.kestrel ?? {}) as Partial<Record<string, unknown>>
33
+ const pub = rc?.public as { locales?: string[]; primaryLocale?: string; prefixPrimary?: boolean } | undefined
34
+ // `runtimeConfig.media` is a TOP-LEVEL key (sibling to `.kestrel`/`.public`, not nested under `.kestrel`
35
+ // — see `layers/core/modules/kestrel/index.ts`'s `rc.media = {...}`), and it is the ONLY place the S3
36
+ // credentials + every Nitro `NUXT_MEDIA_*`/`NUXT_MEDIA_S3_*` env override (docs/guide/configuration.md) ever
37
+ // land — `resolveKestrel`'s own pure computation never sees them. Merged in wholesale (undefined ⇒ the
38
+ // module never ran ⇒ fall through to the pure fallback), mirroring `output`'s existing merge below.
39
+ const rcMedia = rc?.media as {
40
+ driver?: string
41
+ maxUploadBytes?: number
42
+ allowedMimes?: string
43
+ imagePolicy?: ResolvedKestrel['media']['imagePolicy']
44
+ local?: { dir?: string; baseUrl?: string }
45
+ s3?: { bucket?: string; region?: string; endpoint?: string; prefix?: string; publicBaseUrl?: string; accessKeyId?: string; secretAccessKey?: string; sessionToken?: string }
46
+ } | undefined
47
+ // The locale trio is populated together or not at all (see `locale.ts`'s original `resolved()`) — a
48
+ // partial `runtimeConfig.public` (e.g. `locales` present but `primaryLocale` absent) falls through to
49
+ // the fallback for all three rather than mixing sources mid-trio.
50
+ const locale = pub?.locales?.length && pub.primaryLocale
51
+ ? { supportedLocales: pub.locales, primaryLocale: pub.primaryLocale, prefixPrimary: pub.prefixPrimary === true }
52
+ : { supportedLocales: fallback.supportedLocales, primaryLocale: fallback.primaryLocale, prefixPrimary: fallback.prefixPrimary }
53
+ return {
54
+ ...fallback,
55
+ ...locale,
56
+ dbPath: (srv.dbPath as string | undefined) ?? fallback.dbPath,
57
+ siteUrl: (srv.siteUrl as string | undefined) ?? fallback.siteUrl,
58
+ siteName: (srv.siteName as string | undefined) ?? fallback.siteName,
59
+ siteDescription: (srv.siteDescription as string | undefined) ?? fallback.siteDescription,
60
+ media: rcMedia ? {
61
+ dir: rcMedia.local?.dir ?? fallback.media.dir,
62
+ baseUrl: rcMedia.local?.baseUrl ?? fallback.media.baseUrl,
63
+ driver: (rcMedia.driver as 'local' | 's3' | undefined) ?? fallback.media.driver,
64
+ maxUploadBytes: rcMedia.maxUploadBytes ?? fallback.media.maxUploadBytes,
65
+ allowedMimes: rcMedia.allowedMimes ?? fallback.media.allowedMimes,
66
+ imagePolicy: rcMedia.imagePolicy ?? fallback.media.imagePolicy,
67
+ s3: { ...fallback.media.s3, ...rcMedia.s3 },
68
+ } : fallback.media,
69
+ collections: (srv.collections as ResolvedKestrel['collections'] | undefined) ?? fallback.collections,
70
+ aiDisclosure: (srv.aiDisclosure as ResolvedKestrel['aiDisclosure'] | undefined) ?? fallback.aiDisclosure,
71
+ seo: (srv.seo as ResolvedKestrel['seo'] | undefined) ?? fallback.seo,
72
+ delivery: (srv.delivery as ResolvedKestrel['delivery'] | undefined) ?? fallback.delivery,
73
+ deliveryExempt: (srv.deliveryExempt as string[] | undefined) ?? fallback.deliveryExempt,
74
+ output: (srv.output as ResolvedKestrel['output'] | undefined) ?? fallback.output,
75
+ // `revisions` is NOT written into `runtimeConfig.kestrel` by the module today (pre-existing —
76
+ // unrelated to this seam), so this reads as `fallback.revisions` in every real boot right now; kept
77
+ // symmetric with every other field rather than special-cased, so a future module fix (writing
78
+ // `srv.revisions`) is picked up here for free.
79
+ revisions: (srv.revisions as ResolvedKestrel['revisions'] | undefined) ?? fallback.revisions,
80
+ }
81
+ }
@@ -0,0 +1,7 @@
1
+ import { registerPipeline } from '@michaelthielemann/kestrel-core'
2
+ import { buildBlocksPipelines } from '@michaelthielemann/kestrel-fields'
3
+
4
+ // Registration only — nothing resolves a pipeline here; the registry is read on the first request.
5
+ export default defineNitroPlugin(() => {
6
+ for (const def of buildBlocksPipelines()) registerPipeline(def)
7
+ })
@@ -1,5 +1,5 @@
1
- import { registerPopulator } from '../../../core/server/utils/populate'
2
- import { buildFieldTreePopulator } from '../utils/field-populate'
1
+ import { registerPopulator } from '@michaelthielemann/kestrel-core'
2
+ import { buildFieldTreePopulator } from '@michaelthielemann/kestrel-fields'
3
3
 
4
4
  // The single global row populator: walks a record's fields + block tree and dispatches each field to its
5
5
  // per-type populator (media / link / richtext / relation), each registered by its owning layer. It recurses
@@ -2,8 +2,8 @@
2
2
  import { useBlurUp } from '../composables/useBlurUp'
3
3
  import { useMediaVariant } from '../composables/useMediaVariant'
4
4
  import { aiSourceTypeLabel } from '../utils/ai-disclosure'
5
- import type { ResolvedMedia } from '../../server/utils/resolve'
6
- import type { VariantFit, VariantFormat } from '../../../core/server/utils/kestrel-config'
5
+ import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
6
+ import type { VariantFit, VariantFormat } from '@michaelthielemann/kestrel-core'
7
7
 
8
8
  // The engine's responsive-image component: it renders a <picture> from the media's generated derivatives
9
9
  // AND its usage declares (to the generate-time scan) which sizes/formats are actually needed — "not in the
@@ -21,7 +21,7 @@ const props = defineProps<{
21
21
  /** Opt into a structurally-present but UNSTYLED EU AI Act disclosure badge (`.kestrel-img__ai-badge`),
22
22
  * rendered only when `media.aiDisclosure` is set. Off by default: Kestrel must never publish a claim
23
23
  * the consumer did not ask for, in a place/language/style it cannot know. The alternative escape hatch
24
- * is to read `media.aiDisclosure` and render your own element — see docs/media-uploads.md. */
24
+ * is to read `media.aiDisclosure` and render your own element — see docs/guide/ai-disclosure.md. */
25
25
  aiBadge?: boolean
26
26
  }>()
27
27
 
@@ -62,8 +62,8 @@ async function onSaveMeta(alt: string, ai?: { aiSourceType: string | null; aiNot
62
62
  // so it rides alongside as top-level keys — and only when the viewer actually offered it, so a save
63
63
  // from a consumer with the feature off never clears a recorded disclosure. The precondition header
64
64
  // lets the server 409 instead of silently losing a concurrent edit from another open viewer tab.
65
- await $fetch(`/api/media/${f.id}`, {
66
- method: 'PATCH',
65
+ await $fetch(`/api/media/updateAsset/${f.id}`, {
66
+ method: 'POST',
67
67
  body: { translations: { [primary]: { alt } }, ...(ai ?? {}) },
68
68
  ...(f.updatedAt ? { headers: { 'x-kestrel-if-unmodified-since': String(new Date(f.updatedAt).getTime()) } } : {}),
69
69
  })
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watch, nextTick } from 'vue'
3
3
  import type { FieldComponentProps } from '../../../../ui/app/utils/field-component'
4
- import type { FieldOf } from '../../../../core/server/utils/defineCollection'
4
+ import type { FieldOf } from '@michaelthielemann/kestrel-core'
5
5
  import { reorder } from '../../../../ui/app/utils/reorder'
6
6
  import { commonFolder } from '../../utils/library'
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { reactive, toValue } from 'vue'
2
2
  import type { MaybeRefOrGetter } from 'vue'
3
- import type { ResolvedMedia } from '../../server/utils/resolve'
3
+ import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
4
4
 
5
5
  // The client-facing resolved-media shape (`GET /api/media/resolve`) is the server resolver's own
6
6
  // return type — re-exported so consumers keep importing it from here, single-sourced so it can't drift.
@@ -35,7 +35,7 @@ export function useMediaUpload(cb: UploadCallbacks = {}) {
35
35
  fd.append('filename', opts.filename ?? item.filename)
36
36
  if (opts.overwrite) fd.append('overwrite', 'true')
37
37
  try {
38
- await $fetch('/api/media', { method: 'POST', body: fd })
38
+ await $fetch('/api/media/upload', { method: 'POST', body: fd })
39
39
  item.status = 'done'
40
40
  } catch (e) {
41
41
  type ConflictData = { suggestion?: string; existingId?: number }
@@ -1,6 +1,6 @@
1
1
  import { computed, toValue, type MaybeRefOrGetter } from 'vue'
2
- import type { ResolvedMedia } from '../../server/utils/resolve'
3
- import type { ResolvedVariant } from '../../../core/server/utils/kestrel-config'
2
+ import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
3
+ import type { ResolvedVariant } from '@michaelthielemann/kestrel-core'
4
4
  import { buildPictureModel, requestVariantSpecs, type VariantRequest, type PictureModel } from '../utils/picture-model'
5
5
 
6
6
  /**
@@ -1,4 +1,4 @@
1
- import type { AiSourceType } from '../../server/utils/resolve'
1
+ import type { AiSourceType } from '@michaelthielemann/kestrel-media'
2
2
 
3
3
  /**
4
4
  * Default human-readable label for an EU AI Act source type — the badge text when no editor note was
@@ -1,4 +1,4 @@
1
- import type { LibraryFolder } from '../../server/utils/library'
1
+ import type { LibraryFolder } from '@michaelthielemann/kestrel-media'
2
2
 
3
3
  // Re-exported from the server util that produces it (`GET /api/media/library`) so the wire shape is
4
4
  // single-sourced.
@@ -1,7 +1,5 @@
1
1
  import type { LibraryItem } from './library'
2
- import type { OpItem, DeleteReport } from '../../server/utils/media-ops'
3
- import type { Conflict, RelocationReport } from '../../server/utils/relocate-ops'
4
- import type { MediaUsage } from '../../server/utils/usages'
2
+ import type { OpItem, DeleteReport, Conflict, RelocationReport, MediaUsage } from '@michaelthielemann/kestrel-media'
5
3
 
6
4
  // Single-source the HTTP wire shapes from the server utils that own them — one definition, no drift.
7
5
  export type { OpItem, DeleteReport, Conflict, RelocationReport, MediaUsage }
@@ -1,5 +1,5 @@
1
- import type { ResolvedMedia } from '../../server/utils/resolve'
2
- import type { VariantFit, VariantFormat, ResolvedVariant } from '../../../core/server/utils/kestrel-config'
1
+ import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
2
+ import type { VariantFit, VariantFormat, ResolvedVariant } from '@michaelthielemann/kestrel-core'
3
3
  import { variantName } from './variant-name'
4
4
 
5
5
  /** What a `KestrelImg` / `useMediaVariant` call declares it needs. One call may declare MULTIPLE sizes and
@@ -1,4 +1,4 @@
1
- import type { VariantFit } from '../../../core/server/utils/kestrel-config'
1
+ import type { VariantFit } from '@michaelthielemann/kestrel-core'
2
2
 
3
3
  export interface VariantShape {
4
4
  width: number
@@ -1,7 +1,8 @@
1
1
  import type { H3Event } from 'h3'
2
- import { deriveOnDemand, variantKeyFromPath } from '../utils/ondemand'
3
- import { useStorageDriver, mediaRuntimeConfig } from '../../../core/server/utils/storage'
4
- import { DEFAULT_IMAGE_POLICY } from '../../../core/server/utils/kestrel-config'
2
+ import { clientIp } from '@michaelthielemann/kestrel-auth'
3
+ import { allowlistMode, ipAllowed, parseAllowlist } from '@michaelthielemann/kestrel-access'
4
+ import { deriveOnDemand, variantKeyFromPath, useStorageDriver, mediaRuntimeConfig, useMediaDb } from '@michaelthielemann/kestrel-media'
5
+ import { DEFAULT_IMAGE_POLICY } from '@michaelthielemann/kestrel-core'
5
6
 
6
7
  /**
7
8
  * Whether the IP allow-list would deny this request, checked directly rather than relied on by ordering:
@@ -21,13 +22,13 @@ export default defineEventHandler(async (event) => {
21
22
  if (!import.meta.dev || event.method !== 'GET') return
22
23
  if (deniedByAllowlist(event)) return
23
24
  const cfg = mediaRuntimeConfig()
24
- const key = variantKeyFromPath(event.path, cfg.local.baseUrl)
25
+ const key = variantKeyFromPath(event.path, cfg.baseUrl)
25
26
  if (key == null) return
26
27
  const driver = useStorageDriver()
27
28
  if (await driver.exists?.(key)) return
28
29
  // Dev is the diagnosis environment, so surface a real derive failure (missing original, sharp reject)
29
30
  // instead of silently 404ing — a broken preview <img> with no log violates the fail-loud convention.
30
- const result = await deriveOnDemand(useDb(), driver, cfg.imagePolicy ?? DEFAULT_IMAGE_POLICY, key).catch((error) => {
31
+ const result = await deriveOnDemand(useMediaDb().db, driver, cfg.imagePolicy ?? DEFAULT_IMAGE_POLICY, key).catch((error) => {
31
32
  console.warn(`[kestrel] on-demand variant derive failed for ${key}:`, (error as Error)?.message ?? error)
32
33
  return null
33
34
  })
@@ -0,0 +1,7 @@
1
+ import { registerPipeline } from '@michaelthielemann/kestrel-core'
2
+ import { buildMediaPipelines } from '@michaelthielemann/kestrel-media'
3
+
4
+ // Registration only — nothing resolves a pipeline here; the registry is read on the first request.
5
+ export default defineNitroPlugin(() => {
6
+ for (const def of buildMediaPipelines()) registerPipeline(def)
7
+ })
@@ -1,10 +1,13 @@
1
1
  import { eq, getTableColumns } from 'drizzle-orm'
2
- import { media as mediaTable } from '../collections/media'
3
- import { registerFieldPopulator } from '../../../core/server/utils/populate'
4
- import { buildMediaFieldPopulator } from '../utils/populate'
5
- import { resolveMedia } from '../utils/resolve'
6
- import { useStorageDriver } from '../../../core/server/utils/storage'
7
- import { mediaCollectionEnabled } from '../utils/media-enabled'
2
+ import { registerFieldPopulator } from '@michaelthielemann/kestrel-core'
3
+ import {
4
+ media as mediaTable,
5
+ buildMediaFieldPopulator,
6
+ resolveMedia,
7
+ useStorageDriver,
8
+ mediaCollectionEnabled,
9
+ useMediaDb,
10
+ } from '@michaelthielemann/kestrel-media'
8
11
 
9
12
  export default defineNitroPlugin(() => {
10
13
  // A consumer who turned the built-in off has no `media` table, so a row that still carries a media id
@@ -13,7 +16,7 @@ export default defineNitroPlugin(() => {
13
16
  const cols = getTableColumns(mediaTable) as Record<string, never>
14
17
  registerFieldPopulator('media', buildMediaFieldPopulator((id, locale) => {
15
18
  const driver = useStorageDriver()
16
- const row = useDb().select().from(mediaTable).where(eq(cols.id, id)).get()
19
+ const row = useMediaDb().db.select().from(mediaTable).where(eq(cols.id, id)).get()
17
20
  return row ? resolveMedia(row as never, locale, (k) => driver.publicUrl(k)) : null
18
21
  }))
19
22
  })
@@ -1,13 +1,17 @@
1
- import { recordVariants, saveDiscoveredVariants } from '../utils/variant-capture'
1
+ import { isRendererContext } from '@michaelthielemann/kestrel-access'
2
+ import { recordVariants, saveDiscoveredVariants, useMediaDb } from '@michaelthielemann/kestrel-media'
2
3
 
3
4
  /**
4
5
  * Auto-discovery capture. `KestrelImg`/`useMediaVariant` stash their concrete specs on the render's
5
6
  * `event.context.kestrelVariants` during SSR. Renders count when they are either a PUBLISH render (the
6
- * incremental publisher marks them with the `x-kestrel-publish` header, see publisher.ts `renderRoute`) OR
7
- * a build-time PRERENDER render (the classic `nuxt generate` topology) — so live preview / stray runtime
8
- * renders never pollute the scan. For a publish, `publishFull` reconciles the drained accumulator once at
9
- * the end; the generate topology has no such orchestrator, so each prerendered route reconciles inline
10
- * WITHOUT draining and the growing accumulator converges to the full used set by the last route.
7
+ * incremental publisher runs it under `runAsRenderer`, see `render-live.ts`) OR a build-time PRERENDER
8
+ * render (the classic `nuxt generate` topology) — so live preview / stray runtime renders never pollute the
9
+ * scan. For a publish, `publishFull` reconciles the drained accumulator once at the end; the generate
10
+ * topology has no such orchestrator, so each prerendered route reconciles inline WITHOUT draining and the
11
+ * growing accumulator converges to the full used set by the last route.
12
+ *
13
+ * Gating uses the unforgeable AsyncLocalStorage signal `isRendererContext()` (the same one
14
+ * `delivery-live`'s catch-all uses), not the spoofable `x-kestrel-publish` header.
11
15
  *
12
16
  * NOTE: relies on `beforeResponse` firing for the publisher's `localFetch` renders / the prerenderer, and on
13
17
  * the app→server `event.context` bridge — verified by a real publish/generate, not the headless unit suite.
@@ -15,9 +19,9 @@ import { recordVariants, saveDiscoveredVariants } from '../utils/variant-capture
15
19
  export default defineNitroPlugin((nitroApp) => {
16
20
  nitroApp.hooks.hook('beforeResponse', (event) => {
17
21
  const isPrerender = import.meta.prerender === true
18
- if (getRequestHeader(event, 'x-kestrel-publish') !== '1' && !isPrerender) return
22
+ if (!isRendererContext() && !isPrerender) return
19
23
  const specs = (event.context as { kestrelVariants?: unknown[] }).kestrelVariants
20
24
  if (Array.isArray(specs) && specs.length) recordVariants(specs as never)
21
- if (isPrerender) saveDiscoveredVariants(useDb(), { clear: false })
25
+ if (isPrerender) saveDiscoveredVariants(useMediaDb().db, { clear: false })
22
26
  })
23
27
  })
@@ -0,0 +1,5 @@
1
+ import { registerMediaCleanup } from '@michaelthielemann/kestrel-media'
2
+
3
+ export default defineNitroPlugin(() => {
4
+ registerMediaCleanup()
5
+ })
@@ -1,6 +1,5 @@
1
- import { runBackfill } from '../../utils/backfill'
2
- import { useStorageDriver, mediaRuntimeConfig } from '../../../../core/server/utils/storage'
3
- import { DEFAULT_IMAGE_POLICY } from '../../../../core/server/utils/kestrel-config'
1
+ import { runBackfill, useStorageDriver, mediaRuntimeConfig, useMediaDb } from '@michaelthielemann/kestrel-media'
2
+ import { DEFAULT_IMAGE_POLICY } from '@michaelthielemann/kestrel-core'
4
3
 
5
4
  /**
6
5
  * Reconcile every media row's derivatives to the active variant registry: generate the missing sizes/formats
@@ -12,7 +11,7 @@ export default defineTask({
12
11
  meta: { name: 'media:backfill', description: 'Generate missing image variants + prune deregistered ones; {check:true}=dry-run' },
13
12
  async run({ payload }) {
14
13
  const policy = mediaRuntimeConfig().imagePolicy ?? DEFAULT_IMAGE_POLICY
15
- const result = await runBackfill(useDb(), useStorageDriver(), policy, { check: !!(payload as { check?: boolean })?.check })
14
+ const result = await runBackfill(useMediaDb().db, useStorageDriver(), policy, { check: !!(payload as { check?: boolean })?.check })
16
15
  return { result }
17
16
  },
18
17
  })
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, useSlots } from 'vue'
3
- import type { LinkValue } from '../../../core/server/utils/defineCollection'
3
+ import type { LinkValue } from '@michaelthielemann/kestrel-core'
4
4
  import { linkToHref, linkLabel } from '../utils/link-href'
5
5
 
6
6
  // Generic renderer for the `link` field type, for consumer projects + blocks. Renders an <a> for any
@@ -99,7 +99,7 @@ const previewNotice = computed(() => {
99
99
  const previewRequested = route.query[PREVIEW_QUERY] === '1'
100
100
  const { data: previewSession } = previewRequested
101
101
  ? await useAsyncData('kestrel-preview-session', () =>
102
- requestFetch('/api/auth/session').catch(() => ({ authenticated: false })))
102
+ requestFetch<{ authenticated: boolean }>('/api/session').catch(() => ({ authenticated: false })))
103
103
  : { data: ref<{ authenticated: boolean } | null>(null) }
104
104
  const previewActive = computed(() => previewRequested && previewSession.value?.authenticated === true)
105
105
 
@@ -8,14 +8,14 @@
8
8
  * Saved pageLike records preview at their REAL URL instead (higher fidelity); this page is the graceful
9
9
  * fallback so previews never regress to "save first".
10
10
  *
11
- * Admin-gated server-side: `useRequestFetch` forwards the incoming cookies to `/api/auth/session`
11
+ * Admin-gated server-side: `useRequestFetch` forwards the incoming cookies to `/api/session`
12
12
  * (the same seam the catch-all uses for draft rendering), and anonymous requests get a 404 — the
13
13
  * page's existence is not advertised. It is never linked, prerendered, or published (the publisher
14
14
  * enumerates DB page rows only), and carries noindex as belt-and-braces.
15
15
  */
16
16
  const requestFetch = useRequestFetch()
17
17
  const { data: session } = await useAsyncData('kestrel-preview-session', () =>
18
- requestFetch('/api/auth/session').catch(() => ({ authenticated: false })),
18
+ requestFetch<{ authenticated: boolean }>('/api/session').catch(() => ({ authenticated: false })),
19
19
  )
20
20
  if (!session.value?.authenticated) throw createError({ statusCode: 404, statusMessage: 'Page not found' })
21
21
 
@@ -1,4 +1,4 @@
1
- import { localePath } from '../../../core/app/utils/locale-path'
1
+ import { localePath } from '@michaelthielemann/kestrel-core/client'
2
2
 
3
3
  /** One published ancestor of the rendered page, outermost first — the breadcrumb trail. */
4
4
  export interface JsonLdAncestor {
@@ -1,4 +1,4 @@
1
- import type { LinkValue } from '../../../core/server/utils/defineCollection'
1
+ import type { LinkValue } from '@michaelthielemann/kestrel-core'
2
2
 
3
3
  // The server link populator (depth > 0) resolves an internal link's localized target path and attaches
4
4
  // it as `href`. Until that populator runs (or for a not-yet-resolved value), internal links fall back