From fa0f611993cf973454894e2ba57cd196d1498f78 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 15:57:43 +0300 Subject: [PATCH 1/4] =?UTF-8?q?chore(types):=20regenerate=20hub=20types=20?= =?UTF-8?q?from=20gateway=20refactoringV2=20509156d57=20(access,=20routes?= =?UTF-8?q?=2025=E2=80=9327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the access matrix, role package and custom policy types (GetMarketplaceIntegrationAccess*, SetMarketplaceIntegrationRoleAccess, SaveMarketplaceIntegrationCustomAccess*, MarketplaceAccess*Dto, MarketplaceAccessLevel) and the additive fields TestMarketplaceIntegrationResponse.accessChanges, MarketplaceAccessRoleDto.defaultNotApplied and MarketplaceValidationStepResultDto.errorCode. Co-Authored-By: Claude Opus 5.5 --- references/hub_dtos.py | 182 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/references/hub_dtos.py b/references/hub_dtos.py index 01def68..630e932 100644 --- a/references/hub_dtos.py +++ b/references/hub_dtos.py @@ -1,5 +1,5 @@ """ Options: -Date: 2026-10-13 09:01:23 +Date: 2026-10-13 12:57:22 Version: 10.20 Tip: To override a DTO option, remove "#" prefix before updating BaseUrl: http://localhost:5001 @@ -2797,6 +2797,7 @@ class MarketplaceValidationStepResultDto: succeeded: bool = False skipped: bool = False message: Optional[str] = None + error_code: Optional[str] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -3951,6 +3952,15 @@ class SmsIntegrationRequest: is_enabled: bool = False +class MarketplaceAccessLevel(str, Enum): + NONE = 'None' + VIEWER = 'Viewer' + FUNCTIONS_ONLY = 'FunctionsOnly' + READ_ONLY = 'ReadOnly' + FULL_ACCESS = 'FullAccess' + MANAGE = 'Manage' + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class SavePushTemplate(CodeMashRequestBase): @@ -5928,6 +5938,54 @@ class MarketplaceListingProjection(IHasViewId): video_url: Optional[str] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class MarketplaceAccessCustomPolicyDto: + policy_id: Optional[str] = None + name: Optional[str] = None + used_by_roles: int = 0 + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class MarketplaceAccessRoleDto: + role_id: Optional[str] = None + name: Optional[str] = None + scope: Optional[str] = None + is_system: bool = False + fixed: bool = False + package: Optional[MarketplaceAccessLevel] = None + is_custom: bool = False + manage: bool = False + custom: Optional[IReadOnlyList[MarketplaceAccessCustomPolicyDto]] = None + default_not_applied: Optional[str] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class MarketplaceAccessPackageDto: + kind: Optional[MarketplaceAccessLevel] = None + policy_id: Optional[str] = None + name: Optional[str] = None + exists: bool = False + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class MarketplaceAccessToolDto: + name: Optional[str] = None + read_only: bool = False + grantable: bool = False + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class MarketplaceAccessFunctionDto: + function_id: Optional[str] = None + name: Optional[str] = None + function_key: Optional[str] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class MarketplaceIntegrationListProjection(IHasViewId): @@ -5956,6 +6014,14 @@ class MarketplaceMcpToolDto: destructive_hint: Optional[bool] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class MarketplaceAccessChangesDto: + read_only_added: Optional[IReadOnlyList[str]] = None + read_only_removed: Optional[IReadOnlyList[str]] = None + not_grantable: Optional[IReadOnlyList[str]] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class MarketplaceFunctionProjection(IHasViewId): @@ -8050,6 +8116,23 @@ class GetMarketplaceListingsResponse(ResponseBase): list: Optional[PaginatedResponse[MarketplaceListingProjection]] = None +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetMarketplaceIntegrationAccessResponse(ResponseBase): + roles: Optional[IReadOnlyList[MarketplaceAccessRoleDto]] = None + packages: Optional[IReadOnlyList[MarketplaceAccessPackageDto]] = None + tools: Optional[IReadOnlyList[MarketplaceAccessToolDto]] = None + functions: Optional[IReadOnlyList[MarketplaceAccessFunctionDto]] = None + + +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SaveMarketplaceIntegrationCustomAccessResponse(ResponseBase): + policy_id: Optional[str] = None + created: bool = False + used_by_roles: int = 0 + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class GetMarketplaceIntegrationResponse(ResponseBase): @@ -8088,6 +8171,7 @@ class RevealMarketplaceIntegrationSecretsResponse(ResponseBase): class TestMarketplaceIntegrationResponse(ResponseBase): items: Optional[IReadOnlyList[IntegrationTestResultItemDto]] = None steps: Optional[IReadOnlyList[MarketplaceValidationStepResultDto]] = None + access_changes: Optional[MarketplaceAccessChangesDto] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -15332,6 +15416,102 @@ class GetSmsCampaignMessagesRequest(CodeMashListPaginationRequestBase, IReturn[G """ +# @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/access", "GET") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class GetMarketplaceIntegrationAccess(CodeMashRequestBase, IReturn[GetMarketplaceIntegrationAccessResponse]): + # @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + integration_view_id: Optional[str] = None + """ + Integration view id, from get_marketplace_integrations. + """ + + +# @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles", "PUT") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SetMarketplaceIntegrationRoleAccess(CodeMashRequestBase, IReturn[IdResponse]): + # @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + integration_view_id: Optional[str] = None + """ + Integration view id, from get_marketplace_integrations. + """ + + + # @ApiMember(Description="The role's composite id: acc_…_nr_… (account / team role) or pr_…_nr_… (project role).", IsRequired=true) + role_id: Optional[str] = None + """ + The role's composite id: acc_…_nr_… (account / team role) or pr_…_nr_… (project role). + """ + + + # @ApiMember(Description="None, Viewer, FunctionsOnly, ReadOnly or FullAccess.") + package: Optional[MarketplaceAccessLevel] = None + """ + None, Viewer, FunctionsOnly, ReadOnly or FullAccess. + """ + + + # @ApiMember(Description="True to also give the Manage package (edit the connection, its secrets, validation and functions).") + manage: bool = False + """ + True to also give the Manage package (edit the connection, its secrets, validation and functions). + """ + + +# @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom", "POST") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class SaveMarketplaceIntegrationCustomAccess(CodeMashRequestBase, IReturn[SaveMarketplaceIntegrationCustomAccessResponse]): + # @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + integration_view_id: Optional[str] = None + """ + Integration view id, from get_marketplace_integrations. + """ + + + # @ApiMember(Description="The role's composite id: acc_…_nr_… or pr_…_nr_….", IsRequired=true) + role_id: Optional[str] = None + """ + The role's composite id: acc_…_nr_… or pr_…_nr_…. + """ + + + # @ApiMember(Description="The package to start from: None, Viewer, FunctionsOnly, ReadOnly or FullAccess.") + base_package: Optional[MarketplaceAccessLevel] = None + """ + The package to start from: None, Viewer, FunctionsOnly, ReadOnly or FullAccess. + """ + + + # @ApiMember(Description="Tool names the role may run (from the connection's tools).") + tools: List[str] = field(default_factory=list) + """ + Tool names the role may run (from the connection's tools). + """ + + + # @ApiMember(Description="Function ids (func_…) of this connection the role may run.") + functions: List[str] = field(default_factory=list) + """ + Function ids (func_…) of this connection the role may run. + """ + + + # @ApiMember(Description="True to include the Manage rights (edit the connection, its secrets, validation and functions).") + manage: bool = False + """ + True to include the Manage rights (edit the connection, its secrets, validation and functions). + """ + + + # @ApiMember(Description="True to write a new policy for this role only when its current custom policy is shared with other roles.") + new_policy_for_this_role_only: bool = False + """ + True to write a new policy for this role only when its current custom policy is shared with other roles. + """ + + # @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/tools", "GET") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass From d6db1efe86a04123fd7f65e948b4036ef85d0264 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 16:03:53 +0300 Subject: [PATCH 2/4] =?UTF-8?q?feat(marketplace):=20connection=20access=20?= =?UTF-8?q?matrix,=20role=20package=20and=20custom=20policy=20(routes=2025?= =?UTF-8?q?=E2=80=9327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync and async CodeModule gain get_marketplace_integration_access (GET .../access), set_marketplace_integration_role_access (PUT .../access/roles) and save_marketplace_integration_custom_access (POST .../access/roles/custom). The marketplace route table gets their cases (24 -> 27, run on the sync and the async client: verb, path with the id, no body on the GET, the role / ticks in the PUT / POST body) and the docs table its rows. Co-Authored-By: Claude Opus 5.5 --- docs/hub/code.md | 5 ++- src/norbix_python/hub/code.py | 78 +++++++++++++++++++++++++++++++++++ tests/hub/test_code.py | 9 ++-- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/docs/hub/code.md b/docs/hub/code.md index 871574f..1f324e3 100644 --- a/docs/hub/code.md +++ b/docs/hub/code.md @@ -29,7 +29,7 @@ Integration ids look like `int_…`. ## Marketplace -The 24 routes of the gateway contract `Code.Marketplace.md` §14: the catalog +The 27 routes of the gateway contract `Code.Marketplace.md` §14: the catalog of listings, the installed connections (integration ids look like `int_…`), their secrets and token mappings, and the functions added on a connection (`func_…`). Every method, its verb, path, query and body are asserted in @@ -56,6 +56,9 @@ their secrets and token mappings, and the functions added on a connection | `get_marketplace_listing` | `GET` | `/{version}/code/marketplace/listings/{ListingViewId}` | `project` | path: `listing_view_id` | | `test_marketplace_integration` | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/test` | `project` | path: `integration_view_id` | | `get_marketplace_integration_tools` | `GET` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/tools` | `project` | path: `integration_view_id`; returns the stored MCP tool catalog (`tools`, `listedAt`, `catalogHash`, `truncated`) | +| `get_marketplace_integration_access` | `GET` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access` | `project` | path: `integration_view_id`; returns the access matrix (`roles`, `packages`, `tools`, `functions`) | +| `set_marketplace_integration_role_access` | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles` | `project` | path: `integration_view_id`; JSON body: `roleId`, `package` (`None`, `Viewer`, `FunctionsOnly`, `ReadOnly`, `FullAccess`), `manage`; returns the role id | +| `save_marketplace_integration_custom_access` | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom` | `project` | path: `integration_view_id`; JSON body: `roleId`, `basePackage`, `tools`, `functions`, `manage`, `newPolicyForThisRoleOnly`; returns `policyId`, `created`, `usedByRoles` | | `replace_marketplace_integration_secrets` | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets` | `project` | path: `integration_view_id`; JSON body: `secrets` | | `reveal_marketplace_integration_secrets` | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets/reveal` | `project` | path: `integration_view_id` | | `set_marketplace_integration_token_mappings` | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/token-mappings` | `project` | path: `integration_view_id`; JSON body: `tokenMappings` | diff --git a/src/norbix_python/hub/code.py b/src/norbix_python/hub/code.py index 9e5cc95..5b202b2 100644 --- a/src/norbix_python/hub/code.py +++ b/src/norbix_python/hub/code.py @@ -386,6 +386,45 @@ def get_marketplace_integration_tools(self, integration_view_id: str, *, timeout bearer_token=bearer_token, ) + def get_marketplace_integration_access(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/marketplace/integrations/{IntegrationViewId}/access — the access matrix of a connection: roles, packages, tools and functions.""" + return self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/access", + method="GET", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def set_marketplace_integration_role_access(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles — give a role one package (None, Viewer, FunctionsOnly, ReadOnly, FullAccess) and the Manage flag; body `roleId`, `package`, `manage`.""" + return self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles", + method="PUT", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def save_marketplace_integration_custom_access(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom — a custom access policy of the connection for one role; body `roleId`, `basePackage`, `tools`, `functions`, `manage`, `newPolicyForThisRoleOnly`.""" + return self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom", + method="POST", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + def replace_marketplace_integration_secrets(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: """PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets""" return self._transport.send( @@ -898,6 +937,45 @@ async def get_marketplace_integration_tools(self, integration_view_id: str, *, t bearer_token=bearer_token, ) + async def get_marketplace_integration_access(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/code/marketplace/integrations/{IntegrationViewId}/access — the access matrix of a connection: roles, packages, tools and functions.""" + return await self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/access", + method="GET", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def set_marketplace_integration_role_access(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles — give a role one package (None, Viewer, FunctionsOnly, ReadOnly, FullAccess) and the Manage flag; body `roleId`, `package`, `manage`.""" + return await self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles", + method="PUT", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def save_marketplace_integration_custom_access(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom — a custom access policy of the connection for one role; body `roleId`, `basePackage`, `tools`, `functions`, `manage`, `newPolicyForThisRoleOnly`.""" + return await self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom", + method="POST", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + async def replace_marketplace_integration_secrets(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: """PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets""" return await self._transport.send( diff --git a/tests/hub/test_code.py b/tests/hub/test_code.py index 4326c62..6efc8df 100644 --- a/tests/hub/test_code.py +++ b/tests/hub/test_code.py @@ -291,6 +291,9 @@ def test_invoke_code_function_sends_the_function_ref_in_the_body() -> None: ("get_marketplace_listing", "GET", "/v3/code/marketplace/listings/ml_slack_v1", None, None, lambda m: m.get_marketplace_listing("ml_slack_v1")), ("test_marketplace_integration", "POST", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/test", None, None, lambda m: m.test_marketplace_integration("int_6Qp1dW2kRm8NsT4vYc0Hb")), ("get_marketplace_integration_tools", "GET", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/tools", None, None, lambda m: m.get_marketplace_integration_tools("int_6Qp1dW2kRm8NsT4vYc0Hb")), + ("get_marketplace_integration_access", "GET", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/access", None, None, lambda m: m.get_marketplace_integration_access("int_6Qp1dW2kRm8NsT4vYc0Hb")), + ("set_marketplace_integration_role_access", "PUT", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/access/roles", None, {'roleId': 'acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', 'package': 'ReadOnly', 'manage': False}, lambda m: m.set_marketplace_integration_role_access("int_6Qp1dW2kRm8NsT4vYc0Hb", roleId='acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', package='ReadOnly', manage=False)), + ("save_marketplace_integration_custom_access", "POST", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/access/roles/custom", None, {'roleId': 'acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', 'basePackage': 'ReadOnly', 'tools': ['create_issue'], 'functions': ['func_4hT2b9QkLm3XcT1yRfGh7a'], 'manage': False, 'newPolicyForThisRoleOnly': True}, lambda m: m.save_marketplace_integration_custom_access("int_6Qp1dW2kRm8NsT4vYc0Hb", roleId='acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', basePackage='ReadOnly', tools=['create_issue'], functions=['func_4hT2b9QkLm3XcT1yRfGh7a'], manage=False, newPolicyForThisRoleOnly=True)), ("replace_marketplace_integration_secrets", "PUT", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/secrets", None, {'secrets': {'botToken': 'xoxb-new'}}, lambda m: m.replace_marketplace_integration_secrets("int_6Qp1dW2kRm8NsT4vYc0Hb", secrets={'botToken': 'xoxb-new'})), ("reveal_marketplace_integration_secrets", "POST", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/secrets/reveal", None, None, lambda m: m.reveal_marketplace_integration_secrets("int_6Qp1dW2kRm8NsT4vYc0Hb")), ("set_marketplace_integration_token_mappings", "PUT", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/token-mappings", None, {'tokenMappings': [{'key': 'channel', 'value': '#orders', 'resolver': 'Custom'}]}, lambda m: m.set_marketplace_integration_token_mappings("int_6Qp1dW2kRm8NsT4vYc0Hb", tokenMappings=[{'key': 'channel', 'value': '#orders', 'resolver': 'Custom'}])), @@ -351,9 +354,9 @@ async def run() -> None: def test_marketplace_surface_size() -> None: - """24 marketplace routes in the gateway (Code.Marketplace.md §14; 24 = MCP tool catalog). A new one changes this count.""" - assert len(MARKETPLACE_CASES) == 24 - assert len({case[0] for case in MARKETPLACE_CASES}) == 24 + """27 marketplace routes in the gateway (Code.Marketplace.md §14; 24 = MCP tool catalog, 25–27 = connection access). A new one changes this count.""" + assert len(MARKETPLACE_CASES) == 27 + assert len({case[0] for case in MARKETPLACE_CASES}) == 27 client, _ = make_client() for name, *_ in MARKETPLACE_CASES: assert callable(getattr(client.hub.code, name)), name From c7c7075984c1bb38a3aeec1ef6975bb889107e80 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 16:23:50 +0300 Subject: [PATCH 3/4] chore(types): regenerate hub types from gateway refactoringV2 515275038 (oauth, route 28) Adds StartMarketplaceIntegrationSignIn / StartMarketplaceIntegrationSignInResponse (authorizeUrl, redirectUrl, expiresAt) and the additive fields MarketplaceIntegrationDto.signedIn / signedInBy / signedInAt / tokenExpiresAt, MarketplaceIntegrationListProjection.signedIn / signedInAt and MarketplaceListingDto.connectSteps / oauthRedirectUrl. Route 29 (the OAuth callback) is not in the type metadata. Co-Authored-By: Claude Opus 5.5 --- references/hub_dtos.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/references/hub_dtos.py b/references/hub_dtos.py index 630e932..5bf9a89 100644 --- a/references/hub_dtos.py +++ b/references/hub_dtos.py @@ -1,5 +1,5 @@ """ Options: -Date: 2026-10-13 12:57:22 +Date: 2026-10-13 13:23:39 Version: 10.20 Tip: To override a DTO option, remove "#" prefix before updating BaseUrl: http://localhost:5001 @@ -2810,6 +2810,10 @@ class MarketplaceIntegrationDto(IntegrationDto): description: Optional[str] = None config: Optional[IReadOnlyDictionary[str, str]] = None last_validation_steps: Optional[List[MarketplaceValidationStepResultDto]] = None + signed_in: bool = False + signed_in_by: Optional[str] = None + signed_in_at: Optional[datetime.datetime] = None + token_expires_at: Optional[datetime.datetime] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -2955,6 +2959,8 @@ class MarketplaceListingDto(IHasViewId): documentation_url: Optional[str] = None video_url: Optional[str] = None availability: Optional[MarketplaceAvailability] = None + connect_steps: Optional[IReadOnlyList[str]] = None + o_auth_redirect_url: Optional[str] = None is_official: bool = False tags: Optional[IReadOnlyList[str]] = None spec_version: int = 0 @@ -6001,6 +6007,8 @@ class MarketplaceIntegrationListProjection(IHasViewId): last_integration_test_errors: Optional[IReadOnlyList[str]] = None human_delivery_confirmed_at_utc: Optional[datetime.datetime] = None requires_human_delivery_confirmation: bool = False + signed_in: bool = False + signed_in_at: Optional[datetime.datetime] = None @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @@ -8154,6 +8162,14 @@ class GetMarketplaceIntegrationToolsResponse(ResponseBase): truncated: bool = False +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class StartMarketplaceIntegrationSignInResponse(ResponseBase): + authorize_url: Optional[str] = None + redirect_url: Optional[str] = None + expires_at: Optional[datetime.datetime] = None + + @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass class EmptyMarketplaceSecretsResponse(ResponseBase): @@ -15523,6 +15539,17 @@ class GetMarketplaceIntegrationTools(CodeMashRequestBase, IReturn[GetMarketplace """ +# @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start", "POST") +@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) +@dataclass +class StartMarketplaceIntegrationSignIn(CodeMashRequestBase, IReturn[StartMarketplaceIntegrationSignInResponse]): + # @ApiMember(Description="Integration view id, from get_marketplace_integrations.", IsRequired=true) + integration_view_id: Optional[str] = None + """ + Integration view id, from get_marketplace_integrations. + """ + + # @Route("/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets", "PUT") @dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE) @dataclass From 95a39a53b444a98bc42fca2bee9c64ee7c35f321 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Tue, 13 Oct 2026 16:26:26 +0300 Subject: [PATCH 4/4] feat(marketplace): start_marketplace_integration_sign_in, the OAuth sign-in start (route 28) Sync and async CodeModule gain start_marketplace_integration_sign_in (POST .../integrations/{IntegrationViewId}/oauth/start); the marketplace route table gets its case (27 -> 28, run on both clients) and the docs table its row. Route 29 (the callback) is not in the type metadata. Co-Authored-By: Claude Opus 5.5 --- docs/hub/code.md | 3 ++- src/norbix_python/hub/code.py | 26 ++++++++++++++++++++++++++ tests/hub/test_code.py | 7 ++++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/hub/code.md b/docs/hub/code.md index 1f324e3..0a5d3c7 100644 --- a/docs/hub/code.md +++ b/docs/hub/code.md @@ -29,7 +29,7 @@ Integration ids look like `int_…`. ## Marketplace -The 27 routes of the gateway contract `Code.Marketplace.md` §14: the catalog +The 28 routes of the gateway contract `Code.Marketplace.md` §14: the catalog of listings, the installed connections (integration ids look like `int_…`), their secrets and token mappings, and the functions added on a connection (`func_…`). Every method, its verb, path, query and body are asserted in @@ -59,6 +59,7 @@ their secrets and token mappings, and the functions added on a connection | `get_marketplace_integration_access` | `GET` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access` | `project` | path: `integration_view_id`; returns the access matrix (`roles`, `packages`, `tools`, `functions`) | | `set_marketplace_integration_role_access` | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles` | `project` | path: `integration_view_id`; JSON body: `roleId`, `package` (`None`, `Viewer`, `FunctionsOnly`, `ReadOnly`, `FullAccess`), `manage`; returns the role id | | `save_marketplace_integration_custom_access` | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/access/roles/custom` | `project` | path: `integration_view_id`; JSON body: `roleId`, `basePackage`, `tools`, `functions`, `manage`, `newPolicyForThisRoleOnly`; returns `policyId`, `created`, `usedByRoles` | +| `start_marketplace_integration_sign_in` | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start` | `project` | path: `integration_view_id`; returns `authorizeUrl` (open it in a browser), `redirectUrl`, `expiresAt` | | `replace_marketplace_integration_secrets` | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets` | `project` | path: `integration_view_id`; JSON body: `secrets` | | `reveal_marketplace_integration_secrets` | `POST` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets/reveal` | `project` | path: `integration_view_id` | | `set_marketplace_integration_token_mappings` | `PUT` | `/{version}/code/marketplace/integrations/{IntegrationViewId}/token-mappings` | `project` | path: `integration_view_id`; JSON body: `tokenMappings` | diff --git a/src/norbix_python/hub/code.py b/src/norbix_python/hub/code.py index 5b202b2..9b06b2f 100644 --- a/src/norbix_python/hub/code.py +++ b/src/norbix_python/hub/code.py @@ -425,6 +425,19 @@ def save_marketplace_integration_custom_access(self, integration_view_id: str, * bearer_token=bearer_token, ) + def start_marketplace_integration_sign_in(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start — start the OAuth sign-in of an MCP connection; returns `authorizeUrl` (open it in a browser), `redirectUrl`, `expiresAt`.""" + return self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start", + method="POST", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + def replace_marketplace_integration_secrets(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: """PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets""" return self._transport.send( @@ -976,6 +989,19 @@ async def save_marketplace_integration_custom_access(self, integration_view_id: bearer_token=bearer_token, ) + async def start_marketplace_integration_sign_in(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start — start the OAuth sign-in of an MCP connection; returns `authorizeUrl` (open it in a browser), `redirectUrl`, `expiresAt`.""" + return await self._transport.send( + target="hub", + path="/{version}/code/marketplace/integrations/{IntegrationViewId}/oauth/start", + method="POST", + path_params={"IntegrationViewId": integration_view_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + async def replace_marketplace_integration_secrets(self, integration_view_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: """PUT /{version}/code/marketplace/integrations/{IntegrationViewId}/secrets""" return await self._transport.send( diff --git a/tests/hub/test_code.py b/tests/hub/test_code.py index 6efc8df..6fed20a 100644 --- a/tests/hub/test_code.py +++ b/tests/hub/test_code.py @@ -294,6 +294,7 @@ def test_invoke_code_function_sends_the_function_ref_in_the_body() -> None: ("get_marketplace_integration_access", "GET", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/access", None, None, lambda m: m.get_marketplace_integration_access("int_6Qp1dW2kRm8NsT4vYc0Hb")), ("set_marketplace_integration_role_access", "PUT", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/access/roles", None, {'roleId': 'acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', 'package': 'ReadOnly', 'manage': False}, lambda m: m.set_marketplace_integration_role_access("int_6Qp1dW2kRm8NsT4vYc0Hb", roleId='acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', package='ReadOnly', manage=False)), ("save_marketplace_integration_custom_access", "POST", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/access/roles/custom", None, {'roleId': 'acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', 'basePackage': 'ReadOnly', 'tools': ['create_issue'], 'functions': ['func_4hT2b9QkLm3XcT1yRfGh7a'], 'manage': False, 'newPolicyForThisRoleOnly': True}, lambda m: m.save_marketplace_integration_custom_access("int_6Qp1dW2kRm8NsT4vYc0Hb", roleId='acc_6Qp1dW2kRm8NsT4vYc0Hb_nr_3kLm9QwErTyU', basePackage='ReadOnly', tools=['create_issue'], functions=['func_4hT2b9QkLm3XcT1yRfGh7a'], manage=False, newPolicyForThisRoleOnly=True)), + ("start_marketplace_integration_sign_in", "POST", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/oauth/start", None, None, lambda m: m.start_marketplace_integration_sign_in("int_6Qp1dW2kRm8NsT4vYc0Hb")), ("replace_marketplace_integration_secrets", "PUT", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/secrets", None, {'secrets': {'botToken': 'xoxb-new'}}, lambda m: m.replace_marketplace_integration_secrets("int_6Qp1dW2kRm8NsT4vYc0Hb", secrets={'botToken': 'xoxb-new'})), ("reveal_marketplace_integration_secrets", "POST", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/secrets/reveal", None, None, lambda m: m.reveal_marketplace_integration_secrets("int_6Qp1dW2kRm8NsT4vYc0Hb")), ("set_marketplace_integration_token_mappings", "PUT", "/v3/code/marketplace/integrations/int_6Qp1dW2kRm8NsT4vYc0Hb/token-mappings", None, {'tokenMappings': [{'key': 'channel', 'value': '#orders', 'resolver': 'Custom'}]}, lambda m: m.set_marketplace_integration_token_mappings("int_6Qp1dW2kRm8NsT4vYc0Hb", tokenMappings=[{'key': 'channel', 'value': '#orders', 'resolver': 'Custom'}])), @@ -354,9 +355,9 @@ async def run() -> None: def test_marketplace_surface_size() -> None: - """27 marketplace routes in the gateway (Code.Marketplace.md §14; 24 = MCP tool catalog, 25–27 = connection access). A new one changes this count.""" - assert len(MARKETPLACE_CASES) == 27 - assert len({case[0] for case in MARKETPLACE_CASES}) == 27 + """28 marketplace routes in the gateway (Code.Marketplace.md §14; 24 = MCP tool catalog, 25–27 = connection access, 28 = OAuth sign-in start; 29, the callback, is not in the metadata). A new one changes this count.""" + assert len(MARKETPLACE_CASES) == 28 + assert len({case[0] for case in MARKETPLACE_CASES}) == 28 client, _ = make_client() for name, *_ in MARKETPLACE_CASES: assert callable(getattr(client.hub.code, name)), name