Skip to content

fix(devtools): route /api/entity and /api/entities to the integration router - #667

Merged
seanspeaks merged 2 commits into
nextfrom
fix/v1-entity-gateway-routes
Oct 24, 2026
Merged

seanspeaks merged 2 commits into
nextfrom
fix/v1-entity-gateway-routes

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The auth Lambda serves core's integration router (createIntegrationRouter() plus the redirect route in handlers/routers/auth.js). That router registers the v1 entity routes, but the API Gateway HTTP API that base-definition-factory.js generates only declared /api/integrations, /api/integrations/{proxy+} and /api/authorize. HTTP APIs have no implicit catch-all, and the composed definition has no $default or /{proxy+} route, so every /api/entity and /api/entities/* request got a 404 from the gateway before it reached Express. This happened in deployed apps and under frigg start (serverless-offline). The Frigg UI calls these routes from IntegrationConfigurationModal.

Fix: add ANY /api/entity, ANY /api/entity/{proxy+}, ANY /api/entities and ANY /api/entities/{proxy+} to the auth function. Like /api/integrations, the bare /api/entities path is declared so the whole prefix belongs to the Management API. Express returns its own 404 for a path it doesn't handle.

Guard: infrastructure/__tests__/management-api-gateway-routes.test.js reads the routes from a live createIntegrationRouter() instead of a hard-coded list. It composes a full app definition (with admin scripts, built-in reports, websockets, a webhook integration, an extension binding and an integration named entities), with discovery and the Prisma layer stubbed. It then models HTTP API route selection and asserts that every Management API route selects a route on auth. A route added to core later (for example the v2 routes) fails this test until the gateway routes are added too. The test also keeps the neighbouring routes on their own functions and checks that no method + path pair is declared on two functions.

Route table (composed app, after the fix)

HTTP API matching works like this: literal segments must match exactly, and {param} matches one segment. {proxy+} is greedy and matches one or more trailing segments, so it does not match the bare prefix. That is why both /x and /x/{proxy+} are declared. A non-greedy match beats a greedy one, and ANY matches every method. There is no catch-all.

