Skip to content

BE-310: Protect sensitive properties from enumeration attacks#8332

Open
TimDiekmann wants to merge 30 commits intomainfrom
t/be-310-dont-allow-filtering-users-by-email
Open

BE-310: Protect sensitive properties from enumeration attacks#8332
TimDiekmann wants to merge 30 commits intomainfrom
t/be-310-dont-allow-filtering-users-by-email

Conversation

@TimDiekmann
Copy link
Member

@TimDiekmann TimDiekmann commented Jan 28, 2026

🌟 What is the purpose of this PR?

This PR adds protection against enumeration attacks by preventing filtering on sensitive properties like email for User entities. It implements a filter transformation system that automatically excludes User entities when filtering by email, while preserving legitimate query paths.

🔍 What does this change?

  • Adds a FilterProtectionConfig system to specify which properties should be protected for which entity types
  • Implements a filter transformation algorithm that prevents User enumeration via email filters
  • Adds a new skip_filter_protection flag to disable protection in development/testing
  • Updates the AI worker to exclude email from entity embeddings for User entities

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

🛡 What tests cover this?

  • Comprehensive integration tests for all filter protection cases
  • Truth table verification for various filter combinations
  • Tests for multi-property and multi-type protection scenarios
  • Tests for actor identity-based exclusions

❓ How to test this?

  1. Checkout the branch
  2. Try filtering entities by email
  3. Confirm that User entities are not returned when filtering by email
  4. Verify that non-User entities (like Invitations) can still be filtered by email
  5. Verify that a User can query their own entity by email, but not other Users

Introduces a filter transformation system that prevents attackers from
discovering users by filtering on protected properties like email. Even
when the API strips email from responses, returning results reveals user
existence.

The protection works by transforming filters at query time:
- Email conditions get `AND NOT(type=User)` added at leaf level
- NOT depth tracking ensures De Morgan-correct transformations
- OR branches are preserved (legitimate matches via other conditions)
- Machine actors bypass protection for system operations

Configurable via FilterProtectionConfig with property→entity type mapping.
CLI flag `--skip-filter-protection` available for dev/testing.
Refactors filter protection to correctly handle entities with multiple types.

- Introduces FilterExpressionList enum to allow Filter::In to accept either
  a ParameterList or a Path, enabling IN checks against array columns
- Changes type exclusion from Equal(EntityTypeEdge) to In(TypeBaseUrls)
  which properly checks if a type is present in an entity's type array
- Updates all Filter::In usages to wrap ParameterList in FilterExpressionList
- Use let chains to collapse nested if-let statements
- Change &Option<T> to Option<&T> for better API ergonomics
- Add #[must_use] to transform_filter
- Replace wildcard enum match with explicit variants
- Use #[expect] instead of #[allow] with reasons
- Fix doc comment punctuation
- Use is_none_or instead of map_or(true, ...)
- Add const to pure functions
- Use self instead of &self for Copy types
Prevents email enumeration attacks via CosineDistance queries on embeddings.
When generating embeddings for User entities, the email property is now
excluded from both individual property embeddings and the combined embedding.
Add early check for empty FilterProtectionConfig to avoid unnecessary
filter transformation when no properties are protected.
Extend test coverage for FilterProtectionConfig:

Multi-property tests (2 properties / 1 type):
- email AND phone both protected for User
- Tests: individual filters, AND, OR combinations

Multi-type tests (2 types / 1 property each):
- email protected for User, secret_code protected for SecretEntity
- Tests: verify cross-type protection independence

Also adds DatabaseTestWrapper::new_with_settings() to allow custom
FilterProtectionConfig in tests.
Document the expected behavior with ASCII truth tables showing:
- MP-1 to MP-4: Multi-property cases (email + phone → User)
- MT-1 to MT-4: Multi-type cases (email → User, secret → SecretEntity)

Tables clarify the cross-protection behavior in OR expressions where
each type can still match via the branch that isn't protected for them.
- Remove unused #[expect(clippy::struct_excessive_bools)] on MultiTypeRow
- Auto-format eprintln! statements
…ction

Allow users to query their own User entity by email while still
protecting other users' emails from enumeration.

