@michaelthielemann/kestrel 3.0.0 → 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 (353) 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 +6 -8
  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/Button.vue +2 -2
  133. package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
  134. package/layers/ui/app/components/ui/Checkbox.vue +2 -2
  135. package/layers/ui/app/components/ui/Combobox.vue +5 -5
  136. package/layers/ui/app/components/ui/DatePicker.vue +2 -2
  137. package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
  138. package/layers/ui/app/components/ui/NumberInput.vue +4 -4
  139. package/layers/ui/app/components/ui/Richtext.vue +1 -1
  140. package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
  141. package/layers/ui/app/components/ui/Select.vue +2 -2
  142. package/layers/ui/app/components/ui/TextInput.vue +4 -4
  143. package/layers/ui/app/components/ui/Textarea.vue +2 -2
  144. package/layers/ui/app/components/ui/TimeInput.vue +2 -2
  145. package/layers/ui/app/components/ui/Toasts.vue +3 -3
  146. package/layers/ui/app/composables/useLinkField.ts +1 -1
  147. package/layers/ui/app/composables/useRepeater.ts +1 -1
  148. package/layers/ui/app/i18n/de.ts +7 -7
  149. package/layers/ui/app/i18n/en.ts +7 -39
  150. package/layers/ui/app/utils/field-component.ts +1 -1
  151. package/layers/ui/app/utils/field-registry.ts +1 -1
  152. package/layers/ui/app/utils/icons.ts +2 -6
  153. package/layers/ui/app/utils/localized.ts +1 -1
  154. package/nuxt.config.ts +1 -1
  155. package/package.json +53 -19
  156. package/scripts/consumer-template-ci.mjs +157 -0
  157. package/scripts/coverage-all.mjs +163 -0
  158. package/scripts/dashboard.mjs +89 -0
  159. package/scripts/hash-password.mjs +1 -1
  160. package/scripts/lib/coverage-model.mjs +51 -0
  161. package/scripts/port-graph.mjs +350 -0
  162. package/scripts/typecheck.mjs +11 -0
  163. package/templates/starter/app/blocks/Prose.vue +1 -1
  164. package/layers/access/server/utils/csrf.ts +0 -39
  165. package/layers/access/server/utils/grant-registry.ts +0 -36
  166. package/layers/access/server/utils/guard.ts +0 -44
  167. package/layers/access/server/utils/ip-allowlist.ts +0 -79
  168. package/layers/access/server/utils/policy.ts +0 -98
  169. package/layers/access/server/utils/public-resources.ts +0 -11
  170. package/layers/access/server/utils/render-context.ts +0 -41
  171. package/layers/access/server/utils/require-admin.ts +0 -15
  172. package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
  173. package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
  174. package/layers/auth/server/api/auth/login.post.ts +0 -38
  175. package/layers/auth/server/api/auth/logout.post.ts +0 -9
  176. package/layers/auth/server/api/auth/session.get.ts +0 -1
  177. package/layers/auth/server/utils/admin-credential.ts +0 -42
  178. package/layers/auth/server/utils/client-ip.ts +0 -88
  179. package/layers/auth/server/utils/login-throttle.ts +0 -126
  180. package/layers/auth/server/utils/password.ts +0 -52
  181. package/layers/auth/server/utils/session-cookie.ts +0 -44
  182. package/layers/auth/server/utils/session-epoch.ts +0 -96
  183. package/layers/auth/server/utils/session.ts +0 -88
  184. package/layers/collections/server/collections/pages.ts +0 -19
  185. package/layers/collections/server/utils/populate-relations.ts +0 -79
  186. package/layers/core/app/utils/bulk-actions.ts +0 -9
  187. package/layers/core/app/utils/filter-ops.ts +0 -98
  188. package/layers/core/app/utils/list-limits.ts +0 -27
  189. package/layers/core/app/utils/locale-path.ts +0 -12
  190. package/layers/core/app/utils/slugify.ts +0 -16
  191. package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
  192. package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
  193. package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
  194. package/layers/core/server/api/[collection]/[id].get.ts +0 -11
  195. package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
  196. package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
  197. package/layers/core/server/api/[collection]/index.get.ts +0 -24
  198. package/layers/core/server/api/[collection]/index.post.ts +0 -8
  199. package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
  200. package/layers/core/server/api/[collection]/index.put.ts +0 -24
  201. package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
  202. package/layers/core/server/api/[collection]/options.get.ts +0 -18
  203. package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
  204. package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
  205. package/layers/core/server/api/[collection]/translations.get.ts +0 -25
  206. package/layers/core/server/api/collections/[name].get.ts +0 -4
  207. package/layers/core/server/api/collections/index.get.ts +0 -3
  208. package/layers/core/server/api/references/broken.get.ts +0 -9
  209. package/layers/core/server/api/references/referrers.get.ts +0 -24
  210. package/layers/core/server/database/record-refs.ts +0 -20
  211. package/layers/core/server/plugins/03.record-refs.ts +0 -12
  212. package/layers/core/server/schema/bootstrap.ts +0 -41
  213. package/layers/core/server/schema/desired.ts +0 -99
  214. package/layers/core/server/schema/dev-sync.ts +0 -23
  215. package/layers/core/server/schema/dialect.ts +0 -61
  216. package/layers/core/server/schema/diff.ts +0 -110
  217. package/layers/core/server/schema/introspect.ts +0 -68
  218. package/layers/core/server/schema/model.ts +0 -61
  219. package/layers/core/server/schema/render-sqlite.ts +0 -89
  220. package/layers/core/server/schema/sync.ts +0 -201
  221. package/layers/core/server/utils/blocks.ts +0 -6
  222. package/layers/core/server/utils/collection-types.ts +0 -28
  223. package/layers/core/server/utils/crud.ts +0 -578
  224. package/layers/core/server/utils/db.ts +0 -26
  225. package/layers/core/server/utils/defineCollection.ts +0 -213
  226. package/layers/core/server/utils/duplicate.ts +0 -162
  227. package/layers/core/server/utils/field-layout.ts +0 -125
  228. package/layers/core/server/utils/filter-predicate.ts +0 -101
  229. package/layers/core/server/utils/http.ts +0 -50
  230. package/layers/core/server/utils/if-unmodified.ts +0 -13
  231. package/layers/core/server/utils/kestrel-config.ts +0 -451
  232. package/layers/core/server/utils/key-lock.ts +0 -33
  233. package/layers/core/server/utils/locale.ts +0 -53
  234. package/layers/core/server/utils/page-route.ts +0 -58
  235. package/layers/core/server/utils/page-slug.ts +0 -127
  236. package/layers/core/server/utils/picker.ts +0 -82
  237. package/layers/core/server/utils/populate.ts +0 -50
  238. package/layers/core/server/utils/prerender-memo.ts +0 -23
  239. package/layers/core/server/utils/read-capture.ts +0 -37
  240. package/layers/core/server/utils/record-ref-index.ts +0 -200
  241. package/layers/core/server/utils/registry.ts +0 -24
  242. package/layers/core/server/utils/resolve-scope.ts +0 -79
  243. package/layers/core/server/utils/seo.ts +0 -42
  244. package/layers/core/server/utils/serialize-collection.ts +0 -147
  245. package/layers/core/server/utils/sql.ts +0 -2
  246. package/layers/core/server/utils/storage.local.ts +0 -117
  247. package/layers/core/server/utils/storage.s3.ts +0 -197
  248. package/layers/core/server/utils/storage.ts +0 -90
  249. package/layers/core/server/utils/write-effects.ts +0 -40
  250. package/layers/core/server/utils/write-events.ts +0 -31
  251. package/layers/fields/app/utils/condition.ts +0 -71
  252. package/layers/fields/app/utils/define-block.ts +0 -13
  253. package/layers/fields/app/utils/field-constraints.ts +0 -17
  254. package/layers/fields/app/utils/field-meta.ts +0 -33
  255. package/layers/fields/app/utils/field-validate.ts +0 -75
  256. package/layers/fields/app/utils/field-value.ts +0 -7
  257. package/layers/fields/app/utils/richtext-links.ts +0 -48
  258. package/layers/fields/server/api/blocks/index.get.ts +0 -9
  259. package/layers/fields/server/field-registry/index.ts +0 -249
  260. package/layers/fields/server/field-registry/naming.ts +0 -10
  261. package/layers/fields/server/field-registry/sanitize.ts +0 -43
  262. package/layers/fields/server/field-registry/types.ts +0 -19
  263. package/layers/fields/server/utils/block-ids.ts +0 -43
  264. package/layers/fields/server/utils/block-populate.ts +0 -41
  265. package/layers/fields/server/utils/buildCollection.ts +0 -90
  266. package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
  267. package/layers/fields/server/utils/buildTable.ts +0 -113
  268. package/layers/fields/server/utils/conditional-required.ts +0 -25
  269. package/layers/fields/server/utils/defineBlock.ts +0 -86
  270. package/layers/fields/server/utils/defineFieldType.ts +0 -33
  271. package/layers/fields/server/utils/extract-refs.ts +0 -152
  272. package/layers/fields/server/utils/field-populate.ts +0 -68
  273. package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
  274. package/layers/media/server/api/media/[id].delete.ts +0 -18
  275. package/layers/media/server/api/media/[id].get.ts +0 -10
  276. package/layers/media/server/api/media/[id].patch.test.ts +0 -66
  277. package/layers/media/server/api/media/[id].patch.ts +0 -65
  278. package/layers/media/server/api/media/backfill.post.ts +0 -18
  279. package/layers/media/server/api/media/copy.post.ts +0 -7
  280. package/layers/media/server/api/media/delete.post.ts +0 -13
  281. package/layers/media/server/api/media/folders.post.ts +0 -18
  282. package/layers/media/server/api/media/index.get.ts +0 -15
  283. package/layers/media/server/api/media/index.post.ts +0 -173
  284. package/layers/media/server/api/media/library.get.ts +0 -17
  285. package/layers/media/server/api/media/move.post.ts +0 -7
  286. package/layers/media/server/api/media/rename.post.ts +0 -7
  287. package/layers/media/server/api/media/resolve.get.ts +0 -13
  288. package/layers/media/server/collections/media-settings.ts +0 -25
  289. package/layers/media/server/collections/media.ts +0 -43
  290. package/layers/media/server/database/folders.ts +0 -10
  291. package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
  292. package/layers/media/server/schema-tables/folders.ts +0 -4
  293. package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
  294. package/layers/media/server/utils/ai-signal-detect.ts +0 -155
  295. package/layers/media/server/utils/backfill.ts +0 -144
  296. package/layers/media/server/utils/derive.ts +0 -59
  297. package/layers/media/server/utils/folder-paths.ts +0 -42
  298. package/layers/media/server/utils/folders.ts +0 -12
  299. package/layers/media/server/utils/library.ts +0 -91
  300. package/layers/media/server/utils/media-cleanup.ts +0 -27
  301. package/layers/media/server/utils/media-enabled.ts +0 -23
  302. package/layers/media/server/utils/media-ops.ts +0 -118
  303. package/layers/media/server/utils/media-write.ts +0 -13
  304. package/layers/media/server/utils/naming.ts +0 -52
  305. package/layers/media/server/utils/ondemand.ts +0 -69
  306. package/layers/media/server/utils/persist-upload.ts +0 -62
  307. package/layers/media/server/utils/populate.ts +0 -47
  308. package/layers/media/server/utils/record.ts +0 -71
  309. package/layers/media/server/utils/relocate-ops.ts +0 -341
  310. package/layers/media/server/utils/relocate-plan.ts +0 -63
  311. package/layers/media/server/utils/resolve.ts +0 -98
  312. package/layers/media/server/utils/sanitize-svg.ts +0 -12
  313. package/layers/media/server/utils/sniff.ts +0 -68
  314. package/layers/media/server/utils/storage-relocate.ts +0 -139
  315. package/layers/media/server/utils/translations.ts +0 -19
  316. package/layers/media/server/utils/usages.ts +0 -66
  317. package/layers/media/server/utils/variant-capture.ts +0 -70
  318. package/layers/media/server/utils/variants.ts +0 -123
  319. package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
  320. package/layers/public/server/api/links/resolve.get.ts +0 -19
  321. package/layers/public/server/api/preview.get.ts +0 -28
  322. package/layers/public/server/api/preview.post.ts +0 -93
  323. package/layers/public/server/api/publish-status.get.ts +0 -58
  324. package/layers/public/server/api/publish.post.ts +0 -84
  325. package/layers/public/server/api/route.get.ts +0 -54
  326. package/layers/public/server/collections/redirects.ts +0 -75
  327. package/layers/public/server/collections/site.ts +0 -44
  328. package/layers/public/server/database/publish-deps.ts +0 -12
  329. package/layers/public/server/database/publish-status.ts +0 -19
  330. package/layers/public/server/schema-tables/publish-deps.ts +0 -4
  331. package/layers/public/server/schema-tables/publish-status.ts +0 -4
  332. package/layers/public/server/utils/link-resolve.ts +0 -38
  333. package/layers/public/server/utils/llms-full.ts +0 -125
  334. package/layers/public/server/utils/llms.ts +0 -52
  335. package/layers/public/server/utils/page-resolve.ts +0 -208
  336. package/layers/public/server/utils/populate-links.ts +0 -66
  337. package/layers/public/server/utils/preview-token.ts +0 -109
  338. package/layers/public/server/utils/publish/coalesce.ts +0 -22
  339. package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
  340. package/layers/public/server/utils/publish/deps.ts +0 -87
  341. package/layers/public/server/utils/publish/invalidation.ts +0 -225
  342. package/layers/public/server/utils/publish/pending.ts +0 -74
  343. package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
  344. package/layers/public/server/utils/publish/publish-status.ts +0 -72
  345. package/layers/public/server/utils/publish/publisher.ts +0 -362
  346. package/layers/public/server/utils/publish/queue.ts +0 -67
  347. package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
  348. package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
  349. package/layers/public/server/utils/publish/route-for-record.ts +0 -16
  350. package/layers/public/server/utils/publish/route-keys.ts +0 -13
  351. package/layers/public/server/utils/richtext-markdown.ts +0 -260
  352. package/layers/public/server/utils/site-url.ts +0 -40
  353. package/layers/public/server/utils/sitemap.ts +0 -99
