blokctl 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (689) hide show
  1. package/dist/commands/create/project.js +15 -4
  2. package/dist/commands/create/utils/Examples.d.ts +1 -1
  3. package/dist/commands/create/utils/Examples.js +1 -1
  4. package/dist/commands/runtime/shared.js +4 -0
  5. package/dist/scaffold-repo/examples/ts-workflows/mcp-greeter.ts +59 -0
  6. package/dist/scaffold-repo/examples/ts-workflows/runtime-csharp-hello.ts +36 -0
  7. package/dist/scaffold-repo/examples/ts-workflows/runtime-go-hello.ts +36 -0
  8. package/dist/scaffold-repo/examples/ts-workflows/runtime-java-hello.ts +36 -0
  9. package/dist/scaffold-repo/examples/ts-workflows/runtime-php-hello.ts +36 -0
  10. package/dist/scaffold-repo/examples/ts-workflows/runtime-python3-hello.ts +36 -0
  11. package/dist/scaffold-repo/examples/ts-workflows/runtime-ruby-hello.ts +36 -0
  12. package/dist/scaffold-repo/examples/ts-workflows/runtime-rust-hello.ts +36 -0
  13. package/dist/scaffold-repo/infra/development/3055.dat +602 -0
  14. package/dist/scaffold-repo/infra/development/3057.dat +203 -0
  15. package/dist/scaffold-repo/infra/development/3059.dat +19 -0
  16. package/dist/scaffold-repo/infra/development/3061.dat +1003 -0
  17. package/dist/scaffold-repo/infra/development/3062.dat +5465 -0
  18. package/dist/scaffold-repo/infra/development/3063.dat +1003 -0
  19. package/dist/scaffold-repo/infra/development/3065.dat +606 -0
  20. package/dist/scaffold-repo/infra/development/3067.dat +603 -0
  21. package/dist/scaffold-repo/infra/development/3069.dat +112 -0
  22. package/dist/scaffold-repo/infra/development/3071.dat +4584 -0
  23. package/dist/scaffold-repo/infra/development/3073.dat +9 -0
  24. package/dist/scaffold-repo/infra/development/3075.dat +14599 -0
  25. package/dist/scaffold-repo/infra/development/3077.dat +16047 -0
  26. package/dist/scaffold-repo/infra/development/3079.dat +5 -0
  27. package/dist/scaffold-repo/infra/development/3081.dat +5 -0
  28. package/dist/scaffold-repo/infra/development/docker-compose.yml +106 -0
  29. package/dist/scaffold-repo/infra/development/schema.sql +1616 -0
  30. package/dist/scaffold-repo/infra/development/toc.dat +0 -0
  31. package/dist/scaffold-repo/infra/metrics/alertmanager.yml +43 -0
  32. package/dist/scaffold-repo/infra/metrics/alloy-config.alloy +75 -0
  33. package/dist/scaffold-repo/infra/metrics/dashboard.json +1152 -0
  34. package/dist/scaffold-repo/infra/metrics/dashboards/blok-overview.json +314 -0
  35. package/dist/scaffold-repo/infra/metrics/dashboards/blok-system.json +175 -0
  36. package/dist/scaffold-repo/infra/metrics/dashboards/blok-tracing.json +235 -0
  37. package/dist/scaffold-repo/infra/metrics/dashboards/blok-triggers.json +258 -0
  38. package/dist/scaffold-repo/infra/metrics/datasources.yml +43 -0
  39. package/dist/scaffold-repo/infra/metrics/default.yaml +8 -0
  40. package/dist/scaffold-repo/infra/metrics/docker-compose.yml +193 -0
  41. package/dist/scaffold-repo/infra/metrics/loki-config.yaml +50 -0
  42. package/dist/scaffold-repo/infra/metrics/nginx.conf +25 -0
  43. package/dist/scaffold-repo/infra/metrics/prometheus.yml +51 -0
  44. package/dist/scaffold-repo/infra/metrics/rules/blok-alerts.yml +114 -0
  45. package/dist/scaffold-repo/infra/metrics/telegraf.conf +41 -0
  46. package/dist/scaffold-repo/infra/metrics/tempo.yaml +81 -0
  47. package/dist/scaffold-repo/infra/milvus/docker-compose.yml +73 -0
  48. package/dist/scaffold-repo/sdk/javascript/README.md +151 -0
  49. package/dist/scaffold-repo/sdk/javascript/bootstrap.min.css +11660 -0
  50. package/dist/scaffold-repo/sdk/javascript/image-ai.html +316 -0
  51. package/dist/scaffold-repo/sdk/javascript/nanosdk.js +180 -0
  52. package/dist/scaffold-repo/sdk/javascript/nanosdk.min.js +100 -0
  53. package/dist/scaffold-repo/sdk/javascript/pdf.html +63 -0
  54. package/dist/scaffold-repo/sdk/javascript/test.html +68 -0
  55. package/dist/scaffold-repo/sdks/CLAUDE.md +67 -0
  56. package/dist/scaffold-repo/sdks/csharp/.dockerignore +7 -0
  57. package/dist/scaffold-repo/sdks/csharp/Blok.Core.sln +24 -0
  58. package/dist/scaffold-repo/sdks/csharp/Dockerfile +42 -0
  59. package/dist/scaffold-repo/sdks/csharp/README.md +138 -0
  60. package/dist/scaffold-repo/sdks/csharp/proto/blok/runtime/v1/runtime.proto +302 -0
  61. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Blok.Core.csproj +39 -0
  62. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Config/ServerConfig.cs +90 -0
  63. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BlokError.cs +538 -0
  64. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BlokErrorCategory.cs +112 -0
  65. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BlokErrorSeverity.cs +40 -0
  66. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BuildContextSnapshot.cs +168 -0
  67. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/ErrorCategory.cs +13 -0
  68. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/NodeException.cs +44 -0
  69. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Logging/LogEntry.cs +34 -0
  70. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Logging/LogLevel.cs +12 -0
  71. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Logging/Logger.cs +92 -0
  72. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/IMiddleware.cs +14 -0
  73. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/LoggingMiddleware.cs +46 -0
  74. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/MiddlewareChain.cs +35 -0
  75. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/RecoveryMiddleware.cs +63 -0
  76. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Node/INodeHandler.cs +17 -0
  77. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Node/NodeRegistry.cs +128 -0
  78. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Node/TypedNode.cs +140 -0
  79. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/ApiCallNode.cs +129 -0
  80. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/BlokErrorDemoNode.cs +125 -0
  81. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/ChainTestNode.cs +79 -0
  82. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/HelloWorldNode.cs +45 -0
  83. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/TransformDataNode.cs +119 -0
  84. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/TypedGreetNode.cs +23 -0
  85. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Program.cs +26 -0
  86. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Server/BlokNodeRuntimeService.cs +536 -0
  87. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Server/RuntimeServer.cs +183 -0
  88. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Testing/MockContextBuilder.cs +117 -0
  89. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Testing/TestNodeRunner.cs +50 -0
  90. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/Context.cs +56 -0
  91. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/ExecutionMetrics.cs +21 -0
  92. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/ExecutionRequest.cs +15 -0
  93. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/ExecutionResult.cs +104 -0
  94. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/HealthStatus.cs +18 -0
  95. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/NodeConfig.cs +59 -0
  96. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/Request.cs +57 -0
  97. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/Response.cs +24 -0
  98. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/UserNodeRegistry.cs +17 -0
  99. package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Validation/SchemaValidator.cs +198 -0
  100. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Blok.Core.Tests.csproj +25 -0
  101. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Errors/BlokErrorTests.cs +458 -0
  102. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Logging/LoggerTests.cs +129 -0
  103. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Node/NodeRegistryTests.cs +145 -0
  104. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Testing/MockContextTests.cs +153 -0
  105. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/TypedNodeTests.cs +69 -0
  106. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Types/ExecutionResultTests.cs +110 -0
  107. package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Validation/SchemaValidatorTests.cs +233 -0
  108. package/dist/scaffold-repo/sdks/go/Dockerfile +33 -0
  109. package/dist/scaffold-repo/sdks/go/README.md +255 -0
  110. package/dist/scaffold-repo/sdks/go/blok_error.go +803 -0
  111. package/dist/scaffold-repo/sdks/go/blok_error_test.go +433 -0
  112. package/dist/scaffold-repo/sdks/go/cmd/server/main.go +45 -0
  113. package/dist/scaffold-repo/sdks/go/cmd/server/register_user_nodes.go +12 -0
  114. package/dist/scaffold-repo/sdks/go/config.go +157 -0
  115. package/dist/scaffold-repo/sdks/go/config_test.go +129 -0
  116. package/dist/scaffold-repo/sdks/go/define_node.go +87 -0
  117. package/dist/scaffold-repo/sdks/go/define_node_test.go +75 -0
  118. package/dist/scaffold-repo/sdks/go/errors.go +104 -0
  119. package/dist/scaffold-repo/sdks/go/errors_test.go +95 -0
  120. package/dist/scaffold-repo/sdks/go/examples/nodes/apicall.go +117 -0
  121. package/dist/scaffold-repo/sdks/go/examples/nodes/blokerrordemo.go +80 -0
  122. package/dist/scaffold-repo/sdks/go/examples/nodes/chaintest.go +63 -0
  123. package/dist/scaffold-repo/sdks/go/examples/nodes/helloworld.go +41 -0
  124. package/dist/scaffold-repo/sdks/go/examples/nodes/transformdata.go +176 -0
  125. package/dist/scaffold-repo/sdks/go/examples/nodes/typedgreet.go +34 -0
  126. package/dist/scaffold-repo/sdks/go/genpb/blok/runtime/v1/runtime.pb.go +2070 -0
  127. package/dist/scaffold-repo/sdks/go/genpb/blok/runtime/v1/runtime_grpc.pb.go +273 -0
  128. package/dist/scaffold-repo/sdks/go/go.mod +25 -0
  129. package/dist/scaffold-repo/sdks/go/go.sum +66 -0
  130. package/dist/scaffold-repo/sdks/go/grpc_server.go +670 -0
  131. package/dist/scaffold-repo/sdks/go/grpc_server_test.go +474 -0
  132. package/dist/scaffold-repo/sdks/go/logging.go +144 -0
  133. package/dist/scaffold-repo/sdks/go/logging_test.go +139 -0
  134. package/dist/scaffold-repo/sdks/go/middleware.go +152 -0
  135. package/dist/scaffold-repo/sdks/go/middleware_test.go +222 -0
  136. package/dist/scaffold-repo/sdks/go/node.go +50 -0
  137. package/dist/scaffold-repo/sdks/go/proto/blok/runtime/v1/runtime.proto +302 -0
  138. package/dist/scaffold-repo/sdks/go/registry.go +156 -0
  139. package/dist/scaffold-repo/sdks/go/registry_test.go +195 -0
  140. package/dist/scaffold-repo/sdks/go/server.go +207 -0
  141. package/dist/scaffold-repo/sdks/go/server_test.go +186 -0
  142. package/dist/scaffold-repo/sdks/go/shutdown.go +32 -0
  143. package/dist/scaffold-repo/sdks/go/testutil.go +169 -0
  144. package/dist/scaffold-repo/sdks/go/testutil_test.go +124 -0
  145. package/dist/scaffold-repo/sdks/go/types.go +295 -0
  146. package/dist/scaffold-repo/sdks/go/types_test.go +251 -0
  147. package/dist/scaffold-repo/sdks/go/validation.go +210 -0
  148. package/dist/scaffold-repo/sdks/go/validation_test.go +262 -0
  149. package/dist/scaffold-repo/sdks/go/worker.go +577 -0
  150. package/dist/scaffold-repo/sdks/java/Dockerfile +33 -0
  151. package/dist/scaffold-repo/sdks/java/README.md +221 -0
  152. package/dist/scaffold-repo/sdks/java/pom.xml +163 -0
  153. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/Main.java +90 -0
  154. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/UserNodeRegistry.java +19 -0
  155. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/config/ServerConfig.java +244 -0
  156. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BlokError.java +576 -0
  157. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BlokErrorCategory.java +68 -0
  158. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BlokErrorSeverity.java +31 -0
  159. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BuildContextSnapshot.java +132 -0
  160. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/ErrorCategory.java +22 -0
  161. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/NodeException.java +176 -0
  162. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/logging/LogEntry.java +78 -0
  163. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/logging/LogLevel.java +48 -0
  164. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/logging/Logger.java +160 -0
  165. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/LoggingMiddleware.java +65 -0
  166. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/Middleware.java +29 -0
  167. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/MiddlewareChain.java +59 -0
  168. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/RecoveryMiddleware.java +53 -0
  169. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/NodeHandler.java +40 -0
  170. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/NodeReflector.java +18 -0
  171. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/NodeRegistry.java +205 -0
  172. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/TypedNode.java +143 -0
  173. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/ApiCallNode.java +145 -0
  174. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/BlokErrorDemoNode.java +109 -0
  175. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/ChainTestNode.java +65 -0
  176. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/HelloWorldNode.java +62 -0
  177. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/TransformDataNode.java +121 -0
  178. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/TypedGreetNode.java +41 -0
  179. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/server/BlokGrpcServer.java +103 -0
  180. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/server/BlokNodeRuntimeService.java +501 -0
  181. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/server/RuntimeServer.java +176 -0
  182. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/testing/MockContext.java +120 -0
  183. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/testing/TestRunner.java +59 -0
  184. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/Context.java +167 -0
  185. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/ExecutionMetrics.java +80 -0
  186. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/ExecutionRequest.java +61 -0
  187. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/ExecutionResult.java +188 -0
  188. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/HealthStatus.java +77 -0
  189. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/NodeConfig.java +139 -0
  190. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/Request.java +154 -0
  191. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/Response.java +77 -0
  192. package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/validation/SchemaValidator.java +178 -0
  193. package/dist/scaffold-repo/sdks/java/src/main/proto/blok/runtime/v1/runtime.proto +302 -0
  194. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/errors/BlokErrorTest.java +416 -0
  195. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/logging/LoggerTest.java +176 -0
  196. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/node/NodeRegistryTest.java +227 -0
  197. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/node/TypedNodeTest.java +76 -0
  198. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/server/RuntimeServerTest.java +197 -0
  199. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/types/ExecutionResultTest.java +194 -0
  200. package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/validation/SchemaValidatorTest.java +246 -0
  201. package/dist/scaffold-repo/sdks/php/.rr.yaml +29 -0
  202. package/dist/scaffold-repo/sdks/php/Dockerfile +61 -0
  203. package/dist/scaffold-repo/sdks/php/README.md +387 -0
  204. package/dist/scaffold-repo/sdks/php/bin/serve.php +95 -0
  205. package/dist/scaffold-repo/sdks/php/composer.json +41 -0
  206. package/dist/scaffold-repo/sdks/php/examples/ApiCallNode.php +103 -0
  207. package/dist/scaffold-repo/sdks/php/examples/BlokErrorDemoNode.php +90 -0
  208. package/dist/scaffold-repo/sdks/php/examples/ChainTestNode.php +56 -0
  209. package/dist/scaffold-repo/sdks/php/examples/HelloWorldNode.php +39 -0
  210. package/dist/scaffold-repo/sdks/php/examples/TransformDataNode.php +94 -0
  211. package/dist/scaffold-repo/sdks/php/examples/TypedGreetNode.php +57 -0
  212. package/dist/scaffold-repo/sdks/php/phpunit.xml +23 -0
  213. package/dist/scaffold-repo/sdks/php/proto/blok/runtime/v1/runtime.proto +302 -0
  214. package/dist/scaffold-repo/sdks/php/src/Config/ServerConfig.php +83 -0
  215. package/dist/scaffold-repo/sdks/php/src/Errors/BlokError.php +467 -0
  216. package/dist/scaffold-repo/sdks/php/src/Errors/BlokErrorBuilder.php +121 -0
  217. package/dist/scaffold-repo/sdks/php/src/Errors/BlokErrorCategory.php +83 -0
  218. package/dist/scaffold-repo/sdks/php/src/Errors/BlokErrorSeverity.php +30 -0
  219. package/dist/scaffold-repo/sdks/php/src/Errors/BuildContextSnapshot.php +141 -0
  220. package/dist/scaffold-repo/sdks/php/src/Errors/ErrorCategory.php +17 -0
  221. package/dist/scaffold-repo/sdks/php/src/Errors/NodeException.php +107 -0
  222. package/dist/scaffold-repo/sdks/php/src/Errors/Origin.php +35 -0
  223. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ErrorCategory.php +118 -0
  224. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ErrorSeverity.php +72 -0
  225. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteEvent.php +181 -0
  226. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteOptions.php +159 -0
  227. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteRequest.php +328 -0
  228. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteResponse.php +288 -0
  229. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/HealthRequest.php +59 -0
  230. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/HealthResponse/Status.php +54 -0
  231. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/HealthResponse.php +113 -0
  232. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ListNodesRequest.php +32 -0
  233. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ListNodesResponse.php +154 -0
  234. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/LogLine.php +156 -0
  235. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/Metrics.php +165 -0
  236. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeDescriptor.php +174 -0
  237. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeError.php +627 -0
  238. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeRef.php +134 -0
  239. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeRuntimeClient.php +101 -0
  240. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeStarted.php +68 -0
  241. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/PartialResult.php +59 -0
  242. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/Progress.php +85 -0
  243. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/RuntimeState.php +142 -0
  244. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/StepInfo.php +168 -0
  245. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/TriggerInfo.php +289 -0
  246. package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/WorkflowInfo.php +176 -0
  247. package/dist/scaffold-repo/sdks/php/src/Genpb/GPBMetadata/Blok/Runtime/V1/Runtime.php +26 -0
  248. package/dist/scaffold-repo/sdks/php/src/Logging/LogEntry.php +47 -0
  249. package/dist/scaffold-repo/sdks/php/src/Logging/LogLevel.php +50 -0
  250. package/dist/scaffold-repo/sdks/php/src/Logging/Logger.php +133 -0
  251. package/dist/scaffold-repo/sdks/php/src/Node/NodeHandler.php +35 -0
  252. package/dist/scaffold-repo/sdks/php/src/Node/NodeReflector.php +21 -0
  253. package/dist/scaffold-repo/sdks/php/src/Node/NodeRegistry.php +172 -0
  254. package/dist/scaffold-repo/sdks/php/src/Node/TypedNode.php +159 -0
  255. package/dist/scaffold-repo/sdks/php/src/Server/BlokNodeRuntimeService.php +504 -0
  256. package/dist/scaffold-repo/sdks/php/src/Server/DecodeException.php +13 -0
  257. package/dist/scaffold-repo/sdks/php/src/Server/Middleware.php +18 -0
  258. package/dist/scaffold-repo/sdks/php/src/Server/MiddlewarePipeline.php +33 -0
  259. package/dist/scaffold-repo/sdks/php/src/Server/NodeRuntimeInterface.php +40 -0
  260. package/dist/scaffold-repo/sdks/php/src/Server/Server.php +157 -0
  261. package/dist/scaffold-repo/sdks/php/src/Testing/MockContext.php +161 -0
  262. package/dist/scaffold-repo/sdks/php/src/Testing/TestRunner.php +78 -0
  263. package/dist/scaffold-repo/sdks/php/src/Types/Context.php +80 -0
  264. package/dist/scaffold-repo/sdks/php/src/Types/ExecutionMetrics.php +48 -0
  265. package/dist/scaffold-repo/sdks/php/src/Types/ExecutionRequest.php +38 -0
  266. package/dist/scaffold-repo/sdks/php/src/Types/ExecutionResult.php +143 -0
  267. package/dist/scaffold-repo/sdks/php/src/Types/HealthStatus.php +42 -0
  268. package/dist/scaffold-repo/sdks/php/src/Types/NodeConfig.php +72 -0
  269. package/dist/scaffold-repo/sdks/php/src/Types/Request.php +82 -0
  270. package/dist/scaffold-repo/sdks/php/src/Types/Response.php +44 -0
  271. package/dist/scaffold-repo/sdks/php/src/Validation/SchemaValidator.php +197 -0
  272. package/dist/scaffold-repo/sdks/php/tests/Errors/BlokErrorTest.php +402 -0
  273. package/dist/scaffold-repo/sdks/php/tests/Logging/LoggerTest.php +165 -0
  274. package/dist/scaffold-repo/sdks/php/tests/Node/NodeRegistryTest.php +139 -0
  275. package/dist/scaffold-repo/sdks/php/tests/Node/TypedNodeTest.php +84 -0
  276. package/dist/scaffold-repo/sdks/php/tests/Server/BlokNodeRuntimeServiceTest.php +288 -0
  277. package/dist/scaffold-repo/sdks/php/tests/Testing/MockContextTest.php +184 -0
  278. package/dist/scaffold-repo/sdks/php/tests/Types/ExecutionResultTest.php +136 -0
  279. package/dist/scaffold-repo/sdks/php/tests/Validation/SchemaValidatorTest.php +191 -0
  280. package/dist/scaffold-repo/sdks/python3/Dockerfile +62 -0
  281. package/dist/scaffold-repo/sdks/python3/bin/serve.py +174 -0
  282. package/dist/scaffold-repo/sdks/python3/bin/worker.py +60 -0
  283. package/dist/scaffold-repo/sdks/python3/blok/__init__.py +61 -0
  284. package/dist/scaffold-repo/sdks/python3/blok/config/__init__.py +3 -0
  285. package/dist/scaffold-repo/sdks/python3/blok/config/server_config.py +116 -0
  286. package/dist/scaffold-repo/sdks/python3/blok/errors/__init__.py +50 -0
  287. package/dist/scaffold-repo/sdks/python3/blok/errors/blok_error.py +555 -0
  288. package/dist/scaffold-repo/sdks/python3/blok/errors/node_error.py +64 -0
  289. package/dist/scaffold-repo/sdks/python3/blok/logging/__init__.py +3 -0
  290. package/dist/scaffold-repo/sdks/python3/blok/logging/logger.py +82 -0
  291. package/dist/scaffold-repo/sdks/python3/blok/middleware/__init__.py +4 -0
  292. package/dist/scaffold-repo/sdks/python3/blok/middleware/logging_middleware.py +35 -0
  293. package/dist/scaffold-repo/sdks/python3/blok/middleware/recovery_middleware.py +36 -0
  294. package/dist/scaffold-repo/sdks/python3/blok/node/__init__.py +4 -0
  295. package/dist/scaffold-repo/sdks/python3/blok/node/define_node.py +239 -0
  296. package/dist/scaffold-repo/sdks/python3/blok/node/node_handler.py +35 -0
  297. package/dist/scaffold-repo/sdks/python3/blok/node/node_registry.py +136 -0
  298. package/dist/scaffold-repo/sdks/python3/blok/runtime/__init__.py +0 -0
  299. package/dist/scaffold-repo/sdks/python3/blok/runtime/v1/__init__.py +0 -0
  300. package/dist/scaffold-repo/sdks/python3/blok/runtime/v1/runtime_pb2.py +112 -0
  301. package/dist/scaffold-repo/sdks/python3/blok/runtime/v1/runtime_pb2_grpc.py +259 -0
  302. package/dist/scaffold-repo/sdks/python3/blok/server/__init__.py +3 -0
  303. package/dist/scaffold-repo/sdks/python3/blok/server/grpc_server.py +757 -0
  304. package/dist/scaffold-repo/sdks/python3/blok/server/runtime_server.py +165 -0
  305. package/dist/scaffold-repo/sdks/python3/blok/types/__init__.py +13 -0
  306. package/dist/scaffold-repo/sdks/python3/blok/types/context.py +162 -0
  307. package/dist/scaffold-repo/sdks/python3/blok/types/execution_request.py +74 -0
  308. package/dist/scaffold-repo/sdks/python3/blok/types/execution_result.py +103 -0
  309. package/dist/scaffold-repo/sdks/python3/blok/worker/__init__.py +10 -0
  310. package/dist/scaffold-repo/sdks/python3/blok/worker/job_message.py +34 -0
  311. package/dist/scaffold-repo/sdks/python3/blok/worker/worker.py +451 -0
  312. package/dist/scaffold-repo/sdks/python3/blok/worker/worker_config.py +129 -0
  313. package/dist/scaffold-repo/sdks/python3/examples/__init__.py +32 -0
  314. package/dist/scaffold-repo/sdks/python3/examples/api_call_node.py +85 -0
  315. package/dist/scaffold-repo/sdks/python3/examples/blok_error_demo_node.py +79 -0
  316. package/dist/scaffold-repo/sdks/python3/examples/chain_test_node.py +51 -0
  317. package/dist/scaffold-repo/sdks/python3/examples/hello_world_node.py +44 -0
  318. package/dist/scaffold-repo/sdks/python3/examples/transform_data_node.py +71 -0
  319. package/dist/scaffold-repo/sdks/python3/examples/typed_greet_node.py +25 -0
  320. package/dist/scaffold-repo/sdks/python3/proto/blok/runtime/v1/runtime.proto +302 -0
  321. package/dist/scaffold-repo/sdks/python3/pyproject.toml +47 -0
  322. package/dist/scaffold-repo/sdks/python3/requirements.txt +17 -0
  323. package/dist/scaffold-repo/sdks/python3/tests/__init__.py +0 -0
  324. package/dist/scaffold-repo/sdks/python3/tests/test_blok_error.py +341 -0
  325. package/dist/scaffold-repo/sdks/python3/tests/test_define_node.py +135 -0
  326. package/dist/scaffold-repo/sdks/python3/tests/test_discovery.py +99 -0
  327. package/dist/scaffold-repo/sdks/python3/tests/test_grpc_codec.py +260 -0
  328. package/dist/scaffold-repo/sdks/python3/tests/test_grpc_server.py +398 -0
  329. package/dist/scaffold-repo/sdks/ruby/Dockerfile +37 -0
  330. package/dist/scaffold-repo/sdks/ruby/Gemfile +5 -0
  331. package/dist/scaffold-repo/sdks/ruby/README.md +210 -0
  332. package/dist/scaffold-repo/sdks/ruby/Rakefile +11 -0
  333. package/dist/scaffold-repo/sdks/ruby/bin/serve.rb +105 -0
  334. package/dist/scaffold-repo/sdks/ruby/blok.gemspec +43 -0
  335. package/dist/scaffold-repo/sdks/ruby/config.ru +15 -0
  336. package/dist/scaffold-repo/sdks/ruby/examples/api_call_node.rb +99 -0
  337. package/dist/scaffold-repo/sdks/ruby/examples/blok_error_demo_node.rb +79 -0
  338. package/dist/scaffold-repo/sdks/ruby/examples/chain_test_node.rb +51 -0
  339. package/dist/scaffold-repo/sdks/ruby/examples/hello_world_node.rb +40 -0
  340. package/dist/scaffold-repo/sdks/ruby/examples/transform_data_node.rb +82 -0
  341. package/dist/scaffold-repo/sdks/ruby/examples/typed_greet_node.rb +25 -0
  342. package/dist/scaffold-repo/sdks/ruby/lib/blok/config/server_config.rb +76 -0
  343. package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/blok_error.rb +476 -0
  344. package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/build_context_snapshot.rb +99 -0
  345. package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/error_category.rb +16 -0
  346. package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/node_error.rb +84 -0
  347. package/dist/scaffold-repo/sdks/ruby/lib/blok/logging/log_entry.rb +46 -0
  348. package/dist/scaffold-repo/sdks/ruby/lib/blok/logging/log_level.rb +49 -0
  349. package/dist/scaffold-repo/sdks/ruby/lib/blok/logging/logger.rb +71 -0
  350. package/dist/scaffold-repo/sdks/ruby/lib/blok/middleware/logging_middleware.rb +45 -0
  351. package/dist/scaffold-repo/sdks/ruby/lib/blok/middleware/middleware.rb +36 -0
  352. package/dist/scaffold-repo/sdks/ruby/lib/blok/middleware/recovery_middleware.rb +31 -0
  353. package/dist/scaffold-repo/sdks/ruby/lib/blok/node/discovery.rb +59 -0
  354. package/dist/scaffold-repo/sdks/ruby/lib/blok/node/node_handler.rb +43 -0
  355. package/dist/scaffold-repo/sdks/ruby/lib/blok/node/node_registry.rb +107 -0
  356. package/dist/scaffold-repo/sdks/ruby/lib/blok/node/typed_node.rb +180 -0
  357. package/dist/scaffold-repo/sdks/ruby/lib/blok/runtime/v1/runtime_pb.rb +43 -0
  358. package/dist/scaffold-repo/sdks/ruby/lib/blok/runtime/v1/runtime_services_pb.rb +55 -0
  359. package/dist/scaffold-repo/sdks/ruby/lib/blok/server/grpc_server.rb +435 -0
  360. package/dist/scaffold-repo/sdks/ruby/lib/blok/server/runtime_app.rb +86 -0
  361. package/dist/scaffold-repo/sdks/ruby/lib/blok/testing/mock_context.rb +109 -0
  362. package/dist/scaffold-repo/sdks/ruby/lib/blok/testing/test_runner.rb +40 -0
  363. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/context.rb +82 -0
  364. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/execution_metrics.rb +43 -0
  365. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/execution_request.rb +39 -0
  366. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/execution_result.rb +117 -0
  367. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/health_status.rb +42 -0
  368. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/node_config.rb +73 -0
  369. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/request.rb +71 -0
  370. package/dist/scaffold-repo/sdks/ruby/lib/blok/types/response.rb +46 -0
  371. package/dist/scaffold-repo/sdks/ruby/lib/blok/validation/schema_validator.rb +142 -0
  372. package/dist/scaffold-repo/sdks/ruby/lib/blok/version.rb +5 -0
  373. package/dist/scaffold-repo/sdks/ruby/lib/blok.rb +53 -0
  374. package/dist/scaffold-repo/sdks/ruby/proto/blok/runtime/v1/runtime.proto +302 -0
  375. package/dist/scaffold-repo/sdks/ruby/test/errors/blok_error_test.rb +339 -0
  376. package/dist/scaffold-repo/sdks/ruby/test/logging/logger_test.rb +155 -0
  377. package/dist/scaffold-repo/sdks/ruby/test/node/node_registry_test.rb +132 -0
  378. package/dist/scaffold-repo/sdks/ruby/test/node/typed_node_test.rb +67 -0
  379. package/dist/scaffold-repo/sdks/ruby/test/test_helper.rb +6 -0
  380. package/dist/scaffold-repo/sdks/ruby/test/testing/mock_context_test.rb +150 -0
  381. package/dist/scaffold-repo/sdks/ruby/test/types/execution_result_test.rb +129 -0
  382. package/dist/scaffold-repo/sdks/ruby/test/validation/schema_validator_test.rb +270 -0
  383. package/dist/scaffold-repo/sdks/rust/Cargo.lock +3044 -0
  384. package/dist/scaffold-repo/sdks/rust/Cargo.toml +82 -0
  385. package/dist/scaffold-repo/sdks/rust/Dockerfile +40 -0
  386. package/dist/scaffold-repo/sdks/rust/README.md +73 -0
  387. package/dist/scaffold-repo/sdks/rust/build.rs +32 -0
  388. package/dist/scaffold-repo/sdks/rust/proto/blok/runtime/v1/runtime.proto +302 -0
  389. package/dist/scaffold-repo/sdks/rust/src/blok_error.rs +1168 -0
  390. package/dist/scaffold-repo/sdks/rust/src/config.rs +158 -0
  391. package/dist/scaffold-repo/sdks/rust/src/errors.rs +142 -0
  392. package/dist/scaffold-repo/sdks/rust/src/grpc_server.rs +635 -0
  393. package/dist/scaffold-repo/sdks/rust/src/lib.rs +65 -0
  394. package/dist/scaffold-repo/sdks/rust/src/logging.rs +195 -0
  395. package/dist/scaffold-repo/sdks/rust/src/main.rs +79 -0
  396. package/dist/scaffold-repo/sdks/rust/src/middleware.rs +181 -0
  397. package/dist/scaffold-repo/sdks/rust/src/node.rs +172 -0
  398. package/dist/scaffold-repo/sdks/rust/src/nodes/api_call.rs +93 -0
  399. package/dist/scaffold-repo/sdks/rust/src/nodes/blok_error_demo.rs +151 -0
  400. package/dist/scaffold-repo/sdks/rust/src/nodes/chain_test.rs +57 -0
  401. package/dist/scaffold-repo/sdks/rust/src/nodes/hello_world.rs +83 -0
  402. package/dist/scaffold-repo/sdks/rust/src/nodes/mod.rs +18 -0
  403. package/dist/scaffold-repo/sdks/rust/src/nodes/transform_data.rs +142 -0
  404. package/dist/scaffold-repo/sdks/rust/src/nodes/typed_greet.rs +56 -0
  405. package/dist/scaffold-repo/sdks/rust/src/registry.rs +204 -0
  406. package/dist/scaffold-repo/sdks/rust/src/server.rs +63 -0
  407. package/dist/scaffold-repo/sdks/rust/src/testing.rs +175 -0
  408. package/dist/scaffold-repo/sdks/rust/src/types.rs +338 -0
  409. package/dist/scaffold-repo/sdks/rust/src/user_nodes/mod.rs +10 -0
  410. package/dist/scaffold-repo/sdks/rust/src/validation.rs +232 -0
  411. package/dist/scaffold-repo/sdks/rust/src/worker.rs +509 -0
  412. package/dist/scaffold-repo/sdks/rust/tests/grpc_integration.rs +306 -0
  413. package/dist/scaffold-repo/sdks/rust/tests/typed_node.rs +104 -0
  414. package/dist/scaffold-repo/triggers/cron/CHANGELOG.md +22 -0
  415. package/dist/scaffold-repo/triggers/cron/package.json +36 -0
  416. package/dist/scaffold-repo/triggers/cron/src/CronTrigger.ts +552 -0
  417. package/dist/scaffold-repo/triggers/cron/src/index.ts +77 -0
  418. package/dist/scaffold-repo/triggers/cron/tsconfig.json +32 -0
  419. package/dist/scaffold-repo/triggers/grpc/.env.example +8 -0
  420. package/dist/scaffold-repo/triggers/grpc/CHANGELOG.md +143 -0
  421. package/dist/scaffold-repo/triggers/grpc/README.md +25 -0
  422. package/dist/scaffold-repo/triggers/grpc/buf.gen.yaml +11 -0
  423. package/dist/scaffold-repo/triggers/grpc/buf.yaml +9 -0
  424. package/dist/scaffold-repo/triggers/grpc/localhost+2-key.pem +28 -0
  425. package/dist/scaffold-repo/triggers/grpc/localhost+2.pem +27 -0
  426. package/dist/scaffold-repo/triggers/grpc/package.json +61 -0
  427. package/dist/scaffold-repo/triggers/grpc/proto/workflow.proto +33 -0
  428. package/dist/scaffold-repo/triggers/grpc/src/GRpcTrigger.ts +297 -0
  429. package/dist/scaffold-repo/triggers/grpc/src/GrpcClient.ts +87 -0
  430. package/dist/scaffold-repo/triggers/grpc/src/GrpcServer.ts +68 -0
  431. package/dist/scaffold-repo/triggers/grpc/src/MessageDecode.ts +142 -0
  432. package/dist/scaffold-repo/triggers/grpc/src/NanoSDK.ts +146 -0
  433. package/dist/scaffold-repo/triggers/grpc/src/Nodes.ts +10 -0
  434. package/dist/scaffold-repo/triggers/grpc/src/Workflows.ts +5 -0
  435. package/dist/scaffold-repo/triggers/grpc/src/gen/workflow_pb.ts +142 -0
  436. package/dist/scaffold-repo/triggers/grpc/src/index.ts +22 -0
  437. package/dist/scaffold-repo/triggers/grpc/src/server.ts +8 -0
  438. package/dist/scaffold-repo/triggers/grpc/src/types/Message.ts +0 -0
  439. package/dist/scaffold-repo/triggers/grpc/src/types/RuntimeWorkflow.ts +7 -0
  440. package/dist/scaffold-repo/triggers/grpc/src/types/Workflows.ts +7 -0
  441. package/dist/scaffold-repo/triggers/grpc/tsconfig.json +34 -0
  442. package/dist/scaffold-repo/triggers/grpc/vitest.config.ts +29 -0
  443. package/dist/scaffold-repo/triggers/grpc/workflows/countries.json +27 -0
  444. package/dist/scaffold-repo/triggers/http/.blok/trace.db +0 -0
  445. package/dist/scaffold-repo/triggers/http/.blok/trace.db-shm +0 -0
  446. package/dist/scaffold-repo/triggers/http/.blok/trace.db-wal +0 -0
  447. package/dist/scaffold-repo/triggers/http/.dockerignore +23 -0
  448. package/dist/scaffold-repo/triggers/http/.env.example +8 -0
  449. package/dist/scaffold-repo/triggers/http/CHANGELOG.md +326 -0
  450. package/dist/scaffold-repo/triggers/http/Dockerfile +49 -0
  451. package/dist/scaffold-repo/triggers/http/Dockerfile.dev +34 -0
  452. package/dist/scaffold-repo/triggers/http/infra/docker-compose.yml +38 -0
  453. package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Authentication.md +74 -0
  454. package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Building.md +52 -0
  455. package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Create_Node.md +123 -0
  456. package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Create_Workflow.md +177 -0
  457. package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Deployment.md +69 -0
  458. package/dist/scaffold-repo/triggers/http/notes/Core_Concepts/Context.md +156 -0
  459. package/dist/scaffold-repo/triggers/http/notes/examples.md +104 -0
  460. package/dist/scaffold-repo/triggers/http/notes/index.md +51 -0
  461. package/dist/scaffold-repo/triggers/http/package.json +62 -0
  462. package/dist/scaffold-repo/triggers/http/public/favicon.ico +0 -0
  463. package/dist/scaffold-repo/triggers/http/public/metric/index.html +2101 -0
  464. package/dist/scaffold-repo/triggers/http/request.http +7 -0
  465. package/dist/scaffold-repo/triggers/http/src/AppRoutes.ts +64 -0
  466. package/dist/scaffold-repo/triggers/http/src/Nodes.ts +38 -0
  467. package/dist/scaffold-repo/triggers/http/src/Workflows.ts +18 -0
  468. package/dist/scaffold-repo/triggers/http/src/index.ts +86 -0
  469. package/dist/scaffold-repo/triggers/http/src/nodes/chain-init/index.ts +48 -0
  470. package/dist/scaffold-repo/triggers/http/src/nodes/chain-verify/index.ts +123 -0
  471. package/dist/scaffold-repo/triggers/http/src/nodes/eval/index.ts +69 -0
  472. package/dist/scaffold-repo/triggers/http/src/nodes/examples/README.md +14 -0
  473. package/dist/scaffold-repo/triggers/http/src/nodes/examples/chat-ui/index.html +228 -0
  474. package/dist/scaffold-repo/triggers/http/src/nodes/examples/chat-ui/index.ts +65 -0
  475. package/dist/scaffold-repo/triggers/http/src/nodes/examples/index.ts +14 -0
  476. package/dist/scaffold-repo/triggers/http/src/nodes/runtime-bridge/index.ts +84 -0
  477. package/dist/scaffold-repo/triggers/http/src/runner/HonoTraceRouterAdapter.ts +199 -0
  478. package/dist/scaffold-repo/triggers/http/src/runner/HttpTrigger.ts +2039 -0
  479. package/dist/scaffold-repo/triggers/http/src/runner/MessageDecode.ts +57 -0
  480. package/dist/scaffold-repo/triggers/http/src/runner/Util.ts +52 -0
  481. package/dist/scaffold-repo/triggers/http/src/runner/WorkflowRouter.ts +411 -0
  482. package/dist/scaffold-repo/triggers/http/src/runner/metrics/opentelemetry_metrics.ts +84 -0
  483. package/dist/scaffold-repo/triggers/http/src/runner/metrics/opentelemetry_traces.ts +30 -0
  484. package/dist/scaffold-repo/triggers/http/src/runner/nodeCatalog.ts +128 -0
  485. package/dist/scaffold-repo/triggers/http/src/runner/responseEmitter.ts +138 -0
  486. package/dist/scaffold-repo/triggers/http/src/runner/scanWorkflows.ts +201 -0
  487. package/dist/scaffold-repo/triggers/http/src/runner/types/ErrorResponse.ts +3 -0
  488. package/dist/scaffold-repo/triggers/http/src/runner/types/NodeTypes.ts +13 -0
  489. package/dist/scaffold-repo/triggers/http/src/runner/types/Nodes.ts +5 -0
  490. package/dist/scaffold-repo/triggers/http/src/runner/types/RuntimeWorkflow.ts +7 -0
  491. package/dist/scaffold-repo/triggers/http/src/runner/types/WorkflowRequest.ts +8 -0
  492. package/dist/scaffold-repo/triggers/http/src/runner/types/WorkflowResponse.ts +7 -0
  493. package/dist/scaffold-repo/triggers/http/src/runner/types/Workflows.ts +14 -0
  494. package/dist/scaffold-repo/triggers/http/src/workflows/countries-cats-helper.ts +30 -0
  495. package/dist/scaffold-repo/triggers/http/src/workflows/countries-handle-dsl.ts +25 -0
  496. package/dist/scaffold-repo/triggers/http/src/workflows/countries-helper.ts +18 -0
  497. package/dist/scaffold-repo/triggers/http/src/workflows/empty.ts +16 -0
  498. package/dist/scaffold-repo/triggers/http/src/workflows/eval/eval-retrieve.ts +23 -0
  499. package/dist/scaffold-repo/triggers/http/src/workflows/eval/eval-run.ts +39 -0
  500. package/dist/scaffold-repo/triggers/http/src/workflows/eval/foreign-auth.ts +24 -0
  501. package/dist/scaffold-repo/triggers/http/tsconfig.json +39 -0
  502. package/dist/scaffold-repo/triggers/http/vitest.config.ts +39 -0
  503. package/dist/scaffold-repo/triggers/http/workflows/json/agent-message.json +87 -0
  504. package/dist/scaffold-repo/triggers/http/workflows/json/agent-page.json +25 -0
  505. package/dist/scaffold-repo/triggers/http/workflows/json/agent-stream.json +30 -0
  506. package/dist/scaffold-repo/triggers/http/workflows/json/agent-tool-calculator.json +22 -0
  507. package/dist/scaffold-repo/triggers/http/workflows/json/agent-tool-weather.json +22 -0
  508. package/dist/scaffold-repo/triggers/http/workflows/json/chat-memory-message.json +54 -0
  509. package/dist/scaffold-repo/triggers/http/workflows/json/chat-memory-page.json +25 -0
  510. package/dist/scaffold-repo/triggers/http/workflows/json/chat-memory-stream.json +30 -0
  511. package/dist/scaffold-repo/triggers/http/workflows/json/chat-message.json +25 -0
  512. package/dist/scaffold-repo/triggers/http/workflows/json/chat-page.json +20 -0
  513. package/dist/scaffold-repo/triggers/http/workflows/json/chat-stream.json +30 -0
  514. package/dist/scaffold-repo/triggers/http/workflows/json/countries-py.json +27 -0
  515. package/dist/scaffold-repo/triggers/http/workflows/json/countries-vs-facts.json +50 -0
  516. package/dist/scaffold-repo/triggers/http/workflows/json/countries.json +27 -0
  517. package/dist/scaffold-repo/triggers/http/workflows/json/cross-runtime-chain.json +89 -0
  518. package/dist/scaffold-repo/triggers/http/workflows/json/dashboard-gen.json +34 -0
  519. package/dist/scaffold-repo/triggers/http/workflows/json/db-manager.json +34 -0
  520. package/dist/scaffold-repo/triggers/http/workflows/json/empty.json +13 -0
  521. package/dist/scaffold-repo/triggers/http/workflows/json/fanout-enqueue.json +48 -0
  522. package/dist/scaffold-repo/triggers/http/workflows/json/fanout-worker.json +41 -0
  523. package/dist/scaffold-repo/triggers/http/workflows/json/feedback.json +34 -0
  524. package/dist/scaffold-repo/triggers/http/workflows/json/films.json +27 -0
  525. package/dist/scaffold-repo/triggers/http/workflows/json/image-capture.json +34 -0
  526. package/dist/scaffold-repo/triggers/http/workflows/json/launches-by-year.json +27 -0
  527. package/dist/scaffold-repo/triggers/http/workflows/json/loadtest.json +24 -0
  528. package/dist/scaffold-repo/triggers/http/workflows/json/middleware/admin-only.json +31 -0
  529. package/dist/scaffold-repo/triggers/http/workflows/json/middleware/auth-check.json +59 -0
  530. package/dist/scaffold-repo/triggers/http/workflows/json/middleware/github-webhook-verify.json +34 -0
  531. package/dist/scaffold-repo/triggers/http/workflows/json/middleware/jwt-auth.json +46 -0
  532. package/dist/scaffold-repo/triggers/http/workflows/json/middleware/rate-limit.json +70 -0
  533. package/dist/scaffold-repo/triggers/http/workflows/json/middleware/redis-rate-limit.json +71 -0
  534. package/dist/scaffold-repo/triggers/http/workflows/json/mongodb.json +24 -0
  535. package/dist/scaffold-repo/triggers/http/workflows/json/rentals-pdf.json +65 -0
  536. package/dist/scaffold-repo/triggers/http/workflows/json/v05-admin-delete-user.json +42 -0
  537. package/dist/scaffold-repo/triggers/http/workflows/json/v05-async-job-poller.json +45 -0
  538. package/dist/scaffold-repo/triggers/http/workflows/json/v05-csv-import.json +122 -0
  539. package/dist/scaffold-repo/triggers/http/workflows/json/v05-data-export.json +105 -0
  540. package/dist/scaffold-repo/triggers/http/workflows/json/v05-event-router.json +97 -0
  541. package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-handler-issues.json +22 -0
  542. package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-handler-pr.json +22 -0
  543. package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-handler-push.json +22 -0
  544. package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-webhook-router.json +88 -0
  545. package/dist/scaffold-repo/triggers/http/workflows/json/v05-hello-global-mw.json +22 -0
  546. package/dist/scaffold-repo/triggers/http/workflows/json/v05-hello-with-mw.json +23 -0
  547. package/dist/scaffold-repo/triggers/http/workflows/json/v05-jwt-protected.json +23 -0
  548. package/dist/scaffold-repo/triggers/http/workflows/json/v05-multi-tenant-router.json +90 -0
  549. package/dist/scaffold-repo/triggers/http/workflows/json/v05-nested-control-flow.json +207 -0
  550. package/dist/scaffold-repo/triggers/http/workflows/json/v05-order-fulfillment.json +211 -0
  551. package/dist/scaffold-repo/triggers/http/workflows/json/v05-parallel-foreach-with-wait.json +47 -0
  552. package/dist/scaffold-repo/triggers/http/workflows/json/v05-polling-with-backoff.json +55 -0
  553. package/dist/scaffold-repo/triggers/http/workflows/json/v05-protected.json +23 -0
  554. package/dist/scaffold-repo/triggers/http/workflows/json/v05-redis-protected.json +23 -0
  555. package/dist/scaffold-repo/triggers/http/workflows/json/v05-saga.json +122 -0
  556. package/dist/scaffold-repo/triggers/http/workflows/json/v05-tenant-acme.json +22 -0
  557. package/dist/scaffold-repo/triggers/http/workflows/json/v05-tenant-beta.json +22 -0
  558. package/dist/scaffold-repo/triggers/http/workflows/json/v05-tenant-gamma.json +22 -0
  559. package/dist/scaffold-repo/triggers/http/workflows/json/v05-travel-booking.json +200 -0
  560. package/dist/scaffold-repo/triggers/http/workflows/json/v05-user-signup-saga.json +186 -0
  561. package/dist/scaffold-repo/triggers/http/workflows/json/v05-webhook-fanout.json +49 -0
  562. package/dist/scaffold-repo/triggers/http/workflows/json/v06-reliability-showcase.json +80 -0
  563. package/dist/scaffold-repo/triggers/http/workflows/json/v07-sse-publish.json +24 -0
  564. package/dist/scaffold-repo/triggers/http/workflows/json/v07-sse-stream.json +26 -0
  565. package/dist/scaffold-repo/triggers/http/workflows/json/v07-ws-echo.json +38 -0
  566. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github-issues.json +22 -0
  567. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github-pr.json +22 -0
  568. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github-push.json +32 -0
  569. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github.json +87 -0
  570. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-linear-comment.json +22 -0
  571. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-linear-issue.json +22 -0
  572. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-linear.json +74 -0
  573. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-stripe-customer-created.json +22 -0
  574. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-stripe-invoice-paid.json +32 -0
  575. package/dist/scaffold-repo/triggers/http/workflows/json/webhook-stripe.json +24 -0
  576. package/dist/scaffold-repo/triggers/http/workflows/json/workflow-docs.json +34 -0
  577. package/dist/scaffold-repo/triggers/http/workflows/toml/countries.toml +21 -0
  578. package/dist/scaffold-repo/triggers/http/workflows/xml/countries.xml +23 -0
  579. package/dist/scaffold-repo/triggers/http/workflows/yaml/countries.yaml +19 -0
  580. package/dist/scaffold-repo/triggers/mcp/README.md +131 -0
  581. package/dist/scaffold-repo/triggers/mcp/package.json +39 -0
  582. package/dist/scaffold-repo/triggers/mcp/src/McpTrigger.ts +581 -0
  583. package/dist/scaffold-repo/triggers/mcp/src/index.ts +41 -0
  584. package/dist/scaffold-repo/triggers/mcp/tsconfig.json +32 -0
  585. package/dist/scaffold-repo/triggers/pubsub/CHANGELOG.md +22 -0
  586. package/dist/scaffold-repo/triggers/pubsub/package.json +66 -0
  587. package/dist/scaffold-repo/triggers/pubsub/src/PubSubTrigger.ts +424 -0
  588. package/dist/scaffold-repo/triggers/pubsub/src/adapters/AWSSNSAdapter.ts +352 -0
  589. package/dist/scaffold-repo/triggers/pubsub/src/adapters/AzureServiceBusAdapter.ts +303 -0
  590. package/dist/scaffold-repo/triggers/pubsub/src/adapters/GCPPubSubAdapter.ts +327 -0
  591. package/dist/scaffold-repo/triggers/pubsub/src/adapters/KafkaPubSubAdapter.ts +247 -0
  592. package/dist/scaffold-repo/triggers/pubsub/src/adapters/KafkaPubSubAdapter.ts.bak +251 -0
  593. package/dist/scaffold-repo/triggers/pubsub/src/adapters/NATSPubSubAdapter.ts +333 -0
  594. package/dist/scaffold-repo/triggers/pubsub/src/adapters/RedisStreamsPubSubAdapter.ts +256 -0
  595. package/dist/scaffold-repo/triggers/pubsub/src/adapters/factory.ts +88 -0
  596. package/dist/scaffold-repo/triggers/pubsub/src/index.ts +67 -0
  597. package/dist/scaffold-repo/triggers/pubsub/template/.env.example +8 -0
  598. package/dist/scaffold-repo/triggers/pubsub/template/package.json +45 -0
  599. package/dist/scaffold-repo/triggers/pubsub/template/src/Nodes.ts +20 -0
  600. package/dist/scaffold-repo/triggers/pubsub/template/src/Workflows.ts +8 -0
  601. package/dist/scaffold-repo/triggers/pubsub/template/src/index.ts +41 -0
  602. package/dist/scaffold-repo/triggers/pubsub/template/src/runner/PubSubServer.ts +39 -0
  603. package/dist/scaffold-repo/triggers/pubsub/template/src/runner/types/Workflows.ts +7 -0
  604. package/dist/scaffold-repo/triggers/pubsub/template/src/workflows/messages/on-message.ts +42 -0
  605. package/dist/scaffold-repo/triggers/pubsub/template/src/workflows/publish-order.ts +22 -0
  606. package/dist/scaffold-repo/triggers/pubsub/template/tsconfig.json +31 -0
  607. package/dist/scaffold-repo/triggers/pubsub/template/vitest.config.ts +39 -0
  608. package/dist/scaffold-repo/triggers/pubsub/tsconfig.json +32 -0
  609. package/dist/scaffold-repo/triggers/sse/.env.example +8 -0
  610. package/dist/scaffold-repo/triggers/sse/CHANGELOG.md +12 -0
  611. package/dist/scaffold-repo/triggers/sse/Dockerfile +47 -0
  612. package/dist/scaffold-repo/triggers/sse/Dockerfile.dev +33 -0
  613. package/dist/scaffold-repo/triggers/sse/package.json +37 -0
  614. package/dist/scaffold-repo/triggers/sse/src/SSETrigger.ts +489 -0
  615. package/dist/scaffold-repo/triggers/sse/src/bus.ts +208 -0
  616. package/dist/scaffold-repo/triggers/sse/src/index.ts +38 -0
  617. package/dist/scaffold-repo/triggers/sse/src/workflows/events/README.md +129 -0
  618. package/dist/scaffold-repo/triggers/sse/src/workflows/events/publish-demo.ts +40 -0
  619. package/dist/scaffold-repo/triggers/sse/src/workflows/events/stream-demo.ts +43 -0
  620. package/dist/scaffold-repo/triggers/sse/tsconfig.json +22 -0
  621. package/dist/scaffold-repo/triggers/sse/vitest.config.ts +39 -0
  622. package/dist/scaffold-repo/triggers/webhook/CHANGELOG.md +22 -0
  623. package/dist/scaffold-repo/triggers/webhook/package.json +36 -0
  624. package/dist/scaffold-repo/triggers/webhook/src/WebhookTrigger.ts +465 -0
  625. package/dist/scaffold-repo/triggers/webhook/src/index.ts +51 -0
  626. package/dist/scaffold-repo/triggers/webhook/src/verifiers.ts +394 -0
  627. package/dist/scaffold-repo/triggers/webhook/tsconfig.json +32 -0
  628. package/dist/scaffold-repo/triggers/websocket/.env.example +8 -0
  629. package/dist/scaffold-repo/triggers/websocket/CHANGELOG.md +22 -0
  630. package/dist/scaffold-repo/triggers/websocket/package.json +50 -0
  631. package/dist/scaffold-repo/triggers/websocket/src/Backplane.ts +167 -0
  632. package/dist/scaffold-repo/triggers/websocket/src/WebSocketTrigger.ts +910 -0
  633. package/dist/scaffold-repo/triggers/websocket/src/index.ts +50 -0
  634. package/dist/scaffold-repo/triggers/websocket/src/workflows/events/README.md +93 -0
  635. package/dist/scaffold-repo/triggers/websocket/src/workflows/events/echo-demo.ts +56 -0
  636. package/dist/scaffold-repo/triggers/websocket/tsconfig.json +22 -0
  637. package/dist/scaffold-repo/triggers/websocket/vitest.config.ts +39 -0
  638. package/dist/scaffold-repo/triggers/worker/CHANGELOG.md +22 -0
  639. package/dist/scaffold-repo/triggers/worker/package.json +68 -0
  640. package/dist/scaffold-repo/triggers/worker/src/WorkerTrigger.ts +943 -0
  641. package/dist/scaffold-repo/triggers/worker/src/adapters/BullMQAdapter.ts +296 -0
  642. package/dist/scaffold-repo/triggers/worker/src/adapters/InMemoryAdapter.ts +280 -0
  643. package/dist/scaffold-repo/triggers/worker/src/adapters/KafkaAdapter.ts +347 -0
  644. package/dist/scaffold-repo/triggers/worker/src/adapters/NATSAdapter.ts +505 -0
  645. package/dist/scaffold-repo/triggers/worker/src/adapters/PgBossAdapter.ts +319 -0
  646. package/dist/scaffold-repo/triggers/worker/src/adapters/RabbitMQAdapter.ts +318 -0
  647. package/dist/scaffold-repo/triggers/worker/src/adapters/RedisStreamsAdapter.ts +513 -0
  648. package/dist/scaffold-repo/triggers/worker/src/adapters/SQSAdapter.ts +314 -0
  649. package/dist/scaffold-repo/triggers/worker/src/adapters/factory.ts +111 -0
  650. package/dist/scaffold-repo/triggers/worker/src/index.ts +95 -0
  651. package/dist/scaffold-repo/triggers/worker/template/.env.example +18 -0
  652. package/dist/scaffold-repo/triggers/worker/template/package.json +46 -0
  653. package/dist/scaffold-repo/triggers/worker/template/src/Nodes.ts +20 -0
  654. package/dist/scaffold-repo/triggers/worker/template/src/Workflows.ts +8 -0
  655. package/dist/scaffold-repo/triggers/worker/template/src/index.ts +41 -0
  656. package/dist/scaffold-repo/triggers/worker/template/src/runner/WorkerServer.ts +34 -0
  657. package/dist/scaffold-repo/triggers/worker/template/src/runner/types/Workflows.ts +7 -0
  658. package/dist/scaffold-repo/triggers/worker/template/src/workflows/jobs/process-job.ts +40 -0
  659. package/dist/scaffold-repo/triggers/worker/template/tsconfig.json +31 -0
  660. package/dist/scaffold-repo/triggers/worker/template/vitest.config.ts +39 -0
  661. package/dist/scaffold-repo/triggers/worker/tsconfig.json +32 -0
  662. package/dist/scaffold-repo/workflows/json/agent-message.json +87 -0
  663. package/dist/scaffold-repo/workflows/json/agent-page.json +25 -0
  664. package/dist/scaffold-repo/workflows/json/agent-stream.json +30 -0
  665. package/dist/scaffold-repo/workflows/json/agent-tool-calculator.json +22 -0
  666. package/dist/scaffold-repo/workflows/json/agent-tool-weather.json +22 -0
  667. package/dist/scaffold-repo/workflows/json/chat-memory-message.json +54 -0
  668. package/dist/scaffold-repo/workflows/json/chat-memory-page.json +25 -0
  669. package/dist/scaffold-repo/workflows/json/chat-memory-stream.json +30 -0
  670. package/dist/scaffold-repo/workflows/json/chat-message.json +25 -0
  671. package/dist/scaffold-repo/workflows/json/chat-page.json +20 -0
  672. package/dist/scaffold-repo/workflows/json/chat-stream.json +30 -0
  673. package/dist/scaffold-repo/workflows/json/countries-vs-facts.json +50 -0
  674. package/dist/scaffold-repo/workflows/json/countries.json +27 -0
  675. package/dist/scaffold-repo/workflows/json/cron-heartbeat.json +15 -0
  676. package/dist/scaffold-repo/workflows/json/fanout-enqueue.json +48 -0
  677. package/dist/scaffold-repo/workflows/json/fanout-worker.json +41 -0
  678. package/dist/scaffold-repo/workflows/json/pubsub-on-order.json +24 -0
  679. package/dist/scaffold-repo/workflows/json/webhook-github-issues.json +22 -0
  680. package/dist/scaffold-repo/workflows/json/webhook-github-pr.json +22 -0
  681. package/dist/scaffold-repo/workflows/json/webhook-github-push.json +32 -0
  682. package/dist/scaffold-repo/workflows/json/webhook-github.json +87 -0
  683. package/dist/scaffold-repo/workflows/json/webhook-linear-comment.json +22 -0
  684. package/dist/scaffold-repo/workflows/json/webhook-linear-issue.json +22 -0
  685. package/dist/scaffold-repo/workflows/json/webhook-linear.json +74 -0
  686. package/dist/scaffold-repo/workflows/json/webhook-stripe-customer-created.json +22 -0
  687. package/dist/scaffold-repo/workflows/json/webhook-stripe-invoice-paid.json +32 -0
  688. package/dist/scaffold-repo/workflows/json/webhook-stripe.json +24 -0
  689. package/package.json +4 -4