Express route (handler) Before After
GET/POST /api/integrations (integration-router) ANY /api/integrations → auth same
PATCH/DELETE/GET /api/integrations/:integrationId, …/config/options, …/config/options/refresh, …/actions, …/actions/:actionId, …/actions/:actionId/options, …/actions/:actionId/options/refresh, …/test-auth ANY /api/integrations/{proxy+} → auth same
GET /api/integrations/redirect/:appId (auth.js) ANY /api/integrations/{proxy+} → auth same
GET/POST /api/authorize ANY /api/authorize → auth same
POST /api/entity unreachable (gateway 404) ANY /api/entity → auth
GET /api/entity/options/:credentialId unreachable ANY /api/entity/{proxy+} → auth
GET /api/entities/:entityId unreachable ANY /api/entities/{proxy+} → auth
GET /api/entities/:entityId/test-auth unreachable ANY /api/entities/{proxy+} → auth
POST /api/entities/:entityId/options unreachable ANY /api/entities/{proxy+} → auth
POST /api/entities/:entityId/options/refresh unreachable ANY /api/entities/{proxy+} → auth
POST /user/login, /user/create (user.js) ANY /user/{proxy+} → user same
GET /health, /health/detailed, /health/live, /health/ready GET /health, GET /health/{proxy+} → health same
/admin/db-migrate, /status, /resolve, /:migrationId explicit routes → dbMigrationRouter same
/admin/scripts/* (admin-script-router) explicit routes → adminScriptRouter same
/api/v2/reports, /api/v2/reports/* (report-router) GET /api/v2/reports, GET/POST/PUT/DELETE /api/v2/reports/{proxy+} → reportRouter same (no overlap)
POST /api/{name}-integration/webhooks[/:integrationId] → {name}Webhook same
/api/{name}-integration/{binding}{path} (extensions) → {name}__{binding} same
/api/{name}-integration/* (Definition.routes) ANY /api/{name}-integration/{proxy+} → {name} same (an integration named entities gets /api/entities-integration/…, a different literal segment)

Test plan

  • New management-api-gateway-routes.test.js (12 tests). It fails on next and lists the six unreachable entity routes ("no API Gateway route"). It passes with the fix.
  • Updated base-definition-factory.test.js, which now asserts the exact auth route list (44/44 pass).
  • npx jest infrastructure in devtools: the same 18 suites fail before and after this change. They are pre-existing, environment-dependent failures (AWS/discovery, Prisma layer build, health adapters), and nothing new fails.
  • serverless-offline (osls 3.76, Node 24). I ran the real core auth.handler against MongoDB, with the auth events generated by createBaseDefinition:
    • Before (next): POST /api/entity, GET /api/entity/options/x, GET /api/entities/x, GET /api/entities/x/test-auth, POST /api/entities/x/options and POST /api/entities/x/options/refresh all returned 404 from the gateway (existingRoutes: [/api/authorize, /api/integrations, /api/integrations/{proxy*}]).
    • After: all six return 401 No valid authentication provided from Express, with routeKeys ANY /api/entity, ANY /api/entity/{proxy+} and ANY /api/entities/{proxy+}. GET /api/integrations and /api/authorize still return 401.

🤖 Generated with Claude Code

📦 Published PR as canary version: 2.0.0--canary.667.a0620c7.0

✨ Test out this PR locally via:

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

Note

Medium Risk
Touches credential validation, entity persistence, and API Gateway routing for paths the Frigg UI depends on; response shape for GET entity drops embedded credential data (intentional security change).

Overview
Fixes gateway 404s on v1 Management API entity paths by wiring ANY /api/entity, /api/entity/{proxy+}, /api/entities, and /api/entities/{proxy+} to the auth Lambda. Adds management-api-gateway-routes.test.js so every route from a live createIntegrationRouter() must map to auth (and other Lambdas stay unshadowed).

Core: POST /api/entity now goes through CreateEntityFromCredential, which validates the caller’s credential (same 400 for missing/other-user IDs), builds the module from stored tokens, calls the provider via getEntityDetails, and persists via ProcessAuthorizationCallback.findOrCreateEntity so it matches the OAuth callback path. GetModule stops returning credential secrets—only credentialId and authIsValid. Module lookup for stored entities uses shared findModuleDefinitionForEntity (moduleName or legacy modelName). New router and use-case tests cover entity creation, idempotency, and auth boundaries.

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

@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 3, 2026

@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.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@netlify

netlify Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for friggframework-org canceled.

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

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Frigg 2.0.1 review probe — checking whether COMMENT reviews/comments can be posted from this agent.

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Frigg 2.0.1 review (Group 2) — COMMENT only

(Ignore the earlier “review probe” comment from this agent.)

Checklist

  • Composed CloudFormation / httpApi routes /api/entity and /api/entities → auth/integration router: pass
  • v1 500 from missing Module.getEntityModelFromDefinition: fail (not fixed on this branch)

Verdict (would): changes needed

Findings

  1. blocking — packages/core/modules/use-cases/get-module.js:46 (and the same call in get-entity-options-by-id.js, refresh-entity-options.js, test-module-auth.js) — not modified by this PR, but becomes reachable because of it
    • Module.getEntityModelFromDefinition is called on next and on this branch but never defined on Module (removed when mongoose entity models went away; callers left behind).
    • Scenario: Before this PR, GET/POST /api/entity* → API Gateway 404. After merge, the same requests reach Express → use case runs → TypeError: Module.getEntityModelFromDefinition is not a function → HTTP 500. The gateway fix alone regresses “unreachable” into “500 on every authenticated entity options/test-auth/get-module path”. Restore a static helper (e.g. return { modelName: definition.modelName ?? definition.moduleName }) or stop calling it and compare entity.moduleName to def.modelName ?? def.moduleName directly — same fix that belongs with the v1 entity work (d8626c44 on the scope stack still leaves the call in place).

Gateway route table + management-api-gateway-routes.test.js look correct for the routing half.

},
},
{ httpApi: { path: '/api/authorize', method: 'ANY' } },
{ httpApi: { path: '/api/entity', method: 'ANY' } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Checklist note (pass for routing): ANY /api/entity, /api/entity/{proxy+}, /api/entities, /api/entities/{proxy+} correctly attach the Management API entity prefixes to auth.

blocking elsewhere: once these routes work, Express handlers still call Module.getEntityModelFromDefinition (undefined on Module). Authenticated GET /api/entities/:id, options, and test-auth become 500 instead of the previous gateway 404. Fix or land the missing static helper in the same release train as this route PR.

@seanspeaks seanspeaks Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verdict: confirmed. Fixed in f74cf3b. Findings, checked on 6dc4b398:

  • POST /api/entity (packages/core/integrations/integration-router.js:597) called module.findOrCreateEntity(...). Module has no such method; it only exists on ProcessAuthorizationCallback. The module also came from GetModuleInstanceFromType with no credential loaded, so getEntityDetails ran against an API with no tokens.
  • GetModule, TestModuleAuth, GetEntityOptionsById and RefreshEntityOptions call Module.getEntityModelFromDefinition, which has never been defined on Module (git log -S finds no definition). So GET /api/entities/:id, /test-auth, POST /options and /options/refresh all return 500. GetModule would also have hit a second TypeError at module.entity.user.toString(), because the repositories return userId, not user.

Fix (core, hexagonal):

  • New use case modules/use-cases/create-entity-from-credential.js. The handler now just calls createEntityFromCredential.execute({ user, entityType, credentialId }). The use case loads the credential through the credential repository and refuses a missing or foreign one with the same 400 Invalid credential ID, so ids can't be probed. It builds the Module on the stored credential and persists through ProcessAuthorizationCallback.findOrCreateEntity, the same find-or-create (and credential repointing) that /api/authorize uses, with no copy of that logic. Its file name and execute signature match the Stack A version on feat(core): entity scopes and ownership by scope (ADR-056 §3.1–§3.4, §3.6) #682, so that branch's ownership-aware version replaces this one cleanly on rebase.
  • New helper modules/utils/find-module-definition.js (findModuleDefinitionForEntity). It matches on the stored moduleName, or a legacy modelName. The four use cases now use it instead of the missing static.
  • GetModule now returns credentialId + authIsValid and reads userId from entity.userId. Response-shape change: it no longer returns the decrypted credential. The route has 500'd until now, so no client can depend on that field, and returning raw tokens is the exposure flagged on feat(core): entity scopes and ownership by scope (ADR-056 §3.1–§3.4, §3.6) #682.

Tests (all failed before the fix, 6/8 red): integrations/tests/integration-router-entity-routes.test.js runs the real router, use cases and Module over in-memory repositories:

  • POST create (and idempotent re-POST), foreign/missing credential → identical 400, unknown type → 400
  • GET /api/entities/:id returns no token material
  • GET /api/entities/:id/test-auth
  • the v1 entity list (GET /api/integrations → entities.authorized)

There's also modules/use-cases/__tests__/create-entity-from-credential.test.js, which asserts delegation to findOrCreateEntity.

Note: v1 has no GET /api/entities list route. The list is entities.authorized on GET /api/integrations, which already worked, so those two tests pass before and after. /api/entities on the gateway is still correct for the /{proxy+} children.

npx jest modules integrations in core: 873 passed. The 8 failures are pre-existing and in files this change doesn't touch: find-integration-context-by-external-entity-id.test.js (error-code name) and the DocumentDB mapping-encryption test.

Not changed here: POST /api/entity still returns the persisted entity record. That record includes the populated credential for the caller's own credential (the shape the old code meant to return, and the same as Stack A). The DTO-level fix belongs with the one recommended on #682.

🤖 Generated with Claude Code

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Follow-up finding (COMMENT)

blocking — packages/core/integrations/integration-router.js:597

In addition to the missing Module.getEntityModelFromDefinition on GET entity routes: POST /api/entity still does module.findOrCreateEntity(entityDetails), and findOrCreateEntity is not defined on Module (only a private helper on ProcessAuthorizationCallback).

Scenario: After the gateway routes land, Frigg UI “create entity” → Express reaches the handler → TypeError: module.findOrCreateEntity is not a function → 500 on every create. Same class of bug as the GET path; needs the create-entity use-case fix (see d8626c44 on the ownership stack) in the same release train as this PR.

@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.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f74cf3b. Configure here.

const entityDetails = await module.getEntityDetails(
module.api,
null,
null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

POST entity leaks credential secrets

High Severity

POST /api/entity now reaches Express and returns the raw findOrCreateEntity row, whose populated credential includes access and refresh tokens. GET /api/entities/:entityId was sanitized to credentialId and authIsValid in this same change, but the create path still sends secrets to the client.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f74cf3b. Configure here.

seanspeaks and others added 2 commits October 24, 2026 00:32
… router

The auth Lambda serves core's integration router, which registers
POST /api/entity, GET /api/entity/options/:credentialId and the
/api/entities/:entityId family (get, test-auth, options, options/refresh).
The generated HTTP API only declared /api/integrations,
/api/integrations/{proxy+} and /api/authorize, and HTTP APIs have no
implicit catch-all, so every entity route 404'd at the gateway in a
deployed app (and under serverless-offline) before reaching Express.

Add /api/entity, /api/entity/{proxy+}, /api/entities and
/api/entities/{proxy+} (ANY) to the auth function, and add a test that
reads the routes off a live createIntegrationRouter(), composes a full
app definition, and asserts each one selects an API Gateway route on the
auth function, so a route added to core later fails until the gateway
is updated too. It also pins the neighbouring report, admin-script,
webhook, extension and integration-defined routes to their own
functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/entity called module.findOrCreateEntity, which Module does not
have, on a Module built without the credential. It now calls a
CreateEntityFromCredential use case: it checks that the caller owns the
credential (missing and foreign ids get the same 400), builds the Module on
that credential, and persists through
ProcessAuthorizationCallback.findOrCreateEntity, the same find-or-create the
authorization flow uses.

GetModule, TestModuleAuth, GetEntityOptionsById and RefreshEntityOptions
looked up the definition with Module.getEntityModelFromDefinition, which
does not exist, so every /api/entities/:entityId route returned 500. They
now share findModuleDefinitionForEntity (moduleName, or a legacy
modelName). GetModule also read entity.user, which the repositories do not
return, and returned the decrypted credential. It now returns
credentialId and authIsValid, like the v2 entity DTO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
@seanspeaks
seanspeaks force-pushed the fix/v1-entity-gateway-routes branch from f74cf3b to a0620c7 Compare October 24, 2026 04:32
@seanspeaks
seanspeaks merged commit f6928f9 into next Oct 24, 2026
14 checks passed
seanspeaks pushed a commit that referenced this pull request Oct 24, 2026
…on responses

The repositories attach the decrypted credential to every entity they
return, and three responses sent those records as they were, so a member of
an organization received the organization connection's access_token,
refresh_token or api_key:

- v1 GET /api/integrations (entities.authorized): mapModuleClassToModuleDTO
  returned the entity record;
- v1 GET /api/integrations and v2 GET /api/v2/integrations: the integration
  DTOs embedded the entities from findEntitiesByIds;
- v1 GET /api/entities/:id: GetModule returned module.credential.

One shared toPublicEntity (modules/utils/public-entity.js) now decides what an
entity looks like in any response: descriptive fields plus credentialId and
authIsValid, for every caller including the owner. It is used by the module
DTO, GetModule, the v1 integration DTO, the v2 toIntegrationDto, v1 GET
/api/integrations/:id and the POST /api/entity response.

GetModule, TestModuleAuth, GetEntityOptionsById and RefreshEntityOptions also
looked up the module definition with Module.getEntityModelFromDefinition,
which does not exist, so every /api/entities/:entityId route answered 500.
They now use findModuleDefinitionForEntity, ported unchanged from #667
(f74cf3b).

management-api/credential-exposure.test.js drives the real v1 and v2 routers
as an organization member, with repositories that attach the decrypted
credential, and asserts no token, key or credential object appears in any
entity or integration response. On the previous commit it fails on the three
leaks above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@sonarqubecloud

Copy link
Copy Markdown

@seanspeaks

Copy link
Copy Markdown
Contributor Author

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

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.

1 participant