Skip to content

fix(core): only run user actions through the integration actions route, 404 for foreign integrations - #726

Merged
d-klotz merged 2 commits into
nextfrom
fix/actions-route-user-actions-only
Oct 26, 2026
Merged

d-klotz merged 2 commits into
nextfrom
fix/actions-route-user-actions-only

Conversation

@d-klotz

@d-klotz d-klotz commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  1. Any event could be triggered through the user action route. POST /api/integrations/:integrationId/actions/:actionId called integration.send(actionId, req.body) for any event name. The integration's owner could run ON_DELETE, queue/page handlers, ON_WEBHOOK, CRM_CARD or any other internal event inside the HTTP request. /actions/:actionId/options and /options/refresh accepted any id as well.
  2. Another user's or an unknown integration returned 500. GetIntegrationInstance (used by GET /api/integrations/:id, /config/options, /actions*, /test-auth), UpdateIntegration (PATCH) and DeleteIntegrationForUser (DELETE) threw a plain Error for a foreign integration. The real repositories also throw a plain Error for an unknown id instead of returning null. Boom.boomify turned both into 500 Internal Server Error.

Fix

  • integration-router.js: assertUserAction runs before the three action routes dispatch anything. It allows only events in the integration's merged events map whose type is USER_ACTION, the same entries loadUserActions (GET /actions) lists: static, dynamic (merged by initialize()) and extension user actions. Unknown ids, prototype keys and non-user-action events get 404 User action <id> does not exist. The check reads the map initialize() already built, so it adds no I/O, and a failing loadDynamicUserActions still doesn't block static actions.
  • GetIntegrationInstance, UpdateIntegration, DeleteIntegrationForUser: a missing integration and another user's integration both throw Boom.notFound('Integration with id of <id> does not exist'). The repositories' ... not found error maps to the same response, so callers can't tell the two cases apart. Other repository errors are rethrown unchanged. 404 matches the existing not-found response in DeleteIntegrationForUser, and no route returned 403 for this case.
  • docs/frigg-management-api.yml: documents the 404 on the three action routes.

Breaking change

Only for clients that triggered non-USER_ACTION events through /actions/:actionId. Those calls now return 404. In this monorepo only packages/ui/lib/api/api.js calls the route, and it uses ids returned by GET /actions, which are always user actions. No in-repo code matched on the old does not belong to User / No integration found by the ID messages. Queue workers going through backend-utils keep their retry behaviour, because Boom doesn't set a top-level statusCode.

Test plan

  • New integrations/tests/integration-router-user-actions.test.js runs through the real router, GetIntegrationInstance and IntegrationBase over an in-memory repository that throws on a missing id, like the real repositories. It covers:
    • a declared user action and a dynamic user action → 200 with the handler result and request body
    • ON_DELETE, a QUEUE event and GET_CONFIG_OPTIONS → 404, and the handlers don't run
    • unknown, constructor and __proto__ ids → 404
    • handler Boom.conflict / Boom.tooManyRequests → 409 / 429 with their message
    • options and options/refresh: a user action → 200; non-user-action and unknown ids → 404
    • dynamic actions load once per request; a static action still runs when the dynamic loader rejects
    • another user's integration vs an unknown id on POST action, GET actions, options, refresh, GET, config/options, PATCH and DELETE → both 404 with the same message, and no handler runs
  • Use-case tests for the three use cases: 404 for foreign and missing ids, 404 when the repository throws not found, and other repository errors rethrown.
  • npx jest in packages/core (Node 22): 2654 passed, 11 failed, 2 skipped. The 7 failing suites (invocation-scope, mapping-already-exists-error, app-definition-loader, integration-webhook-routers, integration-defined-workers, require-graph, integrations-report) fail the same way on origin/next in the same checkout.

Caveats

  • Malformed ids that the repository rejects with another message (Postgres Invalid ID: ...) still return 500. That case is unchanged.

🤖 Generated with Claude Code


Note

Medium Risk
Changes authorization and error semantics on integration management and action endpoints; legitimate callers using listed user actions are unaffected, but any client relying on triggering internal events via the actions API will break.

Overview
Tightens integration HTTP security by only allowing USER_ACTION events on the action submit/options/refresh routes, and by returning consistent 404 responses when an integration is missing or not owned by the caller.

