blokctl 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create/project.d.ts +3 -0
- package/dist/commands/create/project.js +166 -21
- package/dist/commands/create/utils/Examples.d.ts +2 -2
- package/dist/commands/create/utils/Examples.js +9 -3
- package/dist/commands/dev/index.js +13 -1
- package/dist/commands/runtime/add.js +92 -29
- package/dist/commands/runtime/index.js +1 -0
- package/dist/commands/runtime/shared.js +4 -0
- package/dist/scaffold-repo/examples/ts-workflows/mcp-greeter.ts +59 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-csharp-hello.ts +36 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-go-hello.ts +36 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-java-hello.ts +36 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-php-hello.ts +36 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-python3-hello.ts +36 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-ruby-hello.ts +36 -0
- package/dist/scaffold-repo/examples/ts-workflows/runtime-rust-hello.ts +36 -0
- package/dist/scaffold-repo/infra/development/3055.dat +602 -0
- package/dist/scaffold-repo/infra/development/3057.dat +203 -0
- package/dist/scaffold-repo/infra/development/3059.dat +19 -0
- package/dist/scaffold-repo/infra/development/3061.dat +1003 -0
- package/dist/scaffold-repo/infra/development/3062.dat +5465 -0
- package/dist/scaffold-repo/infra/development/3063.dat +1003 -0
- package/dist/scaffold-repo/infra/development/3065.dat +606 -0
- package/dist/scaffold-repo/infra/development/3067.dat +603 -0
- package/dist/scaffold-repo/infra/development/3069.dat +112 -0
- package/dist/scaffold-repo/infra/development/3071.dat +4584 -0
- package/dist/scaffold-repo/infra/development/3073.dat +9 -0
- package/dist/scaffold-repo/infra/development/3075.dat +14599 -0
- package/dist/scaffold-repo/infra/development/3077.dat +16047 -0
- package/dist/scaffold-repo/infra/development/3079.dat +5 -0
- package/dist/scaffold-repo/infra/development/3081.dat +5 -0
- package/dist/scaffold-repo/infra/development/docker-compose.yml +106 -0
- package/dist/scaffold-repo/infra/development/schema.sql +1616 -0
- package/dist/scaffold-repo/infra/development/toc.dat +0 -0
- package/dist/scaffold-repo/infra/metrics/alertmanager.yml +43 -0
- package/dist/scaffold-repo/infra/metrics/alloy-config.alloy +75 -0
- package/dist/scaffold-repo/infra/metrics/dashboard.json +1152 -0
- package/dist/scaffold-repo/infra/metrics/dashboards/blok-overview.json +314 -0
- package/dist/scaffold-repo/infra/metrics/dashboards/blok-system.json +175 -0
- package/dist/scaffold-repo/infra/metrics/dashboards/blok-tracing.json +235 -0
- package/dist/scaffold-repo/infra/metrics/dashboards/blok-triggers.json +258 -0
- package/dist/scaffold-repo/infra/metrics/datasources.yml +43 -0
- package/dist/scaffold-repo/infra/metrics/default.yaml +8 -0
- package/dist/scaffold-repo/infra/metrics/docker-compose.yml +193 -0
- package/dist/scaffold-repo/infra/metrics/loki-config.yaml +50 -0
- package/dist/scaffold-repo/infra/metrics/nginx.conf +25 -0
- package/dist/scaffold-repo/infra/metrics/prometheus.yml +51 -0
- package/dist/scaffold-repo/infra/metrics/rules/blok-alerts.yml +114 -0
- package/dist/scaffold-repo/infra/metrics/telegraf.conf +41 -0
- package/dist/scaffold-repo/infra/metrics/tempo.yaml +81 -0
- package/dist/scaffold-repo/infra/milvus/docker-compose.yml +73 -0
- package/dist/scaffold-repo/sdk/javascript/README.md +151 -0
- package/dist/scaffold-repo/sdk/javascript/bootstrap.min.css +11660 -0
- package/dist/scaffold-repo/sdk/javascript/image-ai.html +316 -0
- package/dist/scaffold-repo/sdk/javascript/nanosdk.js +180 -0
- package/dist/scaffold-repo/sdk/javascript/nanosdk.min.js +100 -0
- package/dist/scaffold-repo/sdk/javascript/pdf.html +63 -0
- package/dist/scaffold-repo/sdk/javascript/test.html +68 -0
- package/dist/scaffold-repo/sdks/CLAUDE.md +67 -0
- package/dist/scaffold-repo/sdks/csharp/.dockerignore +7 -0
- package/dist/scaffold-repo/sdks/csharp/Blok.Core.sln +24 -0
- package/dist/scaffold-repo/sdks/csharp/Dockerfile +42 -0
- package/dist/scaffold-repo/sdks/csharp/README.md +138 -0
- package/dist/scaffold-repo/sdks/csharp/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Blok.Core.csproj +39 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Config/ServerConfig.cs +90 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BlokError.cs +538 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BlokErrorCategory.cs +112 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BlokErrorSeverity.cs +40 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/BuildContextSnapshot.cs +168 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/ErrorCategory.cs +13 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Errors/NodeException.cs +44 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Logging/LogEntry.cs +34 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Logging/LogLevel.cs +12 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Logging/Logger.cs +92 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/IMiddleware.cs +14 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/LoggingMiddleware.cs +46 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/MiddlewareChain.cs +35 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Middleware/RecoveryMiddleware.cs +63 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Node/INodeHandler.cs +17 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Node/NodeRegistry.cs +128 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Node/TypedNode.cs +140 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/ApiCallNode.cs +129 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/BlokErrorDemoNode.cs +125 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/ChainTestNode.cs +79 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/HelloWorldNode.cs +45 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/TransformDataNode.cs +119 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Nodes/TypedGreetNode.cs +23 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Program.cs +26 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Server/BlokNodeRuntimeService.cs +536 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Server/RuntimeServer.cs +183 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Testing/MockContextBuilder.cs +117 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Testing/TestNodeRunner.cs +50 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/Context.cs +56 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/ExecutionMetrics.cs +21 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/ExecutionRequest.cs +15 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/ExecutionResult.cs +104 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/HealthStatus.cs +18 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/NodeConfig.cs +59 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/Request.cs +57 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Types/Response.cs +24 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/UserNodeRegistry.cs +17 -0
- package/dist/scaffold-repo/sdks/csharp/src/Blok.Core/Validation/SchemaValidator.cs +198 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Blok.Core.Tests.csproj +25 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Errors/BlokErrorTests.cs +458 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Logging/LoggerTests.cs +129 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Node/NodeRegistryTests.cs +145 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Testing/MockContextTests.cs +153 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/TypedNodeTests.cs +69 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Types/ExecutionResultTests.cs +110 -0
- package/dist/scaffold-repo/sdks/csharp/tests/Blok.Core.Tests/Validation/SchemaValidatorTests.cs +233 -0
- package/dist/scaffold-repo/sdks/go/Dockerfile +33 -0
- package/dist/scaffold-repo/sdks/go/README.md +255 -0
- package/dist/scaffold-repo/sdks/go/blok_error.go +803 -0
- package/dist/scaffold-repo/sdks/go/blok_error_test.go +433 -0
- package/dist/scaffold-repo/sdks/go/cmd/server/main.go +45 -0
- package/dist/scaffold-repo/sdks/go/cmd/server/register_user_nodes.go +12 -0
- package/dist/scaffold-repo/sdks/go/config.go +157 -0
- package/dist/scaffold-repo/sdks/go/config_test.go +129 -0
- package/dist/scaffold-repo/sdks/go/define_node.go +87 -0
- package/dist/scaffold-repo/sdks/go/define_node_test.go +75 -0
- package/dist/scaffold-repo/sdks/go/errors.go +104 -0
- package/dist/scaffold-repo/sdks/go/errors_test.go +95 -0
- package/dist/scaffold-repo/sdks/go/examples/nodes/apicall.go +117 -0
- package/dist/scaffold-repo/sdks/go/examples/nodes/blokerrordemo.go +80 -0
- package/dist/scaffold-repo/sdks/go/examples/nodes/chaintest.go +63 -0
- package/dist/scaffold-repo/sdks/go/examples/nodes/helloworld.go +41 -0
- package/dist/scaffold-repo/sdks/go/examples/nodes/transformdata.go +176 -0
- package/dist/scaffold-repo/sdks/go/examples/nodes/typedgreet.go +34 -0
- package/dist/scaffold-repo/sdks/go/genpb/blok/runtime/v1/runtime.pb.go +2070 -0
- package/dist/scaffold-repo/sdks/go/genpb/blok/runtime/v1/runtime_grpc.pb.go +273 -0
- package/dist/scaffold-repo/sdks/go/go.mod +25 -0
- package/dist/scaffold-repo/sdks/go/go.sum +66 -0
- package/dist/scaffold-repo/sdks/go/grpc_server.go +670 -0
- package/dist/scaffold-repo/sdks/go/grpc_server_test.go +474 -0
- package/dist/scaffold-repo/sdks/go/logging.go +144 -0
- package/dist/scaffold-repo/sdks/go/logging_test.go +139 -0
- package/dist/scaffold-repo/sdks/go/middleware.go +152 -0
- package/dist/scaffold-repo/sdks/go/middleware_test.go +222 -0
- package/dist/scaffold-repo/sdks/go/node.go +50 -0
- package/dist/scaffold-repo/sdks/go/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/go/registry.go +156 -0
- package/dist/scaffold-repo/sdks/go/registry_test.go +195 -0
- package/dist/scaffold-repo/sdks/go/server.go +207 -0
- package/dist/scaffold-repo/sdks/go/server_test.go +186 -0
- package/dist/scaffold-repo/sdks/go/shutdown.go +32 -0
- package/dist/scaffold-repo/sdks/go/testutil.go +169 -0
- package/dist/scaffold-repo/sdks/go/testutil_test.go +124 -0
- package/dist/scaffold-repo/sdks/go/types.go +295 -0
- package/dist/scaffold-repo/sdks/go/types_test.go +251 -0
- package/dist/scaffold-repo/sdks/go/validation.go +210 -0
- package/dist/scaffold-repo/sdks/go/validation_test.go +262 -0
- package/dist/scaffold-repo/sdks/go/worker.go +577 -0
- package/dist/scaffold-repo/sdks/java/Dockerfile +33 -0
- package/dist/scaffold-repo/sdks/java/README.md +221 -0
- package/dist/scaffold-repo/sdks/java/pom.xml +163 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/Main.java +90 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/UserNodeRegistry.java +19 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/config/ServerConfig.java +244 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BlokError.java +576 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BlokErrorCategory.java +68 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BlokErrorSeverity.java +31 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/BuildContextSnapshot.java +132 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/ErrorCategory.java +22 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/errors/NodeException.java +176 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/logging/LogEntry.java +78 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/logging/LogLevel.java +48 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/logging/Logger.java +160 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/LoggingMiddleware.java +65 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/Middleware.java +29 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/MiddlewareChain.java +59 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/middleware/RecoveryMiddleware.java +53 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/NodeHandler.java +40 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/NodeReflector.java +18 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/NodeRegistry.java +205 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/node/TypedNode.java +143 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/ApiCallNode.java +145 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/BlokErrorDemoNode.java +109 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/ChainTestNode.java +65 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/HelloWorldNode.java +62 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/TransformDataNode.java +121 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/nodes/TypedGreetNode.java +41 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/server/BlokGrpcServer.java +103 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/server/BlokNodeRuntimeService.java +501 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/server/RuntimeServer.java +176 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/testing/MockContext.java +120 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/testing/TestRunner.java +59 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/Context.java +167 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/ExecutionMetrics.java +80 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/ExecutionRequest.java +61 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/ExecutionResult.java +188 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/HealthStatus.java +77 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/NodeConfig.java +139 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/Request.java +154 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/types/Response.java +77 -0
- package/dist/scaffold-repo/sdks/java/src/main/java/com/blok/blok/validation/SchemaValidator.java +178 -0
- package/dist/scaffold-repo/sdks/java/src/main/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/errors/BlokErrorTest.java +416 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/logging/LoggerTest.java +176 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/node/NodeRegistryTest.java +227 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/node/TypedNodeTest.java +76 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/server/RuntimeServerTest.java +197 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/types/ExecutionResultTest.java +194 -0
- package/dist/scaffold-repo/sdks/java/src/test/java/com/blok/blok/validation/SchemaValidatorTest.java +246 -0
- package/dist/scaffold-repo/sdks/php/.rr.yaml +29 -0
- package/dist/scaffold-repo/sdks/php/Dockerfile +61 -0
- package/dist/scaffold-repo/sdks/php/README.md +387 -0
- package/dist/scaffold-repo/sdks/php/composer.json +41 -0
- package/dist/scaffold-repo/sdks/php/examples/ApiCallNode.php +103 -0
- package/dist/scaffold-repo/sdks/php/examples/BlokErrorDemoNode.php +90 -0
- package/dist/scaffold-repo/sdks/php/examples/ChainTestNode.php +56 -0
- package/dist/scaffold-repo/sdks/php/examples/HelloWorldNode.php +39 -0
- package/dist/scaffold-repo/sdks/php/examples/TransformDataNode.php +94 -0
- package/dist/scaffold-repo/sdks/php/examples/TypedGreetNode.php +57 -0
- package/dist/scaffold-repo/sdks/php/phpunit.xml +23 -0
- package/dist/scaffold-repo/sdks/php/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/php/src/Config/ServerConfig.php +83 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/BlokError.php +467 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/BlokErrorBuilder.php +121 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/BlokErrorCategory.php +83 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/BlokErrorSeverity.php +30 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/BuildContextSnapshot.php +141 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/ErrorCategory.php +17 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/NodeException.php +107 -0
- package/dist/scaffold-repo/sdks/php/src/Errors/Origin.php +35 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ErrorCategory.php +118 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ErrorSeverity.php +72 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteEvent.php +181 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteOptions.php +159 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteRequest.php +328 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ExecuteResponse.php +288 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/HealthRequest.php +59 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/HealthResponse/Status.php +54 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/HealthResponse.php +113 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ListNodesRequest.php +32 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/ListNodesResponse.php +154 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/LogLine.php +156 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/Metrics.php +165 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeDescriptor.php +174 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeError.php +627 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeRef.php +134 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeRuntimeClient.php +101 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/NodeStarted.php +68 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/PartialResult.php +59 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/Progress.php +85 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/RuntimeState.php +142 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/StepInfo.php +168 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/TriggerInfo.php +289 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/Blok/Runtime/V1/WorkflowInfo.php +176 -0
- package/dist/scaffold-repo/sdks/php/src/Genpb/GPBMetadata/Blok/Runtime/V1/Runtime.php +26 -0
- package/dist/scaffold-repo/sdks/php/src/Logging/LogEntry.php +47 -0
- package/dist/scaffold-repo/sdks/php/src/Logging/LogLevel.php +50 -0
- package/dist/scaffold-repo/sdks/php/src/Logging/Logger.php +133 -0
- package/dist/scaffold-repo/sdks/php/src/Node/NodeHandler.php +35 -0
- package/dist/scaffold-repo/sdks/php/src/Node/NodeReflector.php +21 -0
- package/dist/scaffold-repo/sdks/php/src/Node/NodeRegistry.php +172 -0
- package/dist/scaffold-repo/sdks/php/src/Node/TypedNode.php +159 -0
- package/dist/scaffold-repo/sdks/php/src/Server/BlokNodeRuntimeService.php +504 -0
- package/dist/scaffold-repo/sdks/php/src/Server/DecodeException.php +13 -0
- package/dist/scaffold-repo/sdks/php/src/Server/Middleware.php +18 -0
- package/dist/scaffold-repo/sdks/php/src/Server/MiddlewarePipeline.php +33 -0
- package/dist/scaffold-repo/sdks/php/src/Server/NodeRuntimeInterface.php +40 -0
- package/dist/scaffold-repo/sdks/php/src/Server/Server.php +157 -0
- package/dist/scaffold-repo/sdks/php/src/Testing/MockContext.php +161 -0
- package/dist/scaffold-repo/sdks/php/src/Testing/TestRunner.php +78 -0
- package/dist/scaffold-repo/sdks/php/src/Types/Context.php +80 -0
- package/dist/scaffold-repo/sdks/php/src/Types/ExecutionMetrics.php +48 -0
- package/dist/scaffold-repo/sdks/php/src/Types/ExecutionRequest.php +38 -0
- package/dist/scaffold-repo/sdks/php/src/Types/ExecutionResult.php +143 -0
- package/dist/scaffold-repo/sdks/php/src/Types/HealthStatus.php +42 -0
- package/dist/scaffold-repo/sdks/php/src/Types/NodeConfig.php +72 -0
- package/dist/scaffold-repo/sdks/php/src/Types/Request.php +82 -0
- package/dist/scaffold-repo/sdks/php/src/Types/Response.php +44 -0
- package/dist/scaffold-repo/sdks/php/src/Validation/SchemaValidator.php +197 -0
- package/dist/scaffold-repo/sdks/php/tests/Errors/BlokErrorTest.php +402 -0
- package/dist/scaffold-repo/sdks/php/tests/Logging/LoggerTest.php +165 -0
- package/dist/scaffold-repo/sdks/php/tests/Node/NodeRegistryTest.php +139 -0
- package/dist/scaffold-repo/sdks/php/tests/Node/TypedNodeTest.php +84 -0
- package/dist/scaffold-repo/sdks/php/tests/Server/BlokNodeRuntimeServiceTest.php +288 -0
- package/dist/scaffold-repo/sdks/php/tests/Testing/MockContextTest.php +184 -0
- package/dist/scaffold-repo/sdks/php/tests/Types/ExecutionResultTest.php +136 -0
- package/dist/scaffold-repo/sdks/php/tests/Validation/SchemaValidatorTest.php +191 -0
- package/dist/scaffold-repo/sdks/python3/Dockerfile +62 -0
- package/dist/scaffold-repo/sdks/python3/blok/__init__.py +61 -0
- package/dist/scaffold-repo/sdks/python3/blok/config/__init__.py +3 -0
- package/dist/scaffold-repo/sdks/python3/blok/config/server_config.py +116 -0
- package/dist/scaffold-repo/sdks/python3/blok/errors/__init__.py +50 -0
- package/dist/scaffold-repo/sdks/python3/blok/errors/blok_error.py +555 -0
- package/dist/scaffold-repo/sdks/python3/blok/errors/node_error.py +64 -0
- package/dist/scaffold-repo/sdks/python3/blok/logging/__init__.py +3 -0
- package/dist/scaffold-repo/sdks/python3/blok/logging/logger.py +82 -0
- package/dist/scaffold-repo/sdks/python3/blok/middleware/__init__.py +4 -0
- package/dist/scaffold-repo/sdks/python3/blok/middleware/logging_middleware.py +35 -0
- package/dist/scaffold-repo/sdks/python3/blok/middleware/recovery_middleware.py +36 -0
- package/dist/scaffold-repo/sdks/python3/blok/node/__init__.py +4 -0
- package/dist/scaffold-repo/sdks/python3/blok/node/define_node.py +239 -0
- package/dist/scaffold-repo/sdks/python3/blok/node/node_handler.py +35 -0
- package/dist/scaffold-repo/sdks/python3/blok/node/node_registry.py +136 -0
- package/dist/scaffold-repo/sdks/python3/blok/runtime/__init__.py +0 -0
- package/dist/scaffold-repo/sdks/python3/blok/runtime/v1/__init__.py +0 -0
- package/dist/scaffold-repo/sdks/python3/blok/runtime/v1/runtime_pb2.py +112 -0
- package/dist/scaffold-repo/sdks/python3/blok/runtime/v1/runtime_pb2_grpc.py +259 -0
- package/dist/scaffold-repo/sdks/python3/blok/server/__init__.py +3 -0
- package/dist/scaffold-repo/sdks/python3/blok/server/grpc_server.py +757 -0
- package/dist/scaffold-repo/sdks/python3/blok/server/runtime_server.py +165 -0
- package/dist/scaffold-repo/sdks/python3/blok/types/__init__.py +13 -0
- package/dist/scaffold-repo/sdks/python3/blok/types/context.py +162 -0
- package/dist/scaffold-repo/sdks/python3/blok/types/execution_request.py +74 -0
- package/dist/scaffold-repo/sdks/python3/blok/types/execution_result.py +103 -0
- package/dist/scaffold-repo/sdks/python3/blok/worker/__init__.py +10 -0
- package/dist/scaffold-repo/sdks/python3/blok/worker/job_message.py +34 -0
- package/dist/scaffold-repo/sdks/python3/blok/worker/worker.py +451 -0
- package/dist/scaffold-repo/sdks/python3/blok/worker/worker_config.py +129 -0
- package/dist/scaffold-repo/sdks/python3/examples/__init__.py +32 -0
- package/dist/scaffold-repo/sdks/python3/examples/api_call_node.py +85 -0
- package/dist/scaffold-repo/sdks/python3/examples/blok_error_demo_node.py +79 -0
- package/dist/scaffold-repo/sdks/python3/examples/chain_test_node.py +51 -0
- package/dist/scaffold-repo/sdks/python3/examples/hello_world_node.py +44 -0
- package/dist/scaffold-repo/sdks/python3/examples/transform_data_node.py +71 -0
- package/dist/scaffold-repo/sdks/python3/examples/typed_greet_node.py +25 -0
- package/dist/scaffold-repo/sdks/python3/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/python3/pyproject.toml +47 -0
- package/dist/scaffold-repo/sdks/python3/requirements.txt +17 -0
- package/dist/scaffold-repo/sdks/python3/tests/__init__.py +0 -0
- package/dist/scaffold-repo/sdks/python3/tests/test_blok_error.py +341 -0
- package/dist/scaffold-repo/sdks/python3/tests/test_define_node.py +135 -0
- package/dist/scaffold-repo/sdks/python3/tests/test_discovery.py +99 -0
- package/dist/scaffold-repo/sdks/python3/tests/test_grpc_codec.py +260 -0
- package/dist/scaffold-repo/sdks/python3/tests/test_grpc_server.py +398 -0
- package/dist/scaffold-repo/sdks/ruby/Dockerfile +37 -0
- package/dist/scaffold-repo/sdks/ruby/Gemfile +5 -0
- package/dist/scaffold-repo/sdks/ruby/README.md +210 -0
- package/dist/scaffold-repo/sdks/ruby/Rakefile +11 -0
- package/dist/scaffold-repo/sdks/ruby/blok.gemspec +43 -0
- package/dist/scaffold-repo/sdks/ruby/config.ru +15 -0
- package/dist/scaffold-repo/sdks/ruby/examples/api_call_node.rb +99 -0
- package/dist/scaffold-repo/sdks/ruby/examples/blok_error_demo_node.rb +79 -0
- package/dist/scaffold-repo/sdks/ruby/examples/chain_test_node.rb +51 -0
- package/dist/scaffold-repo/sdks/ruby/examples/hello_world_node.rb +40 -0
- package/dist/scaffold-repo/sdks/ruby/examples/transform_data_node.rb +82 -0
- package/dist/scaffold-repo/sdks/ruby/examples/typed_greet_node.rb +25 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/config/server_config.rb +76 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/blok_error.rb +476 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/build_context_snapshot.rb +99 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/error_category.rb +16 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/errors/node_error.rb +84 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/logging/log_entry.rb +46 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/logging/log_level.rb +49 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/logging/logger.rb +71 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/middleware/logging_middleware.rb +45 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/middleware/middleware.rb +36 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/middleware/recovery_middleware.rb +31 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/node/discovery.rb +59 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/node/node_handler.rb +43 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/node/node_registry.rb +107 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/node/typed_node.rb +180 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/runtime/v1/runtime_pb.rb +43 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/runtime/v1/runtime_services_pb.rb +55 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/server/grpc_server.rb +435 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/server/runtime_app.rb +86 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/testing/mock_context.rb +109 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/testing/test_runner.rb +40 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/context.rb +82 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/execution_metrics.rb +43 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/execution_request.rb +39 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/execution_result.rb +117 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/health_status.rb +42 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/node_config.rb +73 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/request.rb +71 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/types/response.rb +46 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/validation/schema_validator.rb +142 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok/version.rb +5 -0
- package/dist/scaffold-repo/sdks/ruby/lib/blok.rb +53 -0
- package/dist/scaffold-repo/sdks/ruby/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/ruby/test/errors/blok_error_test.rb +339 -0
- package/dist/scaffold-repo/sdks/ruby/test/logging/logger_test.rb +155 -0
- package/dist/scaffold-repo/sdks/ruby/test/node/node_registry_test.rb +132 -0
- package/dist/scaffold-repo/sdks/ruby/test/node/typed_node_test.rb +67 -0
- package/dist/scaffold-repo/sdks/ruby/test/test_helper.rb +6 -0
- package/dist/scaffold-repo/sdks/ruby/test/testing/mock_context_test.rb +150 -0
- package/dist/scaffold-repo/sdks/ruby/test/types/execution_result_test.rb +129 -0
- package/dist/scaffold-repo/sdks/ruby/test/validation/schema_validator_test.rb +270 -0
- package/dist/scaffold-repo/sdks/rust/Cargo.lock +3044 -0
- package/dist/scaffold-repo/sdks/rust/Cargo.toml +82 -0
- package/dist/scaffold-repo/sdks/rust/Dockerfile +40 -0
- package/dist/scaffold-repo/sdks/rust/README.md +73 -0
- package/dist/scaffold-repo/sdks/rust/build.rs +32 -0
- package/dist/scaffold-repo/sdks/rust/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/scaffold-repo/sdks/rust/src/blok_error.rs +1168 -0
- package/dist/scaffold-repo/sdks/rust/src/config.rs +158 -0
- package/dist/scaffold-repo/sdks/rust/src/errors.rs +142 -0
- package/dist/scaffold-repo/sdks/rust/src/grpc_server.rs +635 -0
- package/dist/scaffold-repo/sdks/rust/src/lib.rs +65 -0
- package/dist/scaffold-repo/sdks/rust/src/logging.rs +195 -0
- package/dist/scaffold-repo/sdks/rust/src/main.rs +79 -0
- package/dist/scaffold-repo/sdks/rust/src/middleware.rs +181 -0
- package/dist/scaffold-repo/sdks/rust/src/node.rs +172 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/api_call.rs +93 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/blok_error_demo.rs +151 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/chain_test.rs +57 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/hello_world.rs +83 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/mod.rs +18 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/transform_data.rs +142 -0
- package/dist/scaffold-repo/sdks/rust/src/nodes/typed_greet.rs +56 -0
- package/dist/scaffold-repo/sdks/rust/src/registry.rs +204 -0
- package/dist/scaffold-repo/sdks/rust/src/server.rs +63 -0
- package/dist/scaffold-repo/sdks/rust/src/testing.rs +175 -0
- package/dist/scaffold-repo/sdks/rust/src/types.rs +338 -0
- package/dist/scaffold-repo/sdks/rust/src/user_nodes/mod.rs +10 -0
- package/dist/scaffold-repo/sdks/rust/src/validation.rs +232 -0
- package/dist/scaffold-repo/sdks/rust/src/worker.rs +509 -0
- package/dist/scaffold-repo/sdks/rust/tests/grpc_integration.rs +306 -0
- package/dist/scaffold-repo/sdks/rust/tests/typed_node.rs +104 -0
- package/dist/scaffold-repo/triggers/cron/CHANGELOG.md +22 -0
- package/dist/scaffold-repo/triggers/cron/package.json +36 -0
- package/dist/scaffold-repo/triggers/cron/src/CronTrigger.ts +552 -0
- package/dist/scaffold-repo/triggers/cron/src/index.ts +77 -0
- package/dist/scaffold-repo/triggers/cron/tsconfig.json +32 -0
- package/dist/scaffold-repo/triggers/grpc/.env.example +8 -0
- package/dist/scaffold-repo/triggers/grpc/CHANGELOG.md +143 -0
- package/dist/scaffold-repo/triggers/grpc/README.md +25 -0
- package/dist/scaffold-repo/triggers/grpc/buf.gen.yaml +11 -0
- package/dist/scaffold-repo/triggers/grpc/buf.yaml +9 -0
- package/dist/scaffold-repo/triggers/grpc/localhost+2-key.pem +28 -0
- package/dist/scaffold-repo/triggers/grpc/localhost+2.pem +27 -0
- package/dist/scaffold-repo/triggers/grpc/package.json +61 -0
- package/dist/scaffold-repo/triggers/grpc/proto/workflow.proto +33 -0
- package/dist/scaffold-repo/triggers/grpc/src/GRpcTrigger.ts +297 -0
- package/dist/scaffold-repo/triggers/grpc/src/GrpcClient.ts +87 -0
- package/dist/scaffold-repo/triggers/grpc/src/GrpcServer.ts +68 -0
- package/dist/scaffold-repo/triggers/grpc/src/MessageDecode.ts +142 -0
- package/dist/scaffold-repo/triggers/grpc/src/NanoSDK.ts +146 -0
- package/dist/scaffold-repo/triggers/grpc/src/Nodes.ts +10 -0
- package/dist/scaffold-repo/triggers/grpc/src/Workflows.ts +5 -0
- package/dist/scaffold-repo/triggers/grpc/src/gen/workflow_pb.ts +142 -0
- package/dist/scaffold-repo/triggers/grpc/src/index.ts +22 -0
- package/dist/scaffold-repo/triggers/grpc/src/server.ts +8 -0
- package/dist/scaffold-repo/triggers/grpc/src/types/Message.ts +0 -0
- package/dist/scaffold-repo/triggers/grpc/src/types/RuntimeWorkflow.ts +7 -0
- package/dist/scaffold-repo/triggers/grpc/src/types/Workflows.ts +7 -0
- package/dist/scaffold-repo/triggers/grpc/tsconfig.json +34 -0
- package/dist/scaffold-repo/triggers/grpc/vitest.config.ts +29 -0
- package/dist/scaffold-repo/triggers/grpc/workflows/countries.json +27 -0
- package/dist/scaffold-repo/triggers/http/.blok/trace.db +0 -0
- package/dist/scaffold-repo/triggers/http/.blok/trace.db-shm +0 -0
- package/dist/scaffold-repo/triggers/http/.blok/trace.db-wal +0 -0
- package/dist/scaffold-repo/triggers/http/.dockerignore +23 -0
- package/dist/scaffold-repo/triggers/http/.env.example +8 -0
- package/dist/scaffold-repo/triggers/http/CHANGELOG.md +326 -0
- package/dist/scaffold-repo/triggers/http/Dockerfile +49 -0
- package/dist/scaffold-repo/triggers/http/Dockerfile.dev +34 -0
- package/dist/scaffold-repo/triggers/http/infra/docker-compose.yml +38 -0
- package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Authentication.md +74 -0
- package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Building.md +52 -0
- package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Create_Node.md +123 -0
- package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Create_Workflow.md +177 -0
- package/dist/scaffold-repo/triggers/http/notes/CLI_Commands/Deployment.md +69 -0
- package/dist/scaffold-repo/triggers/http/notes/Core_Concepts/Context.md +156 -0
- package/dist/scaffold-repo/triggers/http/notes/examples.md +104 -0
- package/dist/scaffold-repo/triggers/http/notes/index.md +51 -0
- package/dist/scaffold-repo/triggers/http/package.json +62 -0
- package/dist/scaffold-repo/triggers/http/public/favicon.ico +0 -0
- package/dist/scaffold-repo/triggers/http/public/metric/index.html +2101 -0
- package/dist/scaffold-repo/triggers/http/request.http +7 -0
- package/dist/scaffold-repo/triggers/http/src/AppRoutes.ts +64 -0
- package/dist/scaffold-repo/triggers/http/src/Nodes.ts +38 -0
- package/dist/scaffold-repo/triggers/http/src/Workflows.ts +18 -0
- package/dist/scaffold-repo/triggers/http/src/index.ts +86 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/chain-init/index.ts +48 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/chain-verify/index.ts +123 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/eval/index.ts +69 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/examples/README.md +14 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/examples/chat-ui/index.html +228 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/examples/chat-ui/index.ts +65 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/examples/index.ts +14 -0
- package/dist/scaffold-repo/triggers/http/src/nodes/runtime-bridge/index.ts +84 -0
- package/dist/scaffold-repo/triggers/http/src/runner/HonoTraceRouterAdapter.ts +199 -0
- package/dist/scaffold-repo/triggers/http/src/runner/HttpTrigger.ts +2039 -0
- package/dist/scaffold-repo/triggers/http/src/runner/MessageDecode.ts +57 -0
- package/dist/scaffold-repo/triggers/http/src/runner/Util.ts +52 -0
- package/dist/scaffold-repo/triggers/http/src/runner/WorkflowRouter.ts +411 -0
- package/dist/scaffold-repo/triggers/http/src/runner/metrics/opentelemetry_metrics.ts +84 -0
- package/dist/scaffold-repo/triggers/http/src/runner/metrics/opentelemetry_traces.ts +30 -0
- package/dist/scaffold-repo/triggers/http/src/runner/nodeCatalog.ts +128 -0
- package/dist/scaffold-repo/triggers/http/src/runner/responseEmitter.ts +138 -0
- package/dist/scaffold-repo/triggers/http/src/runner/scanWorkflows.ts +201 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/ErrorResponse.ts +3 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/NodeTypes.ts +13 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/Nodes.ts +5 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/RuntimeWorkflow.ts +7 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/WorkflowRequest.ts +8 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/WorkflowResponse.ts +7 -0
- package/dist/scaffold-repo/triggers/http/src/runner/types/Workflows.ts +14 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/countries-cats-helper.ts +30 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/countries-handle-dsl.ts +25 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/countries-helper.ts +18 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/empty.ts +16 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/eval/eval-retrieve.ts +23 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/eval/eval-run.ts +39 -0
- package/dist/scaffold-repo/triggers/http/src/workflows/eval/foreign-auth.ts +24 -0
- package/dist/scaffold-repo/triggers/http/tsconfig.json +39 -0
- package/dist/scaffold-repo/triggers/http/vitest.config.ts +39 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/agent-message.json +87 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/agent-page.json +25 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/agent-stream.json +30 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/agent-tool-calculator.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/agent-tool-weather.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/chat-memory-message.json +54 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/chat-memory-page.json +25 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/chat-memory-stream.json +30 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/chat-message.json +25 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/chat-page.json +20 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/chat-stream.json +30 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/countries-py.json +27 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/countries-vs-facts.json +50 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/countries.json +27 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/cross-runtime-chain.json +89 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/dashboard-gen.json +34 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/db-manager.json +34 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/empty.json +13 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/fanout-enqueue.json +48 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/fanout-worker.json +41 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/feedback.json +34 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/films.json +27 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/image-capture.json +34 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/launches-by-year.json +27 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/loadtest.json +24 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/middleware/admin-only.json +31 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/middleware/auth-check.json +59 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/middleware/github-webhook-verify.json +34 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/middleware/jwt-auth.json +46 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/middleware/rate-limit.json +70 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/middleware/redis-rate-limit.json +71 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/mongodb.json +24 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/rentals-pdf.json +65 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-admin-delete-user.json +42 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-async-job-poller.json +45 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-csv-import.json +122 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-data-export.json +105 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-event-router.json +97 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-handler-issues.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-handler-pr.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-handler-push.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-github-webhook-router.json +88 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-hello-global-mw.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-hello-with-mw.json +23 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-jwt-protected.json +23 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-multi-tenant-router.json +90 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-nested-control-flow.json +207 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-order-fulfillment.json +211 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-parallel-foreach-with-wait.json +47 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-polling-with-backoff.json +55 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-protected.json +23 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-redis-protected.json +23 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-saga.json +122 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-tenant-acme.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-tenant-beta.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-tenant-gamma.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-travel-booking.json +200 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-user-signup-saga.json +186 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v05-webhook-fanout.json +49 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v06-reliability-showcase.json +80 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v07-sse-publish.json +24 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v07-sse-stream.json +26 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/v07-ws-echo.json +38 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github-issues.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github-pr.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github-push.json +32 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-github.json +87 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-linear-comment.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-linear-issue.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-linear.json +74 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-stripe-customer-created.json +22 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-stripe-invoice-paid.json +32 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/webhook-stripe.json +24 -0
- package/dist/scaffold-repo/triggers/http/workflows/json/workflow-docs.json +34 -0
- package/dist/scaffold-repo/triggers/http/workflows/toml/countries.toml +21 -0
- package/dist/scaffold-repo/triggers/http/workflows/xml/countries.xml +23 -0
- package/dist/scaffold-repo/triggers/http/workflows/yaml/countries.yaml +19 -0
- package/dist/scaffold-repo/triggers/mcp/README.md +131 -0
- package/dist/scaffold-repo/triggers/mcp/package.json +39 -0
- package/dist/scaffold-repo/triggers/mcp/src/McpTrigger.ts +581 -0
- package/dist/scaffold-repo/triggers/mcp/src/index.ts +41 -0
- package/dist/scaffold-repo/triggers/mcp/tsconfig.json +32 -0
- package/dist/scaffold-repo/triggers/pubsub/CHANGELOG.md +22 -0
- package/dist/scaffold-repo/triggers/pubsub/package.json +66 -0
- package/dist/scaffold-repo/triggers/pubsub/src/PubSubTrigger.ts +424 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/AWSSNSAdapter.ts +352 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/AzureServiceBusAdapter.ts +303 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/GCPPubSubAdapter.ts +327 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/KafkaPubSubAdapter.ts +247 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/KafkaPubSubAdapter.ts.bak +251 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/NATSPubSubAdapter.ts +333 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/RedisStreamsPubSubAdapter.ts +256 -0
- package/dist/scaffold-repo/triggers/pubsub/src/adapters/factory.ts +88 -0
- package/dist/scaffold-repo/triggers/pubsub/src/index.ts +67 -0
- package/dist/scaffold-repo/triggers/pubsub/template/.env.example +8 -0
- package/dist/scaffold-repo/triggers/pubsub/template/package.json +45 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/Nodes.ts +20 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/Workflows.ts +8 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/index.ts +41 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/runner/PubSubServer.ts +39 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/runner/types/Workflows.ts +7 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/workflows/messages/on-message.ts +42 -0
- package/dist/scaffold-repo/triggers/pubsub/template/src/workflows/publish-order.ts +22 -0
- package/dist/scaffold-repo/triggers/pubsub/template/tsconfig.json +31 -0
- package/dist/scaffold-repo/triggers/pubsub/template/vitest.config.ts +39 -0
- package/dist/scaffold-repo/triggers/pubsub/tsconfig.json +32 -0
- package/dist/scaffold-repo/triggers/sse/.env.example +8 -0
- package/dist/scaffold-repo/triggers/sse/CHANGELOG.md +12 -0
- package/dist/scaffold-repo/triggers/sse/Dockerfile +47 -0
- package/dist/scaffold-repo/triggers/sse/Dockerfile.dev +33 -0
- package/dist/scaffold-repo/triggers/sse/package.json +37 -0
- package/dist/scaffold-repo/triggers/sse/src/SSETrigger.ts +489 -0
- package/dist/scaffold-repo/triggers/sse/src/bus.ts +208 -0
- package/dist/scaffold-repo/triggers/sse/src/index.ts +38 -0
- package/dist/scaffold-repo/triggers/sse/src/workflows/events/README.md +129 -0
- package/dist/scaffold-repo/triggers/sse/src/workflows/events/publish-demo.ts +40 -0
- package/dist/scaffold-repo/triggers/sse/src/workflows/events/stream-demo.ts +43 -0
- package/dist/scaffold-repo/triggers/sse/tsconfig.json +22 -0
- package/dist/scaffold-repo/triggers/sse/vitest.config.ts +39 -0
- package/dist/scaffold-repo/triggers/webhook/CHANGELOG.md +22 -0
- package/dist/scaffold-repo/triggers/webhook/package.json +36 -0
- package/dist/scaffold-repo/triggers/webhook/src/WebhookTrigger.ts +465 -0
- package/dist/scaffold-repo/triggers/webhook/src/index.ts +51 -0
- package/dist/scaffold-repo/triggers/webhook/src/verifiers.ts +394 -0
- package/dist/scaffold-repo/triggers/webhook/tsconfig.json +32 -0
- package/dist/scaffold-repo/triggers/websocket/.env.example +8 -0
- package/dist/scaffold-repo/triggers/websocket/CHANGELOG.md +22 -0
- package/dist/scaffold-repo/triggers/websocket/package.json +50 -0
- package/dist/scaffold-repo/triggers/websocket/src/Backplane.ts +167 -0
- package/dist/scaffold-repo/triggers/websocket/src/WebSocketTrigger.ts +910 -0
- package/dist/scaffold-repo/triggers/websocket/src/index.ts +50 -0
- package/dist/scaffold-repo/triggers/websocket/src/workflows/events/README.md +93 -0
- package/dist/scaffold-repo/triggers/websocket/src/workflows/events/echo-demo.ts +56 -0
- package/dist/scaffold-repo/triggers/websocket/tsconfig.json +22 -0
- package/dist/scaffold-repo/triggers/websocket/vitest.config.ts +39 -0
- package/dist/scaffold-repo/triggers/worker/CHANGELOG.md +22 -0
- package/dist/scaffold-repo/triggers/worker/package.json +68 -0
- package/dist/scaffold-repo/triggers/worker/src/WorkerTrigger.ts +943 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/BullMQAdapter.ts +296 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/InMemoryAdapter.ts +280 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/KafkaAdapter.ts +347 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/NATSAdapter.ts +505 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/PgBossAdapter.ts +319 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/RabbitMQAdapter.ts +318 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/RedisStreamsAdapter.ts +513 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/SQSAdapter.ts +314 -0
- package/dist/scaffold-repo/triggers/worker/src/adapters/factory.ts +111 -0
- package/dist/scaffold-repo/triggers/worker/src/index.ts +95 -0
- package/dist/scaffold-repo/triggers/worker/template/.env.example +18 -0
- package/dist/scaffold-repo/triggers/worker/template/package.json +46 -0
- package/dist/scaffold-repo/triggers/worker/template/src/Nodes.ts +20 -0
- package/dist/scaffold-repo/triggers/worker/template/src/Workflows.ts +8 -0
- package/dist/scaffold-repo/triggers/worker/template/src/index.ts +41 -0
- package/dist/scaffold-repo/triggers/worker/template/src/runner/WorkerServer.ts +34 -0
- package/dist/scaffold-repo/triggers/worker/template/src/runner/types/Workflows.ts +7 -0
- package/dist/scaffold-repo/triggers/worker/template/src/workflows/jobs/process-job.ts +40 -0
- package/dist/scaffold-repo/triggers/worker/template/tsconfig.json +31 -0
- package/dist/scaffold-repo/triggers/worker/template/vitest.config.ts +39 -0
- package/dist/scaffold-repo/triggers/worker/tsconfig.json +32 -0
- package/dist/scaffold-repo/workflows/json/agent-message.json +87 -0
- package/dist/scaffold-repo/workflows/json/agent-page.json +25 -0
- package/dist/scaffold-repo/workflows/json/agent-stream.json +30 -0
- package/dist/scaffold-repo/workflows/json/agent-tool-calculator.json +22 -0
- package/dist/scaffold-repo/workflows/json/agent-tool-weather.json +22 -0
- package/dist/scaffold-repo/workflows/json/chat-memory-message.json +54 -0
- package/dist/scaffold-repo/workflows/json/chat-memory-page.json +25 -0
- package/dist/scaffold-repo/workflows/json/chat-memory-stream.json +30 -0
- package/dist/scaffold-repo/workflows/json/chat-message.json +25 -0
- package/dist/scaffold-repo/workflows/json/chat-page.json +20 -0
- package/dist/scaffold-repo/workflows/json/chat-stream.json +30 -0
- package/dist/scaffold-repo/workflows/json/countries-vs-facts.json +50 -0
- package/dist/scaffold-repo/workflows/json/countries.json +27 -0
- package/dist/scaffold-repo/workflows/json/cron-heartbeat.json +15 -0
- package/dist/scaffold-repo/workflows/json/fanout-enqueue.json +48 -0
- package/dist/scaffold-repo/workflows/json/fanout-worker.json +41 -0
- package/dist/scaffold-repo/workflows/json/pubsub-on-order.json +24 -0
- package/dist/scaffold-repo/workflows/json/webhook-github-issues.json +22 -0
- package/dist/scaffold-repo/workflows/json/webhook-github-pr.json +22 -0
- package/dist/scaffold-repo/workflows/json/webhook-github-push.json +32 -0
- package/dist/scaffold-repo/workflows/json/webhook-github.json +87 -0
- package/dist/scaffold-repo/workflows/json/webhook-linear-comment.json +22 -0
- package/dist/scaffold-repo/workflows/json/webhook-linear-issue.json +22 -0
- package/dist/scaffold-repo/workflows/json/webhook-linear.json +74 -0
- package/dist/scaffold-repo/workflows/json/webhook-stripe-customer-created.json +22 -0
- package/dist/scaffold-repo/workflows/json/webhook-stripe-invoice-paid.json +32 -0
- package/dist/scaffold-repo/workflows/json/webhook-stripe.json +24 -0
- package/dist/services/runtime-detector.d.ts +2 -0
- package/dist/services/runtime-detector.js +35 -11
- package/dist/services/runtime-setup.d.ts +1 -0
- package/dist/services/runtime-setup.js +17 -7
- 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
|
+
}
|