From 51619b96b3e7d6a7e23ce8754f67f9b2737f825e Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 24 Aug 2026 16:23:21 +0200 Subject: [PATCH] fix(api): refuse plus-tag emails for starter credits Skip the signup grant when the local part contains +, except @agenta.ai. Signup still succeeds. The person just does not get the free connection. --- .../core/starter_credits_bridge/service.py | 10 ++++++ .../src/core/starter_credits_bridge/types.py | 4 +++ .../test_starter_credits_bridge_seeding.py | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/api/ee/src/core/starter_credits_bridge/service.py b/api/ee/src/core/starter_credits_bridge/service.py index a940703f22c..6d5db931af2 100644 --- a/api/ee/src/core/starter_credits_bridge/service.py +++ b/api/ee/src/core/starter_credits_bridge/service.py @@ -36,6 +36,7 @@ from ee.src.core.starter_credits_bridge.client import StarterCreditsProxyClient from ee.src.core.starter_credits_bridge.types import ( DEVELOPMENT_POLICY_VALUES, + PLUS_ALIAS_ALLOWLIST_DOMAINS, KeyAliasExistsError, MintedKey, MintPolicy, @@ -612,8 +613,17 @@ async def _mint_policy_allows( domain = _email_domain(organization_email) freemail = policy.is_freemail(domain) + if "+" in local_part and domain not in PLUS_ALIAS_ALLOWLIST_DOMAINS: + log.warning( + "[starter_credits_bridge] policy refused mint; skipping seed", + rule="plus_local_part", + domain=domain, + ) + return False + if ( not freemail + and domain not in PLUS_ALIAS_ALLOWLIST_DOMAINS and policy.block_digit_locals and any(character.isdigit() for character in local_part) ): diff --git a/api/ee/src/core/starter_credits_bridge/types.py b/api/ee/src/core/starter_credits_bridge/types.py index 7fdf774fc6d..81d66598c44 100644 --- a/api/ee/src/core/starter_credits_bridge/types.py +++ b/api/ee/src/core/starter_credits_bridge/types.py @@ -88,6 +88,10 @@ "bol.com.br", ) +# Plus-tags (`jane+1@gmail.com`) are one inbox and many grant-eligible strings. +# Internal testers who need a plus tag use this domain. +PLUS_ALIAS_ALLOWLIST_DOMAINS: tuple[str, ...] = ("agenta.ai",) + class MintPolicy(BaseModel): """Mint policy: velocity caps, domain classification, eligibility rules, and diff --git a/api/ee/tests/pytest/unit/test_starter_credits_bridge_seeding.py b/api/ee/tests/pytest/unit/test_starter_credits_bridge_seeding.py index b53e912464a..c3e2052542b 100644 --- a/api/ee/tests/pytest/unit/test_starter_credits_bridge_seeding.py +++ b/api/ee/tests/pytest/unit/test_starter_credits_bridge_seeding.py @@ -989,6 +989,28 @@ async def test_digit_rule_can_be_disabled_by_policy(self): assert await service._mint_policy_allows("john99@acme.test", policy) is True + @pytest.mark.parametrize( + "address", + ["person+trial@freemail.test", "person+trial@acme.test"], + ) + async def test_plus_local_part_refuses_before_any_counter(self, address): + assert await service._mint_policy_allows(address, _policy()) is False + assert self.engine.counts == {} + + async def test_plus_local_part_allows_an_address_without_a_plus(self): + assert ( + await service._mint_policy_allows("person@freemail.test", _policy()) is True + ) + + @pytest.mark.parametrize( + "address", + ["person+trial@agenta.ai", "person+1@Agenta.AI"], + ) + async def test_plus_local_part_allows_the_agenta_domain(self, address): + policy = _policy(work_domain_daily=10) + + assert await service._mint_policy_allows(address, policy) is True + async def test_global_hourly_cap_blocks(self): policy = _policy(global_hourly=2) @@ -1108,6 +1130,20 @@ async def test_a_digit_local_refusal_names_the_rule_and_the_domain(self): assert fields["domain"] == "acme.test" assert "john99" not in repr((message, fields)) + async def test_a_plus_local_refusal_names_only_the_rule_and_domain(self): + assert ( + await service._mint_policy_allows( + "person+private@freemail.test", + _policy(), + ) + is False + ) + + message, fields = self.records[-1] + assert fields == {"rule": "plus_local_part", "domain": "freemail.test"} + assert "person" not in repr((message, fields)) + assert "private" not in repr((message, fields)) + async def test_a_velocity_refusal_names_the_rule_and_the_domain(self): policy = _policy(work_domain_daily=1)