@michaelthielemann/kestrel 4.1.3 → 5.0.2

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