@@ -4,7 +4,6 @@ import type { Catalog } from '../composables/useT'
4
4
  // carry `{name}` placeholders interpolated by `useT().t`. Content (collection data) is NOT translated
5
5
  // here — only the dashboard chrome. Keep `en` the complete set; other catalogs may be partial.
6
6
  export const en: Catalog = {
7
- // common / shared
8
7
  'common.new': 'New {label}',
9
8
  'common.save': 'Save',
10
9
  'common.publish': 'Publish',
@@ -14,7 +13,6 @@ export const en: Catalog = {
14
13
  'common.close': 'Close',
15
14
  'common.retry': 'Retry',
16
15
 
17
- // navigation rail + shell
18
16
  'nav.dashboard': 'Dashboard',
19
17
  'nav.signOut': 'Sign out',
20
18
  'theme.dark': 'Dark mode',
@@ -27,21 +25,17 @@ export const en: Catalog = {
27
25
  'a11y.rowActions': 'Actions',
28
26
  'lang.label': 'Language',
29
27
 
30
- // account menu (rail foot)
31
28
  'account.label': 'Account',
32
29
  'account.name': 'Admin',
33
30
 
34
- // text input (password reveal)
35
31
  'input.showPassword': 'Show password',
36
32
  'input.hidePassword': 'Hide password',
37
33
 
38
- // login
39
34
  'login.signIn': 'Sign in',
40
35
  'login.password': 'Password',
41
36
  'login.invalid': 'Invalid credentials',
42
37
  'login.error': 'Something went wrong — please try again',
43
38
 
44
- // dashboard
45
39
  'dash.title': 'Dashboard',
46
40
  'dash.lede': 'Choose a collection to manage its content.',
47
41
  'dash.empty.title': 'No collections yet',
@@ -49,10 +43,8 @@ export const en: Catalog = {
49
43
  'dash.mode.singleton': 'Singleton',
50
44
  'dash.mode.collection': 'Collection',
51
45
 
52
- // collection (list page wrapper)
53
46
  'collection.unknown': 'Unknown collection: {name}',
54
47
 
55
- // record editor page + toasts
56
48
  'editor.back': '{collection}',
57
49
  'editor.newRecord': 'New {collection}',
58
50
  'editor.editRecord': 'Edit {collection} #{id}',
@@ -61,6 +53,8 @@ export const en: Catalog = {
61
53
  'editor.fixBlockContent': 'Some block content is invalid. Check the highlighted block fields.',
62
54
  'editor.saveConflict': 'This conflicts with an existing record.',
63
55
  'editor.saveFailed': 'Save failed.',
56
+ 'editor.quarantined.title': 'Record quarantined',
57
+ 'editor.quarantined.desc': 'This record no longer matches its collection’s schema and is shown read-only. It cannot be edited or saved until the underlying data is fixed.',
64
58
  'toast.saved': 'Saved',
65
59
  'toast.deleted': 'Deleted',
66
60
  'toast.duplicated': 'Duplicated',
@@ -69,7 +63,6 @@ export const en: Catalog = {
69
63
  'toast.region': 'Notifications',
70
64
  'toast.dismiss': 'Dismiss',
71
65
 
72
- // collection list
73
66
  'list.translations': 'Translations',
74
67
  'list.deadRefs': 'Refs',
75
68
  'list.filter': 'Filter',
@@ -113,7 +106,9 @@ export const en: Catalog = {
113
106
  'list.result': '{total} result',
114
107
  'list.results': '{total} results',
115
108
  'list.loadError': "Couldn't load this collection.",
116
- // row selection + bulk actions
109
+ 'list.quarantined': 'Quarantined',
110
+ 'list.quarantinedOne': '{n} quarantined record',
111
+ 'list.quarantinedCount': '{n} quarantined records',
117
112
  'list.selectAll': 'Select all rows on this page',
118
113
  'list.selectRow': 'Select {name}',
119
114
  'list.selected': '{n} selected',
@@ -129,8 +124,9 @@ export const en: Catalog = {
129
124
  'list.deleteRefsUnverified': 'References could not be verified — some records may be left with a broken link. Proceed with caution.',
130
125
  'list.opFailed': 'The action could not be completed.',
131
126
  'list.refCheckFailed': 'References could not be checked.',
127
+ 'list.actionConfirm': 'Run "{name}" on {n} record(s)?',
128
+ 'list.actionDone': '{name} done',
132
129
 
133
- // editor preview pane
134
130
  'preview.ariaLabel': 'Live preview',
135
131
  'preview.label': 'Preview',
136
132
  'preview.deviceLabel': 'Preview size',
@@ -142,7 +138,6 @@ export const en: Catalog = {
142
138
  'preview.width': 'Width',
143
139
  'preview.height': 'Height',
144
140
 
145
- // page settings (system fields on pageLike collections)
146
141
  'pageSettings.slugLabel': 'Slug',
147
142
  'pageSettings.slugHint': 'URL path, e.g. /about (blank = auto-generated from the title)',
148
143
  'pageSettings.layoutLabel': 'Layout',
@@ -153,7 +148,6 @@ export const en: Catalog = {
153
148
  'pageSettings.statusDraft': 'Draft',
154
149
  'pageSettings.statusPublished': 'Published',
155
150
 
156
- // editor action bar — live preview + status "Ampel"
157
151
  'editor.openInNewTab': 'Open in new tab',
158
152
  'editor.previewUnsaved': 'Preview unsaved changes in a new tab',
159
153
  'editor.previewFailed': 'Could not open the preview.',
@@ -189,7 +183,6 @@ export const en: Catalog = {
189
183
  'editorStatus.live.draft': 'Draft — not generated. It stays off the live site until published.',
190
184
  'editorStatus.live.updatedAt': 'Last published {time}',
191
185
 
192
- // SEO section + Google preview
193
186
  'seo.sectionLabel': 'SEO',
194
187
  'seo.metaTitle': 'Meta title',
195
188
  'seo.metaDescription': 'Meta description',
@@ -208,7 +201,6 @@ export const en: Catalog = {
208
201
  'seo.keywords': 'Keywords',
209
202
  'seo.keywordsHint': 'Comma-separated topics for this page.',
210
203
 
211
- // locale bar (translations)
212
204
  'localeBar.fieldLabel': 'Locale',
213
205
  'localeBar.groupLabel': 'Content locale',
214
206
  'localeBar.editLocale': 'Edit {loc}',
@@ -216,12 +208,10 @@ export const en: Catalog = {
216
208
  'localeBar.createTranslation': 'Create {loc} translation',
217
209
  'localeBar.copyFailed': "Couldn't copy from {locale}.",
218
210
 
219
- // editor history (undo/redo)
220
211
  'history.label': 'Edit history',
221
212
  'history.undo': 'Undo',
222
213
  'history.redo': 'Redo',
223
214
 
224
- // block editor
225
215
  'blocks.empty': 'No blocks yet.',
226
216
  'blocks.moveUp': 'Move block {n} up',
227
217
  'blocks.moveDown': 'Move block {n} down',
@@ -238,7 +228,6 @@ export const en: Catalog = {
238
228
  'blocks.fieldsLabel': 'Block fields',
239
229
  'blocks.treeLabel': 'Block structure',
240
230
 
241
- // dead references (derived stale-reference warnings)
242
231
  'deadRefs.listWarning': 'Links to a deleted or unpublished record',
243
232
  'deadRefs.blockBadge': 'Broken reference',
244
233
  'deadRefs.fieldNote': 'Points at a deleted or unpublished record.',
@@ -256,16 +245,13 @@ export const en: Catalog = {
256
245
  'refs.reason.unpublished': 'Unpublished',
257
246
  'refs.dashAlert': '{n} broken reference(s) — review',
258
247
 
259
- // field gallery
260
248
  'gallery.title': 'Field gallery',
261
249
  'gallery.lede.before': 'Every implemented field widget, live. The relation and internal-link pickers query ',
262
250
  'gallery.lede.after': ' — create a post or two to see results.',
263
251
 
264
- // media library
265
252
  'media.title': 'Media',
266
253
  'media.picker.titleSingle': 'Select a file',
267
254
  'media.picker.titleMultiple': 'Select media',
268
- // media — right-click context menu
269
255
  'media.menu.cut': 'Cut',
270
256
  'media.menu.copy': 'Copy',
271
257
  'media.menu.paste': 'Paste',
@@ -295,7 +281,6 @@ export const en: Catalog = {
295
281
  'media.selected': 'selected',
296
282
  'media.useSelected': 'Use selected',
297
283
  'media.dropToUpload': 'Drop files to upload',
298
- // media — upload conflict resolution
299
284
  'media.conflicts.title': 'Resolve upload conflicts',
300
285
  'media.conflicts.desc': '{count} file(s) already exist. Choose how to resolve each.',
301
286
  'media.conflicts.applyAll': 'Apply to all:',
@@ -306,20 +291,17 @@ export const en: Catalog = {
306
291
  'media.conflicts.overwrite': 'Overwrite',
307
292
  'media.conflicts.rename': 'Rename',
308
293
  'media.conflicts.skip': 'Skip',
309
- // media — new folder / rename dialogs
310
294
  'media.newFolder': 'New folder',
311
295
  'media.folderName': 'Folder name',
312
296
  'media.folderNamePlaceholder': 'e.g. holiday',
313
297
  'media.rename': 'Rename',
314
298
  'media.newName': 'New name',
315
- // media — delete dialog
316
299
  'media.deleteSummary': 'Delete {files} {filesUnit} and {folders} {foldersUnit} — {size}.',
317
300
  'media.file': 'file',
318
301
  'media.files': 'files',
319
302
  'media.folder': 'folder',
320
303
  'media.folders': 'folders',
321
304
  'media.usageWarning': 'These files are used by published content and links will break:',
322
- // media — move/copy conflict dialog
323
305
  'media.conflictTitleMove': 'Move — conflicts',
324
306
  'media.conflictTitleCopy': 'Copy — conflicts',
325
307
  'media.conflictItems': '{count} item(s) already exist in "{dest}":',
@@ -329,7 +311,6 @@ export const en: Catalog = {
329
311
  'media.skip': 'Skip',
330
312
  'media.replace': 'Replace',
331
313
  'media.keepBoth': 'Keep both',
332
- // media — table view
333
314
  'media.colName': 'Name',
334
315
  'media.colType': 'Type',
335
316
  'media.colSize': 'Size',
@@ -337,7 +318,6 @@ export const en: Catalog = {
337
318
  'media.typeFolder': 'Folder',
338
319
  'media.parentFolder': 'Go to the parent folder',
339
320
 
340
- // media — viewer
341
321
  'mediaViewer.folder': 'Folder',
342
322
  'mediaViewer.uploaded': 'Uploaded',
343
323
  'mediaViewer.alt': 'Alt text',
@@ -351,7 +331,6 @@ export const en: Catalog = {
351
331
  'mediaViewer.aiNote': 'Disclosure note',
352
332
  'mediaViewer.aiNoteHint': 'Free text, e.g. the tool used. Pre-filled with any AI signals found in the file at upload — evidence only, not a verified claim.',
353
333
 
354
- // richtext toolbar
355
334
  'richtext.bold': 'Bold',
356
335
  'richtext.italic': 'Italic',
357
336
  'richtext.underline': 'Underline',
@@ -386,7 +365,6 @@ export const en: Catalog = {
386
365
  'richtext.linkUrl': 'Link URL',
387
366
  'richtext.apply': 'Apply',
388
367
 
389
- // media toolbar
390
368
  'mediaToolbar.searchPlaceholder': 'Search…',
391
369
  'mediaToolbar.searchAriaLabel': 'Search media',
392
370
  'mediaToolbar.viewGrid': 'Grid',
@@ -395,18 +373,14 @@ export const en: Catalog = {
395
373
  'mediaToolbar.upload': 'Upload',
396
374
  'mediaToolbar.newFolder': 'New folder',
397
375
 
398
- // media path bar
399
376
  'mediaPath.navAriaLabel': 'Path',
400
377
  'mediaPath.editAriaLabel': 'Edit path',
401
378
  'mediaPath.inputAriaLabel': 'Enter path',
402
379
 
403
- // collections nav
404
380
  'nav.collections': 'Collections',
405
381
 
406
- // field widgets — datetime
407
382
  'field.datetime.range_start': 'Start',
408
383
  'field.datetime.range_end': 'End',
409
- // field widgets — link
410
384
  'field.link.type_internal': 'Internal',
411
385
  'field.link.type_url': 'URL',
412
386
  'field.link.type_email': 'Email',
@@ -415,14 +389,11 @@ export const en: Catalog = {
415
389
  'field.link.url_placeholder': 'https://…',
416
390
  'field.link.label_placeholder': 'Link text (optional)',
417
391
  'field.link.hash_placeholder': 'Anchor (optional, e.g. section-id)',
418
- // field widgets — relation
419
392
  'field.relation.placeholder': 'Search…',
420
- // field widgets — internal-link picker
421
393
  'field.linkPicker.loadError': 'Could not load collections.',
422
394
  'field.linkPicker.chooseCollection': 'Choose a collection…',
423
395
  'field.linkPicker.collectionLabel': 'Collection',
424
396
  'field.linkPicker.searchRecords': 'Search records…',
425
- // field widgets — repeater
426
397
  'field.repeater.moved': 'Moved item to position {n} of {total}',
427
398
  'field.repeater.removed': 'Item {n} removed, {remaining} remaining',
428
399
  'field.repeater.duplicated': 'Duplicated item {n}',
@@ -435,7 +406,6 @@ export const en: Catalog = {
435
406
  'field.repeater.remove_label': 'Remove item {n}',
436
407
  'field.repeater.empty': 'No items yet.',
437
408
  'field.repeater.add': 'Add item',
438
- // field widgets — media
439
409
  'field.media.empty': 'No media selected.',
440
410
  'field.media.add': 'Add media',
441
411
  'field.media.replace': 'Replace',
@@ -446,7 +416,6 @@ export const en: Catalog = {
446
416
  'field.media.move_later': 'Move {name} later',
447
417
  'field.media.moved': 'Moved {name} to position {pos} of {total}',
448
418
 
449
- // combobox
450
419
  'combobox.moved': 'Moved {label} to position {pos} of {total}',
451
420
  'combobox.move_earlier': 'Move {label} earlier',
452
421
  'combobox.move_later': 'Move {label} later',
@@ -456,7 +425,6 @@ export const en: Catalog = {
456
425
  'combobox.loading': 'Loading…',
457
426
  'combobox.no_results': 'No results',
458
427
 
459
- // date pickers
460
428
  'datePicker.openCalendar': 'Open calendar',
461
429
  'datePicker.previousMonth': 'Previous month',
462
430
  'datePicker.nextMonth': 'Next month',
@@ -1,4 +1,4 @@
1
- import type { FieldDef } from '../../../core/server/utils/defineCollection'
1
+ import type { FieldDef } from '@michaelthielemann/kestrel-core'
2
2
 
3
3
  /** A record reference: its id plus a human label. Shared by UiCombobox and FieldRelation. */
4
4
  export interface FieldOption { value: number; label: string }
@@ -1,5 +1,5 @@
1
1
  import { defineAsyncComponent, type Component } from 'vue'
2
- import type { FieldType } from '../../../core/server/utils/defineCollection'
2
+ import type { FieldType } from '@michaelthielemann/kestrel-core'
3
3
  import FieldText from '../components/field/Text.vue'
4
4
  import FieldNumber from '../components/field/Number.vue'
5
5
  import FieldBoolean from '../components/field/Boolean.vue'
@@ -3,19 +3,14 @@ export type IconName =
3
3
  | 'copy' | 'trash' | 'x' | 'plus' | 'pencil' | 'check'
4
4
  | 'home' | 'file-text' | 'folder' | 'folder-plus' | 'panel-left-close' | 'panel-left-open' | 'log-out'
5
5
  | 'sun' | 'moon' | 'calendar' | 'upload' | 'layout-grid' | 'table'
6
- // collection icons + list toolbar
7
6
  | 'settings' | 'image' | 'newspaper' | 'columns' | 'filter' | 'search' | 'sliders'
8
- // richtext toolbar
9
7
  | 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code' | 'square-code' | 'highlighter'
10
8
  | 'subscript' | 'superscript' | 'list' | 'list-ordered' | 'quote' | 'minus' | 'message-square'
11
9
  | 'align-left' | 'align-center' | 'align-right' | 'align-justify'
12
10
  | 'eraser' | 'undo' | 'redo' | 'link' | 'link-2'
13
- | 'external-link' | 'triangle-alert'
14
- // password reveal
11
+ | 'external-link' | 'triangle-alert' | 'globe'
15
12
  | 'eye' | 'eye-off'
16
- // preview device widths + reload
17
13
  | 'monitor' | 'tablet' | 'smartphone' | 'rotate-cw'
18
- // block-type tiles
19
14
  | 'clock' | 'user'
20
15
 
21
16
  export const icons: Record<IconName, string> = {
@@ -74,6 +69,7 @@ export const icons: Record<IconName, string> = {
74
69
  'link': '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
75
70
  'link-2': '<path d="M9 17H7A5 5 0 0 1 7 7h2"/><path d="M15 7h2a5 5 0 1 1 0 10h-2"/><line x1="8" x2="16" y1="12" y2="12"/>',
76
71
  'external-link': '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"/>',
72
+ 'globe': '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
77
73
  'triangle-alert': '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
78
74
  'settings': '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
79
75
  'image': '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
@@ -1,4 +1,4 @@
1
- import type { Localized } from '../../../core/server/utils/defineCollection'
1
+ import type { Localized } from '@michaelthielemann/kestrel-core'
2
2
 
3
3
  /**
4
4
  * Resolve a (possibly localized) label for the active admin language. A plain string is returned as-is
package/nuxt.config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import kestrel from './kestrel.config'
2
- import { layerKestrelOption } from './layers/core/server/utils/kestrel-config'
2
+ import { layerKestrelOption } from '@michaelthielemann/kestrel-core'
3
3
 
4
4
  // Silence known-harmless, un-actionable THIRD-PARTY build noise (output unaffected — applied to both the
5
5
  // Vite client and the Nitro server Rollup passes). Everything else still surfaces — crucially, any
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaelthielemann/kestrel",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "A slim, collection-driven Nuxt 4 CMS meta-layer with a runtime schema engine. Add `extends: ['@michaelthielemann/kestrel']`, define collections, and the database migrates itself.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",
@@ -24,7 +24,6 @@
24
24
  "bin": {
25
25
  "kestrel": "./scripts/kestrel.mjs"
26
26
  },
27
- "packageManager": "pnpm@11.9.0",
28
27
  "publishConfig": {
29
28
  "access": "public",
30
29
  "provenance": true
@@ -45,21 +44,6 @@
45
44
  "node": ">=22.12.0"
46
45
  },
47
46
  "//lint": "The root eslint.config.mjs imports the generated playground/.nuxt/eslint.config.mjs (Nuxt-aware: auto-import globals for every layer + extension) — `nuxt prepare` must run first or that import 404s on a fresh checkout. Cheap on a warm .nuxt cache, so it's chained rather than a separate step.",
48
- "scripts": {
49
- "dev": "nuxt dev",
50
- "build": "nuxt build",
51
- "preview": "nuxt preview",
52
- "generate": "nuxt generate",
53
- "db:generate": "drizzle-kit generate",
54
- "db:migrate": "drizzle-kit migrate",
55
- "test": "vitest run",
56
- "test:watch": "vitest",
57
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
58
- "test:nuxt": "vitest run --config vitest.nuxt.config.ts",
59
- "typecheck": "node scripts/typecheck.mjs",
60
- "lint": "nuxt prepare playground && eslint .",
61
- "hash-password": "node scripts/hash-password.mjs"
62
- },
63
47
  "dependencies": {
64
48
  "@babel/parser": "^7.29.8",
65
49
  "@internationalized/date": "^3.12.3",
@@ -78,10 +62,12 @@
78
62
  "dompurify": "^3.4.13",
79
63
  "drizzle-orm": "^0.45.2",
80
64
  "drizzle-zod": "^0.8.3",
65
+ "effect": "~3.22.1",
81
66
  "exifr": "^7.1.3",
82
67
  "file-type": "^22.0.1",
83
68
  "jsdom": "^29.1.1",
84
69
  "nanoid": "^5.1.16",
70
+ "nitropack": "^2.13.4",
85
71
  "nuxt": "^4.5.2",
86
72
  "reka-ui": "^2.10.1",
87
73
  "sanitize-html": "^2.17.6",
@@ -89,7 +75,17 @@
89
75
  "sharp": "^0.35.3",
90
76
  "thumbhash": "^0.1.1",
91
77
  "typescript": "^6.0.3",
92
- "zod": "^4.4.3"
78
+ "zod": "^4.4.3",
79
+ "@michaelthielemann/kestrel-access": "0.1.0",
80
+ "@michaelthielemann/kestrel-auth": "0.1.0",
81
+ "@michaelthielemann/kestrel-core": "0.1.0",
82
+ "@michaelthielemann/kestrel-contracts": "0.1.0",
83
+ "@michaelthielemann/kestrel-collections": "0.1.0",
84
+ "@michaelthielemann/kestrel-delivery-live": "0.1.0",
85
+ "@michaelthielemann/kestrel-delivery-static": "0.1.0",
86
+ "@michaelthielemann/kestrel-fields": "0.1.0",
87
+ "@michaelthielemann/kestrel-media": "0.1.0",
88
+ "@michaelthielemann/kestrel-publishing": "0.1.0"
93
89
  },
94
90
  "//peers": "Framework singletons the layer sources import by bare specifier. Peers, not dependencies: a second copy breaks identity (two Vue instances; an h3 v1 `createError` handed to an h3 v2 error handler, which surfaces our 400/404/409 as bare 500s). All optional, because kestrel's own `nuxt` dependency already supplies them transitively — the entry pins identity where a copy exists rather than demanding one, and a required peer would ERESOLVE-fail npm installs on any Nuxt whose bundled range we did not anticipate. Each is also a devDependency so this repo resolves it without auto-install-peers writing a phantom root dependency into the lockfile.",
95
91
  "peerDependencies": {
@@ -113,20 +109,58 @@
113
109
  }
114
110
  },
115
111
  "devDependencies": {
112
+ "@arethetypeswrong/cli": "^0.18.5",
113
+ "@fast-check/vitest": "^0.4.1",
114
+ "@microsoft/api-extractor": "^7.58.13",
116
115
  "@nuxt/eslint": "^1.9.0",
117
116
  "@nuxt/kit": "^4.5.2",
118
117
  "@nuxt/test-utils": "^4.1.0",
118
+ "@stryker-mutator/core": "^10.0.0",
119
+ "@stryker-mutator/vitest-runner": "^10.0.0",
119
120
  "@vitejs/plugin-vue": "^6.0.8",
121
+ "@vitest/coverage-v8": "4.1.10",
120
122
  "@vue/test-utils": "^2.4.11",
121
123
  "drizzle-kit": "^0.31.10",
122
124
  "eslint": "^9.39.1",
125
+ "eslint-plugin-boundaries": "^7.2.0",
126
+ "eslint-plugin-jsdoc": "^64.2.1",
127
+ "eslint-plugin-tsdoc": "^0.5.2",
123
128
  "eslint-plugin-vuejs-accessibility": "^2.4.1",
129
+ "fast-check": "^4.9.0",
124
130
  "h3": "^1.15.11",
125
131
  "happy-dom": "^20.11.2",
126
132
  "playwright-core": "^1.62.1",
133
+ "publint": "^0.3.24",
134
+ "typedoc": "^0.28.20",
127
135
  "vitest": "^4.1.10",
136
+ "vitest-openapi": "^1.0.3",
128
137
  "vue": "3.5.35",
129
138
  "vue-router": "^5.2.0",
130
139
  "vue-tsc": "^3.3.9"
140
+ },
141
+ "scripts": {
142
+ "dev": "nuxt dev",
143
+ "build": "nuxt build",
144
+ "build:packages": "pnpm -F @michaelthielemann/kestrel-contracts build && pnpm -F @michaelthielemann/kestrel-core build && pnpm -F @michaelthielemann/kestrel-fields build && pnpm -F @michaelthielemann/kestrel-auth build && pnpm -F @michaelthielemann/kestrel-access build && pnpm -F @michaelthielemann/kestrel-media build && pnpm -F @michaelthielemann/kestrel-collections build && pnpm -F @michaelthielemann/kestrel-publishing build && pnpm -F @michaelthielemann/kestrel-delivery-live build && pnpm -F @michaelthielemann/kestrel-delivery-static build",
145
+ "preview": "nuxt preview",
146
+ "generate": "nuxt generate",
147
+ "db:generate": "drizzle-kit generate",
148
+ "db:migrate": "drizzle-kit migrate",
149
+ "test": "vitest run",
150
+ "test:coverage": "node scripts/coverage-all.mjs",
151
+ "test:watch": "vitest",
152
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
153
+ "test:nuxt": "vitest run --config vitest.nuxt.config.ts",
154
+ "test:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r test",
155
+ "typecheck": "node scripts/typecheck.mjs",
156
+ "typecheck:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r typecheck",
157
+ "lint": "nuxt prepare playground && eslint .",
158
+ "docs:api": "nuxt prepare playground && typedoc",
159
+ "api:check": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run -c packages/kestrel-core/api-extractor.json && api-extractor run -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run -c packages/kestrel-fields/api-extractor.json && api-extractor run -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run -c packages/kestrel-delivery-static/api-extractor.json",
160
+ "api:update": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run --local -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run --local -c packages/kestrel-core/api-extractor.json && api-extractor run --local -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run --local -c packages/kestrel-fields/api-extractor.json && api-extractor run --local -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run --local -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run --local -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run --local -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run --local -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run --local -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run --local -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run --local -c packages/kestrel-delivery-static/api-extractor.json",
161
+ "pkg:lint": "publint . && pnpm -F @michaelthielemann/kestrel-contracts build && publint packages/kestrel-contracts && attw --pack packages/kestrel-contracts --profile esm-only && pnpm -F @michaelthielemann/kestrel-core build && publint packages/kestrel-core && attw --pack packages/kestrel-core --profile esm-only && pnpm -F @michaelthielemann/kestrel-fields build && publint packages/kestrel-fields && attw --pack packages/kestrel-fields --profile esm-only && pnpm -F @michaelthielemann/kestrel-auth build && publint packages/kestrel-auth && attw --pack packages/kestrel-auth --profile esm-only && pnpm -F @michaelthielemann/kestrel-access build && publint packages/kestrel-access && attw --pack packages/kestrel-access --profile esm-only && pnpm -F @michaelthielemann/kestrel-media build && publint packages/kestrel-media && attw --pack packages/kestrel-media --profile esm-only && pnpm -F @michaelthielemann/kestrel-collections build && publint packages/kestrel-collections && attw --pack packages/kestrel-collections --profile esm-only && pnpm -F @michaelthielemann/kestrel-publishing build && publint packages/kestrel-publishing && attw --pack packages/kestrel-publishing --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-live build && publint packages/kestrel-delivery-live && attw --pack packages/kestrel-delivery-live --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-static build && publint packages/kestrel-delivery-static && attw --pack packages/kestrel-delivery-static --profile esm-only",
162
+ "ci:consumer-template": "node scripts/consumer-template-ci.mjs",
163
+ "dashboard": "npx tsx scripts/dashboard.mjs",
164
+ "hash-password": "node scripts/hash-password.mjs"
131
165
  }
132
- }
166
+ }
@@ -0,0 +1,157 @@
1
+ // The ONLY real package-consumption test. Every earlier gate (api:check, pkg:lint) inspects one
2
+ // package's own tarball in isolation; this script is the one that scaffolds an actual consumer from
3
+ // templates/starter (via create-kestrel's real CLI), packs the engine + every @michaelthielemann/kestrel-* package the way
4
+ // `npm publish` would, installs them into that consumer with a real `npm install` (no workspace protocol,
5
+ // no symlinks back into this repo), builds it, boots the production server, and hits it over real HTTP.
6
+ //
7
+ // Local: `pnpm ci:consumer-template`. CI: the `consumer-template` job in .github/workflows/ci.yml.
8
+ import { execFileSync, spawn } from 'node:child_process'
9
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
10
+ import { tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+ import { setTimeout as sleep } from 'node:timers/promises'
13
+
14
+ const root = process.cwd()
15
+ const log = (msg) => console.log(`[consumer-template-ci] ${msg}`)
16
+ const run = (cmd, args, opts = {}) => execFileSync(cmd, args, { stdio: 'inherit', ...opts })
17
+
18
+ // @michaelthielemann/kestrel-* in the order `pnpm --filter "@michaelthielemann/kestrel-*" -r exec` resolves them (a real topological walk of
19
+ // the workspace graph, not a hand guess) — mirrors the publish order in .github/workflows/release.yml.
20
+ const PACKAGES = ['contracts', 'core', 'auth', 'access', 'fields', 'collections', 'media', 'publishing', 'delivery-live', 'delivery-static']
21
+
22
+ const work = mkdtempSync(join(tmpdir(), 'kestrel-consumer-ci-'))
23
+ const tarballDir = join(work, 'tarballs')
24
+ const consumerDir = join(work, 'site')
25
+ mkdirSync(tarballDir, { recursive: true })
26
+ log(`work dir: ${work}`)
27
+
28
+ let server
29
+ try {
30
+ log('building @michaelthielemann/kestrel-* packages (dist/ must exist before pnpm pack)')
31
+ run('pnpm', ['--filter', '@michaelthielemann/kestrel-*', '-r', 'build'], { cwd: root })
32
+
33
+ log('packing the engine + every @michaelthielemann/kestrel-* package as npm publish would')
34
+ const tarball = {}
35
+ for (const dir of ['.', ...PACKAGES.map((n) => `packages/kestrel-${n}`)]) {
36
+ const out = execFileSync('pnpm', ['pack', '--pack-destination', tarballDir], { cwd: join(root, dir) }).toString().trim()
37
+ const file = out.split('\n').pop().trim()
38
+ const name = JSON.parse(readFileSync(join(root, dir, 'package.json'), 'utf8')).name
39
+ tarball[name] = file
40
+ log(` ${name} -> ${file}`)
41
+ }
42
+
43
+ log('scaffolding a consumer from templates/starter via the real create-kestrel CLI')
44
+ const password = 'consumer-ci-password'
45
+ run('node', [join(root, 'packages/create-kestrel/index.mjs'), consumerDir, '--yes', '--password', password], { cwd: work })
46
+
47
+ log('pointing the scaffolded package.json at the packed tarballs (no registry, no workspace protocol)')
48
+ const pkgPath = join(consumerDir, 'package.json')
49
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
50
+ pkg.dependencies['@michaelthielemann/kestrel'] = `file:${tarball['@michaelthielemann/kestrel']}`
51
+ // npm's `overrides` redirects a dependency AT ANY DEPTH — the engine tarball's own manifest declares
52
+ // "@michaelthielemann/kestrel-core": "0.1.0" etc. (pnpm pack already rewrote the workspace:* protocol to that real
53
+ // version), which would otherwise 404 against the real registry since these packages are not published
54
+ // yet in this checkout's tag. This is what actually forces npm's resolver to read each package's real
55
+ // `exports` map / `main` / `types` from a standalone tarball, the same way a real end user's install
56
+ // would — not a symlink into this repo's own packages/*/src.
57
+ pkg.overrides = Object.fromEntries(PACKAGES.map((n) => [`@michaelthielemann/kestrel-${n}`, `file:${tarball[`@michaelthielemann/kestrel-${n}`]}`]))
58
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
59
+
60
+ // The runner's ambient npm (10.9.8 as of this writing) hits a real npm/arborist bug resolving nuxt's
61
+ // large optional-peer set (`TypeError: Cannot read properties of null (reading 'edgesOut')`, inside
62
+ // `#loadPeerSet`) — reproduced directly, unrelated to anything this script does. npm >= 11 does not
63
+ // have it. `npx --yes npm@11` sidesteps needing a global npm upgrade (which needs root the runner may
64
+ // not have) while still using real npm, not pnpm.
65
+ log('npm install (real npm 11 via npx — the most representative simulation of an actual end user)')
66
+ run('npx', ['--yes', 'npm@11', 'install'], { cwd: consumerDir })
67
+
68
+ log('verifying no @michaelthielemann/kestrel-* dependency resolved back into this repo (would prove nothing)')
69
+ for (const n of PACKAGES) {
70
+ let resolved = ''
71
+ try {
72
+ resolved = execFileSync('npm', ['ls', `@michaelthielemann/kestrel-${n}`, '--json'], { cwd: consumerDir }).toString()
73
+ } catch (err) {
74
+ // `npm ls` exits non-zero on a peer-dep mismatch warning even when the tree itself resolved fine;
75
+ // its stdout (on the error object) still carries the real tree, which is what matters here.
76
+ resolved = err.stdout?.toString() ?? ''
77
+ }
78
+ if (resolved.includes(root)) throw new Error(`@michaelthielemann/kestrel-${n} resolved into ${root} — the override did not take effect`)
79
+ }
80
+
81
+ log('reading the .env create-kestrel wrote (session secret + admin password hash)')
82
+ const env = Object.fromEntries(
83
+ readFileSync(join(consumerDir, '.env'), 'utf8')
84
+ .split('\n')
85
+ .map((l) => /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(l))
86
+ .filter(Boolean)
87
+ .map((m) => [m[1], m[2]]),
88
+ )
89
+ // KESTREL_SECURE_COOKIES=false is what create-kestrel writes for a brand-new site (local http dev) —
90
+ // the app itself refuses it under a production boot ("KESTREL_SECURE_COOKIES=false is not allowed in
91
+ // production"), which is the correct, secure-by-default behavior, not a bug this script should route
92
+ // around by keeping the flag. Unset it; the production default is secure.
93
+ delete env.KESTREL_SECURE_COOKIES
94
+
95
+ const devPort = 41822
96
+ log('nuxt dev (brief boot, then kill) — schema auto-syncs additively at dev boot; a production boot never touches it (by design), so this is what a real `pnpm dev` first run also does')
97
+ const dev = spawn('npx', ['nuxt', 'dev', '--port', String(devPort)], {
98
+ cwd: consumerDir,
99
+ env: { ...process.env, ...env },
100
+ stdio: 'inherit',
101
+ })
102
+ try {
103
+ await waitForServer(`http://localhost:${devPort}/robots.txt`, 60000)
104
+ } finally {
105
+ dev.kill()
106
+ await sleep(1000) // let the dev Nitro process release the sqlite file before the production build reopens it
107
+ }
108
+
109
+ log('nuxt build (production) — the real exports-map/deps resolution surface, not a dev-mode shortcut')
110
+ run('npx', ['nuxt', 'build'], { cwd: consumerDir, env: { ...process.env, ...env } })
111
+
112
+ log('booting the built server and hitting it over real HTTP')
113
+ const port = 41823
114
+ server = spawn('node', [join(consumerDir, '.output/server/index.mjs')], {
115
+ cwd: consumerDir,
116
+ env: { ...process.env, ...env, PORT: String(port), NITRO_PORT: String(port) },
117
+ stdio: 'inherit',
118
+ })
119
+ await waitForServer(`http://localhost:${port}/robots.txt`, 30000)
120
+
121
+ const checks = [
122
+ ['GET /robots.txt', `http://localhost:${port}/robots.txt`, 200],
123
+ ['GET /sitemap.xml', `http://localhost:${port}/sitemap.xml`, 200],
124
+ ['GET /admin (login page, SPA shell)', `http://localhost:${port}/admin`, 200],
125
+ ]
126
+ for (const [label, url, wantStatus] of checks) {
127
+ const res = await fetch(url)
128
+ if (res.status !== wantStatus) throw new Error(`${label} -> ${res.status}, expected ${wantStatus}`)
129
+ log(` ${label} -> ${res.status} OK`)
130
+ }
131
+
132
+ const loginRes = await fetch(`http://localhost:${port}/api/login`, {
133
+ method: 'POST',
134
+ headers: { 'content-type': 'application/json' },
135
+ body: JSON.stringify({ password }),
136
+ })
137
+ if (loginRes.status !== 200) throw new Error(`POST /api/login -> ${loginRes.status}, expected 200 (the packed admin auth stack did not resolve)`)
138
+ log(' POST /api/login -> 200 OK (the packed @michaelthielemann/kestrel-auth + @michaelthielemann/kestrel-access stack authenticates for real)')
139
+
140
+ log('PASS — a real npm install of the packed engine + packages boots and serves under production, and login round-trips through the packed auth stack.')
141
+ } finally {
142
+ server?.kill()
143
+ if (!process.env.KEEP_CONSUMER_TEMPLATE_CI_WORKDIR) rmSync(work, { recursive: true, force: true })
144
+ else log(`KEEP_CONSUMER_TEMPLATE_CI_WORKDIR set — leaving ${work} in place`)
145
+ }
146
+
147
+ async function waitForServer(url, timeoutMs) {
148
+ const deadline = Date.now() + timeoutMs
149
+ for (;;) {
150
+ try {
151
+ const res = await fetch(url)
152
+ if (res.status) return
153
+ } catch { /* not up yet */ }
154
+ if (Date.now() > deadline) throw new Error(`server did not come up at ${url} within ${timeoutMs}ms`)
155
+ await sleep(250)
156
+ }
157
+ }