- Add CellFilterExpression::ActorId variant for dynamic actor comparison
- Add CellFilter::NotEqual for uuid != ActorId conditions
- Add lifetime parameters to CellFilter types for borrowed data
- Update hash_default() to use All([type=User, uuid!=ActorId])
- Add integration tests for ActorId validation (positive/negative cases)
- Update unit tests for new exclusion filter structure
@vercel
Copy link

vercel bot commented Jan 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
hash Ignored Ignored Preview Feb 4, 2026 1:37pm
hashdotdesign Ignored Ignored Preview Feb 4, 2026 1:37pm
hashdotdesign-tokens Ignored Ignored Preview Feb 4, 2026 1:37pm
petrinaut Skipped Skipped Feb 4, 2026 1:37pm

@github-actions github-actions bot added area/apps > hash* Affects HASH (a `hash-*` app) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team area/tests New or updated tests area/apps area/apps > hash-graph labels Jan 28, 2026
Copy link
Member Author

TimDiekmann commented Jan 28, 2026

@codecov
Copy link

codecov bot commented Jan 28, 2026

Codecov Report

❌ Patch coverage is 77.30627% with 246 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.71%. Comparing base (0be3f12) to head (9806505).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
libs/@local/graph/store/src/filter/protection.rs 91.68% 46 Missing and 5 partials ⚠️
...s-store/src/store/postgres/knowledge/entity/mod.rs 0.00% 47 Missing ⚠️
libs/@local/graph/store/src/filter/mod.rs 45.12% 45 Missing ⚠️
.../hash-api/src/graph/knowledge/system-types/user.ts 0.00% 43 Missing ⚠️
...postgres-store/src/store/postgres/query/compile.rs 83.09% 6 Missing and 6 partials ⚠️
...edge/primitive/entity/after-create-entity-hooks.ts 0.00% 8 Missing ⚠️
...@local/graph/authorization/src/policies/context.rs 0.00% 6 Missing ⚠️
...ty-hooks/text-after-update-entity-hook-callback.ts 0.00% 4 Missing ⚠️
.../hash-api/src/graph/knowledge/system-types/text.ts 0.00% 4 Missing ⚠️
...cal/graph/authorization/src/policies/components.rs 50.00% 4 Missing ⚠️
... and 11 more
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #8332       +/-   ##
===========================================
- Coverage   60.09%   49.71%   -10.39%     
===========================================
  Files        1235      493      -742     
  Lines      118033    56672    -61361     
  Branches     5167     1507     -3660     
===========================================
- Hits        70932    28173    -42759     
+ Misses      46278    28211    -18067     
+ Partials      823      288      -535     
Flag Coverage Δ
apps.hash-ai-worker-py ?
apps.hash-ai-worker-ts ?
apps.hash-api 0.00% <0.00%> (ø)
backend-integration-tests ?
blockprotocol.type-system ?
deer ?
error-stack ?
local.claude-hooks ?
local.harpc-client ?
local.hash-backend-utils ?
local.hash-graph-sdk ?
local.hash-isomorphic-utils ?
local.hash-subgraph ?
rust.antsi 0.00% <ø> (ø)
rust.deer ?
rust.error-stack 90.88% <ø> (ø)
rust.harpc-codec 84.70% <ø> (ø)
rust.harpc-tower ?
rust.harpc-types ?
rust.hash-graph-temporal-versioning ?
rust.hashql-ast ?
rust.hashql-core ?
rust.hashql-diagnostics 72.43% <ø> (ø)
rust.hashql-eval 68.54% <ø> (ø)
rust.hashql-mir ?
rust.hashql-syntax-jexpr ?
rust.sarif ?
sarif ?
tests.hash-backend-integration ?
unit-tests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 28, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing t/be-310-dont-allow-filtering-users-by-email (9806505) with main (3b21d31)

Summary

✅ 21 untouched benchmarks
🗄️ 12 archived benchmarks run1

Footnotes

  1. 12 benchmarks were run, but are now archived. If they were deleted in another branch, consider rebasing to remove them from the report. Instead if they were added back, click here to restore them.