@@ -0,0 +1,2039 @@
1
+ import type { Server } from "node:http";
2
+ import * as path from "node:path";
3
+ import { workflow } from "@blokjs/helper";
4
+ import type { TriggerOpts } from "@blokjs/helper";
5
+ import type { GlobalOptions, HMREvent, ParamsDictionary, TriggerResponse } from "@blokjs/runner";
6
+ import { TriggerBase } from "@blokjs/runner";
7
+ import { NodeMap } from "@blokjs/runner";
8
+ import { DefaultLogger } from "@blokjs/runner";
9
+ import { registerTraceRoutes } from "@blokjs/runner";
10
+ import { RoutingDiagnostics } from "@blokjs/runner";
11
+ import { RuntimeRegistry, WorkflowRegistry } from "@blokjs/runner";
12
+ import { ConcurrencyLimitError } from "@blokjs/runner";
13
+ import { QueueExpiredError } from "@blokjs/runner";
14
+ import { ConcurrencyMetrics } from "@blokjs/runner";
15
+ import { bootstrapTracing } from "@blokjs/runner";
16
+ import { DeferredDispatchSignal } from "@blokjs/runner";
17
+ import { DeferredRunScheduler, getSchedulerClaimLeaseMs } from "@blokjs/runner";
18
+ import { Janitor } from "@blokjs/runner";
19
+ import { PayloadTooLargeError } from "@blokjs/runner";
20
+ import { RunTracker } from "@blokjs/runner";
21
+ import { traceRedactSensitive } from "@blokjs/runner";
22
+ import type { TraceAuthorizeFn } from "@blokjs/runner";
23
+ import type { ScheduledDispatchRow } from "@blokjs/runner";
24
+ import type { NodeBase } from "@blokjs/shared";
25
+ import { type Context, GlobalError, type RequestContext, type StreamContext } from "@blokjs/shared";
26
+ import type { HttpBindings } from "@hono/node-server";
27
+ import { serve } from "@hono/node-server";
28
+ import { serveStatic } from "@hono/node-server/serve-static";
29
+ import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
30
+ import {
31
+ type Counter,
32
+ type Span,
33
+ SpanKind,
34
+ SpanStatusCode,
35
+ context,
36
+ metrics,
37
+ propagation,
38
+ trace,
39
+ } from "@opentelemetry/api";
40
+ import { Hono, type Context as HonoContext } from "hono";
41
+ import { cors } from "hono/cors";
42
+ import { streamSSE } from "hono/streaming";
43
+ import { v4 as uuid } from "uuid";
44
+ import apps from "../AppRoutes";
45
+ import nodes from "../Nodes";
46
+ import workflows from "../Workflows";
47
+ import { createTraceRouterAdapter } from "./HonoTraceRouterAdapter";
48
+ import MessageDecode from "./MessageDecode";
49
+ import { handleDynamicRoute, validateRoute } from "./Util";
50
+ import {
51
+ type ManualRegistration,
52
+ type RouteCollision,
53
+ type RouteEntry,
54
+ buildRouteTable,
55
+ readMiddlewareFlag,
56
+ } from "./WorkflowRouter";
57
+ import { bootstrapMetrics } from "./metrics/opentelemetry_metrics";
58
+ import { buildNodeCatalog } from "./nodeCatalog";
59
+ import { emitWorkflowResponse, normalizeResponseEnvelope } from "./responseEmitter";
60
+ import { scanWorkflows } from "./scanWorkflows";
61
+ import NodeTypes from "./types/NodeTypes";
62
+ import type RuntimeWorkflow from "./types/RuntimeWorkflow";
63
+ import type WorkflowRequest from "./types/WorkflowRequest";
64
+
65
+ /**
66
+ * v0.7 — exported so sibling triggers (WebSocket / SSE / Webhook) can
67
+ * construct a `Hono<AppBindings>` instance externally and pass it into
68
+ * HttpTrigger via the optional constructor argument. The single shared
69
+ * app then routes HTTP, WS upgrades, SSE streams, and webhook POSTs on
70
+ * one TCP port via Hono's path-routing tree.
71
+ *
72
+ * Kept as a public type rather than an internal so the orchestrator
73
+ * pattern documented in [additional-triggers-plan.mdx](../../../../docs/c/devtools/additional-triggers-plan.mdx#server-architecture)
74
+ * remains type-safe end-to-end.
75
+ */
76
+ export type AppBindings = { Bindings: HttpBindings };
77
+
78
+ /**
79
+ * v0.6 — file-based routing is ON by default. Operators opt OUT via
80
+ * either:
81
+ * - `BLOK_FILE_BASED_ROUTING=false` — explicit kill switch.
82
+ * - `BLOK_ROUTING_LEGACY=1` / `=true` — full legacy mode (also
83
+ * re-enables the filename-derived URL fallback for path-less
84
+ * workflows in `WorkflowRouter.buildRouteTable`).
85
+ *
86
+ * The `BLOK_ROUTES=v2` opt-in is kept as a no-op for back-compat
87
+ * (it's the historical alias for "explicit routing on").
88
+ *
89
+ * Both opt-outs are deprecated and will be removed in a future
90
+ * release; the boot log surfaces a loud warning when either is set.
91
+ */
92
+ export function isFileBasedRoutingEnabled(): boolean {
93
+ const explicitFalse = process.env.BLOK_FILE_BASED_ROUTING === "false";
94
+ const legacyFlag = process.env.BLOK_ROUTING_LEGACY === "1" || process.env.BLOK_ROUTING_LEGACY === "true";
95
+ if (explicitFalse || legacyFlag) return false;
96
+ return true;
97
+ }
98
+
99
+ /**
100
+ * Read a workflow object's `name`, covering both shapes: a JSON / raw object
101
+ * literal carries `name` on the root, while a v2 `workflow()` builder carries
102
+ * it on the nested `_config`. Returns `undefined` when neither is a string.
103
+ */
104
+ function readWorkflowName(wf: unknown): string | undefined {
105
+ if (!wf || typeof wf !== "object") return undefined;
106
+ const w = wf as { name?: unknown; _config?: { name?: unknown } };
107
+ if (typeof w.name === "string") return w.name;
108
+ if (typeof w._config?.name === "string") return w._config.name;
109
+ return undefined;
110
+ }
111
+
112
+ /**
113
+ * F8 — does a workflow object expose an `http` trigger block? Reads both the
114
+ * root `trigger` (JSON / raw object literals) and `_config.trigger` (v2
115
+ * `workflow()` builders). Used to gate the `/__blok/rpc/:name` mount so only
116
+ * http-callable workflows run via RPC: a worker/cron-only workflow registered
117
+ * for sub-workflow lookup must NOT be reachable (and middleware-resolvable as
118
+ * http) over HTTP. Mirrors the route table's `extractHttpTrigger` filter.
119
+ */
120
+ function hasHttpTrigger(wf: unknown): boolean {
121
+ if (!wf || typeof wf !== "object") return false;
122
+ const obj = wf as { trigger?: unknown; _config?: { trigger?: unknown } };
123
+ const trigger = (obj.trigger ?? obj._config?.trigger) as Record<string, unknown> | undefined;
124
+ if (!trigger || typeof trigger !== "object") return false;
125
+ const http = (trigger as Record<string, unknown>).http;
126
+ return !!http && typeof http === "object";
127
+ }
128
+
129
+ async function resolveManualRegistrations(workflowsMap: Record<string, unknown>): Promise<ManualRegistration[]> {
130
+ return Promise.all(
131
+ Object.keys(workflowsMap ?? {}).map(async (key) => ({
132
+ key,
133
+ workflow: await workflowsMap[key],
134
+ })),
135
+ );
136
+ }
137
+
138
+ async function resolveManualWorkflowMap(workflowsMap: Record<string, unknown>): Promise<Record<string, unknown>> {
139
+ const entries = await resolveManualRegistrations(workflowsMap);
140
+ return Object.fromEntries(entries.map(({ key, workflow }) => [key, workflow]));
141
+ }
142
+
143
+ /**
144
+ * OBS-06 (T9) — pre-run boot failures. When `Configuration.init` (workflow
145
+ * parse / node resolution / runtime registry) or the middleware-chain resolver
146
+ * throws BEFORE `this.run()` reaches `tracker.startRun()`, the failure is a
147
+ * config/deploy problem, not workflow logic — but it surfaced only as a generic
148
+ * 500 with no distinct metric (`blok_workflow_errors_total` fires inside the
149
+ * run, which never started, so it never sees a boot failure). This counter
150
+ * separates "the workflow couldn't even boot" from "a step threw", so an alert
151
+ * can page on deploy/config breakage distinctly. Lazily created once so it binds
152
+ * to the trigger's MeterProvider; the wrap re-throws, so behaviour is unchanged.
153
+ */
154
+ let _bootErrorCounter: Counter | null = null;
155
+ function recordBootError(phase: "configuration_init" | "middleware", err: unknown): void {
156
+ if (!_bootErrorCounter) {
157
+ _bootErrorCounter = metrics.getMeter("blok").createCounter("blok_boot_error_total", {
158
+ description: "Workflow boot failures before the run started (config/middleware resolution)",
159
+ unit: "1",
160
+ });
161
+ }
162
+ const error_class = err instanceof Error && err.name ? err.name : "Error";
163
+ _bootErrorCounter.add(1, { trigger_type: "http", phase, error_class });
164
+ }
165
+
166
+ /** Test-only: drop the cached counter so a fresh MeterProvider is picked up. */
167
+ export function _resetBootErrorCounterForTests(): void {
168
+ _bootErrorCounter = null;
169
+ }
170
+
171
+ export default class HttpTrigger extends TriggerBase {
172
+ private app: Hono<AppBindings>;
173
+ private port: string | number = process.env.PORT || 4000;
174
+ private initializer = 0;
175
+ private nodeMap: GlobalOptions = <GlobalOptions>{};
176
+ private server: Server | null = null;
177
+
178
+ /**
179
+ * v0.7 — callbacks registered by sibling same-port triggers (e.g.
180
+ * `WebSocketTrigger`) that need access to the `http.Server` instance
181
+ * AFTER `serve()` resolves. Run in registration order inside the
182
+ * `serve()` ready callback. Errors caught + logged (don't bring the
183
+ * server down on a hook failure). See
184
+ * [additional-triggers-plan.mdx](../../../../docs/c/devtools/additional-triggers-plan.mdx#server-architecture)
185
+ * for why this hook exists.
186
+ */
187
+ private serverHooks: Array<(server: Server) => void | Promise<void>> = [];
188
+
189
+ /**
190
+ * v0.7 — callbacks that run during `listen()` AFTER the workflow
191
+ * registry is populated but BEFORE the legacy catch-all route is
192
+ * registered on the Hono app. Sibling triggers (`WebSocketTrigger`,
193
+ * the upcoming `SSETrigger`) that mount path-specific routes on the
194
+ * shared app use this hook so their routes are matched FIRST — if we
195
+ * registered them after the catch-all, Hono would dispatch
196
+ * `/ws/<path>` requests through the workflow lookup path instead of
197
+ * the upgrade handler.
198
+ *
199
+ * Hooks are async-friendly; errors are caught + logged.
200
+ */
201
+ private preCatchAllHooks: Array<() => void | Promise<void>> = [];
202
+ protected tracer = trace.getTracer(
203
+ process.env.PROJECT_NAME || "trigger-http-workflow",
204
+ process.env.PROJECT_VERSION || "0.0.1",
205
+ );
206
+ private logger = new DefaultLogger();
207
+
208
+ /**
209
+ * Security review FW-1 — operator-supplied authorize hook for the
210
+ * `/__blok/*` trace API + Studio backend. In production the trace
211
+ * router refuses to serve any route until this is registered (or
212
+ * the operator sets `BLOK_TRACE_AUTH_DISABLED=1`). See
213
+ * docs/d/security/cookbook.mdx#secure-the-trace-api-and-studio.
214
+ */
215
+ private traceAuthFn: TraceAuthorizeFn | undefined;
216
+
217
+ /** OBS-02 — graceful shutdown for the OTel tracer provider, if tracing was enabled. */
218
+ private tracingShutdown: (() => Promise<void>) | null = null;
219
+
220
+ /**
221
+ * Register the authorize hook for `/__blok/*` (Blok Studio + trace
222
+ * API). Call before {@link listen}.
223
+ *
224
+ * In `BLOK_ENV=production` (or `NODE_ENV=production`), the trace
225
+ * router returns `503` until this is registered or
226
+ * `BLOK_TRACE_AUTH_DISABLED=1` is set.
227
+ *
228
+ * @param authorize Function that returns `true` to allow a trace
229
+ * API request, `false` (or throws) to reject with
230
+ * `401`. Sees the raw request including method,
231
+ * path, headers, query, and body.
232
+ */
233
+ public setTraceAuth(authorize: TraceAuthorizeFn): void {
234
+ this.traceAuthFn = authorize;
235
+ }
236
+
237
+ /**
238
+ * v0.7 — register a callback to run after the HTTP server is ready
239
+ * (after `serve()` resolves). The callback receives the underlying
240
+ * `http.Server` instance.
241
+ *
242
+ * Used by sibling same-port triggers — most notably `WebSocketTrigger`
243
+ * which calls `@hono/node-ws`'s `injectWebSocket(server)` to attach
244
+ * its `upgrade` event listener to the same server.
245
+ *
246
+ * Hooks run in registration order. Errors are caught and logged so a
247
+ * misbehaving hook doesn't bring the server down. Call BEFORE
248
+ * `listen()`.
249
+ *
250
+ * @param cb Receives the bound `http.Server`. Sync or async.
251
+ */
252
+ public addServerHook(cb: (server: Server) => void | Promise<void>): void {
253
+ this.serverHooks.push(cb);
254
+ }
255
+
256
+ /**
257
+ * v0.7 — register a callback that fires during `listen()` AFTER the
258
+ * workflow registry is populated but BEFORE the catch-all workflow
259
+ * route is mounted. Sibling triggers that mount explicit Hono routes
260
+ * on the shared app (WebSocketTrigger's upgrade endpoints, the
261
+ * upcoming SSETrigger's stream endpoints) call this so their routes
262
+ * win over the catch-all `/:workflow{.+}` matcher. Call BEFORE
263
+ * `listen()`.
264
+ */
265
+ public addPreCatchAllHook(cb: () => void | Promise<void>): void {
266
+ this.preCatchAllHooks.push(cb);
267
+ }
268
+
269
+ /**
270
+ * @param app v0.7 — optional pre-constructed Hono app. When provided,
271
+ * HttpTrigger registers its HTTP routes onto the shared
272
+ * app instead of constructing its own. This is the entry
273
+ * point for the same-port multiplex pattern documented in
274
+ * [additional-triggers-plan.mdx](../../../../docs/c/devtools/additional-triggers-plan.mdx#server-architecture):
275
+ * an orchestrator builds ONE Hono app, threads it into
276
+ * every same-port trigger (HTTP + future WebSocket / SSE /
277
+ * Webhook), and the HTTP trigger's `serve()` call hosts
278
+ * everything on port 4000.
279
+ *
280
+ * When omitted, HttpTrigger constructs its own app
281
+ * (existing v0.6 behavior, fully backward-compatible).
282
+ */
283
+ constructor(app?: Hono<AppBindings>) {
284
+ super();
285
+
286
+ this.app = app ?? new Hono<AppBindings>();
287
+ this.initializer = this.startCounter();
288
+ this.loadNodes();
289
+ this.loadWorkflows();
290
+ }
291
+
292
+ loadNodes() {
293
+ this.nodeMap.nodes = new NodeMap();
294
+ // Register every node under its OWN `node.name` (the canonical `use:` ref
295
+ // per ADR 0002) — the `Nodes.ts` map keys are cosmetic and the collision
296
+ // guard (NodeMap.addNode) catches two nodes claiming one ref. `Nodes.ts`
297
+ // auto-discovers local `src/nodes/` (top-level await + `discoverNodes`,
298
+ // #360) and only hand-lists the third-party npm nodes, so the map values
299
+ // are the single source of truth. HMR re-runs THIS same keying path.
300
+ this.nodeMap.nodes.addNodes(Object.values(nodes) as unknown as NodeBase[]);
301
+ }
302
+
303
+ loadWorkflows() {
304
+ this.nodeMap.workflows = workflows as unknown as GlobalOptions["workflows"];
305
+ }
306
+
307
+ /**
308
+ * Gracefully stop the HTTP server, waiting for in-flight requests to complete.
309
+ */
310
+ async stop(): Promise<void> {
311
+ await this.waitForInFlightRequests();
312
+ if (this.tracingShutdown) {
313
+ // Flush pending spans before exit so the last requests aren't lost.
314
+ await this.tracingShutdown().catch((err) =>
315
+ this.logger.error(`[blok][tracing] shutdown failed: ${(err as Error).message}`),
316
+ );
317
+ this.tracingShutdown = null;
318
+ }
319
+ return new Promise<void>((resolve) => {
320
+ if (this.server) {
321
+ this.server.close(() => {
322
+ this.server = null;
323
+ resolve();
324
+ });
325
+ } else {
326
+ resolve();
327
+ }
328
+ });
329
+ }
330
+
331
+ /**
332
+ * OBS-02 — install the OpenTelemetry SDK at boot when an OTLP endpoint is
333
+ * configured, so the spans the runner already creates export to a backend
334
+ * (Tempo/Jaeger/…). No-op when `OTEL_EXPORTER_OTLP_ENDPOINT` is unset or
335
+ * `BLOK_TRACING_DISABLED=1`. Stores the shutdown so `stop()` can flush.
336
+ */
337
+ private async maybeBootstrapTracing(): Promise<void> {
338
+ if (process.env.BLOK_TRACING_DISABLED === "1") return;
339
+ const base = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
340
+ if (!base) return;
341
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ? base : `${base.replace(/\/$/, "")}/v1/traces`;
342
+ try {
343
+ const result = await bootstrapTracing({
344
+ serviceName: process.env.APP_NAME || process.env.PROJECT_NAME || "blok-http",
345
+ serviceVersion: process.env.PROJECT_VERSION,
346
+ exporter: "otlp",
347
+ endpoint,
348
+ });
349
+ if (result) {
350
+ this.tracingShutdown = result.shutdown;
351
+ this.logger.log(`[blok][tracing] OTLP distributed tracing enabled → ${endpoint}`);
352
+ } else {
353
+ this.logger.error(
354
+ "[blok][tracing] OTEL_EXPORTER_OTLP_ENDPOINT is set but the OTel trace SDK isn't installed — tracing is OFF.",
355
+ );
356
+ }
357
+ } catch (err) {
358
+ this.logger.error(`[blok][tracing] failed to initialize: ${(err as Error).message}`);
359
+ }
360
+ }
361
+
362
+ protected override async onHmrNodeChange(event: HMREvent): Promise<void> {
363
+ this.hmr?.invalidateModule(event.filePath);
364
+ this.loadNodes();
365
+ console.log(`[HMR] Node reloaded: ${event.relativePath}`);
366
+ }
367
+
368
+ getApp(): Hono<AppBindings> {
369
+ return this.app;
370
+ }
371
+
372
+ /**
373
+ * v0.7 — expose the runner's `GlobalOptions` (nodes + workflows) so
374
+ * sibling triggers on the shared app (WebSocketTrigger, SSETrigger)
375
+ * dispatch through the same node registry instead of maintaining
376
+ * their own. Read AFTER the constructor — `loadNodes()` +
377
+ * `loadWorkflows()` have populated the map by then.
378
+ */
379
+ getNodeMap(): GlobalOptions {
380
+ return this.nodeMap;
381
+ }
382
+
383
+ /**
384
+ * Scan the workflow directories on disk + the manually-registered TS
385
+ * workflows in `Workflows.ts` and return the route table. Called once
386
+ * at boot from `listen()` so workflow URLs are decided before serving.
387
+ *
388
+ * **Default ON since v0.6** (the v0.4 commit that introduced explicit-
389
+ * path routing promised this for v0.5; finally lands here). Opt out
390
+ * via `BLOK_FILE_BASED_ROUTING=false`, or via the existing
391
+ * `BLOK_ROUTING_LEGACY=1` escape hatch (which also enables the
392
+ * filename-derived URL fallback for un-migrated workflows). Both
393
+ * fall back to the legacy catch-all `/<key>/<path>` scheme — and
394
+ * both will be removed in a future release. The boot log warns
395
+ * loudly when the legacy path is active so operators notice.
396
+ */
397
+ private async buildFileBasedRoutes(): Promise<RouteEntry[]> {
398
+ const enabled = isFileBasedRoutingEnabled();
399
+ if (!enabled) {
400
+ this.logger.log(
401
+ "[blok][routing] file-based routing is DISABLED — every request will go through the legacy catch-all `/<workflow-key>/<sub>` dispatch. Unset `BLOK_FILE_BASED_ROUTING=false` / `BLOK_ROUTING_LEGACY=1` to re-enable. The legacy mode will be removed in a future release.",
402
+ );
403
+ return [];
404
+ }
405
+
406
+ const workflowsRoot = process.env.WORKFLOWS_PATH || process.env.VITE_WORKFLOWS_PATH || `${process.cwd()}/workflows`;
407
+
408
+ // JSON workflows live under WORKFLOWS_PATH/json/<nested>.json.
409
+ // stripLeadingSegments=1 elides the `json/` segment from URLs.
410
+ const scannedJson = await scanWorkflows(
411
+ [
412
+ {
413
+ dir: path.join(workflowsRoot, "json"),
414
+ kind: "json",
415
+ stripLeadingSegments: 0,
416
+ },
417
+ ],
418
+ {
419
+ onLoadError: (file, err) => {
420
+ this.logger.error(`[blok] workflow load error in ${file}: ${err.message}`);
421
+ },
422
+ },
423
+ );
424
+
425
+ const manual = Object.keys(this.nodeMap.workflows ?? {}).map((key) => ({
426
+ key,
427
+ workflow: (this.nodeMap.workflows as Record<string, unknown>)[key],
428
+ }));
429
+
430
+ // Boot is tolerant of route-table collisions: a single bad
431
+ // workflow pair shouldn't drop the WHOLE route table and force
432
+ // every URL through the legacy catch-all (which then rejects
433
+ // every post-v0.4 explicit path → total outage). Collisions are
434
+ // captured into `RoutingDiagnostics` for Studio to surface, and
435
+ // the offending workflow is skipped.
436
+ const diagnostics = RoutingDiagnostics.getInstance();
437
+ diagnostics.clear();
438
+ const collisions: RouteCollision[] = [];
439
+ const table = buildRouteTable(scannedJson, manual, {
440
+ onWarning: (msg) => this.logger.log(`[blok] route warning: ${msg}`),
441
+ onCollision: (collision) => {
442
+ collisions.push(collision);
443
+ this.logger.error(`[blok] route collision — ${collision.message}`);
444
+ diagnostics.record({
445
+ kind: collision.kind,
446
+ method: collision.method,
447
+ path: collision.path,
448
+ winnerSource: collision.winnerSource,
449
+ droppedSource: collision.droppedSource,
450
+ message: collision.message,
451
+ });
452
+ },
453
+ });
454
+ if (collisions.length > 0) {
455
+ this.logger.error(
456
+ `[blok] file-based routing — ${collisions.length} workflow(s) dropped due to route collisions; the rest are still registered. See GET /__blok/routing for details.`,
457
+ );
458
+ }
459
+
460
+ if (table.length > 0) {
461
+ this.logger.log(`[blok] file-based routing — ${table.length} route(s) registered:`);
462
+ for (const r of table) {
463
+ this.logger.log(`[blok] ${r.method.padEnd(7)} ${r.path} ← ${r.workflowKey}`);
464
+ }
465
+ }
466
+
467
+ // Tier 2 · feed the WorkflowRegistry so the `subworkflow:` step
468
+ // primitive can look up child workflows by name. The route table
469
+ // may contain multiple entries per workflow (one per method/path);
470
+ // dedupe by workflow name before registering. clear-then-register
471
+ // keeps HMR semantics — a re-scan invalidates stale entries.
472
+ const registry = WorkflowRegistry.getInstance();
473
+ registry.clear();
474
+ // F7 — track name→source instead of a bare Set so a SECOND file claiming
475
+ // the same workflow `name` from a DIFFERENT source surfaces as a loud
476
+ // collision diagnostic. Previously this silent pre-dedupe routed around
477
+ // `WorkflowRegistry.register`'s same-name/different-source throw, leaving
478
+ // the registry-keyed callers (sub-workflow lookup, RPC mount) silently
479
+ // bound to whichever entry sorted first by specificity. We keep the
480
+ // deterministic winner (first-registered) so boot stays tolerant, but no
481
+ // longer hide the collision.
482
+ const registered = new Map<string, string>();
483
+ for (const r of table) {
484
+ const wfName = readWorkflowName(r.workflow) ?? r.workflowKey;
485
+ const existingSource = registered.get(wfName);
486
+ if (existingSource !== undefined) {
487
+ if (existingSource !== r.source) {
488
+ const message = `[blok] workflow name collision — "${wfName}" is claimed by ${existingSource} and ${r.source}; only ${existingSource} is reachable as a sub-workflow / via RPC. Rename one workflow so each \`name\` is unique.`;
489
+ this.logger.error(message);
490
+ diagnostics.record({
491
+ kind: "duplicate",
492
+ winnerSource: existingSource,
493
+ droppedSource: r.source,
494
+ message,
495
+ });
496
+ }
497
+ continue;
498
+ }
499
+ registered.set(wfName, r.source);
500
+ registry.register({
501
+ name: wfName,
502
+ source: r.source,
503
+ workflow: r.workflow,
504
+ // Bug 01 — derive the middleware marker from the workflow object.
505
+ // `buildRouteTable` already excludes middleware, so in practice
506
+ // this is always false for routed entries; reading it here keeps
507
+ // the flag correct if that exclusion is ever relaxed and covers
508
+ // raw-object-literal / legacy `Workflow()` middleware that slipped
509
+ // in with a trigger.
510
+ isMiddleware: readMiddlewareFlag(r.workflow),
511
+ });
512
+ }
513
+ if (registered.size > 0) {
514
+ this.logger.log(`[blok] workflow registry — ${registered.size} workflow(s) callable as sub-workflow`);
515
+ }
516
+
517
+ // v0.5 · register middleware-only workflows (workflow.middleware === true).
518
+ // These don't appear in the route table because they have no trigger;
519
+ // register them under their own `name` so `trigger.http.middleware: [...]`
520
+ // lookups in `runMiddlewareChain` can find them.
521
+ let middlewareCount = 0;
522
+ for (const sw of scannedJson) {
523
+ if (!readMiddlewareFlag(sw.workflow)) continue;
524
+ const wfName = readWorkflowName(sw.workflow) ?? sw.name;
525
+ if (!wfName) continue;
526
+ if (registered.has(wfName)) continue;
527
+ registered.set(wfName, sw.source);
528
+ registry.register({
529
+ name: wfName,
530
+ source: sw.source,
531
+ workflow: sw.workflow,
532
+ isMiddleware: true,
533
+ });
534
+ middlewareCount++;
535
+ }
536
+ if (middlewareCount > 0) {
537
+ this.logger.log(`[blok] middleware registry — ${middlewareCount} middleware workflow(s) registered`);
538
+ }
539
+
540
+ return table;
541
+ }
542
+
543
+ /**
544
+ * Bug 01 — register TS-authored middleware from the static `Workflows.ts`
545
+ * map. A trigger-less `workflow({ middleware: true })` never appears in the
546
+ * route table (and the v2 helper carries the flag on `_config`, not the
547
+ * root), so neither `buildFileBasedRoutes` nor `scanAndRegisterMiddleware`
548
+ * (JSON-only) picks it up. This pass runs UNCONDITIONALLY from `listen()`
549
+ * so TS middleware is registered whether file-based routing is on or off.
550
+ *
551
+ * Dedupe by name against whatever is already in the registry so it stays
552
+ * idempotent across HMR re-scans and never collides with a same-named JSON
553
+ * workflow already registered from a different source.
554
+ */
555
+ private registerManualMiddleware(workflows: Record<string, unknown>): void {
556
+ const registry = WorkflowRegistry.getInstance();
557
+ let count = 0;
558
+ for (const key of Object.keys(workflows ?? {})) {
559
+ const wf = workflows[key];
560
+ if (!readMiddlewareFlag(wf)) continue;
561
+ const wfName = readWorkflowName(wf) ?? key;
562
+ const source = `Workflows.ts[${JSON.stringify(key)}]`;
563
+ const existing = registry.get(wfName);
564
+ if (existing) {
565
+ // Same workflow already registered (HMR re-run or a JSON dupe) —
566
+ // leave it. The route-table / JSON passes own the canonical entry.
567
+ continue;
568
+ }
569
+ registry.register({
570
+ name: wfName,
571
+ source,
572
+ workflow: wf,
573
+ isMiddleware: true,
574
+ });
575
+ count++;
576
+ }
577
+ if (count > 0) {
578
+ this.logger.log(`[blok] middleware registry — ${count} TS middleware workflow(s) registered`);
579
+ }
580
+ }
581
+
582
+ /**
583
+ * v0.5 · scan WORKFLOWS_PATH/json/ for ALL workflows and register them
584
+ * in WorkflowRegistry. Runs even when file-based routing is OFF — the
585
+ * catch-all dispatch path also honours `trigger.http.middleware: [...]`
586
+ * references AND `subworkflow:` step lookups, both of which need the
587
+ * registry populated.
588
+ *
589
+ * Two passes by purpose:
590
+ * 1. Middleware-only workflows (`middleware: true`) registered with
591
+ * `isMiddleware: true` — fed to the trigger-level / workflow-level
592
+ * middleware chain dispatcher in `runMiddlewareChain`.
593
+ * 2. Non-middleware workflows registered without the marker — fed to
594
+ * `SubworkflowNode` so authors can compose workflows via the
595
+ * `subworkflow: "<name>"` step shape regardless of whether the
596
+ * route table was built (file-based-routing on) or not.
597
+ *
598
+ * Idempotent: if `buildFileBasedRoutes` already ran (file-based
599
+ * routing enabled) and registered these entries, the same `(name,
600
+ * source)` pairs skip re-adding. Name collisions across files surface
601
+ * as registration errors per `WorkflowRegistry.register`.
602
+ */
603
+ private async scanAndRegisterMiddleware(): Promise<void> {
604
+ const workflowsRoot = process.env.WORKFLOWS_PATH || process.env.VITE_WORKFLOWS_PATH || `${process.cwd()}/workflows`;
605
+ const scanned = await scanWorkflows(
606
+ [
607
+ {
608
+ dir: path.join(workflowsRoot, "json"),
609
+ kind: "json",
610
+ stripLeadingSegments: 0,
611
+ },
612
+ ],
613
+ {
614
+ onLoadError: (file, err) => {
615
+ this.logger.error(`[blok] middleware scan: workflow load error in ${file}: ${err.message}`);
616
+ },
617
+ },
618
+ );
619
+
620
+ const registry = WorkflowRegistry.getInstance();
621
+ let middlewareCount = 0;
622
+ let subworkflowCount = 0;
623
+ for (const sw of scanned) {
624
+ const wfObj = sw.workflow as { name?: unknown; middleware?: unknown } | undefined;
625
+ if (!wfObj) continue;
626
+ const wfName = typeof wfObj.name === "string" ? wfObj.name : sw.name;
627
+ if (!wfName) continue;
628
+
629
+ // Skip workflows already registered from the same source —
630
+ // usually means buildFileBasedRoutes already grabbed them.
631
+ const existing = registry.get(wfName);
632
+ if (existing && existing.source === sw.source) continue;
633
+
634
+ if (wfObj.middleware === true) {
635
+ // Middleware-only workflow — only re-register if no entry
636
+ // exists or the existing entry is also a middleware (keep
637
+ // non-middleware route-table entries intact).
638
+ if (existing && !existing.isMiddleware) continue;
639
+ registry.register({
640
+ name: wfName,
641
+ source: sw.source,
642
+ workflow: sw.workflow,
643
+ isMiddleware: true,
644
+ });
645
+ middlewareCount++;
646
+ } else {
647
+ // Non-middleware workflow — register for sub-workflow
648
+ // lookup. Skip if anything is already registered under
649
+ // this name (route table or middleware re-claim).
650
+ if (existing) continue;
651
+ registry.register({
652
+ name: wfName,
653
+ source: sw.source,
654
+ workflow: sw.workflow,
655
+ });
656
+ subworkflowCount++;
657
+ }
658
+ }
659
+ if (middlewareCount > 0) {
660
+ this.logger.log(
661
+ `[blok] middleware registry — ${middlewareCount} middleware workflow(s) registered (catch-all path)`,
662
+ );
663
+ }
664
+ if (subworkflowCount > 0) {
665
+ this.logger.log(
666
+ `[blok] workflow registry — ${subworkflowCount} workflow(s) registered for sub-workflow lookup (catch-all path)`,
667
+ );
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Register every entry in the route table as an explicit Hono route.
673
+ * Each handler delegates to `runWorkflowExecution` with the route's
674
+ * pre-loaded workflow object passed directly to `Configuration.init`
675
+ * (no disk re-read at request time, no LocalStorage fallback dance).
676
+ *
677
+ * Registered BEFORE the catch-all so explicit routes win. Method
678
+ * "ANY" maps to `app.all(...)`; otherwise `app[method](...)`. Falls
679
+ * back to `app.all(...)` for HEAD/OPTIONS where Hono lacks a
680
+ * dedicated method.
681
+ */
682
+ private registerExplicitRoutes(routes: readonly RouteEntry[]): void {
683
+ for (const route of routes) {
684
+ const handler = async (c: HonoContext<AppBindings>): Promise<Response> => {
685
+ const requestId = c.req.query("requestId") || (uuid() as string);
686
+ const { body, rawBody } = await this.parseBody(c);
687
+ return this.runWorkflowExecution(c, {
688
+ workflowName: route.workflowKey,
689
+ subPath: "/",
690
+ body,
691
+ rawBody,
692
+ requestId,
693
+ explicitRoute: true,
694
+ preloadedWorkflow: route.workflow,
695
+ });
696
+ };
697
+
698
+ const method = route.method.toUpperCase();
699
+ switch (method) {
700
+ case "GET":
701
+ this.app.get(route.path, handler);
702
+ break;
703
+ case "POST":
704
+ this.app.post(route.path, handler);
705
+ break;
706
+ case "PUT":
707
+ this.app.put(route.path, handler);
708
+ break;
709
+ case "DELETE":
710
+ this.app.delete(route.path, handler);
711
+ break;
712
+ case "PATCH":
713
+ this.app.patch(route.path, handler);
714
+ break;
715
+ default:
716
+ // ANY (or HEAD/OPTIONS/unknown) — register on all methods.
717
+ this.app.all(route.path, handler);
718
+ break;
719
+ }
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Parse the request body using the same content-type rules the
725
+ * catch-all handler uses. Extracted so both paths (catch-all and
726
+ * explicit routes) parse bodies identically.
727
+ *
728
+ * Returns BOTH the parsed body (whatever shape the content-type
729
+ * dictates) AND the raw body string captured BEFORE parsing.
730
+ * `rawBody` is what webhook HMAC verifiers need to match a
731
+ * provider's signature byte-exactly (Stripe `Stripe-Signature`,
732
+ * Slack `X-Slack-Signature`, GitHub when bodies contain content
733
+ * the JSON.stringify round-trip would mangle). For application/json
734
+ * payloads we parse the raw text manually instead of calling
735
+ * `c.req.json()` because `c.req.text()` and `c.req.json()` both
736
+ * consume the body stream — we only get one shot.
737
+ *
738
+ * `rawBody` is the empty string for GET/HEAD (no body), for
739
+ * non-text content-types where capture doesn't apply
740
+ * (multipart/form-data — Hono's `parseBody()` reads the stream and
741
+ * we can't get the raw bytes back without parsing twice), and
742
+ * when the underlying read throws.
743
+ */
744
+ private async parseBody(c: HonoContext<AppBindings>): Promise<{ body: unknown; rawBody: string }> {
745
+ if (c.req.method === "GET" || c.req.method === "HEAD") return { body: {}, rawBody: "" };
746
+ const contentType = c.req.header("content-type") || "";
747
+
748
+ // multipart needs Hono's stream parser — raw bytes aren't recoverable
749
+ // after the parse, so rawBody stays empty. Webhook providers that
750
+ // sign multipart bodies are vanishingly rare; the rest is the
751
+ // common case.
752
+ if (contentType.includes("multipart/form-data")) {
753
+ try {
754
+ return { body: await c.req.parseBody(), rawBody: "" };
755
+ } catch {
756
+ return { body: {}, rawBody: "" };
757
+ }
758
+ }
759
+
760
+ // For application/json and application/x-www-form-urlencoded we
761
+ // CAN capture the raw body and still parse — read the text once,
762
+ // then parse off the captured string ourselves.
763
+ let rawBody = "";
764
+ try {
765
+ rawBody = await c.req.text();
766
+ } catch {
767
+ return { body: {}, rawBody: "" };
768
+ }
769
+
770
+ if (contentType.includes("application/json")) {
771
+ try {
772
+ return { body: rawBody.length === 0 ? {} : JSON.parse(rawBody), rawBody };
773
+ } catch {
774
+ // Malformed JSON — preserve rawBody (a webhook verifier
775
+ // might still want it for its 4xx error response) and let
776
+ // downstream handle the empty parsed body. Matches pre-
777
+ // v0.6 behaviour of returning {} on parse failure.
778
+ return { body: {}, rawBody };
779
+ }
780
+ }
781
+
782
+ if (contentType.includes("application/x-www-form-urlencoded")) {
783
+ try {
784
+ const parsed: Record<string, string> = {};
785
+ for (const [k, v] of new URLSearchParams(rawBody)) parsed[k] = v;
786
+ return { body: parsed, rawBody };
787
+ } catch {
788
+ return { body: {}, rawBody };
789
+ }
790
+ }
791
+
792
+ // Default — text body, parsed body = raw text. Matches pre-v0.6
793
+ // `c.req.text()` fallback.
794
+ return { body: rawBody, rawBody };
795
+ }
796
+
797
+ async listen(): Promise<number> {
798
+ // Metrics opt-out gate. ON by default; `BLOK_METRICS_DISABLED=1` skips the
799
+ // exporter + global MeterProvider entirely (every blok_* instrument then
800
+ // no-ops) and the `/metrics` route below is not registered. Previously the
801
+ // exporter installed itself at module-import + via a Dockerfile --preload,
802
+ // so it could never be turned off.
803
+ //
804
+ // MUST run BEFORE maybeBootstrapTracing(): `metrics.setGlobalMeterProvider`
805
+ // is first-registration-wins, and bootstrapping the OTel trace SDK touches
806
+ // the global meter provider — if tracing goes first, the Prometheus
807
+ // MeterProvider never wins the global slot, so blok_* instruments (created
808
+ // via `metrics.getMeter(...)`) record into a no-op meter and `/metrics`
809
+ // shows "# no registered metrics" whenever OTLP tracing is enabled. Setting
810
+ // the metrics provider first lets metrics + tracing coexist.
811
+ const metricsBootstrap = await bootstrapMetrics();
812
+ if (!metricsBootstrap) {
813
+ this.logger.log("[blok][metrics] disabled (BLOK_METRICS_DISABLED=1) — no /metrics endpoint, instruments no-op.");
814
+ }
815
+
816
+ // OBS-02 — opt-in distributed tracing. When OTEL_EXPORTER_OTLP_ENDPOINT
817
+ // is set, install an OTel SDK so the spans created throughout the runner
818
+ // (every trigger handler, gRPC runtime call, etc. via `startActiveSpan` /
819
+ // `recordException`) actually export to Tempo/Jaeger/etc. Without this the
820
+ // global tracer is a no-op: spans run but go nowhere. No-op + zero
821
+ // overhead when the env var is unset.
822
+ await this.maybeBootstrapTracing();
823
+
824
+ try {
825
+ this.nodeMap.workflows = (await resolveManualWorkflowMap(
826
+ (workflows as Record<string, unknown>) ?? {},
827
+ )) as GlobalOptions["workflows"];
828
+ } catch (err) {
829
+ this.logger.error(`[blok] TS workflow registration failed: ${(err as Error).message}`);
830
+ }
831
+
832
+ // File-based routing — scan workflow folders, build the route table,
833
+ // and register each entry as an explicit Hono route BEFORE the
834
+ // catch-all. **Default ON since v0.6**; opt out via
835
+ // `BLOK_FILE_BASED_ROUTING=false` or `BLOK_ROUTING_LEGACY=1`.
836
+ // When off, all requests fall through to the catch-all (legacy
837
+ // /<workflow-key> URL scheme).
838
+ let fileBasedRoutes: RouteEntry[] = [];
839
+ try {
840
+ fileBasedRoutes = await this.buildFileBasedRoutes();
841
+ } catch (err) {
842
+ this.logger.error(`[blok] file-based routing setup failed: ${(err as Error).message}`);
843
+ }
844
+
845
+ // v0.5 · scan + register middleware-only workflows even when
846
+ // file-based routing is off (the catch-all dispatch path also
847
+ // honours `trigger.http.middleware: [...]`). buildFileBasedRoutes
848
+ // already does this for routed + middleware workflows when enabled;
849
+ // this fallback covers the off case so middleware works uniformly.
850
+ try {
851
+ await this.scanAndRegisterMiddleware();
852
+ } catch (err) {
853
+ this.logger.error(`[blok] middleware scan failed: ${(err as Error).message}`);
854
+ }
855
+
856
+ // Bug 01 · register TS-authored middleware (`workflow({ middleware: true })`
857
+ // exported from `Workflows.ts`). Runs unconditionally — trigger-less TS
858
+ // middleware never enters the route table, and the JSON-only scan above
859
+ // won't see it — so without this pass `runMiddlewareChain` 500s for the
860
+ // documented (recommended) TS authoring path.
861
+ try {
862
+ this.registerManualMiddleware((this.nodeMap.workflows as Record<string, unknown>) ?? {});
863
+ } catch (err) {
864
+ this.logger.error(`[blok] TS middleware registration failed: ${(err as Error).message}`);
865
+ }
866
+
867
+ // v0.5.4 · process-global middleware. Read the BLOK_GLOBAL_MIDDLEWARE
868
+ // env var as a fallback registration path — useful when the operator
869
+ // wants to add ops middleware (request-id, audit-log) without
870
+ // rebuilding the trigger image. The programmatic API
871
+ // (`WorkflowRegistry.setGlobalMiddleware([...])`) takes precedence:
872
+ // if the registry already has a global chain set, the env var is
873
+ // ignored. This lets a programmatic boot-time setup override an
874
+ // operator's CI-injected env without surprising overrides.
875
+ const registry = WorkflowRegistry.getInstance();
876
+ if (registry.getGlobalMiddleware().length === 0 && process.env.BLOK_GLOBAL_MIDDLEWARE) {
877
+ const fromEnv = process.env.BLOK_GLOBAL_MIDDLEWARE.split(",")
878
+ .map((s) => s.trim())
879
+ .filter((s) => s.length > 0);
880
+ if (fromEnv.length > 0) {
881
+ registry.setGlobalMiddleware(fromEnv);
882
+ this.logger.log(`[blok] global middleware registered from BLOK_GLOBAL_MIDDLEWARE env: ${fromEnv.join(", ")}`);
883
+ }
884
+ }
885
+ const globalChain = registry.getGlobalMiddleware();
886
+ if (globalChain.length > 0) {
887
+ this.logger.log(`[blok] process-global middleware chain (applies to every workflow): ${globalChain.join(" → ")}`);
888
+ }
889
+
890
+ // v0.7 — fire pre-catch-all hooks. Sibling triggers (WebSocketTrigger,
891
+ // SSETrigger) registered routes on the shared Hono app here so they
892
+ // match BEFORE the legacy `/:workflow{.+}` catch-all below. The
893
+ // workflow registry is fully populated by this point, so hooks can
894
+ // walk it to discover the routes they need to mount.
895
+ for (const hook of this.preCatchAllHooks) {
896
+ try {
897
+ const result = hook();
898
+ if (result instanceof Promise) {
899
+ await result.catch((err: unknown) => {
900
+ const msg = err instanceof Error ? err.message : String(err);
901
+ this.logger.error(`[blok] pre-catch-all hook failed: ${msg}`);
902
+ });
903
+ }
904
+ } catch (err) {
905
+ const msg = err instanceof Error ? err.message : String(err);
906
+ this.logger.error(`[blok] pre-catch-all hook failed: ${msg}`);
907
+ }
908
+ }
909
+
910
+ return new Promise((done) => {
911
+ // Static files
912
+ this.app.use("/public/*", serveStatic({ root: "./" }));
913
+
914
+ // CORS — configurable via BLOK_CORS_ORIGIN.
915
+ // Default (unset): NO CORS headers are emitted (same-origin policy).
916
+ // Set BLOK_CORS_ORIGIN=* to opt into the permissive wildcard (public
917
+ // API); set a single origin or a comma-separated allow-list for a
918
+ // credentialed app. Previously this was an unconditional `cors()` —
919
+ // Hono's default is `origin: "*"`, which can't be tightened and is a
920
+ // footgun for any API that returns user-scoped data.
921
+ const corsOriginEnv = process.env.BLOK_CORS_ORIGIN;
922
+ if (corsOriginEnv) {
923
+ const origins = corsOriginEnv
924
+ .split(",")
925
+ .map((s) => s.trim())
926
+ .filter((s) => s.length > 0);
927
+ if (origins.length > 0) {
928
+ this.app.use(cors({ origin: origins.length === 1 ? origins[0] : origins }));
929
+ }
930
+ }
931
+
932
+ // Health check
933
+ this.app.all("/health-check", (c) => {
934
+ return c.text("Online and ready for action", 200);
935
+ });
936
+
937
+ // Prometheus metrics — uses raw Node.js req/res since the
938
+ // OpenTelemetry Prometheus exporter expects (IncomingMessage, ServerResponse).
939
+ // Only registered when metrics are enabled (see the bootstrap gate above);
940
+ // with BLOK_METRICS_DISABLED=1 there is no /metrics route at all (→ 404).
941
+ if (metricsBootstrap) {
942
+ this.app.get("/metrics", (c) => {
943
+ try {
944
+ metricsBootstrap.metricsHandler(c.env.incoming, c.env.outgoing);
945
+ return RESPONSE_ALREADY_SENT;
946
+ } catch (error) {
947
+ return c.text("Error serving metrics", 500);
948
+ }
949
+ });
950
+ }
951
+
952
+ // --- Typed client RPC mount (P1.3) ---
953
+ // `POST /__blok/rpc/:name` runs a registered workflow BY NAME and
954
+ // returns its output as JSON — the name-keyed entrypoint the typed
955
+ // `@blokjs/client` calls (SPEC-blok-client-sdk.md §4.3). Registered
956
+ // BEFORE the `/__blok` trace router so it isn't swallowed by it. The
957
+ // request body is the workflow's input; the workflow's own middleware
958
+ // chain (auth, etc.) still runs inside `runWorkflowExecution`.
959
+ this.app.post("/__blok/rpc/:name", async (c) => {
960
+ const name = c.req.param("name");
961
+ const entry = WorkflowRegistry.getInstance().get(name);
962
+ // F8 — only run http-callable workflows over RPC. A workflow is
963
+ // reachable here only if it (a) is registered, (b) is not
964
+ // middleware, and (c) actually declares a `trigger.http` block.
965
+ // Without (c), a worker/cron-only workflow registered for
966
+ // sub-workflow lookup would be executable over HTTP with no
967
+ // trigger-surface gate — and `runWorkflowExecution` would resolve
968
+ // its middleware against the wrong (`http`) trigger kind, silently
969
+ // dropping the worker/cron middleware chain (e.g. auth).
970
+ if (!entry || entry.isMiddleware === true || !hasHttpTrigger(entry.workflow)) {
971
+ return c.json({ error: `Workflow "${name}" is not registered for RPC.` }, 404);
972
+ }
973
+
974
+ // Mount-level auth gate. The RPC surface is in the /__blok/
975
+ // namespace but is registered BEFORE the trace router, so the
976
+ // trace-auth gate (FW-1) never covers it — meaning any
977
+ // http-triggered workflow without its own auth middleware was
978
+ // callable unauthenticated. Mirror the trace gate here: in
979
+ // production, refuse unless an authorize hook is registered (or
980
+ // BLOK_RPC_AUTH_DISABLED=1 opts out, e.g. /__blok/* is firewalled
981
+ // at the network layer). Reuses the operator's `setTraceAuth`
982
+ // hook so there's one auth surface for the whole /__blok/ mount.
983
+ // Per-workflow middleware still runs inside runWorkflowExecution.
984
+ const isProd = process.env.BLOK_ENV === "production" || process.env.NODE_ENV === "production";
985
+ if (isProd && process.env.BLOK_RPC_AUTH_DISABLED !== "1") {
986
+ if (!this.traceAuthFn) {
987
+ return c.json(
988
+ {
989
+ error: "RPC endpoint requires auth in production",
990
+ hint: "Register an authorize hook before listen() — `trigger.setTraceAuth(req => ...)` — or set BLOK_RPC_AUTH_DISABLED=1 to opt out (typically because /__blok/* is firewalled).",
991
+ },
992
+ 503,
993
+ );
994
+ }
995
+ const raw = c.req.raw;
996
+ const allowed = await Promise.resolve()
997
+ .then(() =>
998
+ // biome-ignore lint/style/noNonNullAssertion: guarded above
999
+ this.traceAuthFn!({
1000
+ method: raw.method,
1001
+ params: c.req.param(),
1002
+ query: Object.fromEntries(new URL(raw.url).searchParams),
1003
+ headers: Object.fromEntries(raw.headers),
1004
+ body: null,
1005
+ on: () => {},
1006
+ }),
1007
+ )
1008
+ .catch(() => false);
1009
+ if (!allowed) return c.json({ error: "Unauthorized" }, 401);
1010
+ }
1011
+
1012
+ const requestId = c.req.query("requestId") || (uuid() as string);
1013
+ const { body, rawBody } = await this.parseBody(c);
1014
+ const input = body && typeof body === "object" && !Array.isArray(body) ? (body as Record<string, unknown>) : {};
1015
+
1016
+ // Streaming request (P3.2): when the client asks for an SSE stream,
1017
+ // run the workflow with a bound `ctx.stream` and forward its frames.
1018
+ if ((c.req.header("accept") || "").includes("text/event-stream")) {
1019
+ return this.runWorkflowStream(c, { name, preloadedWorkflow: entry.workflow, input, requestId });
1020
+ }
1021
+
1022
+ return this.runWorkflowExecution(c, {
1023
+ workflowName: name,
1024
+ subPath: "",
1025
+ body: input,
1026
+ rawBody,
1027
+ requestId,
1028
+ explicitRoute: true,
1029
+ preloadedWorkflow: entry.workflow,
1030
+ rpcInput: input,
1031
+ });
1032
+ });
1033
+
1034
+ // --- Node catalog (SPEC-B P1.3) ---
1035
+ // `GET /__blok/nodes` lists every node across all runtimes — in-process
1036
+ // module nodes (with their reflected JSON Schema) + each connected
1037
+ // runtime's `ListNodes`. Powers `blokctl nodes list` + the typed
1038
+ // client's runtime-node typing. Registered before the trace router.
1039
+ this.app.get("/__blok/nodes", async (c) => {
1040
+ const moduleNodes = this.nodeMap.nodes?.getNodes?.() as Map<string, unknown> | undefined;
1041
+ const nodes = await buildNodeCatalog(moduleNodes, RuntimeRegistry.getInstance().getAll());
1042
+ return c.json({ nodes, count: nodes.length });
1043
+ });
1044
+
1045
+ // --- Blok Studio: Trace API ---
1046
+ // Must be registered BEFORE AppRoutes and the catch-all workflow handler
1047
+ // so that /__blok/* requests are handled by the trace router, not treated
1048
+ // as workflow lookups.
1049
+ if (process.env.BLOK_TRACE_ENABLED !== "false") {
1050
+ const { traceAdapter, traceApp } = createTraceRouterAdapter();
1051
+ // Security review FW-1 — thread the operator-registered
1052
+ // authorize hook (if any) into the trace router. Production
1053
+ // without `setTraceAuth(...)` returns 503 from inside the
1054
+ // trace router middleware.
1055
+ registerTraceRoutes(traceAdapter, undefined, { authorize: this.traceAuthFn });
1056
+ this.app.route("/__blok", traceApp);
1057
+ }
1058
+
1059
+ /*
1060
+ * You can add your own middleware or routes with custom Hono logic
1061
+ * to extend this project.
1062
+ */
1063
+ this.app.route("/", apps);
1064
+
1065
+ // File-based routing — register every scanned workflow at its
1066
+ // resolved URL (explicit `trigger.http.path` wins; otherwise the
1067
+ // file-derived path is used). Registered BEFORE the catch-all so
1068
+ // matching requests are routed directly without filename-prefix
1069
+ // dispatch. Empty when `BLOK_FILE_BASED_ROUTING=false` or
1070
+ // `BLOK_ROUTING_LEGACY=1` is set.
1071
+ if (fileBasedRoutes.length > 0) this.registerExplicitRoutes(fileBasedRoutes);
1072
+
1073
+ // Catch-all workflow handler — legacy /<workflow-key>/<path> dispatch.
1074
+ // Falls through here only when no explicit file-based route matched.
1075
+ const workflowHandler = async (c: HonoContext<AppBindings>) => {
1076
+ const requestId = c.req.query("requestId") || (uuid() as string);
1077
+ const workflowNameInPath = c.req.param("workflow") as string;
1078
+
1079
+ // Skip internal paths — these are handled by dedicated routers above
1080
+ if (workflowNameInPath === "__blok") {
1081
+ return c.json({ error: "Not found" }, 404);
1082
+ }
1083
+
1084
+ // Compute the sub-path (equivalent to Express req.path in use() middleware context)
1085
+ const fullPath = c.req.path;
1086
+ const subPath = workflowNameInPath ? fullPath.slice(1 + workflowNameInPath.length) || "/" : fullPath;
1087
+
1088
+ const { body, rawBody } = await this.parseBody(c);
1089
+
1090
+ // Remote node execution dispatch (header-based) — only meaningful for
1091
+ // the catch-all path, never for explicit routes.
1092
+ let remoteNodeExecution = false;
1093
+ let runtimeWorkflow: RuntimeWorkflow | undefined;
1094
+ if (c.req.header("x-blok-execute-node") === "true" && c.req.method.toLowerCase() === "post") {
1095
+ remoteNodeExecution = true;
1096
+ const coder = new MessageDecode();
1097
+ const messageContext: Context = coder.requestDecode(body as WorkflowRequest);
1098
+ runtimeWorkflow = messageContext as unknown as RuntimeWorkflow;
1099
+ }
1100
+
1101
+ return this.runWorkflowExecution(c, {
1102
+ workflowName: workflowNameInPath,
1103
+ subPath,
1104
+ body,
1105
+ rawBody,
1106
+ requestId,
1107
+ remoteNodeExecution,
1108
+ runtimeWorkflow,
1109
+ });
1110
+ };
1111
+
1112
+ this.app.all("/:workflow{.+}/*", workflowHandler);
1113
+ this.app.all("/:workflow{.+}", workflowHandler);
1114
+
1115
+ this.server = serve({ fetch: this.app.fetch, port: Number(this.port) }, () => {
1116
+ this.logger.log(`Server is running at http://localhost:${this.port}`);
1117
+
1118
+ // v0.7 — run server hooks (sibling triggers like
1119
+ // WebSocketTrigger call `injectWebSocket(server)` here to
1120
+ // attach their `upgrade` listener to the http.Server). Errors
1121
+ // caught + logged so a misbehaving hook doesn't bring the
1122
+ // server down. The cast is safe — `serve()` returns a Server
1123
+ // instance per @hono/node-server's types.
1124
+ if (this.server && this.serverHooks.length > 0) {
1125
+ for (const hook of this.serverHooks) {
1126
+ try {
1127
+ const result = hook(this.server);
1128
+ if (result instanceof Promise) {
1129
+ result.catch((err: unknown) => {
1130
+ const msg = err instanceof Error ? err.message : String(err);
1131
+ this.logger.error(`[blok] server hook failed: ${msg}`);
1132
+ });
1133
+ }
1134
+ } catch (err) {
1135
+ const msg = err instanceof Error ? err.message : String(err);
1136
+ this.logger.error(`[blok] server hook failed: ${msg}`);
1137
+ }
1138
+ }
1139
+ }
1140
+
1141
+ // Enable HMR in development mode
1142
+ if (process.env.BLOK_HMR === "true" || process.env.NODE_ENV === "development") {
1143
+ this.enableHotReload();
1144
+ }
1145
+
1146
+ // Tier 2 quick-wins follow-up · install process-level handlers
1147
+ // that flip in-flight `running` runs to `crashed` on uncaught
1148
+ // errors, AND scan for orphaned runs from a previous (dead)
1149
+ // process. Both are idempotent + opt-out via
1150
+ // `BLOK_CRASH_AUTOFLIP_DISABLED=1`.
1151
+ try {
1152
+ HttpTrigger.installCrashHandlers(this.logger);
1153
+ const orphaned = HttpTrigger.recoverOrphanedRuns(undefined, this.logger);
1154
+ if (orphaned > 0) {
1155
+ this.logger.log(`[crash-autoflip] flipped ${orphaned} orphaned run(s) to crashed on boot`);
1156
+ }
1157
+ } catch (err) {
1158
+ this.logger.error(`[crash-autoflip] setup failed: ${err instanceof Error ? err.message : String(err)}`);
1159
+ }
1160
+
1161
+ // Tier 2 follow-up · start the periodic storage janitor for
1162
+ // stale idempotency_cache + concurrency_locks + scheduled_dispatches
1163
+ // rows. Idempotent (singleton); opt-out via `BLOK_JANITOR_DISABLED=1`.
1164
+ try {
1165
+ Janitor.getInstance(RunTracker.getInstance().getStore(), this.logger).start();
1166
+ } catch (err) {
1167
+ this.logger.error(`[janitor] setup failed: ${err instanceof Error ? err.message : String(err)}`);
1168
+ }
1169
+
1170
+ // Tier 2 follow-up · install graceful shutdown handlers
1171
+ // (SIGTERM / SIGINT) so backend connections + janitor +
1172
+ // scheduler all drain cleanly on process exit. Idempotent;
1173
+ // opt-out via `BLOK_GRACEFUL_SHUTDOWN_DISABLED=1`.
1174
+ try {
1175
+ HttpTrigger.installShutdownHandlers(this, this.logger);
1176
+ } catch (err) {
1177
+ this.logger.error(`[shutdown] setup failed: ${err instanceof Error ? err.message : String(err)}`);
1178
+ }
1179
+
1180
+ // Tier 2 #5+#7 follow-up · re-fire HTTP dispatches that were
1181
+ // pending when this process (or its predecessor) crashed.
1182
+ // Idempotent — safe to call multiple times.
1183
+ this.recoverDispatches().catch((err: unknown) => {
1184
+ this.logger.error(
1185
+ `[scheduling] HTTP dispatch recovery failed: ${err instanceof Error ? err.message : String(err)}`,
1186
+ );
1187
+ });
1188
+
1189
+ done(this.endCounter(this.initializer));
1190
+ }) as Server;
1191
+ });
1192
+ }
1193
+
1194
+ /**
1195
+ * Stream a workflow's SSE events over the name-keyed RPC mount (P3.2).
1196
+ *
1197
+ * Mirrors the dedicated SSE trigger's dispatch: open an `streamSSE` response,
1198
+ * bind a `ctx.stream` (StreamContext) to the Hono stream, run the workflow
1199
+ * once, and forward every `ctx.stream.writeSSE(...)` / `@blokjs/sse-emit`
1200
+ * frame to the client. The input arrives as `ctx.request.body` and its scalar
1201
+ * fields are mirrored into query + params (same contract as the unary mount).
1202
+ *
1203
+ * `ctx.stream.subscribe()` (the in-process SSE event bus) is intentionally
1204
+ * NOT wired here — bus-subscription workflows should use the dedicated SSE
1205
+ * trigger. Push-style streaming (`@blokjs/sse-emit`, `streamTo: "sse"`) works.
1206
+ */
1207
+ private async runWorkflowStream(
1208
+ c: HonoContext<AppBindings>,
1209
+ opts: { name: string; preloadedWorkflow: unknown; input: Record<string, unknown>; requestId: string },
1210
+ ): Promise<Response> {
1211
+ const { name, preloadedWorkflow, input, requestId } = opts;
1212
+ const lastEventId = c.req.header("Last-Event-ID") || c.req.header("last-event-id") || null;
1213
+ const headers = Object.fromEntries([...c.req.raw.headers.entries()]);
1214
+
1215
+ return streamSSE(c, async (honoStream) => {
1216
+ const abortController = new AbortController();
1217
+ let closed = false;
1218
+ honoStream.onAbort(() => {
1219
+ closed = true;
1220
+ abortController.abort();
1221
+ });
1222
+
1223
+ const stream: StreamContext = {
1224
+ get id() {
1225
+ return requestId;
1226
+ },
1227
+ get lastEventId() {
1228
+ return lastEventId;
1229
+ },
1230
+ get closed() {
1231
+ return closed;
1232
+ },
1233
+ get signal() {
1234
+ return abortController.signal;
1235
+ },
1236
+ async writeSSE({ event, data, id, retry }) {
1237
+ if (closed) return;
1238
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
1239
+ await honoStream
1240
+ .writeSSE({
1241
+ data: payload,
1242
+ ...(event ? { event } : {}),
1243
+ ...(id ? { id } : {}),
1244
+ ...(typeof retry === "number" ? { retry } : {}),
1245
+ })
1246
+ .catch(() => {
1247
+ /* client gone — swallow */
1248
+ });
1249
+ },
1250
+ async writeComment(text) {
1251
+ if (closed) return;
1252
+ await honoStream.write(`: ${text}\n\n`).catch(() => {});
1253
+ },
1254
+ close() {
1255
+ closed = true;
1256
+ abortController.abort();
1257
+ },
1258
+ subscribe() {
1259
+ throw new Error(
1260
+ "[blok] ctx.stream.subscribe() (the SSE event bus) is not available over /__blok/rpc — " +
1261
+ "use the dedicated SSE trigger for bus-subscription workflows.",
1262
+ );
1263
+ },
1264
+ };
1265
+
1266
+ try {
1267
+ await this.configuration.init(name, this.nodeMap, preloadedWorkflow);
1268
+ const ctx: Context = this.createContext(undefined, name, requestId);
1269
+ const query: Record<string, string> = {};
1270
+ const params: Record<string, string> = {};
1271
+ for (const [k, v] of Object.entries(input)) {
1272
+ if (v !== null && v !== undefined && typeof v !== "object") {
1273
+ query[k] = String(v);
1274
+ params[k] = String(v);
1275
+ }
1276
+ }
1277
+ ctx.request = {
1278
+ body: input,
1279
+ rawBody: "",
1280
+ headers,
1281
+ params,
1282
+ query,
1283
+ method: c.req.method,
1284
+ path: c.req.path,
1285
+ url: c.req.url,
1286
+ } as unknown as RequestContext;
1287
+ ctx.stream = stream;
1288
+
1289
+ await this.applyMiddlewareChain(ctx, this.nodeMap);
1290
+ await this.run(ctx);
1291
+ } catch (err) {
1292
+ const msg = err instanceof Error ? err.message : String(err);
1293
+ this.logger.error(`[blok] rpc stream "${name}" failed: ${msg}`);
1294
+ if (!closed) {
1295
+ await honoStream.writeSSE({ event: "error", data: JSON.stringify({ message: msg }) }).catch(() => {});
1296
+ }
1297
+ }
1298
+ });
1299
+ }
1300
+
1301
+ /**
1302
+ * F15 · enforce `trigger.http.headers`. The schema documents this field as
1303
+ * "Required headers for incoming requests (validated at trigger entry)" but
1304
+ * nothing ever read it. For each declared header:
1305
+ * - presence is checked case-insensitively (HTTP header names are
1306
+ * case-insensitive; `ctx.request.headers` keys arrive lower-cased from
1307
+ * `Headers.entries()`);
1308
+ * - when a non-empty string VALUE is declared, the incoming value must
1309
+ * match exactly (case-sensitive) — useful for a fixed API version or a
1310
+ * content-type precondition. A declared empty / non-string value only
1311
+ * asserts presence.
1312
+ *
1313
+ * Throws a `GlobalError` with code 400 + a structured JSON body on the first
1314
+ * violation, short-circuiting before any step runs. No-op when the workflow
1315
+ * declares no `headers`.
1316
+ */
1317
+ private validateRequiredHeaders(requestHeaders: Record<string, unknown> | undefined): void {
1318
+ const http = this.configuration?.trigger?.http as { headers?: unknown } | undefined;
1319
+ const declared = http?.headers as Record<string, unknown> | undefined;
1320
+ if (!declared || typeof declared !== "object") return;
1321
+
1322
+ // Lower-case the incoming header map once for case-insensitive lookup.
1323
+ const incoming: Record<string, string> = {};
1324
+ for (const [k, v] of Object.entries(requestHeaders ?? {})) {
1325
+ incoming[k.toLowerCase()] = typeof v === "string" ? v : String(v ?? "");
1326
+ }
1327
+
1328
+ for (const [rawKey, rawExpected] of Object.entries(declared)) {
1329
+ const key = rawKey.toLowerCase();
1330
+ const present = Object.hasOwn(incoming, key);
1331
+ if (!present) {
1332
+ throw this.headerError(`Missing required header "${rawKey}".`, rawKey, undefined);
1333
+ }
1334
+ if (typeof rawExpected === "string" && rawExpected.length > 0 && incoming[key] !== rawExpected) {
1335
+ throw this.headerError(`Header "${rawKey}" must equal "${rawExpected}".`, rawKey, rawExpected);
1336
+ }
1337
+ }
1338
+ }
1339
+
1340
+ /** Build the 400 `GlobalError` raised by {@link validateRequiredHeaders}. */
1341
+ private headerError(message: string, header: string, expected: string | undefined): GlobalError {
1342
+ const err = new GlobalError(message);
1343
+ err.setCode(400);
1344
+ err.setName("RequiredHeaderError");
1345
+ err.setJson({
1346
+ error: "required_header",
1347
+ message,
1348
+ header,
1349
+ ...(expected !== undefined ? { expected } : {}),
1350
+ });
1351
+ return err;
1352
+ }
1353
+
1354
+ /**
1355
+ * Execute a workflow request — the shared work both the legacy catch-all
1356
+ * and the explicit file-based routes funnel into.
1357
+ *
1358
+ * @param c - Hono context
1359
+ * @param opts.workflowName - workflow identifier (catch-all: extracted from URL; explicit: closure-bound)
1360
+ * @param opts.subPath - sub-path after the workflow key (catch-all only; "/" for explicit routes)
1361
+ * @param opts.body - parsed request body
1362
+ * @param opts.requestId - per-request UUID for tracing
1363
+ * @param opts.explicitRoute - true when called from `registerExplicitRoutes`. Skips
1364
+ * the runtime method+path validation since Hono already routed by both.
1365
+ * @param opts.preloadedWorkflow - the workflow object pre-parsed at boot (file-based
1366
+ * routing path). When provided, `Configuration.init` uses it directly instead of
1367
+ * re-reading from disk.
1368
+ * @param opts.remoteNodeExecution - legacy catch-all path: header-based dispatch.
1369
+ * @param opts.runtimeWorkflow - legacy catch-all path: workflow synthesised at request
1370
+ * time from the remote-node-execution header payload.
1371
+ */
1372
+ private async runWorkflowExecution(
1373
+ c: HonoContext<AppBindings>,
1374
+ opts: {
1375
+ workflowName: string;
1376
+ subPath: string;
1377
+ body: unknown;
1378
+ /**
1379
+ * Raw request body string captured BEFORE JSON / form parsing.
1380
+ * Empty string when the trigger couldn't (or didn't need to)
1381
+ * capture it. Surfaced as `ctx.request.rawBody` so webhook HMAC
1382
+ * verifiers (Stripe, Slack, byte-exact GitHub) can sign the
1383
+ * exact bytes the provider signed.
1384
+ */
1385
+ rawBody?: string;
1386
+ requestId: string;
1387
+ explicitRoute?: boolean;
1388
+ preloadedWorkflow?: unknown;
1389
+ remoteNodeExecution?: boolean;
1390
+ runtimeWorkflow?: RuntimeWorkflow;
1391
+ /**
1392
+ * v0.7 · typed-client RPC mount (`/__blok/rpc/:name`). When set, the
1393
+ * input object's top-level SCALAR fields are mirrored into
1394
+ * `ctx.request.query` + `ctx.request.params` (in addition to arriving
1395
+ * as `body`) so a workflow authored for a GET/query or `:param`
1396
+ * trigger still resolves its inputs uniformly through the name-keyed
1397
+ * mount. Nested/object fields stay on `body` only.
1398
+ */
1399
+ rpcInput?: Record<string, unknown>;
1400
+ },
1401
+ ): Promise<Response> {
1402
+ const id = opts.requestId;
1403
+ let workflowNameInPath = opts.workflowName;
1404
+ const subPath = opts.subPath;
1405
+ const body = opts.body;
1406
+ const rawBody = opts.rawBody ?? "";
1407
+ const explicitRoute = opts.explicitRoute === true;
1408
+ let remoteNodeExecution = opts.remoteNodeExecution === true;
1409
+ const runtimeWorkflow = opts.runtimeWorkflow;
1410
+ const preloadedWorkflow = opts.preloadedWorkflow;
1411
+
1412
+ const defaultMeter = metrics.getMeter("default");
1413
+ const workflow_runner_errors = defaultMeter.createCounter("workflow_errors", {
1414
+ description: "Workflow runner errors",
1415
+ });
1416
+
1417
+ // OBS-02 B2.1 — join the caller's distributed trace. Extract a W3C
1418
+ // `traceparent`/`tracestate` from the inbound request headers so the
1419
+ // workflow span nests under the upstream span instead of starting a
1420
+ // fresh root. With no `traceparent` present (or no provider registered)
1421
+ // `propagation.extract` returns the active context unchanged — identical
1422
+ // behaviour to before, zero overhead when tracing is off.
1423
+ const inboundCarrier = Object.fromEntries(c.req.raw.headers.entries());
1424
+ const parentContext = propagation.extract(context.active(), inboundCarrier);
1425
+ return this.tracer.startActiveSpan(
1426
+ `${workflowNameInPath}`,
1427
+ { kind: SpanKind.SERVER },
1428
+ parentContext,
1429
+ async (span: Span) => {
1430
+ try {
1431
+ const start = performance.now();
1432
+ if (remoteNodeExecution && runtimeWorkflow !== undefined) {
1433
+ const workflowModel = runtimeWorkflow.workflow;
1434
+ const node_type = (workflowModel.steps[0] as unknown as ParamsDictionary).type;
1435
+ let set_node_type: NodeTypes = NodeTypes.MODULE;
1436
+ switch (node_type) {
1437
+ case "runtime.python3":
1438
+ set_node_type = NodeTypes.PYTHON3;
1439
+ break;
1440
+ case "runtime.go":
1441
+ set_node_type = NodeTypes.GO;
1442
+ break;
1443
+ case "runtime.rust":
1444
+ set_node_type = NodeTypes.RUST;
1445
+ break;
1446
+ case "runtime.java":
1447
+ set_node_type = NodeTypes.JAVA;
1448
+ break;
1449
+ case "runtime.csharp":
1450
+ set_node_type = NodeTypes.CSHARP;
1451
+ break;
1452
+ case "runtime.php":
1453
+ set_node_type = NodeTypes.PHP;
1454
+ break;
1455
+ case "runtime.ruby":
1456
+ set_node_type = NodeTypes.RUBY;
1457
+ break;
1458
+ case "local":
1459
+ set_node_type = NodeTypes.LOCAL;
1460
+ break;
1461
+ default:
1462
+ set_node_type = NodeTypes.MODULE;
1463
+ break;
1464
+ }
1465
+
1466
+ const trigger = Object.keys(workflowModel.trigger)[0];
1467
+ const trigger_config =
1468
+ ((workflowModel.trigger as unknown as ParamsDictionary)[trigger] as unknown as TriggerOpts) || {};
1469
+
1470
+ let remoteNodeName = workflowNameInPath + subPath;
1471
+ if (remoteNodeName.substring(remoteNodeName.length - 1) === "/") {
1472
+ remoteNodeName = remoteNodeName.substring(0, remoteNodeName.length - 1);
1473
+ }
1474
+
1475
+ const step = workflow({
1476
+ name: `Remote Node: ${remoteNodeName}`,
1477
+ version: "1.0.0",
1478
+ description: "Remote Node",
1479
+ trigger: { [(trigger as string) || "grpc"]: trigger_config },
1480
+ steps: [
1481
+ {
1482
+ id: "node",
1483
+ use: remoteNodeName,
1484
+ type: set_node_type,
1485
+ inputs: ((workflowModel.nodes as unknown as ParamsDictionary).node as unknown as ParamsDictionary)
1486
+ .inputs,
1487
+ },
1488
+ ],
1489
+ } as unknown as Parameters<typeof workflow>[0]);
1490
+
1491
+ this.nodeMap.workflows[id] = step;
1492
+ workflowNameInPath = id;
1493
+ remoteNodeExecution = true;
1494
+ }
1495
+
1496
+ // File-based routing path: pass the pre-loaded workflow object
1497
+ // directly to Configuration.init so it bypasses the disk lookup.
1498
+ // Falls back to the standard nodeMap-resolver path otherwise.
1499
+ try {
1500
+ if (preloadedWorkflow !== undefined) {
1501
+ await this.configuration.init(workflowNameInPath, this.nodeMap, preloadedWorkflow);
1502
+ } else {
1503
+ await this.configuration.init(workflowNameInPath, this.nodeMap);
1504
+ }
1505
+ } catch (bootErr) {
1506
+ recordBootError("configuration_init", bootErr);
1507
+ throw bootErr;
1508
+ }
1509
+ let ctx: Context = this.createContext(
1510
+ undefined,
1511
+ workflowNameInPath || (c.req.param("workflow") as string),
1512
+ id,
1513
+ );
1514
+
1515
+ // For explicit (file-based) routes, Hono already validated the
1516
+ // method + matched path params. `c.req.param()` returns the
1517
+ // captured params directly. For the catch-all, parse via
1518
+ // handleDynamicRoute against the trigger's path pattern.
1519
+ let resolvedParams: Record<string, string>;
1520
+ if (explicitRoute) {
1521
+ const all = (c.req.param() as Record<string, string>) || {};
1522
+ const filtered: Record<string, string> = {};
1523
+ for (const k of Object.keys(all)) {
1524
+ if (k !== "workflow") filtered[k] = all[k];
1525
+ }
1526
+ resolvedParams = filtered;
1527
+ } else {
1528
+ resolvedParams = handleDynamicRoute(
1529
+ this.configuration.trigger.http.path,
1530
+ subPath,
1531
+ (c.req.param() as Record<string, string>) || {},
1532
+ );
1533
+ }
1534
+
1535
+ ctx.logger.log(`Version: ${this.configuration.version}, Method: ${c.req.method}`);
1536
+
1537
+ // Method/path validation only for the catch-all path. Hono
1538
+ // already enforced both for explicit routes.
1539
+ if (!explicitRoute) {
1540
+ const { method, path } = this.configuration.trigger.http;
1541
+ if (method && method !== "*" && method !== "ANY" && c.req.method.toLowerCase() !== method.toLowerCase())
1542
+ throw new Error("Invalid HTTP method");
1543
+ if (!validateRoute(path, subPath)) throw new Error("Invalid HTTP path");
1544
+ }
1545
+
1546
+ // Build RequestContext from Hono request
1547
+ const url = new URL(c.req.url);
1548
+ const queryObj: Record<string, string> = {};
1549
+ for (const [key, value] of url.searchParams.entries()) {
1550
+ if (key !== "requestId") {
1551
+ queryObj[key] = value;
1552
+ }
1553
+ }
1554
+
1555
+ ctx.request = {
1556
+ body,
1557
+ rawBody,
1558
+ headers: Object.fromEntries([...c.req.raw.headers.entries()]),
1559
+ params: resolvedParams,
1560
+ query: queryObj,
1561
+ method: c.req.method,
1562
+ path: explicitRoute ? c.req.path : subPath,
1563
+ url: c.req.url,
1564
+ } as unknown as RequestContext;
1565
+
1566
+ // F15 · required-header validation. `trigger.http.headers`
1567
+ // declares headers the request MUST carry; enforce them at
1568
+ // trigger entry (on BOTH explicit and catch-all routes) now that
1569
+ // `ctx.request.headers` is populated. A missing/mismatched header
1570
+ // short-circuits with 400 before any step runs.
1571
+ this.validateRequiredHeaders((ctx.request as unknown as RequestContext).headers);
1572
+
1573
+ // v0.7 · typed-client RPC mount — mirror the input's scalar fields
1574
+ // into query + params so a workflow authored for a GET/query or
1575
+ // `:param` trigger resolves its inputs through the name-keyed mount.
1576
+ if (opts.rpcInput && typeof opts.rpcInput === "object") {
1577
+ const flat: Record<string, string> = {};
1578
+ for (const [k, v] of Object.entries(opts.rpcInput)) {
1579
+ if (v !== null && v !== undefined && typeof v !== "object") flat[k] = String(v);
1580
+ }
1581
+ const req = ctx.request as unknown as { query: Record<string, string>; params: Record<string, string> };
1582
+ req.query = { ...req.query, ...flat };
1583
+ req.params = { ...req.params, ...flat };
1584
+ }
1585
+
1586
+ // v0.6 · merged middleware chain (process-global → workflow-level
1587
+ // → trigger-level). Implementation lives on `TriggerBase`
1588
+ // so worker + cron triggers reuse the same merge logic.
1589
+ try {
1590
+ await this.applyMiddlewareChain(ctx, this.nodeMap);
1591
+ } catch (bootErr) {
1592
+ recordBootError("middleware", bootErr);
1593
+ throw bootErr;
1594
+ }
1595
+
1596
+ const response: TriggerResponse = await this.run(ctx);
1597
+ ctx = response.ctx;
1598
+ const average = response.metrics;
1599
+
1600
+ const end = performance.now();
1601
+ ctx.logger.log(`Completed in ${(end - start).toFixed(2)}ms`);
1602
+
1603
+ // Normalize the final response into a `{ data, contentType }`
1604
+ // envelope WITHOUT mutating the node's raw return value — this
1605
+ // is what stops a `runtime.*` node's content-type from leaking
1606
+ // into its JSON body. The content-type is sourced from the
1607
+ // SDK's proto `content_type` via the `_stepContentType`
1608
+ // side-channel (set by RuntimeAdapterNode, reset per-step by
1609
+ // RunnerSteps), defaulting to JSON. See `normalizeResponseEnvelope`.
1610
+ const resolvedContentType =
1611
+ ((ctx as Record<string, unknown>)._stepContentType as string | undefined) || "application/json";
1612
+ ctx.response = normalizeResponseEnvelope(ctx.response, resolvedContentType) as typeof ctx.response;
1613
+
1614
+ span.setAttribute("success", true);
1615
+ span.setAttribute("Content-Type", ctx.response.contentType ?? "application/json");
1616
+ span.setAttribute("workflow_request_id", `${ctx.id}`);
1617
+ span.setAttribute("workflow_elapsed_time", `${end - start}`);
1618
+ span.setAttribute("workflow_version", `${this.configuration.version}`);
1619
+ span.setAttribute("workflow_name", `${this.configuration.name}`);
1620
+ span.setAttribute("workflow_memory_avg_mb", `${average.memory.total}`);
1621
+ span.setAttribute("workflow_memory_min_mb", `${average.memory.min}`);
1622
+ span.setAttribute("workflow_memory_max_mb", `${average.memory.max}`);
1623
+ span.setAttribute("workflow_cpu_percentage", `${average.cpu.average}`);
1624
+ span.setAttribute("workflow_cpu_total", `${average.cpu.total}`);
1625
+ span.setAttribute("workflow_cpu_usage", `${average.cpu.usage}`);
1626
+ span.setAttribute("workflow_cpu_model", `${average.cpu.model}`);
1627
+ span.setStatus({ code: SpanStatusCode.OK });
1628
+
1629
+ // Emit the response from the finished workflow's ctx.response.
1630
+ // Honors a `@blokjs/respond` envelope (status / headers /
1631
+ // Set-Cookie / body) and raw binary bodies, falling back to the
1632
+ // default JSON / string-200 behaviour. See `responseEmitter.ts`.
1633
+ return emitWorkflowResponse(c, ctx.response);
1634
+ } catch (e: unknown) {
1635
+ span.setAttribute("success", false);
1636
+ span.setAttribute("workflow_request_id", `${id}`);
1637
+ span.recordException(e as Error);
1638
+
1639
+ // OBS-01 (T5a): removed a dead `workflow_execution.add(0, …)` here.
1640
+ // Adding 0 to a counter is a no-op, and this catch also runs for
1641
+ // control-flow signals (DeferredDispatchSignal, ConcurrencyLimitError),
1642
+ // so even add(1) would over-count. Real workflow error counting is
1643
+ // owned by the canonical `blok_workflow_errors_total`
1644
+ // (PrometheusMetricsBridge), which fires on genuine run failure.
1645
+
1646
+ // Tier 2 #5 + #7 — scheduling-deferred run. Surface as 202
1647
+ // Accepted with a `Location` header pointing at the run's
1648
+ // detail endpoint and a structured JSON body. The actual
1649
+ // dispatch happens later via the in-process scheduler.
1650
+ if (e instanceof DeferredDispatchSignal) {
1651
+ const targetRunId = e.info.intoRunId ?? e.info.runId;
1652
+ this.logger.log(
1653
+ `[scheduling] ${e.info.workflowName} runId=${e.info.runId} status=${e.info.status} ` +
1654
+ `scheduledAt=${e.info.scheduledAt}${e.info.expiresAt ? ` expiresAt=${e.info.expiresAt}` : ""} ` +
1655
+ `pingCount=${e.info.pingCount} → 202`,
1656
+ );
1657
+ span.setStatus({ code: SpanStatusCode.OK, message: `deferred:${e.info.status}` });
1658
+ c.header("Location", `/__blok/runs/${targetRunId}`);
1659
+ return c.json(
1660
+ {
1661
+ runId: e.info.runId,
1662
+ workflowName: e.info.workflowName,
1663
+ status: e.info.status,
1664
+ scheduledAt: e.info.scheduledAt,
1665
+ expiresAt: e.info.expiresAt,
1666
+ debounced: e.info.debounced,
1667
+ pingCount: e.info.pingCount,
1668
+ intoRunId: e.info.intoRunId,
1669
+ },
1670
+ 202,
1671
+ );
1672
+ }
1673
+
1674
+ // PR 2 A4 — durable scheduler payload too large. Surface as
1675
+ // 413 Payload Too Large with structured info so callers know
1676
+ // the dispatch was rejected because the body would push the
1677
+ // `scheduled_dispatches` row past the configured cap. Operators
1678
+ // raise the cap via BLOK_DISPATCH_PAYLOAD_MAX_BYTES (default 1MB).
1679
+ if (e instanceof PayloadTooLargeError) {
1680
+ span.setStatus({ code: SpanStatusCode.OK, message: "payload_too_large_for_durable_scheduling" });
1681
+ this.logger.log(
1682
+ `[scheduling] payload too large: ${e.actualBytes} bytes > cap of ${e.maxBytes} bytes → 413`,
1683
+ );
1684
+ return c.json(
1685
+ {
1686
+ error: "Payload too large for durable scheduling",
1687
+ actualBytes: e.actualBytes,
1688
+ maxBytes: e.maxBytes,
1689
+ configurable: "BLOK_DISPATCH_PAYLOAD_MAX_BYTES",
1690
+ },
1691
+ 413,
1692
+ );
1693
+ }
1694
+
1695
+ // PR 1-5 polish — queue-mode TTL elapsed. The tracker already
1696
+ // flipped the run to `expired` (see TriggerBase queue branch);
1697
+ // surface as 410 Gone (NOT 429) so HTTP clients don't retry a
1698
+ // permanently-dead run. No `Retry-After` header — would
1699
+ // contradict the 410 contract. Distinct from
1700
+ // ConcurrencyLimitError (transient, 429) below.
1701
+ if (e instanceof QueueExpiredError) {
1702
+ span.setStatus({ code: SpanStatusCode.OK, message: "queue_expired" });
1703
+ this.logger.log(
1704
+ `[concurrency] ${e.info.workflowName} key='${e.info.concurrencyKey}' ` +
1705
+ `queueExpiredAt=${e.info.queueExpiredAt} → 410`,
1706
+ );
1707
+ return c.json(
1708
+ {
1709
+ error: "Queued run expired",
1710
+ workflowName: e.info.workflowName,
1711
+ concurrencyKey: e.info.concurrencyKey,
1712
+ queueExpiredAt: e.info.queueExpiredAt,
1713
+ runId: e.info.runId,
1714
+ },
1715
+ 410,
1716
+ );
1717
+ }
1718
+
1719
+ // Tier 2 #6 — concurrency gate denial. Surface as 429 with a
1720
+ // Retry-After header (in seconds, rounded up) and a structured
1721
+ // JSON body so callers can build smart back-off without
1722
+ // parsing the message string.
1723
+ if (e instanceof ConcurrencyLimitError) {
1724
+ const retryAfterSeconds = Math.max(1, Math.ceil(e.info.retryAfterMs / 1000));
1725
+ span.setStatus({ code: SpanStatusCode.OK, message: "concurrency_limit_reached" });
1726
+ this.logger.log(
1727
+ `[concurrency] ${e.info.workflowName} key='${e.info.concurrencyKey}' ` +
1728
+ `limit=${e.info.concurrencyLimit} inFlight=${e.info.currentInFlight} → 429`,
1729
+ );
1730
+ c.header("Retry-After", String(retryAfterSeconds));
1731
+ return c.json(
1732
+ {
1733
+ error: "Concurrency limit reached",
1734
+ workflowName: e.info.workflowName,
1735
+ concurrencyKey: e.info.concurrencyKey,
1736
+ concurrencyLimit: e.info.concurrencyLimit,
1737
+ currentInFlight: e.info.currentInFlight,
1738
+ retryAfterMs: e.info.retryAfterMs,
1739
+ runId: e.info.runId,
1740
+ },
1741
+ 429,
1742
+ );
1743
+ }
1744
+
1745
+ if (e instanceof GlobalError) {
1746
+ const error_context = e as GlobalError;
1747
+
1748
+ if (error_context.context.message === "{}" && error_context.context.json instanceof DOMException) {
1749
+ workflow_runner_errors.add(1, {
1750
+ env: process.env.NODE_ENV,
1751
+ workflow_version: `${this.configuration?.version || "unknown"}`,
1752
+ workflow_name: `${this.configuration?.name || "unknown"}`,
1753
+ workflow_path: `${workflowNameInPath}`,
1754
+ });
1755
+ span.setStatus({
1756
+ code: SpanStatusCode.ERROR,
1757
+ message: (error_context.context.json as Error).toString(),
1758
+ });
1759
+ this.logger.error(`${(error_context.context.json as Error).toString()}`);
1760
+ return c.json(
1761
+ {
1762
+ origin: error_context.context.name,
1763
+ error: (error_context.context.json as Error).toString(),
1764
+ },
1765
+ 500,
1766
+ );
1767
+ }
1768
+
1769
+ if (error_context.context.code === undefined) error_context.setCode(500);
1770
+ const code = error_context.context.code as number;
1771
+
1772
+ if (error_context.hasJson()) {
1773
+ workflow_runner_errors.add(1, {
1774
+ env: process.env.NODE_ENV,
1775
+ workflow_version: `${this.configuration?.version || "unknown"}`,
1776
+ workflow_name: `${this.configuration?.name || "unknown"}`,
1777
+ workflow_path: `${workflowNameInPath}`,
1778
+ });
1779
+ span.setStatus({ code: SpanStatusCode.ERROR, message: JSON.stringify(error_context.context.json) });
1780
+ this.logger.error(`${JSON.stringify(error_context.context.json)}`);
1781
+ return c.json(error_context.context.json as object, code as 500);
1782
+ }
1783
+
1784
+ workflow_runner_errors.add(1, {
1785
+ env: process.env.NODE_ENV,
1786
+ workflow_version: `${this.configuration?.version || "unknown"}`,
1787
+ workflow_name: `${this.configuration?.name || "unknown"}`,
1788
+ workflow_path: `${workflowNameInPath}`,
1789
+ });
1790
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error_context.message });
1791
+ this.logger.error(`${error_context.message}`, error_context.stack?.replace(/\n/g, " "));
1792
+ return c.json({ error: error_context.message }, code as 500);
1793
+ }
1794
+
1795
+ workflow_runner_errors.add(1, {
1796
+ env: process.env.NODE_ENV,
1797
+ workflow_version: `${this.configuration?.version || "unknown"}`,
1798
+ workflow_name: `${this.configuration?.name || "unknown"}`,
1799
+ workflow_path: `${workflowNameInPath}`,
1800
+ });
1801
+ span.setStatus({ code: SpanStatusCode.ERROR, message: (e as Error).message });
1802
+ this.logger.error(
1803
+ `${workflowNameInPath}: ${(e as Error).message}`,
1804
+ `${(e as Error).stack?.replace(/\n/g, " ")}`,
1805
+ );
1806
+ return c.json({ error: (e as Error).message }, 500);
1807
+ } finally {
1808
+ if (remoteNodeExecution) {
1809
+ delete this.nodeMap.workflows[id];
1810
+ }
1811
+ span.end();
1812
+ }
1813
+ },
1814
+ );
1815
+ }
1816
+
1817
+ // === Tier 2 #5+#7 follow-up · durable scheduling for HTTP delays ===
1818
+
1819
+ /** Header keys that are NEVER persisted to disk (sensitive credentials). */
1820
+ private static readonly DISPATCH_HEADER_DENYLIST = new Set([
1821
+ "authorization",
1822
+ "cookie",
1823
+ "set-cookie",
1824
+ "x-api-key",
1825
+ "x-auth-token",
1826
+ "proxy-authorization",
1827
+ ]);
1828
+
1829
+ /** PR 2 A4 — default 1MB cap on the durable scheduler payload row. */
1830
+ private static readonly DEFAULT_DISPATCH_PAYLOAD_MAX_BYTES = 1_048_576;
1831
+
1832
+ private getDispatchPayloadMaxBytes(): number {
1833
+ const raw = process.env.BLOK_DISPATCH_PAYLOAD_MAX_BYTES;
1834
+ if (!raw || !/^\d+$/.test(raw)) return HttpTrigger.DEFAULT_DISPATCH_PAYLOAD_MAX_BYTES;
1835
+ return Number(raw);
1836
+ }
1837
+
1838
+ /**
1839
+ * Tier 2 #5+#7 follow-up · extract a JSON-serializable subset of the
1840
+ * Hono-built ctx that's enough for `restoreDispatch()` to reconstruct
1841
+ * an equivalent ctx after a process restart. Sensitive headers are
1842
+ * stripped before persistence.
1843
+ *
1844
+ * PR 2 A4 — caps the serialized payload at `BLOK_DISPATCH_PAYLOAD_MAX_BYTES`
1845
+ * (default 1MB). Throws `PayloadTooLargeError` on overflow; the HTTP
1846
+ * transport translates to 413 Payload Too Large with structured info.
1847
+ * Prevents sqlite bloat + boot-recovery latency on uncapped request bodies.
1848
+ */
1849
+ protected override extractDispatchPayload(ctx: Context): unknown {
1850
+ const req = ctx.request as RequestContext | undefined;
1851
+ if (!req) return null;
1852
+ const headers: Record<string, unknown> = {};
1853
+ const rawHeaders = (req.headers ?? {}) as Record<string, unknown>;
1854
+ for (const [k, v] of Object.entries(rawHeaders)) {
1855
+ if (HttpTrigger.DISPATCH_HEADER_DENYLIST.has(k.toLowerCase())) continue;
1856
+ headers[k] = v;
1857
+ }
1858
+ // Security review FW-7 — pipe body/params/query through the
1859
+ // sensitive-field redactor before persisting. Without redaction,
1860
+ // a delayed POST with `{password, ssn}` writes raw plaintext to
1861
+ // scheduled_dispatches.payload_json, which survives until the
1862
+ // dispatch fires or the Janitor sweeps the row.
1863
+ //
1864
+ // `traceRedactSensitive` (vs full `traceSanitize`) skips the
1865
+ // 10KB trace-storage truncation — the dispatch path has its
1866
+ // own 1MB cap below; double-truncating would silently shrink
1867
+ // payloads below the cap into a tiny preview envelope.
1868
+ const payload = {
1869
+ method: req.method,
1870
+ path: req.path,
1871
+ url: (req as unknown as { url?: string }).url,
1872
+ headers,
1873
+ body: traceRedactSensitive(req.body),
1874
+ params: traceRedactSensitive(req.params),
1875
+ query: traceRedactSensitive(req.query),
1876
+ workflowName: ctx.workflow_name,
1877
+ workflowPath: ctx.workflow_path,
1878
+ };
1879
+
1880
+ // PR 2 A4 · size cap. Serialize once + measure; the same JSON
1881
+ // gets written to sqlite by upsertScheduledDispatch.
1882
+ const serialized = JSON.stringify(payload);
1883
+ const maxBytes = this.getDispatchPayloadMaxBytes();
1884
+ if (serialized.length > maxBytes) {
1885
+ throw new PayloadTooLargeError(serialized.length, maxBytes);
1886
+ }
1887
+ return payload;
1888
+ }
1889
+
1890
+ /**
1891
+ * Tier 2 #5+#7 follow-up · scan the durable scheduler table on boot
1892
+ * and re-fire HTTP dispatches that were pending when the process died.
1893
+ *
1894
+ * Behavior per row:
1895
+ * - Past-due AND past TTL: mark `expired`, delete the row.
1896
+ * - Past-due, not expired: immediately invoke restoreDispatch.
1897
+ * - Future-scheduled: register a fresh timer pointing at restoreDispatch.
1898
+ *
1899
+ * Idempotent — safe to call multiple times (HMR re-loads, etc.). The
1900
+ * underlying scheduler's `schedule()` replaces existing timers for the
1901
+ * same runId; persisted rows just register the same timer again.
1902
+ *
1903
+ * Skips rows whose `workflowName` doesn't match a workflow this trigger
1904
+ * owns (multi-trigger processes).
1905
+ */
1906
+ async recoverDispatches(): Promise<{ recovered: number; expired: number; skipped: number }> {
1907
+ const tracker = RunTracker.getInstance();
1908
+ if (!tracker.active) return { recovered: 0, expired: 0, skipped: 0 };
1909
+
1910
+ const now = Date.now();
1911
+ let recovered = 0;
1912
+ let expired = 0;
1913
+ let skipped = 0;
1914
+
1915
+ // Tier C #2 — atomically claim eligible rows so multi-process PG
1916
+ // deployments don't double-fire. Kill-switch
1917
+ // BLOK_SCHEDULER_CLAIM_DISABLED=1 reverts to the legacy
1918
+ // getScheduledDispatches path (suitable for sqlite-per-process
1919
+ // deployments that want the old behavior).
1920
+ const claimDisabled = process.env.BLOK_SCHEDULER_CLAIM_DISABLED === "1";
1921
+ let rows: ReturnType<typeof tracker.getStore.prototype.getScheduledDispatches>;
1922
+ if (claimDisabled) {
1923
+ rows = tracker.getStore().getScheduledDispatches({ triggerType: "http" });
1924
+ } else {
1925
+ const processId = DeferredRunScheduler.getInstance().getProcessId();
1926
+ const leaseMs = getSchedulerClaimLeaseMs();
1927
+ // PG stores expose a true async claim API; sync wrappers exist
1928
+ // for sqlite/in-memory parity but PG users get the strongest
1929
+ // cross-process guarantee by hitting PG directly.
1930
+ const store = tracker.getStore();
1931
+ const asyncClaim = (
1932
+ store as unknown as {
1933
+ claimDispatchesAsync?: (
1934
+ processId: string,
1935
+ leaseMs: number,
1936
+ now: number,
1937
+ opts?: { triggerType?: string },
1938
+ ) => Promise<ReturnType<typeof store.claimDispatches>>;
1939
+ }
1940
+ ).claimDispatchesAsync;
1941
+ rows = asyncClaim
1942
+ ? await asyncClaim.call(store, processId, leaseMs, now, { triggerType: "http" })
1943
+ : store.claimDispatches(processId, leaseMs, now, { triggerType: "http" });
1944
+ }
1945
+
1946
+ for (const row of rows) {
1947
+ // Skip rows for workflows this trigger doesn't own. The
1948
+ // WorkflowRegistry tracks which workflows are registered;
1949
+ // fallback: if the workflow name matches our own configuration,
1950
+ // we own it.
1951
+ const ownsWorkflow =
1952
+ row.workflowName === this.configuration.name || WorkflowRegistry.getInstance().has(row.workflowName);
1953
+ if (!ownsWorkflow) {
1954
+ skipped++;
1955
+ continue;
1956
+ }
1957
+
1958
+ // Past TTL → mark expired and delete.
1959
+ if (row.expiresAt !== undefined && now > row.expiresAt) {
1960
+ tracker.markRunExpired(row.runId, { expiresAt: row.expiresAt, expiredAt: now });
1961
+ tracker.getStore().deleteScheduledDispatch(row.runId);
1962
+ ConcurrencyMetrics.getInstance().recordDispatchExpired({
1963
+ workflow_name: row.workflowName,
1964
+ trigger_type: "http",
1965
+ dispatch_status: row.dispatchStatus,
1966
+ });
1967
+ expired++;
1968
+ continue;
1969
+ }
1970
+
1971
+ // Live (past-due or future): re-register the timer.
1972
+ DeferredRunScheduler.getInstance().schedule(
1973
+ row.runId,
1974
+ row.scheduledAt,
1975
+ async () => {
1976
+ await this.restoreDispatch(row);
1977
+ },
1978
+ {
1979
+ workflowName: row.workflowName,
1980
+ triggerType: "http",
1981
+ expiresAt: row.expiresAt,
1982
+ dispatchStatus: row.dispatchStatus,
1983
+ payload: row.payload,
1984
+ },
1985
+ );
1986
+ ConcurrencyMetrics.getInstance().recordDispatchRecovered({
1987
+ workflow_name: row.workflowName,
1988
+ trigger_type: "http",
1989
+ dispatch_status: row.dispatchStatus,
1990
+ });
1991
+ recovered++;
1992
+ }
1993
+
1994
+ if (recovered + expired > 0) {
1995
+ this.logger.log(
1996
+ `[scheduling] HTTP dispatch recovery: ${recovered} re-scheduled, ${expired} expired, ${skipped} skipped`,
1997
+ );
1998
+ }
1999
+ return { recovered, expired, skipped };
2000
+ }
2001
+
2002
+ /**
2003
+ * Tier 2 #5+#7 follow-up · re-create a Context from a persisted
2004
+ * dispatch payload and re-enter `dispatchDeferred`.
2005
+ *
2006
+ * Public method for testability (call from tests with a hand-built row);
2007
+ * normally invoked by the scheduler timer registered in `recoverDispatches`.
2008
+ */
2009
+ async restoreDispatch(row: ScheduledDispatchRow): Promise<void> {
2010
+ const payload = (row.payload ?? {}) as {
2011
+ method?: string;
2012
+ path?: string;
2013
+ url?: string;
2014
+ headers?: Record<string, unknown>;
2015
+ body?: unknown;
2016
+ params?: Record<string, string>;
2017
+ query?: Record<string, string>;
2018
+ workflowName?: string;
2019
+ workflowPath?: string;
2020
+ };
2021
+
2022
+ const ctx: Context = this.createContext(undefined, payload.workflowPath || "", row.runId);
2023
+ ctx.request = {
2024
+ body: payload.body,
2025
+ headers: (payload.headers ?? {}) as Record<string, string>,
2026
+ params: (payload.params ?? {}) as ParamsDictionary,
2027
+ query: (payload.query ?? {}) as Record<string, string>,
2028
+ method: payload.method ?? "POST",
2029
+ path: payload.path ?? "/",
2030
+ url: payload.url,
2031
+ } as unknown as RequestContext;
2032
+
2033
+ // Stash the existing traceRunId so the re-entered run() reuses it
2034
+ // (otherwise a new run would be created with a different id).
2035
+ (ctx as Record<string, unknown>)._traceRunId = row.runId;
2036
+
2037
+ await this.dispatchDeferred(ctx, row.runId, row.expiresAt);
2038
+ }
2039
+ }