Skip to content

feat(core): entity scopes and ownership by scope (ADR-056 §3.1–§3.4, §3.6) - #682

Open
seanspeaks wants to merge 10 commits into
claude/internal-mcp-04-membershipsfrom
claude/internal-mcp-05-scope-ownership
Open

seanspeaks wants to merge 10 commits into
claude/internal-mcp-04-membershipsfrom
claude/internal-mcp-05-scope-ownership

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This implements ADR-056 §3.1–§3.4 and the entity, integration and list parts of §3.6. An entity or integration is now owned by a person, by an organization, or globally, and lists show only what belongs to the active organization.

Stack: this is the first of three PRs: #681 (memberships) → this → #683 (org roles) → #684 (credential scoping). It is stacked on #681 and also merges #679 (entity registry), because the scope chain reads the registry's per-type scope. Until #679 merges, the diff against #681 includes #679's commits.

Scope resolution (modules/scope-*.js, re-exported from entity-scope.js)

  • Pure resolution in ADR order:

    1. request override ({ default, allow });
    2. integration binding;
    3. integration;
    4. app entity type (registry settings.scope);
    5. appDefinition.ownership;
    6. a default derived from user.primary.

    ownership.individualAcross ('organization' or 'all') is supported.

  • Validated at app-definition load (OwnershipConfigError). This includes §3.3: an organization-owned integration cannot bind an individual entity. Schemas, OpenAPI and types are updated.

Data

  • organizationId (nullable) on Entity and Integration, in both Prisma schemas, with a Postgres migration (20261016000000_add_entity_integration_organization) and an index on [userId, organizationId].
    • The migration was checked on a throwaway Postgres 16: it applies cleanly and migrate diff is empty.
    • There is no data migration.
    • There is no scope column, because the owner already encodes the scope.
    • organizationId is deliberately not a foreign key.
  • Release note: a plain CREATE INDEX takes a SHARE lock while it builds, and Prisma migrations cannot use CONCURRENTLY. Adopters with very large Entity or Integration tables can pre-create the index by hand under the same name.

Ownership on create

  • Authorize: v1 and v2 authorize own the entity by its resolved scope and set organizationId. organizationId comes only from the active organization, never from provider data. A tenant request for global returns 403.
  • Integration create: stores organizationId and derives the owner per the ADR table.
  • Security fix (v1): POST /api/integrations used to accept any entity id. It now uses the same ownership resolver as v2, which returns 404 before anything is written.
  • Mixed scopes: an integration that mixes scopes (an individual integration using an org-owned entity) is refused with 400 before anything is persisted.

Lists and access

  • What lists return: /api/v2/entities, /credentials and /integrations return, for the active organization:
    • its organization records;
    • the caller's individual records, by the §3.6 rule owner = caller AND (organizationId = active OR (all AND organizationId IS NULL)).
  • List fields: each record carries scope, owner, organizationId and access. Lists take ?scope= and ?owner=me|org filters.
  • One rule everywhere: one clause list is both the repository query (Postgres, Mongo and DocumentDB; on Mongo it matches null or unset) and the in-memory access check. Proxy, test-auth, options, delete, reauthorize and every v1 per-integration route use the same rule.

Legacy rows (existing data, organizationId null)

  • Default individual app with no organizations: unchanged.
  • Org-primary apps:
    • Org-owned legacy rows stay visible to the organization.
    • A person's legacy individual rows show in the organization named by their legacy organizationId pointer.
    • Repeating POST /api/integrations for a legacy org-owned integration still reuses it, with no duplicate and no second ON_CREATE. This is tested with org-only headers and with a pointer mismatch.
  • Entity proxy: until F6 adds managementApi.proxy.scopes, the entity proxy refuses organization-scope entities (ADR §3.5: narrowest by default). Org-primary apps that proxy through org entities must opt in once F6 lands.

Left for later PRs