Implement JSONB key deletion to remove protected properties from entity
responses at the database level. Properties like email are now masked
directly in SQL SELECT statements, ensuring they never leave the database
unless the actor is the entity owner.

- Add CaseWhen, JsonDeleteKeys, ArrayConcat, ArrayLiteral expressions
- Implement property_masking module for building masking expressions
- Integrate masking into SelectCompiler for Properties and PropertyMetadata
- Add integration tests for response masking and sorting protection
- Add unit tests for masked expression generation
- Remove email property filtering from TypeScript SDK entity mapping
- Optimize getUser to accept pre-fetched emails to skip Kratos calls
- Add private IP exception for Kratos webhooks in local development
@vercel vercel bot temporarily deployed to Preview – petrinaut January 30, 2026 09:21 Inactive
@github-actions github-actions bot added the area/apps > hash-api Affects the HASH API (app) label Jan 30, 2026
Base automatically changed from t/sre-415-fix-vercel-not-building to main February 1, 2026 21:44
@vercel vercel bot temporarily deployed to Preview – petrinaut February 1, 2026 22:22 Inactive
@github-actions github-actions bot added the type/eng > frontend Owned by the @frontend team label Feb 1, 2026
@TimDiekmann TimDiekmann force-pushed the t/be-310-dont-allow-filtering-users-by-email branch from c4ddf18 to 55aabe8 Compare February 1, 2026 22:23
@github-actions github-actions bot removed the type/eng > frontend Owned by the @frontend team label Feb 1, 2026
@vercel vercel bot temporarily deployed to Preview – petrinaut February 1, 2026 22:26 Inactive
indietyp
indietyp previously approved these changes Feb 2, 2026
Copy link
Member

@indietyp indietyp left a comment

Choose a reason for hiding this comment

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

LGTM some non blocking comments

@TimDiekmann TimDiekmann added this pull request to the merge queue Feb 2, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 2, 2026
@TimDiekmann TimDiekmann force-pushed the t/be-310-dont-allow-filtering-users-by-email branch from ff03373 to 2f45f6f Compare February 2, 2026 14:56
@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

Benchmark results

@rust/hash-graph-benches – Integrations

