@michaelthielemann/kestrel 4.1.3 → 5.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 (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
@@ -0,0 +1,22 @@
1
+ import { silentLogger } from "../logger.js";
2
+ import { StepRegistry } from "../registry.js";
3
+ import { runPipeline as execute } from "../runner.js";
4
+ export function runPipeline(pipeline, input, options) {
5
+ const registry = new StepRegistry();
6
+ const byPrefix = new Map();
7
+ for (const [name, fn] of Object.entries(options.steps)) {
8
+ const dot = name.indexOf(".");
9
+ if (dot === -1)
10
+ throw new Error(`fake step "${name}" must be named "<module>.<step>"`);
11
+ const prefix = name.slice(0, dot);
12
+ const group = byPrefix.get(prefix) ?? {};
13
+ group[name.slice(dot + 1)] = fn;
14
+ byPrefix.set(prefix, group);
15
+ }
16
+ for (const [prefix, group] of byPrefix) {
17
+ const descriptions = Object.fromEntries(Object.keys(group).map((key) => [key, { summary: `${prefix}.${key}`, reads: [], writes: ["result"] }]));
18
+ registry.register("test", prefix, group, descriptions);
19
+ }
20
+ const resolved = { name: pipeline.name, steps: pipeline.steps.map((s) => registry.resolve(s, `pipelines/${pipeline.name}`)) };
21
+ return execute(resolved, { trigger: { kind: "http", name: "test" }, ...input }, options.logger ?? silentLogger);
22
+ }
@@ -0,0 +1,10 @@
1
+ import { type Logger } from "../logger.ts";
2
+ import type { Runner } from "../runner.ts";
3
+ import { type CronSpec } from "./cronMatch.ts";
4
+ export interface CronEntry {
5
+ expression: string;
6
+ spec: CronSpec;
7
+ pipeline: string;
8
+ }
9
+ export declare function createCronEntry(expression: string, pipeline: string): CronEntry;
10
+ export declare function startCron(entries: readonly CronEntry[], run: Runner, logger: Logger, now?: () => Date): () => void;
@@ -0,0 +1,44 @@
1
+ import { logWarn } from "../logger.js";
2
+ import { cronMatches, parseCron } from "./cronMatch.js";
3
+ export function createCronEntry(expression, pipeline) {
4
+ return { expression, spec: parseCron(expression), pipeline };
5
+ }
6
+ export function startCron(entries, run, logger, now = () => new Date()) {
7
+ let timer;
8
+ let stopped = false;
9
+ const running = new Set();
10
+ const tick = () => {
11
+ const date = now();
12
+ for (const entry of entries) {
13
+ if (!cronMatches(entry.spec, date))
14
+ continue;
15
+ if (running.has(entry.pipeline)) {
16
+ logWarn(logger, `cron "${entry.expression}" skipped: pipeline "${entry.pipeline}" is still running`, { cron: entry.expression, pipeline: entry.pipeline });
17
+ continue;
18
+ }
19
+ running.add(entry.pipeline);
20
+ void run(entry.pipeline, { trigger: { kind: "cron", name: entry.expression } })
21
+ .then((res) => {
22
+ if (res.status >= 400)
23
+ logger.error(`cron "${entry.expression}" pipeline "${entry.pipeline}" ended with ${res.status}`, { runId: res.runId, error: res.error });
24
+ }, (err) => {
25
+ logger.error(`cron "${entry.expression}" pipeline "${entry.pipeline}" threw`, { error: err instanceof Error ? (err.stack ?? err.message) : String(err) });
26
+ })
27
+ .finally(() => running.delete(entry.pipeline));
28
+ }
29
+ schedule();
30
+ };
31
+ const schedule = () => {
32
+ if (stopped)
33
+ return;
34
+ const ms = 60_000 - (now().getTime() % 60_000);
35
+ timer = setTimeout(tick, ms);
36
+ timer.unref();
37
+ };
38
+ schedule();
39
+ return () => {
40
+ stopped = true;
41
+ if (timer)
42
+ clearTimeout(timer);
43
+ };
44
+ }
@@ -0,0 +1,10 @@
1
+ export interface CronSpec {
2
+ minute: Set<number>;
3
+ hour: Set<number>;
4
+ dayOfMonth: Set<number>;
5
+ month: Set<number>;
6
+ dayOfWeek: Set<number>;
7
+ anyDay: boolean;
8
+ }
9
+ export declare function parseCron(expression: string): CronSpec;
10
+ export declare function cronMatches(spec: CronSpec, date: Date): boolean;
@@ -0,0 +1,68 @@
1
+ const RANGES = [
2
+ ["minute", 0, 59],
3
+ ["hour", 0, 23],
4
+ ["day of month", 1, 31],
5
+ ["month", 1, 12],
6
+ ["day of week", 0, 7],
7
+ ];
8
+ function parseField(field, label, min, max) {
9
+ const values = new Set();
10
+ let restricted = false;
11
+ for (const part of field.split(",")) {
12
+ const [rangePart, stepPart] = part.split("/");
13
+ if (rangePart === undefined || rangePart === "")
14
+ throw new Error(`cron ${label}: empty entry in "${field}"`);
15
+ const step = stepPart === undefined ? 1 : Number(stepPart);
16
+ if (!Number.isInteger(step) || step < 1)
17
+ throw new Error(`cron ${label}: invalid step in "${part}"`);
18
+ let lo;
19
+ let hi;
20
+ if (rangePart === "*") {
21
+ lo = min;
22
+ hi = max;
23
+ if (stepPart !== undefined)
24
+ restricted = true;
25
+ }
26
+ else {
27
+ restricted = true;
28
+ const [a, b] = rangePart.split("-");
29
+ lo = Number(a);
30
+ hi = b === undefined ? (stepPart === undefined ? lo : max) : Number(b);
31
+ if (!Number.isInteger(lo) || !Number.isInteger(hi) || lo < min || hi > max || lo > hi) {
32
+ throw new Error(`cron ${label}: "${part}" is outside ${min}-${max}`);
33
+ }
34
+ }
35
+ for (let v = lo; v <= hi; v += step)
36
+ values.add(v);
37
+ }
38
+ return { values, restricted };
39
+ }
40
+ export function parseCron(expression) {
41
+ const fields = expression.trim().split(/\s+/);
42
+ if (fields.length !== 5)
43
+ throw new Error(`cron expression "${expression}" must have 5 fields`);
44
+ const parsed = fields.map((f, i) => {
45
+ const [label, min, max] = RANGES[i];
46
+ return parseField(f, label, min, max);
47
+ });
48
+ const dow = parsed[4].values;
49
+ if (dow.has(7)) {
50
+ dow.delete(7);
51
+ dow.add(0);
52
+ }
53
+ return {
54
+ minute: parsed[0].values,
55
+ hour: parsed[1].values,
56
+ dayOfMonth: parsed[2].values,
57
+ month: parsed[3].values,
58
+ dayOfWeek: dow,
59
+ anyDay: parsed[2].restricted && parsed[4].restricted,
60
+ };
61
+ }
62
+ export function cronMatches(spec, date) {
63
+ if (!spec.minute.has(date.getMinutes()) || !spec.hour.has(date.getHours()) || !spec.month.has(date.getMonth() + 1))
64
+ return false;
65
+ const domOk = spec.dayOfMonth.has(date.getDate());
66
+ const dowOk = spec.dayOfWeek.has(date.getDay());
67
+ return spec.anyDay ? domOk || dowOk : domOk && dowOk;
68
+ }
@@ -0,0 +1,58 @@
1
+ import { type IncomingMessage, type Server } from "node:http";
2
+ import type { AddressInfo } from "node:net";
3
+ import { type UploadedFile } from "../context.ts";
4
+ import type { Logger } from "../logger.ts";
5
+ import type { RunResult, Runner } from "../runner.ts";
6
+ export interface Route {
7
+ method: string;
8
+ segments: readonly string[];
9
+ pipeline: string;
10
+ }
11
+ export declare function parseRoute(http: string, pipeline: string): Route;
12
+ /** Method plus segment shape; two routes differing only in a param name are the same pattern. */
13
+ export declare function routePattern(route: Route): string;
14
+ export declare function matchRoute(routes: readonly Route[], method: string, pathname: string): {
15
+ route: Route;
16
+ params: Record<string, string>;
17
+ } | undefined;
18
+ export declare class BodyTooLarge extends Error {
19
+ }
20
+ export declare function parseQuery(searchParams: URLSearchParams): Record<string, unknown>;
21
+ export interface ParsedBody {
22
+ payload: Record<string, unknown>;
23
+ files: UploadedFile[];
24
+ }
25
+ export declare function parseRequestBody(contentType: string, body: Uint8Array): Promise<ParsedBody>;
26
+ export interface HttpResponse {
27
+ status: number;
28
+ headers: Record<string, string>;
29
+ body: Uint8Array | string;
30
+ }
31
+ export interface ResponseMeta {
32
+ runId?: string;
33
+ requestId?: string;
34
+ retryable?: boolean;
35
+ retryAfterSeconds?: number;
36
+ }
37
+ export declare function buildResponse(status: number, body: unknown, meta?: ResponseMeta, extraInline?: readonly string[]): HttpResponse;
38
+ export declare function responseForRun(result: RunResult, extraInline?: readonly string[]): HttpResponse;
39
+ /** The one shape for errors raised before a pipeline runs (unknown route, unreadable body). */
40
+ export declare function errorResponse(status: number, error: string, requestId?: string): HttpResponse;
41
+ /** RFC 6266: a quoted ASCII fallback, plus filename* only when the name is not printable ASCII. */
42
+ export declare function disposition(contentType: string, filename: string | undefined, extraInline?: readonly string[]): string;
43
+ export interface HttpTimeouts {
44
+ requestMs?: number;
45
+ headersMs?: number;
46
+ keepAliveMs?: number;
47
+ }
48
+ export interface HttpOptions {
49
+ corsOrigin?: string;
50
+ maxBodyBytes?: number;
51
+ trustProxy?: boolean;
52
+ healthPath?: string | null;
53
+ inlineTypes?: readonly string[];
54
+ timeouts?: HttpTimeouts;
55
+ }
56
+ export declare function clientIp(req: IncomingMessage, trustProxy: boolean): string | undefined;
57
+ export declare function createHttpServer(routes: readonly Route[], run: Runner, logger: Logger, options?: HttpOptions): Server;
58
+ export declare function listen(server: Server, port: number, host: string): Promise<AddressInfo>;
@@ -0,0 +1,292 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { isBinaryResult, requestIdOf } from "../context.js";
4
+ export function parseRoute(http, pipeline) {
5
+ const [method, path] = http.split(" ");
6
+ if (method === undefined || path === undefined || !path.startsWith("/"))
7
+ throw new Error(`invalid http trigger "${http}"`);
8
+ const segments = path === "/" ? [] : path.slice(1).split("/");
9
+ if (segments.some((s) => s === ""))
10
+ throw new Error(`invalid http trigger "${http}": empty path segment`);
11
+ const wildcard = segments.findIndex((s) => s.startsWith("*"));
12
+ if (wildcard !== -1 && wildcard !== segments.length - 1)
13
+ throw new Error(`invalid http trigger "${http}": wildcard must be the last segment`);
14
+ return { method, segments, pipeline };
15
+ }
16
+ /** literal > :param > end of route > *rest, so a literal route wins no matter when it was registered. */
17
+ function specificity(segment) {
18
+ if (segment === undefined)
19
+ return 1;
20
+ if (segment.startsWith("*"))
21
+ return 0;
22
+ if (segment.startsWith(":"))
23
+ return 2;
24
+ return 3;
25
+ }
26
+ function bySpecificity(a, b) {
27
+ const len = Math.max(a.segments.length, b.segments.length);
28
+ for (let i = 0; i < len; i++) {
29
+ const diff = specificity(b.segments[i]) - specificity(a.segments[i]);
30
+ if (diff !== 0)
31
+ return diff;
32
+ }
33
+ return 0;
34
+ }
35
+ function decodeSegment(part) {
36
+ try {
37
+ return decodeURIComponent(part);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ /** Method plus segment shape; two routes differing only in a param name are the same pattern. */
44
+ export function routePattern(route) {
45
+ return `${route.method} /${route.segments.map((s) => (s.startsWith(":") ? ":" : s.startsWith("*") ? "*" : s)).join("/")}`;
46
+ }
47
+ export function matchRoute(routes, method, pathname) {
48
+ const parts = pathname === "/" ? [] : pathname.replace(/\/+$/, "").slice(1).split("/");
49
+ for (const route of [...routes].sort(bySpecificity)) {
50
+ if (route.method !== method)
51
+ continue;
52
+ const last = route.segments[route.segments.length - 1];
53
+ const wildcard = last?.startsWith("*") ? route.segments.length - 1 : -1;
54
+ if (wildcard === -1 ? route.segments.length !== parts.length : parts.length < wildcard)
55
+ continue;
56
+ const params = {};
57
+ let ok = true;
58
+ for (let i = 0; i < route.segments.length; i++) {
59
+ const seg = route.segments[i];
60
+ if (i === wildcard) {
61
+ const rest = parts.slice(i).map(decodeSegment);
62
+ // An encoded "/" would make the rest indistinguishable from a deeper path, so it never matches.
63
+ if (rest.some((p) => p === undefined || p.includes("/")))
64
+ ok = false;
65
+ else
66
+ params[seg.slice(1)] = rest.join("/");
67
+ break;
68
+ }
69
+ const part = decodeSegment(parts[i]);
70
+ if (part === undefined)
71
+ ok = false;
72
+ else if (seg.startsWith(":"))
73
+ params[seg.slice(1)] = part;
74
+ else if (seg !== part)
75
+ ok = false;
76
+ if (!ok)
77
+ break;
78
+ }
79
+ if (ok)
80
+ return { route, params };
81
+ }
82
+ return undefined;
83
+ }
84
+ export class BodyTooLarge extends Error {
85
+ }
86
+ async function readBody(req, maxBytes) {
87
+ const chunks = [];
88
+ let size = 0;
89
+ for await (const chunk of req) {
90
+ size += chunk.length;
91
+ if (size > maxBytes)
92
+ throw new BodyTooLarge(`body exceeds ${maxBytes} bytes`);
93
+ chunks.push(chunk);
94
+ }
95
+ return Buffer.concat(chunks);
96
+ }
97
+ export function parseQuery(searchParams) {
98
+ const payload = {};
99
+ for (const key of new Set(searchParams.keys())) {
100
+ const values = searchParams.getAll(key);
101
+ payload[key] = values.length > 1 ? values : values[0];
102
+ }
103
+ return payload;
104
+ }
105
+ export async function parseRequestBody(contentType, body) {
106
+ if (contentType.startsWith("multipart/form-data")) {
107
+ const copy = new Uint8Array(body.byteLength);
108
+ copy.set(body);
109
+ const form = await new Response(copy.buffer, { headers: { "content-type": contentType } }).formData();
110
+ const payload = {};
111
+ const files = [];
112
+ for (const [field, value] of form) {
113
+ if (typeof value === "string")
114
+ payload[field] = value;
115
+ else
116
+ files.push({ field, filename: value.name, contentType: value.type || "application/octet-stream", data: new Uint8Array(await value.arrayBuffer()) });
117
+ }
118
+ return { payload, files };
119
+ }
120
+ const text = Buffer.from(body).toString("utf8");
121
+ if (text.trim() === "")
122
+ return { payload: {}, files: [] };
123
+ const parsed = JSON.parse(text);
124
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
125
+ throw new Error("body must be a JSON object");
126
+ return { payload: parsed, files: [] };
127
+ }
128
+ const DEFAULT_RETRY_AFTER_SECONDS = 1;
129
+ function retryAfterSecondsOf(meta) {
130
+ const seconds = meta.retryAfterSeconds;
131
+ return typeof seconds === "number" && Number.isInteger(seconds) && seconds > 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
132
+ }
133
+ export function buildResponse(status, body, meta = {}, extraInline = []) {
134
+ const headers = { "x-content-type-options": "nosniff", "cache-control": "no-store" };
135
+ if (meta.runId !== undefined)
136
+ headers["x-kestrel-run-id"] = meta.runId;
137
+ if (meta.requestId !== undefined)
138
+ headers["x-request-id"] = meta.requestId;
139
+ if (meta.retryable === true && (status === 429 || status === 503))
140
+ headers["retry-after"] = String(retryAfterSecondsOf(meta));
141
+ if (isBinaryResult(body)) {
142
+ return {
143
+ status,
144
+ headers: {
145
+ // Caller headers may add, never weaken: the mandatory ones are spread last.
146
+ ...Object.fromEntries(Object.entries(body.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v])),
147
+ ...headers,
148
+ "content-type": body.contentType,
149
+ "content-length": String(body.data.byteLength),
150
+ "content-disposition": disposition(body.contentType, body.filename, extraInline),
151
+ "content-security-policy": "default-src 'none'; sandbox",
152
+ },
153
+ body: body.data,
154
+ };
155
+ }
156
+ return { status, headers: { ...headers, "content-type": "application/json; charset=utf-8" }, body: JSON.stringify(body ?? null) };
157
+ }
158
+ function errorBody(result) {
159
+ const body = { error: result.error, code: result.code, retryable: result.retryable, runId: result.runId };
160
+ if (result.step !== undefined)
161
+ body.step = result.step;
162
+ if (result.details !== undefined)
163
+ body.details = result.details;
164
+ return body;
165
+ }
166
+ function metaOf(result) {
167
+ const meta = { runId: result.runId };
168
+ if (result.requestId !== undefined)
169
+ meta.requestId = result.requestId;
170
+ if (result.retryable !== undefined)
171
+ meta.retryable = result.retryable;
172
+ const retryAfterSeconds = result.details?.retryAfterSeconds;
173
+ if (typeof retryAfterSeconds === "number")
174
+ meta.retryAfterSeconds = retryAfterSeconds;
175
+ return meta;
176
+ }
177
+ export function responseForRun(result, extraInline = []) {
178
+ if (result.status >= 400)
179
+ return buildResponse(result.status, errorBody(result), metaOf(result), extraInline);
180
+ return buildResponse(result.status, result.result, metaOf(result), extraInline);
181
+ }
182
+ /** Fixed code for errors raised before a pipeline runs, where there is no KestrelError to read one from. */
183
+ const EDGE_CODE_BY_STATUS = {
184
+ 400: "VALIDATION",
185
+ 404: "NOT_FOUND",
186
+ 413: "PAYLOAD_TOO_LARGE",
187
+ 500: "INTERNAL",
188
+ };
189
+ /** The one shape for errors raised before a pipeline runs (unknown route, unreadable body). */
190
+ export function errorResponse(status, error, requestId) {
191
+ const runId = randomUUID();
192
+ const code = EDGE_CODE_BY_STATUS[status] ?? "INTERNAL";
193
+ const body = { error, code, retryable: false, runId };
194
+ return buildResponse(status, body, requestId === undefined ? { runId } : { runId, requestId });
195
+ }
196
+ const INLINE_SAFE = new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif", "video/mp4", "video/webm", "audio/mpeg", "audio/ogg", "audio/wav"]);
197
+ /** RFC 6266: a quoted ASCII fallback, plus filename* only when the name is not printable ASCII. */
198
+ export function disposition(contentType, filename, extraInline = []) {
199
+ const mediaType = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
200
+ const mode = INLINE_SAFE.has(mediaType) || extraInline.includes(mediaType) ? "inline" : "attachment";
201
+ if (filename === undefined)
202
+ return mode;
203
+ const ascii = `${mode}; filename="${filename.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "\\$&")}"`;
204
+ if (/^[\x20-\x7e]*$/.test(filename))
205
+ return ascii;
206
+ const encoded = encodeURIComponent(filename).replace(/['()!*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
207
+ return `${ascii}; filename*=UTF-8''${encoded}`;
208
+ }
209
+ function sendError(res, status, error, requestId) {
210
+ write(res, errorResponse(status, error, requestId));
211
+ }
212
+ function write(res, response) {
213
+ res.writeHead(response.status, response.headers);
214
+ res.end(typeof response.body === "string" ? response.body : Buffer.from(response.body));
215
+ }
216
+ export function clientIp(req, trustProxy) {
217
+ if (trustProxy) {
218
+ const forwarded = req.headers["x-forwarded-for"];
219
+ const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim();
220
+ if (first)
221
+ return first;
222
+ }
223
+ return req.socket.remoteAddress ?? undefined;
224
+ }
225
+ export function createHttpServer(routes, run, logger, options = {}) {
226
+ const started = Date.now();
227
+ const healthPath = options.healthPath === undefined ? "/health" : options.healthPath;
228
+ const server = createServer((req, res) => {
229
+ if (options.corsOrigin !== undefined) {
230
+ res.setHeader("access-control-allow-origin", options.corsOrigin);
231
+ res.setHeader("access-control-allow-headers", "content-type, authorization");
232
+ res.setHeader("access-control-allow-methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
233
+ res.setHeader("vary", "origin");
234
+ if (req.method === "OPTIONS") {
235
+ res.writeHead(204, { "x-content-type-options": "nosniff", "cache-control": "no-store", "access-control-max-age": "600" });
236
+ res.end();
237
+ return;
238
+ }
239
+ }
240
+ void handle(req, res).catch((err) => {
241
+ logger.error("http handler failed", { error: err instanceof Error ? err.stack : String(err) });
242
+ if (!res.headersSent)
243
+ sendError(res, 500, "internal error", requestIdOf(req.headers["x-request-id"]));
244
+ });
245
+ });
246
+ server.requestTimeout = options.timeouts?.requestMs ?? 30_000;
247
+ server.headersTimeout = options.timeouts?.headersMs ?? 10_000;
248
+ server.keepAliveTimeout = options.timeouts?.keepAliveMs ?? 5_000;
249
+ return server;
250
+ async function handle(req, res) {
251
+ const url = new URL(req.url ?? "/", "http://localhost");
252
+ const requestId = requestIdOf(req.headers["x-request-id"]);
253
+ if (healthPath && url.pathname === healthPath && req.method === "GET") {
254
+ const meta = requestId === undefined ? { runId: randomUUID() } : { runId: randomUUID(), requestId };
255
+ return write(res, buildResponse(200, { ok: true, uptimeSeconds: Math.round((Date.now() - started) / 1000) }, meta));
256
+ }
257
+ const begin = performance.now();
258
+ const match = matchRoute(routes, req.method ?? "GET", url.pathname);
259
+ if (!match)
260
+ return sendError(res, 404, `no route for ${req.method} ${url.pathname}`, requestId);
261
+ let body;
262
+ try {
263
+ body = await parseRequestBody(req.headers["content-type"] ?? "", await readBody(req, options.maxBodyBytes ?? 10 * 1024 * 1024));
264
+ }
265
+ catch (err) {
266
+ if (err instanceof BodyTooLarge)
267
+ return sendError(res, 413, err.message, requestId);
268
+ return sendError(res, 400, `invalid body: ${err instanceof Error ? err.message : String(err)}`, requestId);
269
+ }
270
+ const payload = { ...parseQuery(url.searchParams), ...body.payload };
271
+ const headers = {};
272
+ for (const [k, v] of Object.entries(req.headers))
273
+ if (typeof v === "string")
274
+ headers[k] = v;
275
+ const input = { trigger: { kind: "http", name: `${req.method} ${url.pathname}` }, payload, params: match.params, headers, files: body.files };
276
+ const ip = clientIp(req, options.trustProxy ?? false);
277
+ if (ip !== undefined)
278
+ input.ip = ip;
279
+ const result = await run(match.route.pipeline, input);
280
+ logger.info("http", { runId: result.runId, ...(requestId === undefined ? {} : { requestId }), method: req.method, path: url.pathname, pipeline: match.route.pipeline, status: result.status, ms: Math.round((performance.now() - begin) * 100) / 100, ip });
281
+ write(res, responseForRun(result, options.inlineTypes));
282
+ }
283
+ }
284
+ export function listen(server, port, host) {
285
+ return new Promise((resolve, reject) => {
286
+ server.once("error", reject);
287
+ server.listen(port, host, () => {
288
+ server.off("error", reject);
289
+ resolve(server.address());
290
+ });
291
+ });
292
+ }