Test plan

  • Core suite, run with a no-op global setup (Mongo binaries can't run here): 14 suites and 61 tests fail, the same set by name as base; 3357 pass.
  • packages/schemas: 194 pass.
  • ESLint on changed files: 0 errors.
  • Two independent reviews, one on spec and compatibility and one on code quality.
  • 38 of 38 mutations killed on this branch.

Docs: docs/guides/entity-scopes.md is new; organization-memberships.md and the core README are updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6


Note

High Risk
Changes core tenancy, authorization, integration binding, and list visibility across v1/v2 APIs, with a DB migration and legacy compatibility rules—mistakes could leak or hide the wrong org’s data.

Overview
Implements ADR-056 entity scopes: entities and integrations are owned by individual, organization, or (tenant-refused) global scope, with nullable organizationId on both models, Postgres migration, and Mongo null OR unset matching for legacy rows.

Scope resolution lives in scope-*.js / createScopePolicy: validated at loadAppDefinition (OwnershipConfigError), applied on authorize/create, and used for list + access via one clause set (findIntegrationsByOwnership / entity equivalents + in-memory describeRecord). v2 (and docs) expose scope, owner, organizationId, access and ?scope / ?owner filters. Credentials stay unscoped in this PR; entity proxy fails closed for organization-scoped entities until F6 proxy.scopes.

Integration create resolves owner + org through ResolveIntegrationOwnership (mixed scopes, scope mismatch, org+individual entity → 400; invisible entities → 404). CreateIntegration stores organizationId, dedupes within org (with adoptUnscoped for legacy rows), and loads modules before persisting to avoid orphan IN_CREATION rows. v1 POST /api/integrations and authorize/entity routes now follow the same rules (security fix: no arbitrary entity binding).

Also wires ADR-059 entity registry: loadAppDefinition returns entityTypeRegistry / moduleDefinitions; health, workers, webhooks, and createFriggCommands pass the full offered module set (registry-only types authorizeable). getModulesDefinitionFromIntegrationClasses is deprecated; GetIntegrationsByUserId splits app-level “all for user id” from request-scoped GetIntegrationsForUser. Integration GET responses strip credentials via toPublicEntities.

New guide docs/guides/entity-scopes.md; membership docs mark F5 scoping done.

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

📦 Published PR as canary version: 2.0.0--canary.682.451c957.0

✨ Test out this PR locally via:

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

Add getEntityTypeRegistry(appDefinition), a pure function that merges the
optional appDefinition.entities map with the modules bound by integrations.
Inference stays on unless entities.$infer is false.

With no entities key nothing new can fail: two different definitions under
one moduleName keep the first and log a warning. With entities present
(even {}) the rules are strict: a key must equal moduleName and getName(),
one name is one definition (same object, or same package@version), and
$infer: false requires every bound module to be registered.

loadAppDefinition builds the registry once, at load, so errors fail fast,
and returns entityTypeRegistry and moduleDefinitions. The v1 router, v2
dependencies, health, the webhook loader and the worker entry read those;
createIntegrationCommands and createQueueWorker take moduleDefinitions and
fall back to the integration's own modules when none is given.
getModulesDefinitionFromIntegrationClasses is removed.

GET /api/v2/entities/types[/:type] lists registry-only types, looks them up
by registry key, and each description gains usedBy. app-definition.schema.json
gains entities; types/modules/index.d.ts adds the registry types.

This commit does not change which fields are encrypted:
loadModuleEncryptionSchemas keeps its existing (broken) require path, with a
comment pointing to claude/internal-mcp-03b-module-credential-encryption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Conflicts: the loader keeps both the adopter-JWT config assert and the
registry; v2 dependencies keep the registry imports and F4's
buildUserAuthentication; types reference both user and modules. The registry
router test gains F4's membership-repository mock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ntities and integrations (ADR-056 §3.1-§3.4, §3.6)

- scope-resolution / scope-config / scope-visibility / scope-policy (re-exported
  by entity-scope.js): scope resolution (request > binding > integration >
  entity type > app > default from user.primary), ownership on create, one
  visibility clause list used as the repository query and as the in-memory
  access check, legacy-row rules.
- Entity and Integration gain a nullable organizationId (both Prisma schemas,
  one PostgreSQL migration). No scope column: the owner's user type says it.
- AuthorizeEntityForUser (v1 and v2) owns by scope and pairs with the active
  organization. Lists, integration create (v1 and v2) and every entity load use
  the same rule; v1 integration create now checks entity ownership.
- ResolveIntegrationOwnership refuses mixed-scope integrations with a 400 before
  anything is written; CreateIntegration validates before persisting; an
  organization-owned legacy integration is adopted on a repeated create.
- The entity proxy fails closed for organization-scoped entities until
  proxy.scopes (F6).
- ownership config validated at app-definition load; schemas, OpenAPI, types and
  docs/guides/entity-scopes.md updated.

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

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

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

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

Stale Bugbot comment from a previous run.

Comment thread packages/core/modules/repositories/module-repository-postgres.js
Comment thread packages/core/integrations/integration-router.js
…ps' into claude/internal-mcp-05-scope-ownership
…rize uses the scope owner

- _mapEntity (postgres, mongo, documentdb) spread the provider's `data` over
  `id` and `userId` and only pinned `organizationId` afterwards, so a stored
  data.id or data.userId replaced the row's identity, which drives scope,
  access and every later load. `data` is now spread first and every column
  is pinned after it. The pre-F5 mappers had the same order (data last), so
  this was an existing weakness that F5's ownership checks made dangerous.
  The integration repositories map `config` explicitly and do not spread.
- v1 GET /api/authorize built the module with user.getId() while POST stores
  under the resolved scope owner. It now asks AuthorizeEntityForUser for the
  owner, as POST does.

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

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

Stale Bugbot comment from a previous run.

Comment thread packages/core/integrations/integration-router.js
Comment thread packages/core/integrations/integration-router.js
Comment thread packages/core/modules/use-cases/test-module-auth.js
…cated registry wrapper

#679 removed the export. ADR-059 §2 keeps it as a deprecated wrapper so
adopters that import it keep working. It now returns the entity type
registry's definitions (optionally including app.entities), logs a
deprecation warning once per process, and is exported again from
@friggframework/core and integrations/. Typed in types/modules.

The "registered": false list flag of ADR-059 §4 is not built; the
reference doc says so (the ADR is amended on the ADR branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
findEntitiesByOwnership and findIntegrationsByOwnership read only the
first cursor batch (findMany returns cursor.firstBatch), so lists past
~101 documents lost rows, the same defect #681 fixed for memberships.
Both now use findManyDrained. The bulk credential read behind entity
lists is drained too, or entities past the first batch of the $in read
came back without their credential.

Tested over a fake server holding 2500 documents per collection (more
than one getMore); each of the three reads reverted to findMany fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ernal-mcp-05-scope-ownership

Brings in the deprecated getModulesDefinitionFromIntegrationClasses wrapper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
… routes build the module for the owner

POST /api/entity took user.getId() as the owner and set no organizationId,
so it fell outside the ownership rules (an org-primary member could create
an org-owned row). It also called Module.findOrCreateEntity, which no
longer exists, so it failed on every call. It now goes through a new
CreateEntityFromCredential use case: the owner comes from
AuthorizeEntityForUser.resolveOwnership (as /api/authorize), the credential
must belong to that owner (400 INVALID_CREDENTIAL otherwise, the same for a
missing one), the module is built with the credential loaded, and the
entity is found or created with the resolved organization.

TestModuleAuth, GetModule, GetEntityOptionsById and RefreshEntityOptions
(v1 test-auth, GET /api/entities/:id and the options routes) built the
Module with the caller's id. A member using an organization's entity there
would, on a token refresh, upsert a new credential under the member. They
now build it for entity.userId, as v2 does.

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

@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 d8626c4. Configure here.

if (!credential || !sameId(credential.userId, ownership.ownerId)) {
throw Boom.badRequest('Invalid credential ID', {
code: 'INVALID_CREDENTIAL',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Credential data can override owner check

Medium Severity

CreateEntityFromCredential authorizes the request with credential.userId from findCredentialById, but those adapters still spread the JSON data blob after the identity columns. A userId (or id) key inside stored token data replaces the real owner, so the new INVALID_CREDENTIAL check can accept another user's credential or reject a valid one.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d8626c4. Configure here.

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Frigg 2.0.1 review (Stack A) — COMMENT only

Verdict (would): changes needed

Cross-org gates (describeRecord / visibilityClauses / GetOwned* → 404) are generally sound. v2 DTOs strip secrets (credentialId only). v1 still embeds full credentials, and this PR newly lets individual-primary org members use org-owned entities — expanding who can pull those secrets.

Blocking

  1. Org members get raw credentials via v1 entity/integration payloads
    • packages/core/modules/use-cases/get-module.js (~69–76) — GET /api/entities/:id returns credential
    • map-module-dto.js / integration DTOs — entity / entities[] can carry populated credential blobs
      Failure: Member of org X (individual primary) lists or gets an org Slack/CRM connection → receives OAuth tokens / API keys in JSON. Before this PR those org rows were not visible under individual primary. v2 is fine; v1 is still on by default.
      Fix: Mirror v2 (credentialId + authIsValid only) in GetModule, module/integration DTOs — or disable v1 when ownership scopes are enabled.

Should-fix

  1. ENTITY_IN_USE may disclose foreign integration ids via global findIntegrationsByEntityId. Filter to integrations the caller may see, or omit ids.
  2. Hydration trusts entitiesIds without re-checking visibility — defense-in-depth: skip entities that fail describeRecord so legacy/corrupt ids cannot pull another org’s credential into the DTO (especially until v1 secret stripping lands).

Nits

  • Missing vs foreign → same 404 on ownership load path looks good.
  • Unscoped org rows { userId: activeOrgId, organizationId: null } match ADR legacy behavior.

No formal approve / request-changes on this review.

// Otherwise fall back to simple equality check
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
? userIdOrUser.ownsUserId(entity.userId)
? this.scopePolicy.canUseEntity(userIdOrUser, entity)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blocking — widening canUseEntity exposes v1 credential payloads to org members

Switching from ownsUserId to canUseEntity correctly implements ADR-056 use-access — but the return value below still includes credential: module.credential (full decrypted secrets). Integration DTOs similarly embed populated entities[].

Scenario: Individual-primary member of org X calls v1 GET /api/entities/:id (or lists integrations) for an org-owned connection → receives OAuth tokens / API keys. Those rows were not visible under individual primary before this PR. v2 masks credentials; v1 is still on by default.

Please strip secrets from v1 entity/integration responses (mirror v2: credentialId + authIsValid only), or refuse to enable ownership scopes while v1 returns raw credentials.

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, and wider than described. Checked on this branch's head (d8626c44) with a throwaway test (not committed).

Live today, for org members

  • GET /api/entities (integration-router.js → GetEntitiesForUser.execute, modules/use-cases/get-entities-for-user.js:30-40). findEntitiesByOwnership populates credential through _mapEntity (module-repository-postgres.js:238-241, module-repository-mongo.js:199-202). mapModuleClassToModuleDTO (modules/utils/map-module-dto.js:12) then returns entity: moduleInstance.entity unfiltered. A member of org 17 who lists entities gets org 17's entity with entity.credential.data.access_token / refresh_token. The test asserted this and it passed.
  • GET /api/integrations (v1) and GET /api/v2/integrations. GetIntegrationsForUser.toDto (integrations/use-cases/get-integrations-for-user.js:69-98) embeds entities from findEntitiesByIds, which also populates the credential. v1 mapIntegrationClassToIntegrationDTO passes them through (map-integration-dto.js:12), and so does v2 toIntegrationDto (management-api/v2/dto.js, entities: integration.entities ?? []). So "v2 masks credentials" holds only for v2 entity responses. v2 integration responses leak the same way. (The same toIntegrationDto is on feat: Management API v2, /api/meta, v1 deprecation and the locked-down entity proxy (ADR-052, ADR-053) #671.)

Latent: GET /api/entities/:entityId

  • GetModule.execute (modules/use-cases/get-module.js:69-76) returns credential: module.credential, which is the full decrypted record. Today this route 500s before it gets there: Module.getEntityModelFromDefinition (get-module.js:49) is not defined anywhere (typeof Module.getEntityModelFromDefinition === 'undefined' on this head). The tests that cover it mock the static. Once the static exists (fix(devtools): route /api/entity and /api/entities to the integration router #667 adds the missing piece), the member gets credential.data.refresh_token. The test asserted that too.

Minimal fix (recommended before ownership scopes merge)

  1. Add one shared mapper, e.g. modules/utils/public-entity.js → toPublicEntity(entity). It returns the entity without credential, plus credentialId and authIsValid, the same fields v2's toEntityDto exposes.
  2. Use it in mapModuleClassToModuleDTO (entity: toPublicEntity(...), which already has credentialId), in GetModule (replace credential: with credentialId + authIsValid), in mapIntegrationClassToIntegrationDTO (entities.map(toPublicEntity)), and in v2 toIntegrationDto.
  3. Apply it to every caller, not only members. An owner has no need for their own refresh token over HTTP either, and one rule is easier to test. The only in-repo consumer I found reads result.credential?.id from the authorize response (packages/ui/lib/api/api.js:267), which is a different route.
  4. Add a regression test: an org member lists entities and integrations, and asserts that no access_token / refresh_token / data appears anywhere in the JSON.

🤖 Generated with Claude Code

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.

Confirmed, and fixed in 451c957b (pushing now).

What leaked (reproduced by the new test on the previous commit):

  • v1 GET /api/integrations: in entities.authorized, the entity record came from mapModuleClassToModuleDTO.
  • 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.

Fix: one shared toPublicEntity in modules/utils/public-entity.js.

  • It keeps a fixed set of descriptive fields and reduces the credential to credentialId and authIsValid, for every caller including the owner.
  • It is used by:
    • the module DTO;
    • GetModule;
    • the v1 integration DTO;
    • v2 toIntegrationDto;
    • v1 GET /api/integrations/:id;
    • the POST /api/entity response.

#667: this branch doesn't contain it, and its CreateEntityFromCredential differs from #667's, so I didn't rebase.

  • I ported findModuleDefinitionForEntity unchanged from f74cf3b. GetModule, TestModuleAuth, GetEntityOptionsById and RefreshEntityOptions no longer call the missing Module.getEntityModelFromDefinition.
  • GET /api/entities/:id answers 200 again.

Test: management-api/credential-exposure.test.js uses the real v1 and v2 routers and use cases.

  • The repositories attach the decrypted credential, as the real ones do.
  • It signs in as a member of the organization that owns the connection.
  • It calls:
    • v1 /api/integrations, /api/integrations/:id and /api/entities/:id;
    • v2 /api/v2/entities, /entities/:id, /integrations and /integrations/:id.
  • It asserts no access_token, refresh_token, api_key, id_token or client_secret value or key, and no credential object, appears in any body.
  • It also asserts the connection is present, so a 404 or an empty list can't pass.

Mutation checks:

  • Undoing the module DTO, the v1 integration DTO or GetModule each fails it.
  • Undoing the v2 toIntegrationDto is caught by a new unit test in dto.test.js. In the router test that layer is covered twice, so the router test alone doesn't catch it.

Wider run: the modules, integrations, management-api, handlers, user and credential suites fail the same 61 tests by name as before, with 12 more passing.


Generated by Claude Code

…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

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

@gitguardian

gitguardian Bot commented Oct 24, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
38932763 Triggered Generic High Entropy Secret 451c957 packages/core/management-api/credential-exposure.test.js View secret
38932765 Triggered Generic High Entropy Secret 451c957 packages/core/management-api/credential-exposure.test.js View secret
38932766 Triggered Generic High Entropy Secret 451c957 packages/core/management-api/credential-exposure.test.js View secret
38932764 Triggered Generic High Entropy Secret 451c957 packages/core/management-api/credential-exposure.test.js View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@sonarqubecloud

Copy link
Copy Markdown

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