assertUserAction in the integration router now blocks dispatch for unknown ids, prototype keys, and internal events (ON_DELETE, queue handlers, etc.) that were previously runnable via POST .../actions/:actionId. Dynamic user actions without an explicit type are normalized to USER_ACTION in IntegrationBase so they stay aligned with GET /actions listing.

GetIntegrationInstance, UpdateIntegration, and DeleteIntegrationForUser map both repository “not found” and cross-user access to the same Boom.notFound message (fixing prior 500s from plain Errors). The management API spec documents the new action-route 404s. Breaking: clients that invoked non–user-action event names through /actions/:actionId now get 404.

Reviewed by Cursor Bugbot for commit 7cf570c. Bugbot is set up for automated code reviews on this repo. Configure here.

📦 Published PR as canary version: 2.0.0--canary.726.7cf570c.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.726.7cf570c.0
npm install @friggframework/core@2.0.0--canary.726.7cf570c.0
npm install @friggframework/devtools@2.0.0--canary.726.7cf570c.0
npm install @friggframework/eslint-config@2.0.0--canary.726.7cf570c.0
npm install @friggframework/prettier-config@2.0.0--canary.726.7cf570c.0
npm install @friggframework/schemas@2.0.0--canary.726.7cf570c.0
npm install @friggframework/serverless-plugin@2.0.0--canary.726.7cf570c.0
npm install @friggframework/test@2.0.0--canary.726.7cf570c.0
npm install @friggframework/ui@2.0.0--canary.726.7cf570c.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/core@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/devtools@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/eslint-config@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/prettier-config@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/schemas@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/test@2.0.0--canary.726.7cf570c.0
yarn add @friggframework/ui@2.0.0--canary.726.7cf570c.0

Version

Published prerelease version: v2.0.0-next.125

Changelog

🚀 Enhancement

  • @friggframework/admin-scripts, @friggframework/core, @friggframework/devtools, @friggframework/eslint-config, @friggframework/prettier-config, @friggframework/schemas, @friggframework/serverless-plugin, @friggframework/test, @friggframework/ui
    • feat(devtools): frigg validate, and make the app-definition schema match the runtime (ADR-051) #666 (@seanspeaks)

🐛 Bug Fix

  • @friggframework/core
    • fix(core): only run user actions through the integration actions route, 404 for foreign integrations #726 (@d-klotz)

⚠️ Pushed to next

  • docs(adr): accept ADR-065 module documentation and catalog publishing (@seanspeaks)

Authors: 2

…e, 404 for foreign integrations

POST /api/integrations/:integrationId/actions/:actionId dispatched any
event name, so an integration owner could run ON_DELETE, queue handlers
or webhook events inside the HTTP request. The action routes (and their
options/refresh routes) now only accept events typed USER_ACTION,
including dynamic and extension user actions; anything else is a 404.

GetIntegrationInstance, UpdateIntegration and DeleteIntegrationForUser
answered another user's or an unknown integration with a plain Error
(500). They now throw Boom.notFound with the same message for both,
mapping the repositories' "not found" error instead of leaking it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@d-klotz d-klotz added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 26, 2026
@netlify

netlify Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for friggframework-org canceled.

Name Link
🔨 Latest commit 7cf570c
🔍 Latest deploy log https://app.netlify.com/projects/friggframework-org/deploys/6adf7d469f699d935e47eebf

… gate agree

loadUserActions listed every loadDynamicUserActions entry, while the
actions route only runs events typed USER_ACTION, so an untyped dynamic
action was listed but answered 404. Dynamic entries now default to
USER_ACTION in one helper used by initialize() and loadUserActions(),
and loadUserActions lists only USER_ACTION entries. A dynamic entry
typed as something else is neither listed nor runnable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new blocking issues. 1 optional suggestion (a nit or a note on pre-existing code) was found and not posted.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
20.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@d-klotz
d-klotz merged commit 2767886 into next Oct 26, 2026
16 of 17 checks passed
@d-klotz
d-klotz deleted the fix/actions-route-user-actions-only branch October 26, 2026 16:25
@seanspeaks

Copy link
Copy Markdown
Contributor

🚀 PR was released in v2.0.0-next.125 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants