diff --git a/api/controllers/console/workspace/network_access_group.py b/api/controllers/console/workspace/network_access_group.py index 19204fa0105..1d8626904b1 100644 --- a/api/controllers/console/workspace/network_access_group.py +++ b/api/controllers/console/workspace/network_access_group.py @@ -123,6 +123,10 @@ class NetworkAccessGroupDeleteResponse(ResponseModel): deleted: bool +class NetworkAccessGroupCurrentIPResponse(ResponseModel): + client_ip: str + + class NetworkAccessGroupCurrentIPCheckResponse(ResponseModel): client_ip: str allowed: bool @@ -189,6 +193,7 @@ register_response_schema_models( NetworkAccessGroupListResponse, NetworkAccessGroupMutationResponse, NetworkAccessGroupDeleteResponse, + NetworkAccessGroupCurrentIPResponse, NetworkAccessGroupCurrentIPCheckResponse, AppNetworkAccessGroupBindingResponse, AppNetworkAccessGroupResponse, @@ -366,6 +371,34 @@ class CurrentWorkspaceNetworkAccessGroupApi(Resource): return _serialize_response(NetworkAccessGroupDeleteResponse, payload) +@console_ns.route("/workspaces/current/network-access-groups/current-ip") +class CurrentWorkspaceNetworkAccessGroupCurrentIPApi(Resource): + @console_ns.response( + 200, + "Trusted current client IP retrieved without requiring an existing network access group", + console_ns.models[NetworkAccessGroupCurrentIPResponse.__name__], + ) + @console_ns.response(403, "The workspace role does not allow network access policy reads") + @console_ns.response(503, "A trusted current client IP is unavailable") + @console_account_admission(editions=frozenset({DeploymentEdition.CLOUD})) + def get(self, request_context: RequestContext) -> dict[str, Any]: + @after_this_request + def disable_client_ip_response_cache(response: Response) -> Response: + response.headers["Cache-Control"] = "no-store" + return response + + try: + payload = application_services().network_access_groups.get_current_ip( + request_context, + client_ip_supplier=_resolve_current_network_access_client_ip, + ) + except NetworkAccessClientIPUnavailableError as exc: + raise ServiceUnavailable("Current client IP is unavailable.") from exc + except NetworkAccessGroupError as exc: + raise _translate_service_error(exc) from exc + return _serialize_response(NetworkAccessGroupCurrentIPResponse, payload) + + @console_ns.route("/workspaces/current/network-access-groups//check-current-ip") class CurrentWorkspaceNetworkAccessGroupCurrentIPCheckApi(Resource): @console_ns.response( diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index f629458521d..4e1c7321a1f 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -10771,6 +10771,15 @@ Update a plugin endpoint | ---- | ----------- | ------ | | 201 | Workspace network access group created successfully | **application/json**: [NetworkAccessGroupMutationResponse](#networkaccessgroupmutationresponse)
| +### [GET] /workspaces/current/network-access-groups/current-ip +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Trusted current client IP retrieved without requiring an existing network access group | **application/json**: [NetworkAccessGroupCurrentIPResponse](#networkaccessgroupcurrentipresponse)
| +| 403 | The workspace role does not allow network access policy reads | | +| 503 | A trusted current client IP is unavailable | | + ### [DELETE] /workspaces/current/network-access-groups/{group_id} #### Parameters @@ -19987,6 +19996,12 @@ Model with provider entity. | client_ip | string | | Yes | | policy_version | integer | | Yes | +#### NetworkAccessGroupCurrentIPResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| client_ip | string | | Yes | + #### NetworkAccessGroupDeleteResponse | Name | Type | Description | Required | diff --git a/api/services/network_access_group_service.py b/api/services/network_access_group_service.py index d692b90d779..9005604fb68 100644 --- a/api/services/network_access_group_service.py +++ b/api/services/network_access_group_service.py @@ -287,6 +287,23 @@ class NetworkAccessGroupService: self._finalize_effective_enabled(payload) return payload + def get_current_ip( + self, + context: RequestContext, + *, + client_ip_supplier: Callable[[], str], + ) -> dict[str, str]: + """Return trusted request metadata without requiring a saved policy. + + Like policy reads and the existing IP preflight, this read-only helper + authorizes the persisted workspace role before resolving the request IP. + It does not create a policy, enable enforcement, or require a paid plan; + policy mutations keep their separate paid-plan admission. + """ + + self._ensure_workspace_role(context, _POLICY_READ_ROLES) + return {"client_ip": client_ip_supplier()} + def check_current_ip( self, context: RequestContext, diff --git a/api/tests/unit_tests/controllers/console/workspace/test_network_access_group.py b/api/tests/unit_tests/controllers/console/workspace/test_network_access_group.py index 436ea98740f..dae277dcdb1 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_network_access_group.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_network_access_group.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch from uuid import UUID import pytest -from flask import Flask +from flask import Flask, Response from pydantic import ValidationError from werkzeug.exceptions import BadGateway, BadRequest, Conflict, Forbidden, HTTPException, NotFound, ServiceUnavailable @@ -16,15 +16,18 @@ from controllers.console.workspace.network_access_group import ( AppNetworkAccessGroupApi, AppNetworkAccessGroupUpdatePayload, CurrentWorkspaceNetworkAccessGroupApi, + CurrentWorkspaceNetworkAccessGroupCurrentIPApi, CurrentWorkspaceNetworkAccessGroupCurrentIPCheckApi, CurrentWorkspaceNetworkAccessGroupsApi, NetworkAccessGroupCreatePayload, + NetworkAccessGroupCurrentIPResponse, NetworkAccessGroupDeleteQuery, NetworkAccessGroupUpdatePayload, _translate_service_error, _translate_upstream_error, ) from core.network_access.client_ip import NetworkAccessClientIPUnavailableError +from enums import DeploymentEdition from machinery.context import RequestContext from services.network_access_group_service import ( NetworkAccessGroupAccessDeniedError, @@ -497,3 +500,147 @@ def test_group_response_rejects_enforcing_count_greater_than_used_by_count() -> CurrentWorkspaceNetworkAccessGroupsApi(), request_context=_request_context(), ) + + +@pytest.mark.parametrize( + ("peer", "forwarded", "expected"), + [ + ("172.18.0.3", "203.0.113.7", "203.0.113.7"), + ("172.18.0.3", "2001:db8::42", "2001:db8::42"), + ("172.18.0.3", "::ffff:203.0.113.7", "203.0.113.7"), + ("203.0.113.7", "192.0.2.1", "203.0.113.7"), + ("172.18.0.3", "192.0.2.1, 203.0.113.7, 172.18.0.4", "203.0.113.7"), + ], + ids=["ipv4", "ipv6", "mapped-ipv6", "untrusted-peer-spoof", "trusted-proxy-appended-peer"], +) +def test_current_ip_read_uses_trusted_resolver_and_ignores_client_supplied_ip( + config_overrides: Callable[..., None], + peer: str, + forwarded: str, + expected: str, +) -> None: + app = Flask(__name__) + service = MagicMock() + config_overrides(NETWORK_ACCESS_TRUSTED_PROXY_CIDRS="172.18.0.0/16") + service.get_current_ip.side_effect = lambda _context, *, client_ip_supplier: {"client_ip": client_ip_supplier()} + api = CurrentWorkspaceNetworkAccessGroupCurrentIPApi() + + with ( + app.test_request_context( + "/workspaces/current/network-access-groups/current-ip?client_ip=192.0.2.1&group_id=ignored", + headers={ + "X-Forwarded-For": forwarded, + "CF-Connecting-IP": "192.0.2.1", + "X-Real-IP": "192.0.2.1", + "Forwarded": "for=192.0.2.1", + }, + json={"client_ip": "192.0.2.1"}, + environ_base={"REMOTE_ADDR": peer}, + ), + _application_services(service), + ): + result = unwrap(api.get)(api, request_context=_request_context()) + response = app.process_response(app.make_response(result)) + + assert result == {"client_ip": expected} + assert response.headers["Cache-Control"] == "no-store" + assert service.get_current_ip.call_args.args == (_request_context(),) + assert set(service.get_current_ip.call_args.kwargs) == {"client_ip_supplier"} + service.get_group.assert_not_called() + + +@pytest.mark.parametrize( + ("trusted_proxies", "forwarded"), + [ + ("", "203.0.113.7"), + ("not-a-cidr", "203.0.113.7"), + ("172.18.0.0/16", None), + ("172.18.0.0/16", "malformed"), + ("172.18.0.0/16", "172.18.0.4"), + ], + ids=["missing-proxy-config", "invalid-proxy-config", "missing-xff", "invalid-xff", "all-trusted-hops"], +) +def test_current_ip_read_fails_closed_with_no_store_on_503( + config_overrides: Callable[..., None], trusted_proxies: str, forwarded: str | None +) -> None: + app = Flask(__name__) + service = MagicMock() + config_overrides(NETWORK_ACCESS_TRUSTED_PROXY_CIDRS=trusted_proxies) + service.get_current_ip.side_effect = lambda _context, *, client_ip_supplier: {"client_ip": client_ip_supplier()} + api = CurrentWorkspaceNetworkAccessGroupCurrentIPApi() + + with ( + app.test_request_context( + headers={"X-Forwarded-For": forwarded} if forwarded is not None else {}, + environ_base={"REMOTE_ADDR": "172.18.0.3"}, + ), + _application_services(service), + ): + with pytest.raises(ServiceUnavailable, match="Current client IP is unavailable") as exc_info: + unwrap(api.get)(api, request_context=_request_context()) + response = app.process_response(app.make_response(exc_info.value.get_response())) + + assert response.status_code == 503 + assert response.headers["Cache-Control"] == "no-store" + + +def test_current_ip_read_maps_role_denial_without_resolving_ip() -> None: + app = Flask(__name__) + service = MagicMock() + service.get_current_ip.side_effect = NetworkAccessGroupAccessDeniedError + api = CurrentWorkspaceNetworkAccessGroupCurrentIPApi() + + with ( + app.test_request_context(), + _application_services(service), + patch( + "controllers.console.workspace.network_access_group.resolve_network_access_client_ip" + ) as resolve_client_ip, + ): + with pytest.raises(Forbidden, match="workspace role") as exc_info: + unwrap(api.get)(api, request_context=_request_context()) + response = app.process_response(app.make_response(exc_info.value.get_response())) + + resolve_client_ip.assert_not_called() + assert response.status_code == 403 + assert response.headers["Cache-Control"] == "no-store" + + +@pytest.mark.parametrize("edition", [DeploymentEdition.COMMUNITY, DeploymentEdition.ENTERPRISE]) +def test_current_ip_read_is_cloud_only(config_overrides: Callable[..., None], edition: DeploymentEdition) -> None: + app = Flask(__name__) + service = MagicMock() + config_overrides(DEPLOYMENT_EDITION=edition) + + with app.test_request_context(), _application_services(service), pytest.raises(NotFound): + CurrentWorkspaceNetworkAccessGroupCurrentIPApi().get() + + service.get_current_ip.assert_not_called() + + +def test_current_ip_read_requires_authenticated_account(config_overrides: Callable[..., None]) -> None: + app = Flask(__name__) + service = MagicMock() + config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD, LOGIN_DISABLED=False) + login_manager = MagicMock() + login_manager.unauthorized.return_value = Response(status=401) + + with ( + app.test_request_context(), + _application_services(service), + patch("libs.login._resolve_current_user", return_value=None), + patch("libs.login._get_login_manager", return_value=login_manager), + ): + response = CurrentWorkspaceNetworkAccessGroupCurrentIPApi().get() + + assert isinstance(response, Response) + assert response.status_code == 401 + service.get_current_ip.assert_not_called() + + +def test_current_ip_read_response_schema_exposes_only_client_ip() -> None: + schema = NetworkAccessGroupCurrentIPResponse.model_json_schema(mode="serialization") + + assert schema["required"] == ["client_ip"] + assert set(schema["properties"]) == {"client_ip"} + assert schema["properties"]["client_ip"]["type"] == "string" diff --git a/api/tests/unit_tests/services/test_network_access_group_service.py b/api/tests/unit_tests/services/test_network_access_group_service.py index 6b93bcf59e0..25fd8060b49 100644 --- a/api/tests/unit_tests/services/test_network_access_group_service.py +++ b/api/tests/unit_tests/services/test_network_access_group_service.py @@ -669,6 +669,43 @@ def test_current_ip_check_authorizes_and_loads_tenant_policy_before_reading_ip() assert result == {"client_ip": "2001:db8::42", "allowed": True, "policy_version": 7} +@pytest.mark.parametrize("role", ["owner", "admin", "editor"]) +@pytest.mark.parametrize("paid", [True, False], ids=["paid", "lapsed-or-free"]) +def test_current_ip_read_needs_no_policy_and_preserves_read_admission(role: str, paid: bool) -> None: + harness = _harness(role=role, paid=paid) + order: list[str] = [] + harness.memberships.get_role_for_account.side_effect = lambda **_kwargs: order.append("role") or role + + result = harness.service.get_current_ip( + _context(), + client_ip_supplier=lambda: order.append("ip") or "2001:db8::42", + ) + + assert result == {"client_ip": "2001:db8::42"} + assert order == ["role", "ip"] + harness.memberships.get_role_for_account.assert_called_once_with( + workspace_id=WORKSPACE_ID, + account_id=ACCOUNT_ID, + ) + assert harness.control_plane.method_calls == [] + assert harness.apps.method_calls == [] + harness.entitlement.is_paid_plan.assert_not_called() + + +@pytest.mark.parametrize("role", ["normal", "dataset_operator", "unknown", None]) +def test_current_ip_read_rejects_disallowed_persisted_roles_before_reading_ip(role: str | None) -> None: + harness = _harness(role=role) + supplier = MagicMock(return_value="203.0.113.7") + + with pytest.raises(NetworkAccessGroupAccessDeniedError): + harness.service.get_current_ip(_context(), client_ip_supplier=supplier) + + supplier.assert_not_called() + assert harness.control_plane.method_calls == [] + assert harness.apps.method_calls == [] + harness.entitlement.is_paid_plan.assert_not_called() + + def test_current_ip_check_rejects_role_before_policy_or_ip_lookup() -> None: harness = _harness(role="normal") supplier = MagicMock(return_value="203.0.113.7") diff --git a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts index 20779120424..17a483afb64 100644 --- a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts @@ -92,6 +92,7 @@ import { zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdCheckCurrentIpResponse, zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdPath, zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdResponse, + zGetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse, zGetWorkspacesCurrentNetworkAccessGroupsResponse, zGetWorkspacesCurrentPermissionResponse, zGetWorkspacesCurrentPluginAssetQuery, @@ -1739,6 +1740,20 @@ export const models2 = { } export const get22 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentNetworkAccessGroupsCurrentIp', + path: '/workspaces/current/network-access-groups/current-ip', + tags: ['console'], + }) + .output(zGetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse) + +export const currentIp = { + get: get22, +} + +export const get23 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1750,7 +1765,7 @@ export const get22 = oc .output(zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdCheckCurrentIpResponse) export const checkCurrentIp = { - get: get22, + get: get23, } export const delete8 = oc @@ -1769,7 +1784,7 @@ export const delete8 = oc ) .output(zDeleteWorkspacesCurrentNetworkAccessGroupsByGroupIdResponse) -export const get23 = oc +export const get24 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1798,12 +1813,12 @@ export const put5 = oc export const byGroupId = { delete: delete8, - get: get23, + get: get24, put: put5, checkCurrentIp, } -export const get24 = oc +export const get25 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1826,8 +1841,9 @@ export const post26 = oc .output(zPostWorkspacesCurrentNetworkAccessGroupsResponse) export const networkAccessGroups = { - get: get24, + get: get25, post: post26, + currentIp, byGroupId, } @@ -1836,7 +1852,7 @@ export const networkAccessGroups = { * * Returns permission flags that control workspace features like member invitations and owner transfer. */ -export const get25 = oc +export const get26 = oc .route({ description: 'Returns permission flags that control workspace features like member invitations and owner transfer.', @@ -1850,10 +1866,10 @@ export const get25 = oc .output(zGetWorkspacesCurrentPermissionResponse) export const permission = { - get: get25, + get: get26, } -export const get26 = oc +export const get27 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1865,7 +1881,7 @@ export const get26 = oc .output(zGetWorkspacesCurrentPluginAssetResponse) export const asset = { - get: get26, + get: get27, } export const post27 = oc @@ -1898,7 +1914,7 @@ export const exclude = { post: post28, } -export const get27 = oc +export const get28 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1910,7 +1926,7 @@ export const get27 = oc .output(zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse) export const fetch_ = { - get: get27, + get: get28, } export const autoUpgrade = { @@ -1919,7 +1935,7 @@ export const autoUpgrade = { fetch: fetch_, } -export const get28 = oc +export const get29 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1930,10 +1946,10 @@ export const get28 = oc .output(zGetWorkspacesCurrentPluginDebuggingKeyResponse) export const debuggingKey = { - get: get28, + get: get29, } -export const get29 = oc +export const get30 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1945,10 +1961,10 @@ export const get29 = oc .output(zGetWorkspacesCurrentPluginFetchManifestResponse) export const fetchManifest = { - get: get29, + get: get30, } -export const get30 = oc +export const get31 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1960,7 +1976,7 @@ export const get30 = oc .output(zGetWorkspacesCurrentPluginIconResponse) export const icon = { - get: get30, + get: get31, } export const post29 = oc @@ -2014,7 +2030,7 @@ export const install = { pkg, } -export const get31 = oc +export const get32 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2026,7 +2042,7 @@ export const get31 = oc .output(zGetWorkspacesCurrentPluginInstalledIdsResponse) export const installedIds = { - get: get31, + get: get32, } export const post32 = oc @@ -2063,7 +2079,7 @@ export const latestVersions = { post: post33, } -export const get32 = oc +export const get33 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2075,12 +2091,12 @@ export const get32 = oc .output(zGetWorkspacesCurrentPluginListResponse) export const list2 = { - get: get32, + get: get33, installations, latestVersions, } -export const get33 = oc +export const get34 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2092,14 +2108,14 @@ export const get33 = oc .output(zGetWorkspacesCurrentPluginMarketplacePkgResponse) export const pkg2 = { - get: get33, + get: get34, } export const marketplace2 = { pkg: pkg2, } -export const get34 = oc +export const get35 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2111,7 +2127,7 @@ export const get34 = oc .output(zGetWorkspacesCurrentPluginParametersDynamicOptionsResponse) export const dynamicOptions = { - get: get34, + get: get35, } /** @@ -2155,7 +2171,7 @@ export const change2 = { post: post35, } -export const get35 = oc +export const get36 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2166,7 +2182,7 @@ export const get35 = oc .output(zGetWorkspacesCurrentPluginPermissionFetchResponse) export const fetch2 = { - get: get35, + get: get36, } export const permission2 = { @@ -2174,7 +2190,7 @@ export const permission2 = { fetch: fetch2, } -export const get36 = oc +export const get37 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2186,7 +2202,7 @@ export const get36 = oc .output(zGetWorkspacesCurrentPluginReadmeResponse) export const readme = { - get: get36, + get: get37, } export const post36 = oc @@ -2234,7 +2250,7 @@ export const delete9 = { byIdentifier, } -export const get37 = oc +export const get38 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2246,11 +2262,11 @@ export const get37 = oc .output(zGetWorkspacesCurrentPluginTasksByTaskIdResponse) export const byTaskId = { - get: get37, + get: get38, delete: delete9, } -export const get38 = oc +export const get39 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2262,7 +2278,7 @@ export const get38 = oc .output(zGetWorkspacesCurrentPluginTasksResponse) export const tasks = { - get: get38, + get: get39, deleteAll, byTaskId, } @@ -2367,7 +2383,7 @@ export const upload = { pkg: pkg3, } -export const get39 = oc +export const get40 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2384,7 +2400,7 @@ export const get39 = oc .output(zGetWorkspacesCurrentPluginByCategoryListResponse) export const list3 = { - get: get39, + get: get40, } export const byCategory = { @@ -2437,7 +2453,7 @@ export const delete10 = oc .input(z.object({ params: zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath })) .output(zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse) -export const get40 = oc +export const get41 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2466,12 +2482,12 @@ export const put6 = oc export const byPolicyId = { delete: delete10, - get: get40, + get: get41, put: put6, copy, } -export const get41 = oc +export const get42 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2494,7 +2510,7 @@ export const post46 = oc .output(zPostWorkspacesCurrentRbacAccessPoliciesResponse) export const accessPolicies = { - get: get41, + get: get42, post: post46, byPolicyId, } @@ -2554,7 +2570,7 @@ export const delete11 = oc ) .output(zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse) -export const get42 = oc +export const get43 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2571,10 +2587,10 @@ export const get42 = oc export const memberBindings = { delete: delete11, - get: get42, + get: get43, } -export const get43 = oc +export const get44 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2590,7 +2606,7 @@ export const get43 = oc .output(zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings = { - get: get43, + get: get44, } export const byPolicyId2 = { @@ -2602,7 +2618,7 @@ export const accessPolicies2 = { byPolicyId: byPolicyId2, } -export const get44 = oc +export const get45 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2619,10 +2635,10 @@ export const get44 = oc .output(zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponse) export const accessPolicy = { - get: get44, + get: get45, } -export const get45 = oc +export const get46 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2639,7 +2655,7 @@ export const get45 = oc .output(zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponse) export const userAccessPolicies = { - get: get45, + get: get46, } export const put9 = oc @@ -2670,7 +2686,7 @@ export const users = { byTargetAccountId, } -export const get46 = oc +export const get47 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2698,11 +2714,11 @@ export const put10 = oc .output(zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse) export const whitelist = { - get: get46, + get: get47, put: put10, } -export const get47 = oc +export const get48 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2714,7 +2730,7 @@ export const get47 = oc .output(zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponse) export const whitelistConfig = { - get: get47, + get: get48, } export const byAgentId2 = { @@ -2746,7 +2762,7 @@ export const delete12 = oc ) .output(zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse) -export const get48 = oc +export const get49 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2763,10 +2779,10 @@ export const get48 = oc export const memberBindings2 = { delete: delete12, - get: get48, + get: get49, } -export const get49 = oc +export const get50 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2782,7 +2798,7 @@ export const get49 = oc .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings2 = { - get: get49, + get: get50, } export const byPolicyId3 = { @@ -2794,7 +2810,7 @@ export const accessPolicies4 = { byPolicyId: byPolicyId3, } -export const get50 = oc +export const get51 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2811,10 +2827,10 @@ export const get50 = oc .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyResponse) export const accessPolicy2 = { - get: get50, + get: get51, } -export const get51 = oc +export const get52 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2831,7 +2847,7 @@ export const get51 = oc .output(zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesResponse) export const userAccessPolicies2 = { - get: get51, + get: get52, } export const put11 = oc @@ -2862,7 +2878,7 @@ export const users2 = { byTargetAccountId: byTargetAccountId2, } -export const get52 = oc +export const get53 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2890,11 +2906,11 @@ export const put12 = oc .output(zPutWorkspacesCurrentRbacAppsByAppIdWhitelistResponse) export const whitelist2 = { - get: get52, + get: get53, put: put12, } -export const get53 = oc +export const get54 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2906,7 +2922,7 @@ export const get53 = oc .output(zGetWorkspacesCurrentRbacAppsByAppIdWhitelistConfigResponse) export const whitelistConfig2 = { - get: get53, + get: get54, } export const byAppId = { @@ -2942,7 +2958,7 @@ export const delete13 = oc zDeleteWorkspacesCurrentRbacDatasetsByDatasetIdAccessPoliciesByPolicyIdMemberBindingsResponse, ) -export const get54 = oc +export const get55 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2963,10 +2979,10 @@ export const get54 = oc export const memberBindings3 = { delete: delete13, - get: get54, + get: get55, } -export const get55 = oc +export const get56 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2982,7 +2998,7 @@ export const get55 = oc .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings3 = { - get: get55, + get: get56, } export const byPolicyId4 = { @@ -2994,7 +3010,7 @@ export const accessPolicies6 = { byPolicyId: byPolicyId4, } -export const get56 = oc +export const get57 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3011,10 +3027,10 @@ export const get56 = oc .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdAccessPolicyResponse) export const accessPolicy3 = { - get: get56, + get: get57, } -export const get57 = oc +export const get58 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3031,7 +3047,7 @@ export const get57 = oc .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdUserAccessPoliciesResponse) export const userAccessPolicies3 = { - get: get57, + get: get58, } export const put13 = oc @@ -3062,7 +3078,7 @@ export const users3 = { byTargetAccountId: byTargetAccountId3, } -export const get58 = oc +export const get59 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3090,11 +3106,11 @@ export const put14 = oc .output(zPutWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistResponse) export const whitelist3 = { - get: get58, + get: get59, put: put14, } -export const get59 = oc +export const get60 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3106,7 +3122,7 @@ export const get59 = oc .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistConfigResponse) export const whitelistConfig3 = { - get: get59, + get: get60, } export const byDatasetId = { @@ -3122,7 +3138,7 @@ export const datasets = { byDatasetId, } -export const get60 = oc +export const get61 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3150,7 +3166,7 @@ export const put15 = oc .output(zPutWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse) export const rbacRoles = { - get: get60, + get: get61, put: put15, } @@ -3162,7 +3178,7 @@ export const members2 = { byMemberId: byMemberId2, } -export const get61 = oc +export const get62 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3173,10 +3189,10 @@ export const get61 = oc .output(zGetWorkspacesCurrentRbacMyPermissionsResponse) export const myPermissions = { - get: get61, + get: get62, } -export const get62 = oc +export const get63 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3187,10 +3203,10 @@ export const get62 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponse) export const agent = { - get: get62, + get: get63, } -export const get63 = oc +export const get64 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3201,10 +3217,10 @@ export const get63 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogAppResponse) export const app = { - get: get63, + get: get64, } -export const get64 = oc +export const get65 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3215,10 +3231,10 @@ export const get64 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse) export const dataset = { - get: get64, + get: get65, } -export const get65 = oc +export const get66 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3229,7 +3245,7 @@ export const get65 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogResponse) export const catalog = { - get: get65, + get: get66, agent, app, dataset, @@ -3254,7 +3270,7 @@ export const copy2 = { post: post47, } -export const get66 = oc +export const get67 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3266,7 +3282,7 @@ export const get66 = oc .output(zGetWorkspacesCurrentRbacRolesByRoleIdMembersResponse) export const members3 = { - get: get66, + get: get67, } export const delete14 = oc @@ -3280,7 +3296,7 @@ export const delete14 = oc .input(z.object({ params: zDeleteWorkspacesCurrentRbacRolesByRoleIdPath })) .output(zDeleteWorkspacesCurrentRbacRolesByRoleIdResponse) -export const get67 = oc +export const get68 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3304,13 +3320,13 @@ export const put16 = oc export const byRoleId = { delete: delete14, - get: get67, + get: get68, put: put16, copy: copy2, members: members3, } -export const get68 = oc +export const get69 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3331,7 +3347,7 @@ export const post48 = oc .output(zPostWorkspacesCurrentRbacRolesResponse) export const roles = { - get: get68, + get: get69, post: post48, byRoleId, } @@ -3356,7 +3372,7 @@ export const bindings = { put: put17, } -export const get69 = oc +export const get70 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3372,10 +3388,10 @@ export const get69 = oc .output(zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponse) export const memberBindings4 = { - get: get69, + get: get70, } -export const get70 = oc +export const get71 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3391,7 +3407,7 @@ export const get70 = oc .output(zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings4 = { - get: get70, + get: get71, } export const byPolicyId5 = { @@ -3404,7 +3420,7 @@ export const accessPolicies8 = { byPolicyId: byPolicyId5, } -export const get71 = oc +export const get72 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3415,7 +3431,7 @@ export const get71 = oc .output(zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponse) export const accessPolicy4 = { - get: get71, + get: get72, } export const agents3 = { @@ -3443,7 +3459,7 @@ export const bindings2 = { put: put18, } -export const get72 = oc +export const get73 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3459,10 +3475,10 @@ export const get72 = oc .output(zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdMemberBindingsResponse) export const memberBindings5 = { - get: get72, + get: get73, } -export const get73 = oc +export const get74 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3478,7 +3494,7 @@ export const get73 = oc .output(zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings5 = { - get: get73, + get: get74, } export const byPolicyId6 = { @@ -3491,7 +3507,7 @@ export const accessPolicies9 = { byPolicyId: byPolicyId6, } -export const get74 = oc +export const get75 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3502,7 +3518,7 @@ export const get74 = oc .output(zGetWorkspacesCurrentRbacWorkspaceAppsAccessPolicyResponse) export const accessPolicy5 = { - get: get74, + get: get75, } export const apps2 = { @@ -3530,7 +3546,7 @@ export const bindings3 = { put: put19, } -export const get75 = oc +export const get76 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3546,10 +3562,10 @@ export const get75 = oc .output(zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdMemberBindingsResponse) export const memberBindings6 = { - get: get75, + get: get76, } -export const get76 = oc +export const get77 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3565,7 +3581,7 @@ export const get76 = oc .output(zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings6 = { - get: get76, + get: get77, } export const byPolicyId7 = { @@ -3578,7 +3594,7 @@ export const accessPolicies10 = { byPolicyId: byPolicyId7, } -export const get77 = oc +export const get78 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3589,7 +3605,7 @@ export const get77 = oc .output(zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse) export const accessPolicy6 = { - get: get77, + get: get78, } export const datasets2 = { @@ -3655,7 +3671,7 @@ export const import_ = { post: post50, } -export const get78 = oc +export const get79 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3666,7 +3682,7 @@ export const get78 = oc .output(zGetWorkspacesCurrentSkillsTagsResponse) export const tags = { - get: get78, + get: get79, } export const post51 = oc @@ -3709,7 +3725,7 @@ export const duplicate = { post: post52, } -export const get79 = oc +export const get80 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3721,7 +3737,7 @@ export const get79 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdExportResponse) export const export2 = { - get: get79, + get: get80, } export const post53 = oc @@ -3744,7 +3760,7 @@ export const check = { post: post53, } -export const get80 = oc +export const get81 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3761,10 +3777,10 @@ export const get80 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdFilesContentResponse) export const content = { - get: get80, + get: get81, } -export const get81 = oc +export const get82 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3781,7 +3797,7 @@ export const get81 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse) export const preview = { - get: get81, + get: get82, } export const patch5 = oc @@ -3844,7 +3860,7 @@ export const publish = { post: post54, } -export const get82 = oc +export const get83 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3856,7 +3872,7 @@ export const get82 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdReferencesResponse) export const references = { - get: get82, + get: get83, } export const post55 = oc @@ -3890,7 +3906,7 @@ export const delete15 = oc .input(z.object({ params: zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath })) .output(zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse) -export const get83 = oc +export const get84 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3919,11 +3935,11 @@ export const patch6 = oc export const byVersionId = { delete: delete15, - get: get83, + get: get84, patch: patch6, } -export const get84 = oc +export const get85 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3935,7 +3951,7 @@ export const get84 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdVersionsResponse) export const versions = { - get: get84, + get: get85, byVersionId, } @@ -3955,7 +3971,7 @@ export const delete16 = oc ) .output(zDeleteWorkspacesCurrentSkillsBySkillIdResponse) -export const get85 = oc +export const get86 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3984,7 +4000,7 @@ export const patch7 = oc export const bySkillId = { delete: delete16, - get: get85, + get: get86, patch: patch7, assist, duplicate, @@ -3996,7 +4012,7 @@ export const bySkillId = { versions, } -export const get86 = oc +export const get87 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4020,7 +4036,7 @@ export const post56 = oc .output(zPostWorkspacesCurrentSkillsResponse) export const skills2 = { - get: get86, + get: get87, post: post56, files, import: import_, @@ -4028,7 +4044,7 @@ export const skills2 = { bySkillId, } -export const get87 = oc +export const get88 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4039,10 +4055,10 @@ export const get87 = oc .output(zGetWorkspacesCurrentSummaryResponse) export const summary2 = { - get: get87, + get: get88, } -export const get88 = oc +export const get89 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4053,7 +4069,7 @@ export const get88 = oc .output(zGetWorkspacesCurrentToolLabelsResponse) export const toolLabels = { - get: get88, + get: get89, } export const post57 = oc @@ -4086,7 +4102,7 @@ export const delete17 = { post: post58, } -export const get89 = oc +export const get90 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4097,11 +4113,11 @@ export const get89 = oc .input(z.object({ query: zGetWorkspacesCurrentToolProviderApiGetQuery })) .output(zGetWorkspacesCurrentToolProviderApiGetResponse) -export const get90 = { - get: get89, +export const get91 = { + get: get90, } -export const get91 = oc +export const get92 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4113,7 +4129,7 @@ export const get91 = oc .output(zGetWorkspacesCurrentToolProviderApiRemoteResponse) export const remote = { - get: get91, + get: get92, } export const post59 = oc @@ -4150,7 +4166,7 @@ export const test = { pre, } -export const get92 = oc +export const get93 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4162,7 +4178,7 @@ export const get92 = oc .output(zGetWorkspacesCurrentToolProviderApiToolsResponse) export const tools = { - get: get92, + get: get93, } export const post61 = oc @@ -4183,7 +4199,7 @@ export const update2 = { export const api = { add, delete: delete17, - get: get90, + get: get91, remote, schema, test, @@ -4211,7 +4227,7 @@ export const add2 = { post: post62, } -export const get93 = oc +export const get94 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4228,10 +4244,10 @@ export const get93 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialInfoResponse) export const info = { - get: get93, + get: get94, } -export const get94 = oc +export const get95 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4251,7 +4267,7 @@ export const get94 = oc ) export const byCredentialType = { - get: get94, + get: get95, } export const schema2 = { @@ -4263,7 +4279,7 @@ export const credential = { schema: schema2, } -export const get95 = oc +export const get96 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4280,7 +4296,7 @@ export const get95 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialsResponse) export const credentials3 = { - get: get95, + get: get96, } export const post63 = oc @@ -4323,7 +4339,7 @@ export const delete18 = { post: post64, } -export const get96 = oc +export const get97 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4335,10 +4351,10 @@ export const get96 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderIconResponse) export const icon2 = { - get: get96, + get: get97, } -export const get97 = oc +export const get98 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4350,10 +4366,10 @@ export const get97 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderInfoResponse) export const info2 = { - get: get97, + get: get98, } -export const get98 = oc +export const get99 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4367,7 +4383,7 @@ export const get98 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderOauthClientSchemaResponse) export const clientSchema = { - get: get98, + get: get99, } export const delete19 = oc @@ -4385,7 +4401,7 @@ export const delete19 = oc ) .output(zDeleteWorkspacesCurrentToolProviderBuiltinByProviderOauthCustomClientResponse) -export const get99 = oc +export const get100 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4416,7 +4432,7 @@ export const post65 = oc export const customClient = { delete: delete19, - get: get99, + get: get100, post: post65, } @@ -4425,7 +4441,7 @@ export const oauth = { customClient, } -export const get100 = oc +export const get101 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4437,7 +4453,7 @@ export const get100 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderToolsResponse) export const tools2 = { - get: get100, + get: get101, } export const post66 = oc @@ -4492,7 +4508,7 @@ export const auth = { post: post67, } -export const get101 = oc +export const get102 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4504,14 +4520,14 @@ export const get101 = oc .output(zGetWorkspacesCurrentToolProviderMcpToolsByProviderIdResponse) export const byProviderId = { - get: get101, + get: get102, } export const tools3 = { byProviderId, } -export const get102 = oc +export const get103 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4523,7 +4539,7 @@ export const get102 = oc .output(zGetWorkspacesCurrentToolProviderMcpUpdateByProviderIdResponse) export const byProviderId2 = { - get: get102, + get: get103, } export const update4 = { @@ -4602,7 +4618,7 @@ export const delete21 = { post: post70, } -export const get103 = oc +export const get104 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4613,11 +4629,11 @@ export const get103 = oc .input(z.object({ query: zGetWorkspacesCurrentToolProviderWorkflowGetQuery.optional() })) .output(zGetWorkspacesCurrentToolProviderWorkflowGetResponse) -export const get104 = { - get: get103, +export const get105 = { + get: get104, } -export const get105 = oc +export const get106 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4629,7 +4645,7 @@ export const get105 = oc .output(zGetWorkspacesCurrentToolProviderWorkflowToolsResponse) export const tools4 = { - get: get105, + get: get106, } export const post71 = oc @@ -4650,7 +4666,7 @@ export const update5 = { export const workflow = { create: create2, delete: delete21, - get: get104, + get: get105, tools: tools4, update: update5, } @@ -4662,7 +4678,7 @@ export const toolProvider = { workflow, } -export const get106 = oc +export const get107 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4674,10 +4690,10 @@ export const get106 = oc .output(zGetWorkspacesCurrentToolProvidersResponse) export const toolProviders = { - get: get106, + get: get107, } -export const get107 = oc +export const get108 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4688,10 +4704,10 @@ export const get107 = oc .output(zGetWorkspacesCurrentToolsApiResponse) export const api2 = { - get: get107, + get: get108, } -export const get108 = oc +export const get109 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4702,10 +4718,10 @@ export const get108 = oc .output(zGetWorkspacesCurrentToolsBuiltinResponse) export const builtin2 = { - get: get108, + get: get109, } -export const get109 = oc +export const get110 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4716,10 +4732,10 @@ export const get109 = oc .output(zGetWorkspacesCurrentToolsMcpResponse) export const mcp2 = { - get: get109, + get: get110, } -export const get110 = oc +export const get111 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4730,7 +4746,7 @@ export const get110 = oc .output(zGetWorkspacesCurrentToolsWorkflowResponse) export const workflow2 = { - get: get110, + get: get111, } export const tools5 = { @@ -4740,7 +4756,7 @@ export const tools5 = { workflow: workflow2, } -export const get111 = oc +export const get112 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4752,13 +4768,13 @@ export const get111 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderIconResponse) export const icon3 = { - get: get111, + get: get112, } /** * Get info for a trigger provider */ -export const get112 = oc +export const get113 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4771,7 +4787,7 @@ export const get112 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderInfoResponse) export const info3 = { - get: get112, + get: get113, } /** @@ -4792,7 +4808,7 @@ export const delete22 = oc /** * Get OAuth client configuration for a provider */ -export const get113 = oc +export const get114 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4826,7 +4842,7 @@ export const post72 = oc export const client = { delete: delete22, - get: get113, + get: get114, post: post72, } @@ -4893,7 +4909,7 @@ export const create3 = { /** * Get the request logs for a subscription instance for a trigger provider */ -export const get114 = oc +export const get115 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4914,7 +4930,7 @@ export const get114 = oc ) export const bySubscriptionBuilderId2 = { - get: get114, + get: get115, } export const logs = { @@ -4988,7 +5004,7 @@ export const verifyAndUpdate = { /** * Get a subscription instance for a trigger provider */ -export const get115 = oc +export const get116 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5009,7 +5025,7 @@ export const get115 = oc ) export const bySubscriptionBuilderId5 = { - get: get115, + get: get116, } export const builder = { @@ -5024,7 +5040,7 @@ export const builder = { /** * List all trigger subscriptions for the current tenant's provider */ -export const get116 = oc +export const get117 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5037,13 +5053,13 @@ export const get116 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListResponse) export const list4 = { - get: get116, + get: get117, } /** * Initiate OAuth authorization flow for a trigger provider */ -export const get117 = oc +export const get118 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5060,7 +5076,7 @@ export const get117 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizeResponse) export const authorize = { - get: get117, + get: get118, } export const oauth3 = { @@ -5177,7 +5193,7 @@ export const triggerProvider = { /** * List all trigger providers for the current tenant */ -export const get118 = oc +export const get119 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5189,7 +5205,7 @@ export const get118 = oc .output(zGetWorkspacesCurrentTriggersResponse) export const triggers = { - get: get118, + get: get119, } export const current = { @@ -5237,7 +5253,7 @@ export const webappLogo = { upload: upload3, } -export const get119 = oc +export const get120 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5259,7 +5275,7 @@ export const post81 = oc .output(zPostWorkspacesCustomConfigResponse) export const customConfig = { - get: get119, + get: get120, post: post81, webappLogo, } @@ -5294,7 +5310,7 @@ export const switch3 = { post: post83, } -export const get120 = oc +export const get121 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5306,7 +5322,7 @@ export const get120 = oc .output(zGetWorkspacesByTenantIdModelProvidersByProviderByIconTypeByLangResponse) export const byLang = { - get: get120, + get: get121, } export const byIconType = { @@ -5325,7 +5341,7 @@ export const byTenantId = { modelProviders: modelProviders2, } -export const get121 = oc +export const get122 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -5336,7 +5352,7 @@ export const get121 = oc .output(zGetWorkspacesResponse) export const workspaces = { - get: get121, + get: get122, current, customConfig, info: info4, diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index c332d99398d..1502081a7e3 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -390,6 +390,10 @@ export type NetworkAccessGroupMutationResponse = { group: NetworkAccessGroupResponse } +export type NetworkAccessGroupCurrentIpResponse = { + client_ip: string +} + export type NetworkAccessGroupDeleteResponse = { deleted: boolean } @@ -3948,6 +3952,25 @@ export type PostWorkspacesCurrentNetworkAccessGroupsResponses = { export type PostWorkspacesCurrentNetworkAccessGroupsResponse = PostWorkspacesCurrentNetworkAccessGroupsResponses[keyof PostWorkspacesCurrentNetworkAccessGroupsResponses] +export type GetWorkspacesCurrentNetworkAccessGroupsCurrentIpData = { + body?: never + path?: never + query?: never + url: '/workspaces/current/network-access-groups/current-ip' +} + +export type GetWorkspacesCurrentNetworkAccessGroupsCurrentIpErrors = { + 403: unknown + 503: unknown +} + +export type GetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponses = { + 200: NetworkAccessGroupCurrentIpResponse +} + +export type GetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse = + GetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponses[keyof GetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponses] + export type DeleteWorkspacesCurrentNetworkAccessGroupsByGroupIdData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index 53b4a896f21..4ce6daaf8e3 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -263,6 +263,13 @@ export const zNetworkAccessGroupCreatePayload = z.object({ name: z.string().min(1).max(100), }) +/** + * NetworkAccessGroupCurrentIPResponse + */ +export const zNetworkAccessGroupCurrentIpResponse = z.object({ + client_ip: z.string(), +}) + /** * NetworkAccessGroupDeleteResponse */ @@ -4670,6 +4677,12 @@ export const zPostWorkspacesCurrentNetworkAccessGroupsBody = zNetworkAccessGroup */ export const zPostWorkspacesCurrentNetworkAccessGroupsResponse = zNetworkAccessGroupMutationResponse +/** + * Trusted current client IP retrieved without requiring an existing network access group + */ +export const zGetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse = + zNetworkAccessGroupCurrentIpResponse + export const zDeleteWorkspacesCurrentNetworkAccessGroupsByGroupIdPath = z.object({ group_id: z.uuid(), }) diff --git a/packages/contracts/network-access-current-ip.test.ts b/packages/contracts/network-access-current-ip.test.ts new file mode 100644 index 00000000000..b68d06a4560 --- /dev/null +++ b/packages/contracts/network-access-current-ip.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vite-plus/test' +import { currentIp, workspaces } from './generated/api/console/workspaces/orpc.gen' +import { + zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdCheckCurrentIpPath, + zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdCheckCurrentIpResponse, + zGetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse, +} from './generated/api/console/workspaces/zod.gen' + +describe('generated network access current-IP contract', () => { + it('exposes the policy-independent operation alongside the group preflight', () => { + expect(workspaces.current.networkAccessGroups.currentIp.get).toBe(currentIp.get) + expect(workspaces.current.networkAccessGroups.byGroupId.checkCurrentIp.get).toBeDefined() + }) + + it.each(['192.0.2.1', '2001:db8::1'])('accepts a trusted IP without a saved policy: %s', (ip) => { + expect( + zGetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse.parse({ client_ip: ip }), + ).toEqual({ client_ip: ip }) + }) + + it.each([{}, null, { client_ip: 42 }])('requires a string client_ip: %j', (response) => { + expect( + zGetWorkspacesCurrentNetworkAccessGroupsCurrentIpResponse.safeParse(response).success, + ).toBe(false) + }) + + it('retains the existing saved-group preflight contract', () => { + const schema = zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdCheckCurrentIpResponse + expect(schema.parse({ client_ip: '192.0.2.1', allowed: false, policy_version: 1 })).toEqual({ + client_ip: '192.0.2.1', + allowed: false, + policy_version: 1, + }) + expect(schema.safeParse({ client_ip: '192.0.2.1' }).success).toBe(false) + expect( + zGetWorkspacesCurrentNetworkAccessGroupsByGroupIdCheckCurrentIpPath.safeParse({}).success, + ).toBe(false) + }) +})