diff --git a/api-contracts/openapi/components/schemas/_index.yaml b/api-contracts/openapi/components/schemas/_index.yaml index 03b04c352..a01b8f4bb 100644 --- a/api-contracts/openapi/components/schemas/_index.yaml +++ b/api-contracts/openapi/components/schemas/_index.yaml @@ -358,6 +358,8 @@ V1CreateFilterRequest: $ref: "./v1/filter.yaml#/V1CreateFilterRequest" V1CreateWebhookRequest: $ref: "./v1/webhook.yaml#/V1CreateWebhookRequest" +V1UpdateWebhookRequest: + $ref: "./v1/webhook.yaml#/V1UpdateWebhookRequest" V1UpdateFilterRequest: $ref: "./v1/filter.yaml#/V1UpdateFilterRequest" V1CELDebugRequest: diff --git a/api-contracts/openapi/components/schemas/v1/webhook.yaml b/api-contracts/openapi/components/schemas/v1/webhook.yaml index 41c7342ad..1b79d9c76 100644 --- a/api-contracts/openapi/components/schemas/v1/webhook.yaml +++ b/api-contracts/openapi/components/schemas/v1/webhook.yaml @@ -51,6 +51,7 @@ V1WebhookSourceName: - GENERIC - GITHUB - STRIPE + - SLACK V1WebhookHMACAlgorithm: type: string @@ -184,3 +185,12 @@ V1CreateWebhookRequest: - $ref: "#/V1CreateWebhookRequestBasicAuth" - $ref: "#/V1CreateWebhookRequestAPIKey" - $ref: "#/V1CreateWebhookRequestHMAC" + +V1UpdateWebhookRequest: + type: object + properties: + eventKeyExpression: + type: string + description: The CEL expression to use for the event key. This is used to create the event key from the webhook payload. + required: + - eventKeyExpression diff --git a/api-contracts/openapi/openapi.yaml b/api-contracts/openapi/openapi.yaml index e7eee6fa3..b2275cd8b 100644 --- a/api-contracts/openapi/openapi.yaml +++ b/api-contracts/openapi/openapi.yaml @@ -64,7 +64,7 @@ paths: # !!! IMPORTANT: we directly reference this path in the webhook rate limiter in `webhook_rate_limit.go` # changing this path will break the rate limiter. !!! /api/v1/stable/tenants/{tenant}/webhooks/{v1-webhook}: - $ref: "./paths/v1/webhooks/webhook.yaml#/V1WebhookGetDeleteReceive" + $ref: "./paths/v1/webhooks/webhook.yaml#/V1WebhookGetDeleteReceiveUpdate" /api/v1/stable/tenants/{tenant}/cel/debug: $ref: "./paths/v1/cel/cel.yaml#/V1CELDebug" /api/ready: diff --git a/api-contracts/openapi/paths/v1/webhooks/webhook.yaml b/api-contracts/openapi/paths/v1/webhooks/webhook.yaml index 842cd1fd6..1d519b9d7 100644 --- a/api-contracts/openapi/paths/v1/webhooks/webhook.yaml +++ b/api-contracts/openapi/paths/v1/webhooks/webhook.yaml @@ -1,4 +1,4 @@ -V1WebhookGetDeleteReceive: +V1WebhookGetDeleteReceiveUpdate: get: description: Get a webhook by its name operationId: v1-webhook:get @@ -123,10 +123,7 @@ V1WebhookGetDeleteReceive: application/json: schema: type: object - properties: - message: - type: string - example: 'OK' + additionalProperties: true "400": content: application/json: @@ -142,6 +139,62 @@ V1WebhookGetDeleteReceive: summary: Post a webhook message tags: - Webhook + patch: + x-resources: ["tenant", "v1-webhook"] + description: Update a webhook + operationId: v1-webhook:update + parameters: + - description: The tenant id + in: path + name: tenant + required: true + schema: + type: string + format: uuid + minLength: 36 + maxLength: 36 + - description: The webhook name + in: path + name: v1-webhook + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "../../../components/schemas/_index.yaml#/V1UpdateWebhookRequest" + description: The input to the webhook creation + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/_index.yaml#/V1Webhook" + description: Successfully updated the webhook + "400": + content: + application/json: + schema: + $ref: "../../../components/schemas/_index.yaml#/APIErrors" + description: A malformed or bad request + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/_index.yaml#/APIErrors" + description: Forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/_index.yaml#/APIErrors" + description: Not found + summary: Update a webhook + tags: + - Webhook + V1WebhookListCreate: get: diff --git a/api/v1/server/handlers/v1/webhooks/receive.go b/api/v1/server/handlers/v1/webhooks/receive.go index 27773c00f..6926296b5 100644 --- a/api/v1/server/handlers/v1/webhooks/receive.go +++ b/api/v1/server/handlers/v1/webhooks/receive.go @@ -68,6 +68,16 @@ func (w *V1WebhooksService) V1WebhookReceive(ctx echo.Context, request gen.V1Web return nil, fmt.Errorf("failed to read request body: %w", err) } + isChallenge, challengeResponse, err := w.performChallenge(rawBody, *webhook, *ctx.Request()) + + if err != nil { + return nil, fmt.Errorf("failed to perform challenge: %w", err) + } + + if isChallenge { + return gen.V1WebhookReceive200JSONResponse(challengeResponse), nil + } + ok, validationError := w.validateWebhook(rawBody, *webhook, *ctx.Request()) if !ok { @@ -105,8 +115,17 @@ func (w *V1WebhooksService) V1WebhookReceive(ctx echo.Context, request gen.V1Web delete(payloadMap, "v1-webhook") } + headerMap := make(map[string]string) + + for k, v := range ctx.Request().Header { + if len(v) > 0 { + headerMap[strings.ToLower(k)] = v[0] + } + } + eventKey, err := w.celParser.EvaluateIncomingWebhookExpression(webhook.EventKeyExpression, cel.NewInput( cel.WithInput(payloadMap), + cel.WithHeaders(headerMap), ), ) @@ -161,13 +180,9 @@ func (w *V1WebhooksService) V1WebhookReceive(ctx echo.Context, request gen.V1Web return nil, fmt.Errorf("failed to ingest event") } - msg := "ok" - - return gen.V1WebhookReceive200JSONResponse( - gen.V1WebhookReceive200JSONResponse{ - Message: &msg, - }, - ), nil + return gen.V1WebhookReceive200JSONResponse(map[string]interface{}{ + "message": "ok", + }), nil } func computeHMACSignature(payload []byte, secret []byte, algorithm sqlcv1.V1IncomingWebhookHmacAlgorithm, encoding sqlcv1.V1IncomingWebhookHmacEncoding) (string, error) { @@ -239,11 +254,104 @@ func (vr ValidationError) ToResponse() (gen.V1WebhookReceiveResponseObject, erro } } +type IsValid bool +type IsChallenge bool + +func (w *V1WebhooksService) performChallenge(webhookPayload []byte, webhook sqlcv1.V1IncomingWebhook, request http.Request) (IsChallenge, map[string]interface{}, error) { + switch webhook.SourceName { + case sqlcv1.V1IncomingWebhookSourceNameSLACK: + payload := make(map[string]interface{}) + err := json.Unmarshal(webhookPayload, &payload) + + if err != nil { + return false, nil, fmt.Errorf("failed to parse form data: %s", err) + } + + if challenge, ok := payload["challenge"].(string); ok && challenge != "" { + return true, map[string]interface{}{ + "challenge": challenge, + }, nil + } + + return false, nil, nil + default: + return false, nil, nil + } +} + func (w *V1WebhooksService) validateWebhook(webhookPayload []byte, webhook sqlcv1.V1IncomingWebhook, request http.Request) ( - bool, + IsValid, *ValidationError, ) { switch webhook.SourceName { + case sqlcv1.V1IncomingWebhookSourceNameSLACK: + timestampHeader := request.Header.Get("X-Slack-Request-Timestamp") + + if timestampHeader == "" { + return false, &ValidationError{ + Code: Http403, + ErrorText: "missing or invalid timestamp header: X-Slack-Request-Timestamp", + } + } + + timestamp, err := strconv.ParseInt(strings.TrimSpace(timestampHeader), 10, 64) + + if err != nil { + return false, &ValidationError{ + Code: Http403, + ErrorText: fmt.Sprintf("invalid timestamp in header: %s", err), + } + } + + // qq: should this be utc? + if time.Unix(timestamp, 0).UTC().Before(time.Now().Add(-5 * time.Minute)) { + return false, &ValidationError{ + Code: Http403, + ErrorText: "timestamp in header is out of range", + } + } + + algorithm := webhook.AuthHmacAlgorithm.V1IncomingWebhookHmacAlgorithm + encoding := webhook.AuthHmacEncoding.V1IncomingWebhookHmacEncoding + decryptedSigningSecret, err := w.config.Encryption.Decrypt(webhook.AuthHmacWebhookSigningSecret, "v1_webhook_hmac_signing_secret") + + if err != nil { + return false, &ValidationError{ + Code: Http500, + ErrorText: fmt.Sprintf("failed to decrypt HMAC signing secret: %s", err), + } + } + + sigBaseString := fmt.Sprintf("v0:%d:%s", timestamp, webhookPayload) + + hash, err := computeHMACSignature([]byte(sigBaseString), decryptedSigningSecret, algorithm, encoding) + + if err != nil { + return false, &ValidationError{ + Code: Http500, + ErrorText: fmt.Sprintf("failed to compute HMAC signature: %s", err), + } + } + + expectedSignature := fmt.Sprintf("v0=%s", hash) + + signatureHeader := request.Header.Get(webhook.AuthHmacSignatureHeaderName.String) + + if signatureHeader == "" { + return false, &ValidationError{ + Code: Http403, + ErrorText: fmt.Sprintf("missing or invalid signature header: %s", webhook.AuthHmacSignatureHeaderName.String), + } + } + + if !signaturesMatch(signatureHeader, expectedSignature) { + return false, &ValidationError{ + Code: Http403, + ErrorText: "invalid HMAC signature", + } + } + + return true, nil case sqlcv1.V1IncomingWebhookSourceNameSTRIPE: signatureHeader := request.Header.Get(webhook.AuthHmacSignatureHeaderName.String) diff --git a/api/v1/server/handlers/v1/webhooks/update.go b/api/v1/server/handlers/v1/webhooks/update.go new file mode 100644 index 000000000..96beb3733 --- /dev/null +++ b/api/v1/server/handlers/v1/webhooks/update.go @@ -0,0 +1,30 @@ +package webhooksv1 + +import ( + "github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors" + "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" + "github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1" + "github.com/labstack/echo/v4" +) + +func (w *V1WebhooksService) V1WebhookUpdate(ctx echo.Context, request gen.V1WebhookUpdateRequestObject) (gen.V1WebhookUpdateResponseObject, error) { + webhook := ctx.Get("v1-webhook").(*sqlcv1.V1IncomingWebhook) + + webhook, err := w.config.V1.Webhooks().UpdateWebhook( + ctx.Request().Context(), + webhook.TenantID.String(), + webhook.Name, + request.Body.EventKeyExpression, + ) + + if err != nil { + return gen.V1WebhookUpdate400JSONResponse(apierrors.NewAPIErrors("failed to update webhook")), nil + } + + transformed := transformers.ToV1Webhook(webhook) + + return gen.V1WebhookUpdate200JSONResponse( + transformed, + ), nil +} diff --git a/api/v1/server/oas/gen/openapi.gen.go b/api/v1/server/oas/gen/openapi.gen.go index 20db8f9ff..0916e38ce 100644 --- a/api/v1/server/oas/gen/openapi.gen.go +++ b/api/v1/server/oas/gen/openapi.gen.go @@ -282,6 +282,7 @@ const ( const ( GENERIC V1WebhookSourceName = "GENERIC" GITHUB V1WebhookSourceName = "GITHUB" + SLACK V1WebhookSourceName = "SLACK" STRIPE V1WebhookSourceName = "STRIPE" ) @@ -1740,6 +1741,12 @@ type V1UpdateFilterRequest struct { Scope *string `json:"scope,omitempty"` } +// V1UpdateWebhookRequest defines model for V1UpdateWebhookRequest. +type V1UpdateWebhookRequest struct { + // EventKeyExpression The CEL expression to use for the event key. This is used to create the event key from the webhook payload. + EventKeyExpression string `json:"eventKeyExpression"` +} + // V1Webhook defines model for V1Webhook. type V1Webhook struct { AuthType V1WebhookAuthType `json:"authType"` @@ -2708,6 +2715,9 @@ type V1TaskReplayJSONRequestBody = V1ReplayTaskRequest // V1WebhookCreateJSONRequestBody defines body for V1WebhookCreate for application/json ContentType. type V1WebhookCreateJSONRequestBody = V1CreateWebhookRequest +// V1WebhookUpdateJSONRequestBody defines body for V1WebhookUpdate for application/json ContentType. +type V1WebhookUpdateJSONRequestBody = V1UpdateWebhookRequest + // V1WorkflowRunCreateJSONRequestBody defines body for V1WorkflowRunCreate for application/json ContentType. type V1WorkflowRunCreateJSONRequestBody = V1TriggerWorkflowRunRequest @@ -2981,6 +2991,9 @@ type ServerInterface interface { // Get a webhook // (GET /api/v1/stable/tenants/{tenant}/webhooks/{v1-webhook}) V1WebhookGet(ctx echo.Context, tenant openapi_types.UUID, v1Webhook string) error + // Update a webhook + // (PATCH /api/v1/stable/tenants/{tenant}/webhooks/{v1-webhook}) + V1WebhookUpdate(ctx echo.Context, tenant openapi_types.UUID, v1Webhook string) error // Post a webhook message // (POST /api/v1/stable/tenants/{tenant}/webhooks/{v1-webhook}) V1WebhookReceive(ctx echo.Context, tenant openapi_types.UUID, v1Webhook string) error @@ -4153,6 +4166,34 @@ func (w *ServerInterfaceWrapper) V1WebhookGet(ctx echo.Context) error { return err } +// V1WebhookUpdate converts echo context to params. +func (w *ServerInterfaceWrapper) V1WebhookUpdate(ctx echo.Context) error { + var err error + // ------------- Path parameter "tenant" ------------- + var tenant openapi_types.UUID + + err = runtime.BindStyledParameterWithLocation("simple", false, "tenant", runtime.ParamLocationPath, ctx.Param("tenant"), &tenant) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tenant: %s", err)) + } + + // ------------- Path parameter "v1-webhook" ------------- + var v1Webhook string + + err = runtime.BindStyledParameterWithLocation("simple", false, "v1-webhook", runtime.ParamLocationPath, ctx.Param("v1-webhook"), &v1Webhook) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter v1-webhook: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + ctx.Set(CookieAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.V1WebhookUpdate(ctx, tenant, v1Webhook) + return err +} + // V1WebhookReceive converts echo context to params. func (w *ServerInterfaceWrapper) V1WebhookReceive(ctx echo.Context) error { var err error @@ -6705,6 +6746,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/api/v1/stable/tenants/:tenant/webhooks", wrapper.V1WebhookCreate) router.DELETE(baseURL+"/api/v1/stable/tenants/:tenant/webhooks/:v1-webhook", wrapper.V1WebhookDelete) router.GET(baseURL+"/api/v1/stable/tenants/:tenant/webhooks/:v1-webhook", wrapper.V1WebhookGet) + router.PATCH(baseURL+"/api/v1/stable/tenants/:tenant/webhooks/:v1-webhook", wrapper.V1WebhookUpdate) router.POST(baseURL+"/api/v1/stable/tenants/:tenant/webhooks/:v1-webhook", wrapper.V1WebhookReceive) router.GET(baseURL+"/api/v1/stable/tenants/:tenant/workflow-runs", wrapper.V1WorkflowRunList) router.GET(baseURL+"/api/v1/stable/tenants/:tenant/workflow-runs/display-names", wrapper.V1WorkflowRunDisplayNamesList) @@ -8111,6 +8153,52 @@ func (response V1WebhookGet403JSONResponse) VisitV1WebhookGetResponse(w http.Res return json.NewEncoder(w).Encode(response) } +type V1WebhookUpdateRequestObject struct { + Tenant openapi_types.UUID `json:"tenant"` + V1Webhook string `json:"v1-webhook"` + Body *V1WebhookUpdateJSONRequestBody +} + +type V1WebhookUpdateResponseObject interface { + VisitV1WebhookUpdateResponse(w http.ResponseWriter) error +} + +type V1WebhookUpdate200JSONResponse V1Webhook + +func (response V1WebhookUpdate200JSONResponse) VisitV1WebhookUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type V1WebhookUpdate400JSONResponse APIErrors + +func (response V1WebhookUpdate400JSONResponse) VisitV1WebhookUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type V1WebhookUpdate403JSONResponse APIErrors + +func (response V1WebhookUpdate403JSONResponse) VisitV1WebhookUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type V1WebhookUpdate404JSONResponse APIErrors + +func (response V1WebhookUpdate404JSONResponse) VisitV1WebhookUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + type V1WebhookReceiveRequestObject struct { Tenant openapi_types.UUID `json:"tenant"` V1Webhook string `json:"v1-webhook"` @@ -8120,9 +8208,7 @@ type V1WebhookReceiveResponseObject interface { VisitV1WebhookReceiveResponse(w http.ResponseWriter) error } -type V1WebhookReceive200JSONResponse struct { - Message *string `json:"message,omitempty"` -} +type V1WebhookReceive200JSONResponse map[string]interface{} func (response V1WebhookReceive200JSONResponse) VisitV1WebhookReceiveResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") @@ -11736,6 +11822,8 @@ type StrictServerInterface interface { V1WebhookGet(ctx echo.Context, request V1WebhookGetRequestObject) (V1WebhookGetResponseObject, error) + V1WebhookUpdate(ctx echo.Context, request V1WebhookUpdateRequestObject) (V1WebhookUpdateResponseObject, error) + V1WebhookReceive(ctx echo.Context, request V1WebhookReceiveRequestObject) (V1WebhookReceiveResponseObject, error) V1WorkflowRunList(ctx echo.Context, request V1WorkflowRunListRequestObject) (V1WorkflowRunListResponseObject, error) @@ -12721,6 +12809,35 @@ func (sh *strictHandler) V1WebhookGet(ctx echo.Context, tenant openapi_types.UUI return nil } +// V1WebhookUpdate operation +func (sh *strictHandler) V1WebhookUpdate(ctx echo.Context, tenant openapi_types.UUID, v1Webhook string) error { + var request V1WebhookUpdateRequestObject + + request.Tenant = tenant + request.V1Webhook = v1Webhook + + var body V1WebhookUpdateJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.V1WebhookUpdate(ctx, request.(V1WebhookUpdateRequestObject)) + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(V1WebhookUpdateResponseObject); ok { + return validResponse.VisitV1WebhookUpdateResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("Unexpected response type: %T", response) + } + return nil +} + // V1WebhookReceive operation func (sh *strictHandler) V1WebhookReceive(ctx echo.Context, tenant openapi_types.UUID, v1Webhook string) error { var request V1WebhookReceiveRequestObject @@ -14895,295 +15012,297 @@ func (sh *strictHandler) WorkflowVersionGet(ctx echo.Context, workflow openapi_t // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "ScblQZqEnbedzs9vP/9/AAAA//8SCDW4mvMCAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/cmd/hatchet-migrate/migrate/migrations/20250813183355_v1_0_36.sql b/cmd/hatchet-migrate/migrate/migrations/20250813183355_v1_0_36.sql new file mode 100644 index 000000000..d812aff0e --- /dev/null +++ b/cmd/hatchet-migrate/migrate/migrations/20250813183355_v1_0_36.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TYPE v1_incoming_webhook_source_name ADD VALUE IF NOT EXISTS 'SLACK'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- intentionally blank +-- +goose StatementEnd diff --git a/examples/python/quickstart/poetry.lock b/examples/python/quickstart/poetry.lock index b1dfb6d6b..0bb6a6dde 100644 --- a/examples/python/quickstart/poetry.lock +++ b/examples/python/quickstart/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -114,7 +114,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aiohttp-retry" @@ -199,12 +199,32 @@ files = [ ] [package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "cel-python" +version = "0.2.0" +description = "Pure Python implementation of Google Common Expression Language" +optional = false +python-versions = "<4.0,>=3.8" +groups = ["main"] +files = [ + {file = "cel_python-0.2.0-py3-none-any.whl", hash = "sha256:478ff73def7b39d51e6982f95d937a57c2b088c491c578fe5cecdbd79f476f60"}, + {file = "cel_python-0.2.0.tar.gz", hash = "sha256:75de72a5cf223ec690b236f0cc24da267219e667bd3e7f8f4f20595fcc1c0c0f"}, +] + +[package.dependencies] +jmespath = ">=1.0.1,<2.0.0" +lark = ">=0.12.0,<0.13.0" +python-dateutil = ">=2.9.0.post0,<3.0.0" +pyyaml = ">=6.0.1,<7.0.0" +types-python-dateutil = ">=2.9.0.20240316,<3.0.0.0" +types-pyyaml = ">=6.0.12.20240311,<7.0.0.0" [[package]] name = "frozenlist" @@ -440,20 +460,21 @@ setuptools = "*" [[package]] name = "hatchet-sdk" -version = "1.17.0" +version = "1.0.0a1" description = "" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "hatchet_sdk-1.17.0-py3-none-any.whl", hash = "sha256:ff9c744abafaa0e218e618dfb1c80c88f355146cbb85fc4a08d8b109c601c8bb"}, - {file = "hatchet_sdk-1.17.0.tar.gz", hash = "sha256:2f47791ec23ffd5637422c5879daba85fa834fd8d4effd0a4e4bf7c0bb9c4f12"}, + {file = "hatchet_sdk-1.0.0a1-py3-none-any.whl", hash = "sha256:bfc84358c8842cecd0d95b30645109733b7292dff0db1a776ca862785ee93d7f"}, + {file = "hatchet_sdk-1.0.0a1.tar.gz", hash = "sha256:f0272bbaac6faed75ff727826e9f7b1ac42ae597f9b590e14d392aada9c9692f"}, ] [package.dependencies] aiohttp = ">=3.10.5,<4.0.0" aiohttp-retry = ">=2.8.3,<3.0.0" aiostream = ">=0.5.2,<0.6.0" +cel-python = ">=0.2.0,<0.3.0" grpcio = [ {version = ">=1.64.1,<1.68.dev0 || >=1.69.dev0", markers = "python_version < \"3.13\""}, {version = ">=1.69.0", markers = "python_version >= \"3.13\""}, @@ -462,11 +483,13 @@ grpcio-tools = [ {version = ">=1.64.1,<1.68.dev0 || >=1.69.dev0", markers = "python_version < \"3.13\""}, {version = ">=1.69.0", markers = "python_version >= \"3.13\""}, ] -prometheus-client = ">=0.21.1" -protobuf = ">=5.29.5,<6.0.0" +nest-asyncio = ">=1.6.0,<2.0.0" +prometheus-client = ">=0.21.1,<0.22.0" +protobuf = ">=5.29.1,<6.0.0" pydantic = ">=2.6.3,<3.0.0" pydantic-settings = ">=2.7.1,<3.0.0" python-dateutil = ">=2.9.0.post0,<3.0.0" +pyyaml = ">=6.0.1,<7.0.0" tenacity = ">=8.4.1" urllib3 = ">=1.26.20" @@ -488,6 +511,35 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "lark" +version = "0.12.0" +description = "a modern parsing library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "lark-0.12.0-py2.py3-none-any.whl", hash = "sha256:ed1d891cbcf5151ead1c1d14663bf542443e579e63a76ae175b01b899bd854ca"}, + {file = "lark-0.12.0.tar.gz", hash = "sha256:7da76fcfddadabbbbfd949bbae221efd33938451d90b1fefbbc423c3cccf48ef"}, +] + +[package.extras] +atomic-cache = ["atomicwrites"] +nearley = ["js2py"] +regex = ["regex"] + [[package]] name = "multidict" version = "6.2.0" @@ -593,6 +645,18 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "prometheus-client" version = "0.21.1" @@ -718,23 +782,23 @@ files = [ [[package]] name = "protobuf" -version = "5.29.5" +version = "5.29.4" description = "" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"}, - {file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"}, - {file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"}, - {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"}, - {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"}, - {file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"}, - {file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"}, - {file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"}, - {file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"}, - {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, - {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, + {file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"}, + {file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"}, + {file = "protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922"}, + {file = "protobuf-5.29.4-cp38-cp38-win32.whl", hash = "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de"}, + {file = "protobuf-5.29.4-cp38-cp38-win_amd64.whl", hash = "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68"}, + {file = "protobuf-5.29.4-cp39-cp39-win32.whl", hash = "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe"}, + {file = "protobuf-5.29.4-cp39-cp39-win_amd64.whl", hash = "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812"}, + {file = "protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862"}, + {file = "protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99"}, ] [[package]] @@ -756,7 +820,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -922,6 +986,69 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + [[package]] name = "setuptools" version = "78.0.2" @@ -935,13 +1062,13 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -971,6 +1098,30 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, + {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -996,7 +1147,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1101,4 +1252,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "3cd0ce503113baf99097492923271a82ac4986176ce73805643b0dee91ce0113" +content-hash = "74c12e499aa797ca5c8559af579f1212b0e4e3a77f068f9385db39d70ba304e0" diff --git a/frontend/app/src/lib/api/generated/Api.ts b/frontend/app/src/lib/api/generated/Api.ts index b3bf8f5ed..712d9c815 100644 --- a/frontend/app/src/lib/api/generated/Api.ts +++ b/frontend/app/src/lib/api/generated/Api.ts @@ -107,6 +107,7 @@ import { V1TaskTimingList, V1TriggerWorkflowRunRequest, V1UpdateFilterRequest, + V1UpdateWebhookRequest, V1Webhook, V1WebhookList, V1WebhookSourceName, @@ -908,19 +909,37 @@ export class Api< data?: any, params: RequestParams = {}, ) => - this.request< - { - /** @example "OK" */ - message?: string; - }, - APIErrors - >({ + this.request, APIErrors>({ path: `/api/v1/stable/tenants/${tenant}/webhooks/${v1Webhook}`, method: "POST", body: data, format: "json", ...params, }); + /** + * @description Update a webhook + * + * @tags Webhook + * @name V1WebhookUpdate + * @summary Update a webhook + * @request PATCH:/api/v1/stable/tenants/{tenant}/webhooks/{v1-webhook} + * @secure + */ + v1WebhookUpdate = ( + tenant: string, + v1Webhook: string, + data: V1UpdateWebhookRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v1/stable/tenants/${tenant}/webhooks/${v1Webhook}`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }); /** * @description Evaluate a CEL expression against provided input data. * diff --git a/frontend/app/src/lib/api/generated/data-contracts.ts b/frontend/app/src/lib/api/generated/data-contracts.ts index ca7631559..4aef23e7a 100644 --- a/frontend/app/src/lib/api/generated/data-contracts.ts +++ b/frontend/app/src/lib/api/generated/data-contracts.ts @@ -234,6 +234,7 @@ export enum V1WebhookSourceName { GENERIC = "GENERIC", GITHUB = "GITHUB", STRIPE = "STRIPE", + SLACK = "SLACK", } export enum TenantUIVersion { @@ -956,6 +957,11 @@ export type V1CreateWebhookRequest = | V1CreateWebhookRequestAPIKey | V1CreateWebhookRequestHMAC; +export interface V1UpdateWebhookRequest { + /** The CEL expression to use for the event key. This is used to create the event key from the webhook payload. */ + eventKeyExpression: string; +} + export interface V1CELDebugRequest { /** The CEL expression to evaluate */ expression: string; diff --git a/frontend/app/src/pages/main/v1/webhooks/components/auth-setup.tsx b/frontend/app/src/pages/main/v1/webhooks/components/auth-setup.tsx index 82dd08ffb..ba3917cf3 100644 --- a/frontend/app/src/pages/main/v1/webhooks/components/auth-setup.tsx +++ b/frontend/app/src/pages/main/v1/webhooks/components/auth-setup.tsx @@ -169,42 +169,26 @@ const HMACAuth = ({ register, watch, setValue }: HMACAuthProps) => ( ); -const StripeAuth = ({ register }: BaseAuthMethodProps) => ( - // Stripe only requires a secret, as we know the header key and the encoding info (user doesn't need to provide them) - // See docs: https://docs.stripe.com/webhooks?verify=verify-manually#verify-manually +export const PreconfiguredHMACAuth = ({ + register, + secretLabel = 'Signing Secret', + secretPlaceholder = 'super-secret', +}: BaseAuthMethodProps & { + secretLabel?: string; + secretPlaceholder?: string; +}) => ( + // Intended to be used for Stripe, Slack, Github, etc.
-
-
-
-); - -const GithubAuth = ({ register }: BaseAuthMethodProps) => ( - // Github only requires a secret, as we know the header key and the encoding info (user doesn't need to provide them) - // See docs: https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries#validating-webhook-deliveries -
-
- -
- @@ -240,9 +224,16 @@ export const AuthSetup = ({ throw new Error(`Unhandled auth method: ${exhaustiveCheck}`); } case V1WebhookSourceName.GITHUB: - return ; + return ; case V1WebhookSourceName.STRIPE: - return ; + return ( + + ); + case V1WebhookSourceName.SLACK: + return ; default: // eslint-disable-next-line no-case-declarations const exhaustiveCheck: never = sourceName; diff --git a/frontend/app/src/pages/main/v1/webhooks/components/source-name.tsx b/frontend/app/src/pages/main/v1/webhooks/components/source-name.tsx index 6e6fe961a..f5f2ef159 100644 --- a/frontend/app/src/pages/main/v1/webhooks/components/source-name.tsx +++ b/frontend/app/src/pages/main/v1/webhooks/components/source-name.tsx @@ -1,7 +1,7 @@ import { V1WebhookSourceName } from '@/lib/api'; import { GitHubLogoIcon } from '@radix-ui/react-icons'; import { Webhook } from 'lucide-react'; -import { FaStripeS } from 'react-icons/fa'; +import { FaSlack, FaStripeS } from 'react-icons/fa'; export const SourceName = ({ sourceName, @@ -30,6 +30,13 @@ export const SourceName = ({ Stripe ); + case V1WebhookSourceName.SLACK: + return ( + + + Slack + + ); default: // eslint-disable-next-line no-case-declarations diff --git a/frontend/app/src/pages/main/v1/webhooks/components/webhook-columns.tsx b/frontend/app/src/pages/main/v1/webhooks/components/webhook-columns.tsx index 705b76fbe..01906edfb 100644 --- a/frontend/app/src/pages/main/v1/webhooks/components/webhook-columns.tsx +++ b/frontend/app/src/pages/main/v1/webhooks/components/webhook-columns.tsx @@ -2,15 +2,16 @@ import { ColumnDef, Row } from '@tanstack/react-table'; import { V1Webhook } from '@/lib/api'; import { DataTableColumnHeader } from '@/components/v1/molecules/data-table/data-table-column-header'; import { DotsVerticalIcon } from '@radix-ui/react-icons'; -import { Check, Copy, Loader, Trash2 } from 'lucide-react'; +import { Check, Copy, Loader, Save, Trash2, X } from 'lucide-react'; import { Button } from '@/components/v1/ui/button'; +import { Input } from '@/components/v1/ui/input'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/v1/ui/dropdown-menu'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useWebhooks } from '../hooks/use-webhooks'; import { SourceName } from './source-name'; import { AuthMethod } from './auth-method'; @@ -44,11 +45,7 @@ export const columns = (): ColumnDef[] => { header: ({ column }) => ( ), - cell: ({ row }) => ( - - {row.original.eventKeyExpression} - - ), + cell: ({ row }) => , enableSorting: false, enableHiding: true, }, @@ -137,3 +134,57 @@ const WebhookActionsCell = ({ row }: { row: Row }) => { ); }; + +const EditableExpressionCell = ({ row }: { row: Row }) => { + const { mutations } = useWebhooks(); + const [isEditing, setIsEditing] = useState(false); + const [value, setValue] = useState(row.original.eventKeyExpression || ''); + + const handleSave = useCallback(() => { + if (value !== row.original.eventKeyExpression && value.trim()) { + mutations.updateWebhook({ + webhookName: row.original.name, + webhookData: { eventKeyExpression: value.trim() }, + }); + } + setIsEditing(false); + }, [value, row.original.eventKeyExpression, row.original.name, mutations]); + + const handleCancel = useCallback(() => { + setValue(row.original.eventKeyExpression || ''); + setIsEditing(false); + }, [row.original.eventKeyExpression, setIsEditing, setValue]); + + return ( +
+ setValue(e.target.value) : undefined} + onClick={!isEditing ? () => setIsEditing(true) : undefined} + className={`bg-muted rounded px-2 py-3 font-mono text-xs w-full h-6 ${ + isEditing + ? 'border-input focus:border-ring focus:ring-1 focus:ring-ring cursor-text' + : 'border-transparent cursor-text hover:bg-muted/80' + }`} + readOnly={!isEditing} + autoFocus={isEditing} + /> + + +
+ ); +}; diff --git a/frontend/app/src/pages/main/v1/webhooks/hooks/use-webhooks.tsx b/frontend/app/src/pages/main/v1/webhooks/hooks/use-webhooks.tsx index 3223053db..3bea41989 100644 --- a/frontend/app/src/pages/main/v1/webhooks/hooks/use-webhooks.tsx +++ b/frontend/app/src/pages/main/v1/webhooks/hooks/use-webhooks.tsx @@ -3,6 +3,7 @@ import { useCurrentTenantId } from '@/hooks/use-tenant'; import api, { queries, V1CreateWebhookRequest, + V1UpdateWebhookRequest, V1WebhookAuthType, V1WebhookHMACAlgorithm, V1WebhookHMACEncoding, @@ -44,6 +45,22 @@ export const useWebhooks = (onDeleteSuccess?: () => void) => { }, }); + const { mutate: updateWebhook, isPending: isUpdatePending } = useMutation({ + mutationFn: async ({ + webhookName, + webhookData, + }: { + webhookName: string; + webhookData: V1UpdateWebhookRequest; + }) => api.v1WebhookUpdate(tenantId, webhookName, webhookData), + onSuccess: async () => { + const queryKey = queries.v1Webhooks.list(tenantId).queryKey; + await queryClient.invalidateQueries({ + queryKey, + }); + }, + }); + const createWebhookURL = (name: string) => { return `${window.location.protocol}//${window.location.hostname}/api/v1/stable/tenants/${tenantId}/webhooks/${name}`; }; @@ -58,6 +75,8 @@ export const useWebhooks = (onDeleteSuccess?: () => void) => { isDeletePending, createWebhook, isCreatePending, + updateWebhook, + isUpdatePending, }, }; }; diff --git a/frontend/app/src/pages/main/v1/webhooks/index.tsx b/frontend/app/src/pages/main/v1/webhooks/index.tsx index c9f39e406..75812ff5e 100644 --- a/frontend/app/src/pages/main/v1/webhooks/index.tsx +++ b/frontend/app/src/pages/main/v1/webhooks/index.tsx @@ -33,11 +33,12 @@ import { V1WebhookHMACAlgorithm, V1WebhookHMACEncoding, } from '@/lib/api'; -import { Webhook, Copy, Check, AlertTriangle } from 'lucide-react'; +import { Webhook, Copy, Check, AlertTriangle, Lightbulb } from 'lucide-react'; import { Spinner } from '@/components/v1/ui/loading'; import { SourceName } from './components/source-name'; import { AuthMethod } from './components/auth-method'; import { AuthSetup } from './components/auth-setup'; +import { Link } from 'react-router-dom'; const WebhookEmptyState = () => { return ( @@ -158,7 +159,7 @@ const buildWebhookPayload = (data: WebhookFormData): V1CreateWebhookRequest => { }; case V1WebhookSourceName.STRIPE: if (!data.signingSecret) { - throw new Error('Signing secret is required for GitHub webhooks'); + throw new Error('Signing secret is required for Stripe webhooks'); } return { @@ -177,6 +178,25 @@ const buildWebhookPayload = (data: WebhookFormData): V1CreateWebhookRequest => { signingSecret: data.signingSecret, }, }; + case V1WebhookSourceName.SLACK: + if (!data.signingSecret) { + throw new Error('signing secret is required for Slack webhooks'); + } + + return { + sourceName: data.sourceName, + name: data.name, + eventKeyExpression: data.eventKeyExpression, + authType: V1WebhookAuthType.HMAC, + auth: { + // Slack sends the expected signature and timestamp as headers + // https://api.slack.com/apis/events-api#receiving-events + algorithm: V1WebhookHMACAlgorithm.SHA256, + encoding: V1WebhookHMACEncoding.HEX, + signatureHeaderName: 'X-Slack-Signature', + signingSecret: data.signingSecret, + }, + }; default: // eslint-disable-next-line no-case-declarations const exhaustiveCheck: never = data.sourceName; @@ -190,6 +210,7 @@ const createSourceInlineDescription = (sourceName: V1WebhookSourceName) => { return '(receive incoming webhook requests from any service)'; case V1WebhookSourceName.GITHUB: case V1WebhookSourceName.STRIPE: + case V1WebhookSourceName.SLACK: return ''; default: // eslint-disable-next-line no-case-declarations @@ -212,6 +233,7 @@ const SourceCaption = ({ sourceName }: { sourceName: V1WebhookSourceName }) => { ); case V1WebhookSourceName.GENERIC: case V1WebhookSourceName.STRIPE: + case V1WebhookSourceName.SLACK: return ''; default: // eslint-disable-next-line no-case-declarations @@ -292,18 +314,23 @@ const CreateWebhookModal = () => { - -
- + +
+
+ +
+ Create a webhook
- Create a webhook + + Webhooks are a beta feature +
{
))} + +
+ + Want a new source added? Reach out to support +
+
@@ -385,10 +423,25 @@ const CreateWebhookModal = () => { {errors.eventKeyExpression.message}

)} -

- CEL expression to extract the event key from the webhook payload. - Use `input` to refer to the payload. -

+
+

+ CEL expression to extract the event key from the webhook + payload. See{' '} + + the docs + {' '} + for details. +

+
    +
  • `input` refers to the payload
  • +
  • `headers` refers to the headers
  • +
+
diff --git a/frontend/docs/pages/home/webhooks.mdx b/frontend/docs/pages/home/webhooks.mdx index 4a8b82c76..b82ef8512 100644 --- a/frontend/docs/pages/home/webhooks.mdx +++ b/frontend/docs/pages/home/webhooks.mdx @@ -5,7 +5,12 @@ import UniversalTabs from "@/components/UniversalTabs"; # Webhooks -Webhooks allow external systems to trigger Hatchet workflows by sending HTTP requests to dedicated endpoints. This enables real-time integration with third-party services like GitHub, Stripe, or any system that can send webhook events. + + This feature is currently in development and might change. Reach out for + feedback or if you encounter any problems registering any external webhooks. + + +Webhooks allow external systems to trigger Hatchet workflows by sending HTTP requests to dedicated endpoints. This enables real-time integration with third-party services like GitHub, Stripe, Slack, or any system that can send webhook events. ## Creating a webhook @@ -21,7 +26,10 @@ The **Source** indicates the source of the webhook, which can be a pre-provided #### Event Key Expression -The **Event Key Expression** is a [CEL](https://cel.dev/) expression that you can use to create a dynamic event key from the payload of the incoming webhook. You can either set this to a constant value, like `webhook`, or you could set it to something like `'stripe:' + input.type`, where `'stripe:'` is a prefix for all keys indicating the webhook came from Stripe, and `input.type` selects the `type` field off of the webhook payload and uses it to create the final event key, which would look something like `stripe:payment_intent.created`. +The **Event Key Expression** is a [CEL](https://cel.dev/) expression that you can use to create a dynamic event key from the payload and headers of the incoming webhook. You can either set this to a constant value, like `webhook`, or you could set it to something dynamic using those two options. Some examples: + +1. `'stripe:' + input.type` would create event keys where `'stripe:'` is a prefix for all keys indicating the webhook came from Stripe, and `input.type` selects the `type` field off of the webhook payload and uses it to create the final event key. The result might look something like `stripe:payment_intent.created`. +2. `github: + headers['x-github-event'] + ':' + input.action` could create a key like `github:star:created` The result of the event key expression is what Hatchet will use as the event @@ -43,7 +51,7 @@ If you're using a generic source, then you'll need to specify an authentication The different authentication methods require different fields to be provided: -- **Pre-configured sources** (Stripe, GitHub): Only require a webhook secret +- **Pre-configured sources** (Stripe, GitHub, Slack): Only require a webhook secret - **Generic sources** require different fields depending on the selected authentication method: - **Basic Auth**: Requires a username and password - **API Key**: Requires header name containing the key on incoming requests, and secret key itself diff --git a/internal/cel/cel.go b/internal/cel/cel.go index c4d248675..44180f213 100644 --- a/internal/cel/cel.go +++ b/internal/cel/cel.go @@ -87,6 +87,7 @@ func NewCELParser() *CELParser { incomingWebhookEnv, _ := cel.NewEnv( cel.Declarations( decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)), + decls.NewVar("headers", decls.NewMapType(decls.String, decls.String)), checksumDecl, ), ) @@ -109,6 +110,12 @@ func WithInput(input map[string]interface{}) InputOpts { } } +func WithHeaders(headers map[string]string) InputOpts { + return func(w Input) { + w["headers"] = headers + } +} + func WithParents(parents any) InputOpts { return func(w Input) { w["parents"] = parents @@ -366,13 +373,13 @@ func (p *CELParser) EvaluateEventExpression(expr string, input Input) (bool, err } func (p *CELParser) EvaluateIncomingWebhookExpression(expr string, input Input) (string, error) { - ast, issues := p.eventEnv.Compile(expr) + ast, issues := p.incomingWebhookEnv.Compile(expr) if issues != nil && issues.Err() != nil { return "", fmt.Errorf("failed to compile expression: %w", issues.Err()) } - program, err := p.eventEnv.Program(ast) + program, err := p.incomingWebhookEnv.Program(ast) if err != nil { return "", fmt.Errorf("failed to create program: %w", err) } diff --git a/pkg/client/rest/gen.go b/pkg/client/rest/gen.go index 1df62e6fc..02f4f0edb 100644 --- a/pkg/client/rest/gen.go +++ b/pkg/client/rest/gen.go @@ -279,6 +279,7 @@ const ( const ( GENERIC V1WebhookSourceName = "GENERIC" GITHUB V1WebhookSourceName = "GITHUB" + SLACK V1WebhookSourceName = "SLACK" STRIPE V1WebhookSourceName = "STRIPE" ) @@ -1737,6 +1738,12 @@ type V1UpdateFilterRequest struct { Scope *string `json:"scope,omitempty"` } +// V1UpdateWebhookRequest defines model for V1UpdateWebhookRequest. +type V1UpdateWebhookRequest struct { + // EventKeyExpression The CEL expression to use for the event key. This is used to create the event key from the webhook payload. + EventKeyExpression string `json:"eventKeyExpression"` +} + // V1Webhook defines model for V1Webhook. type V1Webhook struct { AuthType V1WebhookAuthType `json:"authType"` @@ -2705,6 +2712,9 @@ type V1TaskReplayJSONRequestBody = V1ReplayTaskRequest // V1WebhookCreateJSONRequestBody defines body for V1WebhookCreate for application/json ContentType. type V1WebhookCreateJSONRequestBody = V1CreateWebhookRequest +// V1WebhookUpdateJSONRequestBody defines body for V1WebhookUpdate for application/json ContentType. +type V1WebhookUpdateJSONRequestBody = V1UpdateWebhookRequest + // V1WorkflowRunCreateJSONRequestBody defines body for V1WorkflowRunCreate for application/json ContentType. type V1WorkflowRunCreateJSONRequestBody = V1TriggerWorkflowRunRequest @@ -3063,6 +3073,11 @@ type ClientInterface interface { // V1WebhookGet request V1WebhookGet(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1WebhookUpdateWithBody request with any body + V1WebhookUpdateWithBody(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + V1WebhookUpdate(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, body V1WebhookUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1WebhookReceiveWithBody request with any body V1WebhookReceiveWithBody(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3883,6 +3898,30 @@ func (c *Client) V1WebhookGet(ctx context.Context, tenant openapi_types.UUID, v1 return c.Client.Do(req) } +func (c *Client) V1WebhookUpdateWithBody(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1WebhookUpdateRequestWithBody(c.Server, tenant, v1Webhook, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) V1WebhookUpdate(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, body V1WebhookUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1WebhookUpdateRequest(c.Server, tenant, v1Webhook, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) V1WebhookReceiveWithBody(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewV1WebhookReceiveRequestWithBody(c.Server, tenant, v1Webhook, contentType, body) if err != nil { @@ -7062,6 +7101,60 @@ func NewV1WebhookGetRequest(server string, tenant openapi_types.UUID, v1Webhook return req, nil } +// NewV1WebhookUpdateRequest calls the generic V1WebhookUpdate builder with application/json body +func NewV1WebhookUpdateRequest(server string, tenant openapi_types.UUID, v1Webhook string, body V1WebhookUpdateJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewV1WebhookUpdateRequestWithBody(server, tenant, v1Webhook, "application/json", bodyReader) +} + +// NewV1WebhookUpdateRequestWithBody generates requests for V1WebhookUpdate with any type of body +func NewV1WebhookUpdateRequestWithBody(server string, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "tenant", runtime.ParamLocationPath, tenant) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "v1-webhook", runtime.ParamLocationPath, v1Webhook) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/stable/tenants/%s/webhooks/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewV1WebhookReceiveRequestWithBody generates requests for V1WebhookReceive with any type of body func NewV1WebhookReceiveRequestWithBody(server string, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader) (*http.Request, error) { var err error @@ -12238,6 +12331,11 @@ type ClientWithResponsesInterface interface { // V1WebhookGetWithResponse request V1WebhookGetWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, reqEditors ...RequestEditorFn) (*V1WebhookGetResponse, error) + // V1WebhookUpdateWithBodyWithResponse request with any body + V1WebhookUpdateWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1WebhookUpdateResponse, error) + + V1WebhookUpdateWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, body V1WebhookUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*V1WebhookUpdateResponse, error) + // V1WebhookReceiveWithBodyWithResponse request with any body V1WebhookReceiveWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1WebhookReceiveResponse, error) @@ -13385,14 +13483,37 @@ func (r V1WebhookGetResponse) StatusCode() int { return 0 } +type V1WebhookUpdateResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *V1Webhook + JSON400 *APIErrors + JSON403 *APIErrors + JSON404 *APIErrors +} + +// Status returns HTTPResponse.Status +func (r V1WebhookUpdateResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1WebhookUpdateResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type V1WebhookReceiveResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *struct { - Message *string `json:"message,omitempty"` - } - JSON400 *APIErrors - JSON403 *APIErrors + JSON200 *map[string]interface{} + JSON400 *APIErrors + JSON403 *APIErrors } // Status returns HTTPResponse.Status @@ -15958,6 +16079,23 @@ func (c *ClientWithResponses) V1WebhookGetWithResponse(ctx context.Context, tena return ParseV1WebhookGetResponse(rsp) } +// V1WebhookUpdateWithBodyWithResponse request with arbitrary body returning *V1WebhookUpdateResponse +func (c *ClientWithResponses) V1WebhookUpdateWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1WebhookUpdateResponse, error) { + rsp, err := c.V1WebhookUpdateWithBody(ctx, tenant, v1Webhook, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1WebhookUpdateResponse(rsp) +} + +func (c *ClientWithResponses) V1WebhookUpdateWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, body V1WebhookUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*V1WebhookUpdateResponse, error) { + rsp, err := c.V1WebhookUpdate(ctx, tenant, v1Webhook, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1WebhookUpdateResponse(rsp) +} + // V1WebhookReceiveWithBodyWithResponse request with arbitrary body returning *V1WebhookReceiveResponse func (c *ClientWithResponses) V1WebhookReceiveWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, v1Webhook string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*V1WebhookReceiveResponse, error) { rsp, err := c.V1WebhookReceiveWithBody(ctx, tenant, v1Webhook, contentType, body, reqEditors...) @@ -18368,6 +18506,53 @@ func ParseV1WebhookGetResponse(rsp *http.Response) (*V1WebhookGetResponse, error return response, nil } +// ParseV1WebhookUpdateResponse parses an HTTP response from a V1WebhookUpdateWithResponse call +func ParseV1WebhookUpdateResponse(rsp *http.Response) (*V1WebhookUpdateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1WebhookUpdateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest V1Webhook + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest APIErrors + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest APIErrors + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest APIErrors + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + // ParseV1WebhookReceiveResponse parses an HTTP response from a V1WebhookReceiveWithResponse call func ParseV1WebhookReceiveResponse(rsp *http.Response) (*V1WebhookReceiveResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -18383,9 +18568,7 @@ func ParseV1WebhookReceiveResponse(rsp *http.Response) (*V1WebhookReceiveRespons switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - Message *string `json:"message,omitempty"` - } + var dest map[string]interface{} if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } diff --git a/pkg/repository/v1/sqlcv1/models.go b/pkg/repository/v1/sqlcv1/models.go index 5faf3789a..519abbf90 100644 --- a/pkg/repository/v1/sqlcv1/models.go +++ b/pkg/repository/v1/sqlcv1/models.go @@ -1181,6 +1181,7 @@ const ( V1IncomingWebhookSourceNameGENERIC V1IncomingWebhookSourceName = "GENERIC" V1IncomingWebhookSourceNameGITHUB V1IncomingWebhookSourceName = "GITHUB" V1IncomingWebhookSourceNameSTRIPE V1IncomingWebhookSourceName = "STRIPE" + V1IncomingWebhookSourceNameSLACK V1IncomingWebhookSourceName = "SLACK" ) func (e *V1IncomingWebhookSourceName) Scan(src interface{}) error { diff --git a/pkg/repository/v1/sqlcv1/webhooks.sql b/pkg/repository/v1/sqlcv1/webhooks.sql index 0478bc3a8..b46bbf1fc 100644 --- a/pkg/repository/v1/sqlcv1/webhooks.sql +++ b/pkg/repository/v1/sqlcv1/webhooks.sql @@ -69,3 +69,12 @@ FROM v1_incoming_webhook WHERE tenant_id = @tenantId::UUID ; + +-- name: UpdateWebhookExpression :one +UPDATE v1_incoming_webhook +SET + event_key_expression = @eventKeyExpression::TEXT +WHERE + tenant_id = @tenantId::UUID + AND name = @webhookName::TEXT +RETURNING *; diff --git a/pkg/repository/v1/sqlcv1/webhooks.sql.go b/pkg/repository/v1/sqlcv1/webhooks.sql.go index 95585bc82..6946c90aa 100644 --- a/pkg/repository/v1/sqlcv1/webhooks.sql.go +++ b/pkg/repository/v1/sqlcv1/webhooks.sql.go @@ -256,3 +256,42 @@ func (q *Queries) ListWebhooks(ctx context.Context, db DBTX, arg ListWebhooksPar } return items, nil } + +const updateWebhookExpression = `-- name: UpdateWebhookExpression :one +UPDATE v1_incoming_webhook +SET + event_key_expression = $1::TEXT +WHERE + tenant_id = $2::UUID + AND name = $3::TEXT +RETURNING tenant_id, name, source_name, event_key_expression, auth_method, auth__basic__username, auth__basic__password, auth__api_key__header_name, auth__api_key__key, auth__hmac__algorithm, auth__hmac__encoding, auth__hmac__signature_header_name, auth__hmac__webhook_signing_secret, inserted_at, updated_at +` + +type UpdateWebhookExpressionParams struct { + Eventkeyexpression string `json:"eventkeyexpression"` + Tenantid pgtype.UUID `json:"tenantid"` + Webhookname string `json:"webhookname"` +} + +func (q *Queries) UpdateWebhookExpression(ctx context.Context, db DBTX, arg UpdateWebhookExpressionParams) (*V1IncomingWebhook, error) { + row := db.QueryRow(ctx, updateWebhookExpression, arg.Eventkeyexpression, arg.Tenantid, arg.Webhookname) + var i V1IncomingWebhook + err := row.Scan( + &i.TenantID, + &i.Name, + &i.SourceName, + &i.EventKeyExpression, + &i.AuthMethod, + &i.AuthBasicUsername, + &i.AuthBasicPassword, + &i.AuthApiKeyHeaderName, + &i.AuthApiKeyKey, + &i.AuthHmacAlgorithm, + &i.AuthHmacEncoding, + &i.AuthHmacSignatureHeaderName, + &i.AuthHmacWebhookSigningSecret, + &i.InsertedAt, + &i.UpdatedAt, + ) + return &i, err +} diff --git a/pkg/repository/v1/webhooks.go b/pkg/repository/v1/webhooks.go index f662d7950..5ccc07444 100644 --- a/pkg/repository/v1/webhooks.go +++ b/pkg/repository/v1/webhooks.go @@ -15,6 +15,7 @@ type WebhookRepository interface { DeleteWebhook(ctx context.Context, tenantId, webhookId string) (*sqlcv1.V1IncomingWebhook, error) GetWebhook(ctx context.Context, tenantId, webhookId string) (*sqlcv1.V1IncomingWebhook, error) CanCreate(ctx context.Context, tenantId string, webhookLimit int32) (bool, error) + UpdateWebhook(ctx context.Context, tenantId string, webhookId, newExpression string) (*sqlcv1.V1IncomingWebhook, error) } type webhookRepository struct { @@ -207,3 +208,11 @@ func (r *webhookRepository) CanCreate(ctx context.Context, tenantId string, webh Webhooklimit: webhookLimit, }) } + +func (r *webhookRepository) UpdateWebhook(ctx context.Context, tenantId string, webhookName, newExpression string) (*sqlcv1.V1IncomingWebhook, error) { + return r.queries.UpdateWebhookExpression(ctx, r.pool, sqlcv1.UpdateWebhookExpressionParams{ + Tenantid: sqlchelpers.UUIDFromStr(tenantId), + Webhookname: webhookName, + Eventkeyexpression: newExpression, + }) +} diff --git a/sql/schema/v1-core.sql b/sql/schema/v1-core.sql index 17d2ce7e4..cbfe5237c 100644 --- a/sql/schema/v1-core.sql +++ b/sql/schema/v1-core.sql @@ -522,7 +522,7 @@ CREATE TYPE v1_incoming_webhook_hmac_algorithm AS ENUM ('SHA1', 'SHA256', 'SHA51 CREATE TYPE v1_incoming_webhook_hmac_encoding AS ENUM ('HEX', 'BASE64', 'BASE64URL'); -- Can add more sources in the future -CREATE TYPE v1_incoming_webhook_source_name AS ENUM ('GENERIC', 'GITHUB', 'STRIPE'); +CREATE TYPE v1_incoming_webhook_source_name AS ENUM ('GENERIC', 'GITHUB', 'STRIPE', 'SLACK'); CREATE TABLE v1_incoming_webhook ( tenant_id UUID NOT NULL,