feat(backend): add webhook trigger#30
Open
LukasHirt wants to merge 2 commits into
Open
Conversation
Add a webhook trigger alongside manual/schedule/event: an external
caller (a CI pipeline, a form submission, another SaaS's outgoing
webhook) can now kick off a workflow via
POST /hooks/{workflowId}/{token}, without inventing new oCIS event
types. The request body is exposed to the graph as
vars["webhook.body"] (raw string), plus flattened
vars["webhook.body.<key>"] when it parses as a JSON object; a
malformed or non-object body degrades gracefully instead of failing
the run. The resource path stays empty unless the caller passes
?path=.
The webhook token is generated server-side (crypto/rand, 32 bytes),
stored via the same secretbox encryption already used for automation
app-passwords (localdb.TriggerIndexEntry gains a WebhookToken
column), and never included in ordinary workflow GET/List/Patch
responses — it's only readable through new, bearer-token-guarded
GET/POST /me/workflows/{id}/webhook-token(/rotate) endpoints. Running
a webhook-triggered workflow reuses the exact same
"Basic "+base64(automation.Username+":"+automation.AppPassword) auth
construction the cron scheduler and SSE consumer manager already use
for running without a live user session — no new auth plumbing.
Because POST /hooks/{workflowId}/{token} is intentionally mounted
outside the /api/v1beta1 route group (and its Validator.Middleware
bearer-token gate), the token itself is the only credential: it's
checked with crypto/subtle.ConstantTimeCompare rather than ==, unknown
workflow ids and non-webhook trigger types are rejected with the same
401 as a wrong token (no oracle for probing valid ids), and a new
pkg/ratelimit fixed-window limiter caps requests per token as a
compensating control against flooding/guessing.
Tests cover: wrong/missing/mismatched token rejection, a valid token
running with flattened JSON vars, non-JSON/empty/array bodies firing
without flattening or crashing, a disabled workflow not running,
missing automation failing gracefully, rate limiting kicking in after
N requests, webhook token generation/preservation/rotation semantics
in the trigger index sync, and that the /hooks route reaches its
handler without a bearer token while /api/v1beta1 routes still
require one.
docker-compose.yml's Traefik rule for workflows-backend is widened to
also match /workflows/hooks, matching the new public route.
Signed-off-by: Lukas Hirt <info@hirt.cz>
Add a "Webhook Trigger" entry (trigger-webhook) under the Triggers
category in nodeTypes.ts, matching the new backend trigger type.
The NDV's trigger type select gains a Webhook option. Once a workflow
has been saved (same "need a saved workflow id first" constraint the
manual run/executions actions already have), selecting it fetches and
displays the generated webhook URL, masked by default with a Reveal
toggle, a one-click Copy-to-clipboard action, and a Rotate action that
replaces the token — mirroring the connect/disconnect interaction
style of the existing automation affordance in WorkflowList.vue.
Before saving, it shows a hint to save first instead.
useWorkflowsApi gains getWebhookToken/rotateWebhookToken, which call
the new GET/POST /me/workflows/{id}/webhook-token(/rotate) endpoints
and combine the returned path with the backend's own non-API base URL
(stripping the /api/v1beta1 suffix) to build the full webhook URL,
since the webhook route itself lives outside /api/v1beta1.
Signed-off-by: Lukas Hirt <info@hirt.cz>
LukasHirt
force-pushed
the
feat/webhook-trigger
branch
from
July 24, 2026 21:03
9b2862b to
d9c6196
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the "Webhook Trigger" proposal end to end: a trigger, alongside manual/schedule/event, that fires when an external HTTP request hits a per-workflow URL —
POST /hooks/{workflowId}/{token}— so something outside oCIS's own event bus (a CI pipeline, a form submission, another SaaS's outgoing webhook) can kick off a workflow without inventing new oCIS event types.vars["webhook.body"](raw string), plus flattenedvars["webhook.body.<key>"]for each top-level key when it parses as a JSON object. Non-JSON/malformed/array/empty bodies still fire the trigger with just the raw string — no crash, no flattening.?path=/foo/bar.txtexplicitly.webhook."Basic " + base64(automation.Username + ":" + automation.AppPassword)construction the cron scheduler and SSE consumer manager already build for running without a live user session — no new "run headlessly" auth path was invented.Security model (the part the original proposal flagged as the open risk)
POST /hooks/{workflowId}/{token}is deliberately mounted outside the/api/v1beta1route group and itsValidator.Middlewarebearer-token gate — an external caller has no oCIS session to present a bearer token from, so the token embedded in the URL is the credential. Compensating controls:crypto/subtle.ConstantTimeCompare, never==.401 unauthenticated— a prober can't distinguish "wrong token" from "wrong/invalid workflow id".pkg/ratelimitin-memory fixed-window limiter (30 req/token/minute by default) caps abuse of a single token, checked before any DB/oCIS API work.secretboxmechanism asAutomation.AppPassword(localdb.TriggerIndexEntry.WebhookToken), and is never included in the normal workflow GET/List/Patch responses — only readable via new, ordinary-bearer-auth-gatedGET/POST /me/workflows/{id}/webhook-token(/rotate)endpoints (the NDV's "reveal"/"rotate" actions).202for a valid token (just doesn't run), so a valid-token caller can't distinguish "ran" from "currently disabled".Frontend
trigger-webhookentry innodeTypes.tsunderTRIGGER_CATEGORY.WorkflowList.vue.useWorkflowsApigainsgetWebhookToken/rotateWebhookToken, combining the backend's returned path with the frontend's own non-/api/v1beta1base URL (the webhook route lives outside it).Other
docker-compose.yml's Traefik rule forworkflows-backendis widened to also match/workflows/hooks, so the new public route is actually reachable in the local dev stack.Test plan
Backend (
cd backend && go test ./..., all green):pkg/service/hooks_test.go— wrong/missing/mismatched token → 401 (no run); unknown workflow id → 401 (no oracle); non-webhook trigger entry → 401; valid token → 202 + run with flattened JSON vars + execution record stored;?path=honored, empty by default; non-JSON/empty/JSON-array bodies → 202, raw string only, no flattening, no crash; disabled workflow → 202, no run; owner with no automation connected → 503, no run; rate limiting → 202×N then 429, independent budgets per token.pkg/service/workflows_test.go— webhook token generated on first save, preserved across updates and across enable/disable toggles, index entry removed when trigger type changes away from webhook; pre-existing schedule-trigger disable-deletes-entry behavior unchanged.pkg/executor/executor_test.go— newRunWithVarsseeds extra vars before execution (webhook body used in a template), and behaves likeRunwhenextraVarsis nil.pkg/ratelimit/ratelimit_test.go— allows up to max then blocks, independent per-key budgets, resets after the window elapses.pkg/localdb/localdb_test.go— webhook token round-trips encrypted at rest (raw column isn't plaintext), rotation replaces rather than appends,GetTriggerIndexEntrynot-found case, non-webhook entries with an emptywebhook_tokendon't break decryption (migration-safety regression test),NewWebhookTokenuniqueness.pkg/server/http/server_test.go(new) —/hooks/...reachesHooksHandlerwith noAuthorizationheader and rejects a wrong token there (proving it bypassesValidator.Middleware), while/api/v1beta1/...still 401s without a bearer token.go vet ./...,gofmt -l .clean.Frontend (
npm run test:unit,npm run check:types,npm run lint, all green):tests/unit/nodeTypes.spec.ts(new) —trigger-webhookregistered underTRIGGER_CATEGORY, resolvable by id and by existing-node lookup.tests/unit/useWorkflowsApi.spec.ts—getWebhookToken/rotateWebhookTokenhit the right endpoints and build the full URL from the returned path.tests/unit/NodeDetailsPanel.spec.ts(new) — shows a "save first" hint when unsaved; fetches and masks the URL once saved; Reveal shows it; Copy copies without forcing reveal; Rotate replaces the displayed value.Not covered by automated tests (documented tradeoff): the new
WorkflowsHandler.WebhookToken/RotateWebhookTokenHTTP handlers aren't unit-tested at the HTTP layer, sinceWorkflowsHandler.storeis a concrete*webdavstore.Store(no interface seam) rather than mockable — consistent with every other existingWorkflowsHandlermethod, none of which have unit tests either (only e2e coverage against a real stack). The security-critical logic they depend on (syncTriggerIndex's token generation/preservation) is fully unit-tested directly.