policy_resolution_large

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2002 $$42.3 \mathrm{ms} \pm 913 \mathrm{μs}\left({\color{red}38.3 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.24 \mathrm{ms} \pm 15.2 \mathrm{μs}\left({\color{gray}-0.471 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1001 $$13.5 \mathrm{ms} \pm 74.2 \mathrm{μs}\left({\color{gray}1.56 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$44.1 \mathrm{ms} \pm 262 \mathrm{μs}\left({\color{gray}0.361 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$15.6 \mathrm{ms} \pm 116 \mathrm{μs}\left({\color{gray}3.57 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1526 $$24.4 \mathrm{ms} \pm 142 \mathrm{μs}\left({\color{gray}-0.453 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$52.4 \mathrm{ms} \pm 319 \mathrm{μs}\left({\color{red}19.8 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$22.9 \mathrm{ms} \pm 104 \mathrm{μs}\left({\color{red}7.08 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$33.5 \mathrm{ms} \pm 130 \mathrm{μs}\left({\color{red}13.3 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$3.68 \mathrm{ms} \pm 19.8 \mathrm{μs}\left({\color{gray}1.24 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.86 \mathrm{ms} \pm 13.7 \mathrm{μs}\left({\color{gray}0.486 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 51 $$3.25 \mathrm{ms} \pm 17.5 \mathrm{μs}\left({\color{gray}1.41 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$5.16 \mathrm{ms} \pm 41.7 \mathrm{μs}\left({\color{gray}1.14 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.49 \mathrm{ms} \pm 25.3 \mathrm{μs}\left({\color{gray}1.10 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 107 $$4.06 \mathrm{ms} \pm 31.0 \mathrm{μs}\left({\color{gray}-0.873 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$4.60 \mathrm{ms} \pm 45.3 \mathrm{μs}\left({\color{red}11.5 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.37 \mathrm{ms} \pm 19.7 \mathrm{μs}\left({\color{gray}2.95 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$4.06 \mathrm{ms} \pm 38.8 \mathrm{μs}\left({\color{gray}2.44 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.42 \mathrm{ms} \pm 16.8 \mathrm{μs}\left({\color{gray}1.01 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.34 \mathrm{ms} \pm 9.15 \mathrm{μs}\left({\color{gray}-0.614 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1 $$2.42 \mathrm{ms} \pm 12.7 \mathrm{μs}\left({\color{gray}-0.904 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$2.63 \mathrm{ms} \pm 21.0 \mathrm{μs}\left({\color{gray}-2.080 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.52 \mathrm{ms} \pm 14.2 \mathrm{μs}\left({\color{gray}-0.403 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$2.72 \mathrm{ms} \pm 17.5 \mathrm{μs}\left({\color{gray}-0.403 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$2.81 \mathrm{ms} \pm 13.8 \mathrm{μs}\left({\color{gray}-0.305 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.48 \mathrm{ms} \pm 13.5 \mathrm{μs}\left({\color{gray}0.540 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 25 $$2.65 \mathrm{ms} \pm 13.7 \mathrm{μs}\left({\color{gray}1.48 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$3.17 \mathrm{ms} \pm 22.4 \mathrm{μs}\left({\color{gray}1.06 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$2.69 \mathrm{ms} \pm 13.1 \mathrm{μs}\left({\color{gray}0.232 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 26 $$2.95 \mathrm{ms} \pm 16.7 \mathrm{μs}\left({\color{gray}1.86 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$3.05 \mathrm{ms} \pm 19.4 \mathrm{μs}\left({\color{gray}0.736 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.67 \mathrm{ms} \pm 13.2 \mathrm{μs}\left({\color{gray}0.151 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$2.91 \mathrm{ms} \pm 18.7 \mathrm{μs}\left({\color{gray}0.771 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$40.7 \mathrm{ms} \pm 185 \mathrm{μs}\left({\color{gray}1.88 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$78.2 \mathrm{ms} \pm 364 \mathrm{μs}\left({\color{gray}-0.950 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$45.4 \mathrm{ms} \pm 179 \mathrm{μs}\left({\color{gray}3.89 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$47.3 \mathrm{ms} \pm 269 \mathrm{μs}\left({\color{gray}0.320 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$55.1 \mathrm{ms} \pm 342 \mathrm{μs}\left({\color{gray}-0.079 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$42.1 \mathrm{ms} \pm 163 \mathrm{μs}\left({\color{gray}-0.375 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$415 \mathrm{ms} \pm 915 \mathrm{μs}\left({\color{gray}-1.711 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$97.4 \mathrm{ms} \pm 484 \mathrm{μs}\left({\color{gray}0.797 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$86.5 \mathrm{ms} \pm 367 \mathrm{μs}\left({\color{gray}-3.105 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$294 \mathrm{ms} \pm 1.14 \mathrm{ms}\left({\color{gray}3.69 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$16.3 \mathrm{ms} \pm 90.7 \mathrm{μs}\left({\color{red}9.32 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$15.8 \mathrm{ms} \pm 86.0 \mathrm{μs}\left({\color{gray}2.35 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$15.9 \mathrm{ms} \pm 72.6 \mathrm{μs}\left({\color{gray}1.24 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$16.1 \mathrm{ms} \pm 92.7 \mathrm{μs}\left({\color{red}5.68 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$18.2 \mathrm{ms} \pm 131 \mathrm{μs}\left({\color{gray}1.89 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$15.3 \mathrm{ms} \pm 82.5 \mathrm{μs}\left({\color{gray}3.32 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$15.4 \mathrm{ms} \pm 84.9 \mathrm{μs}\left({\color{gray}2.18 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$15.7 \mathrm{ms} \pm 79.7 \mathrm{μs}\left({\color{gray}4.51 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$17.0 \mathrm{ms} \pm 138 \mathrm{μs}\left({\color{red}8.99 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$24.5 \mathrm{ms} \pm 166 \mathrm{μs}\left({\color{red}5.95 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity

Function Value Mean Flame graphs
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1 $$31.2 \mathrm{ms} \pm 286 \mathrm{μs}\left({\color{gray}1.15 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$32.3 \mathrm{ms} \pm 304 \mathrm{μs}\left({\color{gray}4.61 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$30.8 \mathrm{ms} \pm 319 \mathrm{μs}\left({\color{gray}-0.631 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$31.6 \mathrm{ms} \pm 304 \mathrm{μs}\left({\color{gray}2.68 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$31.6 \mathrm{ms} \pm 343 \mathrm{μs}\left({\color{red}7.20 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$31.6 \mathrm{ms} \pm 280 \mathrm{μs}\left({\color{gray}3.96 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$31.3 \mathrm{ms} \pm 266 \mathrm{μs}\left({\color{gray}0.823 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$30.7 \mathrm{ms} \pm 279 \mathrm{μs}\left({\color{gray}2.22 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$31.8 \mathrm{ms} \pm 261 \mathrm{μs}\left({\color{gray}-0.640 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity_type

Function Value Mean Flame graphs
get_entity_type_by_id Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba $$8.33 \mathrm{ms} \pm 39.8 \mathrm{μs}\left({\color{gray}2.61 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$97.0 \mathrm{ms} \pm 452 \mathrm{μs}\left({\color{red}101 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$149 \mathrm{ms} \pm 820 \mathrm{μs}\left({\color{red}55.1 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$102 \mathrm{ms} \pm 532 \mathrm{μs}\left({\color{red}87.9 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$110 \mathrm{ms} \pm 477 \mathrm{μs}\left({\color{red}76.8 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$118 \mathrm{ms} \pm 470 \mathrm{μs}\left({\color{red}68.4 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$123 \mathrm{ms} \pm 525 \mathrm{μs}\left({\color{red}60.9 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$91.2 \mathrm{ms} \pm 497 \mathrm{μs}\left({\color{red}73.5 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$118 \mathrm{ms} \pm 391 \mathrm{μs}\left({\color{red}48.0 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$96.9 \mathrm{ms} \pm 405 \mathrm{μs}\left({\color{red}63.3 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$106 \mathrm{ms} \pm 586 \mathrm{μs}\left({\color{red}57.1 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$108 \mathrm{ms} \pm 524 \mathrm{μs}\left({\color{red}55.8 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$108 \mathrm{ms} \pm 508 \mathrm{μs}\left({\color{red}57.3 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$142 \mathrm{ms} \pm 516 \mathrm{μs}\left({\color{gray}3.68 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$138 \mathrm{ms} \pm 615 \mathrm{μs}\left({\color{gray}-0.463 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$106 \mathrm{ms} \pm 550 \mathrm{μs}\left({\color{lightgreen}-13.085 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$598 \mathrm{ms} \pm 1.25 \mathrm{ms}\left({\color{gray}-2.575 \mathrm{\%}}\right) $$ Flame Graph

@vercel vercel bot temporarily deployed to Preview – petrinaut February 4, 2026 13:37 Inactive
@github-actions github-actions bot dismissed indietyp’s stale review February 4, 2026 13:37

Your organization requires reapproval when changes are made, so Graphite has dismissed approvals. See the output of git range-diff at https://github.com/hashintel/hash/actions/runs/21673574079

Copy link

@cursor cursor bot left a comment

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 and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

}`,
);
}
entity.properties["https://hash.ai/@h/types/property-type/email/"] = emails;
Copy link

Choose a reason for hiding this comment

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

Hardcoded URL strings instead of existing constants

Medium Severity

Lines 307, 316, and 321 use hardcoded URL strings like "https://hash.ai/@h/types/property-type/kratos-identity-id/" and "https://hash.ai/@h/types/property-type/email/" when systemPropertyTypes.kratosIdentityId.propertyTypeBaseUrl and systemPropertyTypes.email.propertyTypeBaseUrl constants are already used elsewhere in this file (e.g., line 252, 324).

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

The strings are actual constants as well. A typo would result in a tsc error. The approach is not great but we talked about this already internally. Not something we're going to change now.

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

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) area/apps > hash-graph area/apps area/libs Relates to first-party libraries/crates/packages (area) area/tests > integration New or updated integration tests area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

2 participants